C# 서버 개발자 전환기 3편
2026-09-15 10:20:38

Interface와 DI

1. Interface

IUserRepository를 구현하는 클래스는 GetUser(int id) 메서드를 제공해야 한다는 규약

1
2
3
4
public interface IUserRepository
{
User GetUser(int id);
}

실제 구현

1
2
3
4
5
6
7
8
9
10
11
/*
: 뒤에 인터페이스면 구현, 클래스면 상속
*/
public class UserRepository : IUserRepository
{
public User GetUser(int id)
{
// DB에서 조회
return ...;
}
}

1.1 Interface를 사용하는 이유

이러한 서비스가 있다고 가정을 하는 경우

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService
{
private UserRepository repository;

public UserService() // js class constructor
{
repository = new UserRepository();
}

public User GetUser(int id)
{
return repository.GetUser(id);
}
}

이렇게 하면 UserService가 UserRepository라는 구체적인 클래스에 강하게 묶여버림
예로 테스트를 위해 UserRepository 대신 TestUserRepository를 사용하려면 UserService 내부의 의존성을 직접 수정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService
{
private IUserRepository repository;

public UserService(IUserRepository repository)
{
this.repository = repository;
}

public User GetUser(int id)
{
return repository.GetUser(id);
}
}

이제 UserService는 IUserRepository 기능만 확인한다
실제 객체가 UserRepository인지 아닌지는 관심 없음

2. DI

필요한 의존성을 밖에서 넣어준다.

1
2
var repository = new UserRepository();
var service = new UserService(repository);

2.1 ASP.NET Core

매번 위처럼 직접 주입하지 않고 DI Container에 등록 하면 된다

1
2
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<UserService>();

누군가 IUserRepository를 필요로 하면 UserRepository를 만들어서 넣어줌

2.2 생명주기

2.2.1 Transient

1
services.AddTransient<IUserRepository, UserRepository>();

사용할 때마다 새로운 객체를 만들어줌, 상태를 가지지 않는 가벼운 객체에 많이 사용

1
2
Service A → UserRepository #1
Service B → UserRepository #2

둘이 같은 IUserRepository를 주입받아도 서로 다른 객체

2.2.2 Scoped

1
services.AddScoped<IUserRepository, UserRepository>();

ASP.NET Core에서는 일반적으로 HTTP Request 하나 = Scope 하나로 생각하면 됨
예를 들어 요청이 2개 들어오면

1
2
3
4
5
6
7
Request #1
├─ UserService
└─ UserRepository #1

Request #2
├─ UserService
└─ UserRepository #2

Request #1 안에서는 같은 객체를 사용함

1
2
3
Request #1
├─ UserService ──┐
└─ OrderService ─┴─ UserRepository #1

둘 다 IUserRepository를 요청하면 같은 객체를 받음

2.2.3 Singleton

1
services.AddSingleton<IUserRepository, UserRepository>();

앱 전체에서 하나의 인스턴스를 공유 (기본적으로 필요할 때 생성)

1
2
3
4
5
6
7
8
9
Application

└─ UserRepository #1

├─ Request 1
├─ Request 2
├─ Request 3
├─ Request 4
└─ Request 10000

2.3 장점

2.3.1 구현체 변경이 쉬워진다

위에서 얘기한 UserRepository, TestUserRepository를 변경하려면

1
2
3
4
// 실제 서버
builder.Services.AddScoped<IUserRepository, UserRepository>();
// 테스트
builder.Services.AddScoped<IUserRepository, TestUserRepository>();

만 바꿔주면 코드 수정을 할 필요가 없어진다

2.3.2 객체 생성/관리를 한 곳에서 한다

DI로 관리할 서비스 의존성을 개발자가 직접 생성할 필요 없이
DI 컨테이너가 객체 생성과 Lifetime을 관리함

2.4 조심해야할점

2.4.1 의존성이 너무 많아질 수 있음

1
2
3
4
5
6
7
8
9
public UserService(
IUserRepository userRepository,
IEmailService emailService,
ILogger logger,
ICache cache,
IPaymentService paymentService,
IEventPublisher eventPublisher)
{
}

갯수와 상관없이 클래스가 이 의존성들을 필요로 하는 것이 해당 클래스의 책임과 자연스럽게 연결되는가?를 생각해봐야함..

2.4.2 DI Container 설정을 잘못하면 런타임 문제가 생김

1
2
3
4
5
6
7
8
9
10
11
12
13
14
services.AddSingleton<CacheService>();
services.AddScoped<AppDbContext>();

...

public class CacheService
{
private readonly AppDbContext db;

public CacheService(AppDbContext db)
{
this.db = db;
}
}

CacheService는 애플리케이션이 살아있는 동안 하나인데, AppDbContext는 HTTP Request마다 새로 만들어지고 Request가 끝나면 폐기되는 객체임
Singleton인 CacheService가 Scoped인 AppDbContext를 의존하게 되면, 수명이 긴 객체가 수명이 짧은 객체를 참조하는 구조가 되어버림
잘못된 Lifetime 조합이며 ASP.NET Core DI 검증에서 예외가 발생할 수 있음

Prev
2026-09-15 10:20:38