종속성 주입 이해
ASP.NET Core 앱은 여러 구성 요소에서 동일한 서비스에 액세스해야 하는 경우가 많습니다. 예를 들어 여러 구성 요소가 데이터베이스에서 데이터를 가져오는 서비스에 액세스해야 할 수 있습니다. ASP.NET Core는 기본 제공 DI(종속성 주입) 컨테이너를 사용하여 앱에서 사용하는 서비스를 관리합니다.
DI(종속성 주입) 및 IoC(Inversion of Control) 컨테이너
종속성 주입 패턴은 IoC(Inversion of Control)의 한 형태입니다. 종속성 주입 패턴에서 구성 요소는 종속성을 자체적으로 만드는 대신 외부 원본에서 받습니다. 이 패턴은 코드를 종속성에서 분리하여 코드를 더 쉽게 테스트하고 유지 관리할 수 있도록 합니다.
다음 Program.cs 파일을 고려합니다.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using MyApp.Services;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<PersonService>();
var app = builder.Build();
app.MapGet("/",
(PersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
}
);
app.Run();
그리고 다음 PersonService.cs 파일:
namespace MyApp.Services;
public class PersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
이 코드를 이해하려면 강조 표시된 app.MapGet 코드로 시작합니다. 이 코드는 루트 URL(/)에 대한 HTTP GET 요청을 인사말 메시지를 반환하는 대리자로 매핑합니다. 대리자의 서명은 PersonService라는 personService 매개 변수를 정의합니다. 앱이 실행되고 클라이언트가 루트 URL을 요청하는 경우 대리자 내의 코드는PersonService 서비스에 ‘의존’하여 인사말 메시지에 포함할 텍스트를 가져옵니다.
대리자는 PersonService 서비스를 어디서 가져오나요? 서비스 컨테이너에서 암시적으로 제공됩니다. 강조 표시된 builder.Services.AddSingleton<PersonService>() 줄은 앱이 시작될 때 PersonService 클래스의 새 인스턴스를 만들고 해당 인스턴스를 필요한 구성 요소에 제공하도록 서비스 컨테이너에 지시합니다.
PersonService 서비스가 필요한 모든 구성 요소는 대리자 서명에 PersonService 형식의 매개 변수를 선언할 수 있습니다. 서비스 컨테이너는 구성 요소가 만들어질 때 PersonService 클래스의 인스턴스를 자동으로 제공합니다. 대리자는 PersonService 인스턴스 자체를 만들지 않고 서비스 컨테이너가 제공하는 인스턴스만 사용합니다.
인터페이스 및 종속성 주입
특정 서비스 구현에 대한 종속성을 방지하려면 대신 특정 인터페이스에 대한 서비스를 구성한 다음, 인터페이스에만 의존할 수 있습니다. 이 방법을 사용하면 서비스 구현을 유연하게 전환할 수 있으므로 코드를 테스트하고 유지 관리하기가 더 쉬워집니다.
PersonService 클래스에 대한 인터페이스를 고려해 보세요.
public interface IPersonService
{
string GetPersonName();
}
이 인터페이스는 GetPersonName을 반환하는 단일 메서드 string을 정의합니다. 이 PersonService 클래스는 IPersonService 인터페이스를 구현합니다.
internal sealed class PersonService : IPersonService
{
public string GetPersonName()
{
return "John Doe";
}
}
PersonService 클래스를 직접 등록하는 대신 IPersonService 인터페이스의 구현으로 등록할 수 있습니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/",
(IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
}
);
app.Run();
이 예제 Program.cs 두 가지 방법으로 이전 예제와 다릅니다.
-
PersonService클래스를 직접 등록하는 것이 아니라 인스턴스가IPersonService인터페이스의PersonService‘구현’으로 등록됩니다. - 이제 대리자 서명에는
IPersonService매개 변수 대신PersonService매개 변수가 필요합니다.
앱이 실행되고 클라이언트가 루트 URL을 요청하면 서비스 컨테이너는 PersonService 인터페이스의 구현으로 등록되므로 IPersonService 클래스의 인스턴스를 제공합니다.
팁
IPersonService를 계약으로 생각해 보세요. 구현에 있어야 하는 메서드 및 속성을 정의 합니다 . 대리자에는 IPersonService의 인스턴스가 필요합니다. 또한 기본 구현에 대해 전혀 신경 쓰지 않으며, 인스턴스에는 계약에 정의된 메서드와 속성만 있습니다.
종속성 주입을 사용하여 테스트
인터페이스를 사용하면 구성 요소를 격리한 상태로 더 쉽게 테스트할 수 있습니다. 테스트 목적으로 IPersonService 인터페이스의 모의 구현을 만들 수 있습니다. 테스트에 모의 구현을 등록하면 서비스 컨테이너는 테스트 중인 구성 요소에 모의 구현을 제공합니다.
예를 들어 하드 코딩된 문자열을 반환하는 대신 GetPersonName 클래스의 PersonService 메서드가 데이터베이스에서 이름을 가져옵니다.
IPersonService 인터페이스에 의존하는 구성 요소를 테스트하려면 하드 코딩된 문자열을 반환하는 IPersonService 인터페이스의 모의 구현을 만들 수 있습니다. 테스트 중인 구성 요소는 실제 구현과 모의 구현 간의 차이를 알지 못합니다.
또한 앱이 인사말 메시지를 반환하는 API 엔드포인트를 매핑한다고 가정해 보세요. 엔드포인트는 인사할 사람의 이름을 가져오는 데 IPersonService 인터페이스에 의존합니다.
IPersonService 서비스를 등록하고 API 엔드포인트를 매핑하는 코드는 다음과 같습니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IPersonService, PersonService>();
var app = builder.Build();
app.MapGet("/", (IPersonService personService) =>
{
return $"Hello, {personService.GetPersonName()}!";
});
app.Run();
이것은 IPersonService를 사용한 이전 예제와 비슷합니다. 대리자에는 서비스 컨테이너가 제공하는 IPersonService 매개 변수가 필요합니다. 앞에서 설명한 대로 인터페이스를 구현하는 PersonService가 데이터베이스에서 인사할 사람의 이름을 가져온다고 가정합니다.
이제 동일한 API 엔드포인트를 테스트하는 다음 XUnit 테스트를 고려해 보세요.
팁
XUnit 또는 Moq에 대해 잘 몰라도 됩니다. 단위 테스트 작성은 이 모듈의 범위를 벗어납니다. 이 예제는 테스트에서 종속성 주입을 사용하는 방법을 설명하기 위한 것입니다.
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using MyWebApp;
using System.Net;
public class GreetingApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public GreetingApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory;
}
[Fact]
public async Task GetGreeting_ReturnsExpectedGreeting()
{
//Arrange
var mockPersonService = new Mock<IPersonService>();
mockPersonService.Setup(service => service.GetPersonName()).Returns("Jane Doe");
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton(mockPersonService.Object);
});
}).CreateClient();
// Act
var response = await client.GetAsync("/");
var responseString = await response.Content.ReadAsStringAsync();
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Equal("Hello, Jane Doe!", responseString);
}
}
앞의 코드는 다음을 수행합니다.
- 하드 코딩된 문자열을 반환하는
IPersonService인터페이스의 모의 구현을 만듭니다. - 모의 구현을 서비스 컨테이너에 등록합니다.
- API 엔드포인트에 요청하기 위한 HTTP 클라이언트를 만듭니다.
- API 엔드포인트의 응답이 예상대로 표시되도록 어설션합니다.
이 테스트는 PersonService 클래스가 인사할 사람의 이름을 가져오는 방법을 신경 쓰지 않습니다. 이름이 인사말 메시지에 포함되는지에만 관심을 둡니다. 이 테스트는 IPersonService 인터페이스의 모의 구현을 사용하여 테스트되는 구성 요소를 서비스의 실제 구현과 격리합니다.