ASP.NET Core - 3. 의존성 주입(Dependency Injection)
Service는 Middleware와 Endpoint사이에 공유되는 객체라고 할 수 있습니다. Service가 제공할 수 있는 기능에는 제한이 없지만 일반적인 경우 대부분 Application의 여러 부분에 걸쳐 필요한 작업(Logging이나 Database 접근과 같은)에 사용됩니다.
ASP.NET Core 의존성 주입기능은 Service를 생성하고 사용하는 데 사용됩니다. 이제 ASP.NET Core platform에서 의존성 주입이 어떻게 지원되는지를 알아보고 의존성 주입을 사용함으로써 어떠한 문제를 해결할 수 있는지를 천천히 확인해 보도록 하겠습니다.
1. Project 만들기
예제로 사용하게될 Project는
[.NET/ASP.NET] - ASP.NET Core - 2. 라우팅(Routing)
상기 글에서 사용하던 Project를 그대로 사용할 것입니다. 이 Project에서 'Services'라는 folder를 만들고 IMyResponse.cs라는 이름의 file을 아래 내용으로 추가합니다.
namespace MyWebApp.Services
{
public interface IMyResponse
{
Task Format(HttpContext context, string content);
}
}
위 interface는 HttpContext객체와 string값을 받는 단일 Method를 정의하고 있습니다. 이어서 해당 Interface의 구현을 위해 MyResponse.cs라는 구현 class를 추가합니다.
namespace MyWebApp.Services
{
public class MyResponse : IMyResponse
{
private int responseCounter = 0;
public async Task Format(HttpContext context, string content)
{
await context.Response.WriteAsync($"Response {++responseCounter}:\n{content}");
}
}
}
MyResponse class는 IMyResponse interface를 구현하고 있으며 문자열을 통해 응답을 수행하도록 하고 있습니다.
(1) Middleware component와 Endpoint 만들기
앞으로 작성하게될 예제 중 몇몇은 Middleware와 Endpoint가 사용 돌 때 어떻게 기능이 다르게 적용되는지를 보여줄 것입니다. Project에 WeatherMiddleware.cs이름의 file을 Middleware folder에 아래와 같이 추가합니다.
namespace MyWebApp.Middleware
{
public class WeatherMiddleware
{
private RequestDelegate next;
public WeatherMiddleware(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path == "/middleware/class")
await context.Response.WriteAsync("Middleware Class: It is raining.");
else
await next(context);
}
}
}
이어서 위에서 추가한 Middleware component와 비슷한 결과를 표시할 Endpoint인 WeatherEndpoint.cs file을 Middleware folder에 추가합니다.
namespace MyWebApp.Middleware
{
public class WeatherEndpoint
{
public static async Task Endpoint(HttpContext context)
{
await context.Response.WriteAsync("Endpoint Class: It is cloudy.");
}
}
}
(2) 요청 Pipeline 설정
Program.cs를 아래와 같이 수정하여 새로운 Pipeline을 구현합니다. 위에서 정의한 class는 비슷한 결과를 표시하는 lambda함수와 함께 적용되었습니다.
using MyWebApp.Middleware;
using MyWebApp;
using MyWebApp.Services;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
IMyResponse res = new MyResponse();
app.MapGet("middleware/function", async (context) =>
{
await res.Format(context, "Middleware Function: It is snowing.");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/function", async context =>
{
await context.Response.WriteAsync("Endpoint Function: It is sunny.");
});
app.Run();
Project를 실행하여 /middleware/function으로 URL을 요청하여 아래와 같이 결과가 표시되는지를 확인합니다. 이때 새로고침과 같은 동작을 통해 반복적인 요청을 수행할 때마다 Response의 Count 수는 증가해야 합니다.
2. Service Location과 강력한 결합의 이해
의존성 주입(Dependency Injection)을 이해하기 위해서는 지금부터 해결해야할 2가지 문제를 파악하는 것이 중요합니다.
(1) Service 배치(Location)에 관한 문제점
대부분의 Project는 Service형태로 Application의 여러 부분에서 사용하기위한 기능을 갖추고 있습니다. 일 번 적으로 Logging이나 설정을 가져오는 것과 같은 것을 예로 들 수 있지만 위에서 만들어본 MyResponse class를 포함하여 사실상 모든 공유 기능이 여기에 포함될 수 있습니다.
각각의 MyResponse 객체는 Browser로 응답을 보낸 counter를 관리하도록 되어 있는데 만약 endpoint에서 생성된 응답에서 하나의 같은 counter로 통일하고자 한다면 응답이 생성되는 모든 부분에서 쉽게 찾고 쉽게 사용될 수 있는 방법인 단일 MyResponse객체를 사용할 수 있도록 해야 합니다.
이를 위해서 Service를 알아내기 위한 다수의 방법이 존재하지만 현재 다루고자 하는 주요 주제를 제외하고 2가지의 주요 접근방식이 사용할 수 있습니다. 하나는 객체를 만들어 application에서 필요로 하는 부분에서 생성자 또는 method의 매개변수를 통해 해당 객체를 전달해 주는 것입니다. 다른 방식은 service class에서 정적 속성을 추가하여 공유된 instance로의 직접적인 접근법을 제공해 주는 것입니다. 특히 두 번째 방식은 singleton pattern이라 하여 의존성 주입이 광범위하게 사용되기 전 일반적으로 사용되던 방식이었습니다.
namespace MyWebApp.Services
{
public class MyResponse : IMyResponse
{
private int responseCounter = 0;
private static MyResponse? myResponse;
public async Task Format(HttpContext context, string content)
{
await context.Response.WriteAsync($"Response {++responseCounter}:\n{content}");
}
public static MyResponse Singleton
{
get
{
if (myResponse == null)
myResponse = new MyResponse();
return myResponse;
}
}
}
}
위 예제는 singleton pattern의 가장 기본적인 방식을 나타낸 것이며 안전한 동시성 접근과 같은 문제에 더 주의를 기울이는 많은 변형적인 존재하기도 합니다. 예제에서 중요한 것은 정적 Singleton속성을 통해 공유된 객체를 가져오는 MyResponse service가 사용자에 의존적으로 바뀌었다는 것입니다.
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
IMyResponse res = new MyResponse();
app.MapGet("middleware/function", async (context) =>
{
//await res.Format(context, "Middleware Function: It is snowing.");
await MyResponse.Singleton.Format(context, "Middleware Function: It is snowing.");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/function", async context =>
{
//await context.Response.WriteAsync("Endpoint Function: It is sunny.");
await MyResponse.Singleton.Format(context, "Endpoint Function: It is sunny.");
});
app.Run();
singleton pattern은 단일 MyResponse 객체를 공유하도록 함으로서 middleware component와 endpoint에서 사용될 수 있도록 합니다. 결과적으로 2개의 다른 URL에 대한 요청에 의해 단일 counter가 증가되는 효과를 가져오게 됩니다. Project를 시작하여 /middleware/function와 /endpoint/function으로 요청을 수행해 counter가 정상적으로 증가되는지를 확인합니다.
singleton pattern은 이해하기 쉽고 사용하기에도 쉽지만 service가 어떻게 위치하는지에 대한 정보가 application전체에 파악되어야 하고 따라서 모든 service class와 service사용자는 공유된 객체에 어떻게 접근할 수 있는지에 대해 이해하고 있어야 합니다. 이것은 새로운 service가 생성됨에 따라 singleton pattern이 다양하게 변할 수 있으며 이들이 바뀔 때 update 되어야 하는 많은 수정사항이 발생할 수 있습니다. 이 pattern은 또한 융통성이 부족하고 모든 service사용자가 항상 단일 service객체를 공유함으로써 service를 관리할 때 유연성을 부여하기가 힘들 수 있습니다.
(2) Component의 강력한 결합에 대한 문제점
위 예제에서는 비록 interface를 정의하기는 했지만 singleton pattern을 사용함으로서 interface를 구현하는 class를 정적 속성을 통해 명확히 공유된 객체를 가져와야 한다는 문제점이 있습니다. 따라서 만약 IMyResponse interface를 구현하는 구현 class를 다른 것으로 바꿔야 하는 경우가 발생한다면 Service를 사용하는 모든 곳에서 기존의 구현 class대신 새로운 것으로 일일이 바꿔줘야 할 것입니다. 이러한 문제를 해결하는 pattern으로 interface를 통해 singleton객체로의 접근을 제공하는 class인 type broker라는 것을 사용할 수 있습니다. Project에 TypeBroker.cs라는 file을 추가하고 아래와 같이 code를 작성합니다.
namespace MyWebApp.Services
{
public class TypeBroker
{
private static IMyResponse response = new MyResponse();
public static IMyResponse Response => response;
}
}
예제에서 Response속성은 IMyResponse interface를 구현하는 공유된 service 객체로의 접근을 제공하고 있습니다. service의 사용자는 여전히 TypeBroker class가 사용하고자 하는 구현체로의 응답이 가능함을 알고 있어야 하지만 이 pattern은 service의 사용자가 class의 생성자대신 interface를 통해 작동이 가능하도록 구현된 것이므로 Program.cs는 아래와 같이 바뀔 수 있습니다.
app.MapGet("middleware/function", async (context) =>
{
//await res.Format(context, "Middleware Function: It is snowing.");
await TypeBroker.Response.Format(context, "Middleware Function: It is snowing.");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/function", async context =>
{
//await context.Response.WriteAsync("Endpoint Function: It is sunny.");
await TypeBroker.Response.Format(context, "Endpoint Function: It is sunny.");
});
이와 같은 접근법은 그저 TypeBroker class만을 다른 것으로 대체함으로서 다른 구현 class로 쉽게 전환할 수 있으며 서비스 사용자가 특정 구현체에 대한 종속성을 생성할 수 없도록 할 수 있습니다. 또한 service class는 어떻게 이들 기능이 배치되는지를 다루지 않으면서 이들이 제공하는 기능에만 집중할 수 있게 합니다. 실제 이와 같은 상황을 알아보기 위해 Services folder에 HTMLResponse.cs file을 아래와 같이 추가합니다.
namespace MyWebApp.Services
{
public class HTMLResponse : IMyResponse
{
public async Task Format(HttpContext context, string content)
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync($@"<h2>HTML Response</h2>");
}
}
}
이 IMyResponse의 구현체는 HttpResponse객체의 ContentType을 설정하고 있으며 간단한 HTML문자열을 응답하도록 하고 있습니다. 위와 같은 새로운 class를 적용하려면 그저 위에서 작성한 TypeBroker class를 아래와 같이 수정해 주기만 하면 됩니다.
public class TypeBroker
{
private static IMyResponse response = new HTMLResponse();
public static IMyResponse Response => response;
}
project를 실행하여 /endpoint/function으로 URL을 요청해 아래와 같은 응답이 표시되는지를 확인합니다.
3. 의존성 주입 사용하기
의존성 주입은 singletone이나 type proker pattern에서 발생하는 강력한 결합의 service접근을 대체하기 위한 다른 접근법을 제공해 줄 수 있으며 다른 ASP.NET Core 기능과도 통합될 수 있습니다. 아래 Program.cs의 수정된 code는 위 예제의 TypeBroker를 ASP.NET Core의 의존성 주입 기능으로 구현한 것을 보여주고 있습니다.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IMyResponse, HTMLResponse>();
var app = builder.Build();
app.UseMiddleware<WeatherMiddleware>();
app.MapGet("middleware/function", async (HttpContext context, IMyResponse myResponse) =>
{
await myResponse.Format(context, "Middleware Function: It is snowing.");
});
app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapGet("endpoint/function", async (HttpContext context, IMyResponse myResponse) =>
{
await myResponse.Format(context, "Endpoint Function: It is sunny.");
});
app.Run();
Service는 IServiceCollection interface에서 정의된 확장 method를 통해 등록되며 해당 구현체는 WebApplicationBuilder.Services 속성을 통해 가져올 수 있습니다. 예제에서는 IMyResponse interface의 service를 생성하기 위해 확장 method를 사용하였습니다.
또한 예제에서 사용된 AddSingleton method는 Service에서 가능한 확장 method중 하나이며 ASP.NET Core에게 단일 객체가 Service의 모든 요청을 충족시키기 위해 사용될 수 있음을 알려주고 있습니다. interface와 구현 class는 generic type 매개변수로서 특정됩니다. 따라서 Service를 사용하기 위해 요청을 처리하는 Method에서 아래와 같이 매개변수를 추가해 줍니다.
app.MapGet("middleware/function", async (HttpContext context, IMyResponse myResponse) =>
Middleware를 등록하거나 endpoint를 생성하는 대부분의 Method는 응답을 생성하는데 필요한 Service를 정의하기 위해 매개변수를 사용할 수 있는 함수를 적용할 수 있을 것입니다. 이전 예제와는 다르게 위와 같이 특정 Type을 명시하는 매개변수를 지정한 것은 C# compiler가 매개변수의 type을 결정할 수 없기 때문입니다.
새롭게 정의한 매개변수는 IMyResponse Interface에 대한 의존성을 선언하였으며 Method역시 interface에 따르게 됩니다. 따라서 Method는 요청을 처리하기 위해 호출되기 전에 매개변수를 조사하여 의존성을 감지하고 Application의 Service 역시 의존성 해결이 가능한지의 여부를 확인하게 됩니다.
AddSingleton method는 dependency injection system이 IMyResponse interface의 의존성은 HTMLResponse 개체를 통해 Resolve될 수 있음을 말해주고 있습니다. 개체는 처리할 Method를 호출함으로써 생성되고 사용될 수 있는데 이러한 과정을 통해 의존성을 해결하는 개체는 이것을 사용하는 Method 외부터로부터 제공되므로 흔히 이것을 '주입되었다.'라고 하는데 그래서 이러한 처리과정을 의존성 주입이라고 합니다.
(1) Middleware class에서 Service사용
Service를 정의하고 해당 Service를 사용하는 위와 같은 방식은 그리 인상적이지 않을 수 있지만 일단 Service라는 것이 한번 정의되고 나면 Application의 거의 모든 곳에서 거의 비슷한 방법으로 해당 Serivce를 사용할 수 있게 됩니다.
using MyWebApp.Services;
namespace MyWebApp.Middleware
{
public class WeatherMiddleware
{
private RequestDelegate next;
private IMyResponse _response;
public WeatherMiddleware(RequestDelegate nextDelegate, IMyResponse response)
{
next = nextDelegate;
_response = response;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Path == "/middleware/class")
await _response.Format(context, "Middleware Class: It is raining.");
else
await next(context);
}
}
}
이 예제는 이전에 만들어둔 WeatherMiddleware class에서 IMyResponse의 Service를 사용할 수 있게끔 수정한 것입니다. 의존성을 선언하기 위해 생성자에서 매개변수를 추가한 것으로 Project를 실행하여 /midleware/class로 URL을 요청해 아래와 같은 결과를 볼 수 있는지 확인합니다.
요청 Pipeline이 설정될때 ASP.NET Core platform는 WeatherMiddleware를 Component로 추가하는 Program.cs의 아래 구문까지 도달하게 되고
app.UseMiddleware<WeatherMiddleware>();
곧이어 Middleware의 Instance를 생성하기 위해 class의 생성자를 확인하게 됩니다. 이런 과정에서 만약 의존성이 해결 가능하고 공유된 Service의 개체가 생성자를 호출하는 데 사용된다면 곧 IMyResponse의 의존성을 발견하게 됩니다.
위 예제를 통해 우리는 2가지 중요한 핵심을 이해할 수 있는데 첫번째는 우선 WeatherMiddleware는 IMyResponse interface의 의존성을 해결하기 위해 어떤 구현 class가 사용될 수 있는지를 알지 못한다는 것입니다. 단지 생성자 매개변수를 통해 interface와 일치하는 어떠한 개체를 전달받을 것이라는 것만 기대하고 있을 뿐입니다. 두 번째는 WeatherMiddleware가 어떤 방법으로 의존성이 해결될 수 있는지에 대한 것도 알지 못한다는 것입니다. 그저 생성자 매개변수를 정의할 뿐이고 상세적인 부분을 파악하기 위한 것은 전적으로 ASP.NET Core에 의존하여 처리하게 됩니다.
이러한 접근법은 위에서 시도한 singleton이나 type broker pattern보다 더 세련된 방법으로 Program.cs에서 사용된 generic type 매개변수를 변경함으로서 Service해결에 사용되는 구현 class를 바꿀 수 있습니다.
(2) Endpoint에서 Service사용하기
WeatherEndpoint class에서의 상황은 쉽지 않습니다. 일단 정적(static)이며 의존성이 선언될 수 있는 생성자를 가지고 있지 않습니다. 이 상태에서 의존성을 해결하기 위해 사용 가능한 몇 가지 접근법을 알아보고자 합니다.
● HttpContext 개체로부터 Service 가져오기
Service는 HttpContext 객체를 통해 접근될 수 있으며 HttpContext 객체는 요청이 endpoint에 route 될 때 전달됩니다.
public class WeatherEndpoint
{
public static async Task Endpoint(HttpContext context)
{
IMyResponse response = context.RequestServices.GetRequiredService<IMyResponse>();
await response.Format(context, "Endpoint Class: It is cloudy.");
//await context.Response.WriteAsync("Endpoint Class: It is cloudy.");
}
}
HttpContext.RequestServices속성은 IServiceProvider interface의 구현체를 반환하는데 이 구현체에서 Program.cs에서 설정된 Service로의 접근을 제공하게 됩니다. Microsoft.Extensions.DependencyInjection namespace는 IServiceProvider Interface의 확장 method를 포함하고 있으며 각각의 개별적인 Service를 가져올 수 있도록 합니다.
GetService<T>() | 이 Method는 Generic Type 매개변수에 의해 특정된 Type의 Service를 반환합니다. 만약 정의된 Service가 없다면 null을 반환합니다. |
GetService(type) | 이 Method는 특정된 type의 Serivce를 반환합니다. 만약 정의된 Service가 없다면 null을 반환합니다. |
GetRequiredService<T>() | 이 Method는 Generic Type 매개변수에 의해 특정된 Type의 Service를 반환합니다. 그러나 만약 Service의 사용이 가능하지 않다면 예외를 발생시키게 됩니다. |
GetRequiredService(type) | 이 Method는 특정된 type의 Service를 반환합니다. 그러나 만약 Service의 사용이 가능하지 않다면 예외를 발생시키게 됩니다. |
Endpoint method가 호출될 때 GetRequiredService<T>() Mehtod는 IMyResponse의 개체를 가져오는 데 사용되며 곧 Format Method를 사용해 응답을 수행하게 됩니다. 아래와 같이 IMyResponse interface의 구현 class인 HTMLResponse class를 아래와 같이 수정하고
public class HTMLResponse : IMyResponse
{
public async Task Format(HttpContext context, string content)
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync($@"<h2>HTML Response</h2><br />{content}");
}
}
project를 실행하여 /endpoint/clsss로 URL을 요청해 다음과 같은 결과가 나오는지를 확인합니다.
● Adapter Function 사용하기
HttpContext.RequestServices method의 단점은 endpoint로 route 되는 모든 요청에서 service가 resolve 되어야 한다는 것입니다. 단일 요청이나 응답에 관련된 기능을 제공하기 때문에 이런 것들이 필요한 몇몇 service들이 존재하지만 이것은 단일 개체가 여러 응답 형식에 사용될 수 있는 IMyResponse service를 위한 것은 아닙니다.
일반적으로 사용되는 접근방식은 모든 요청에서가 아닌 endpoint의 route가 생성될 때 service를 가져오는 것입니다. 따라서 다음과 같이 IMyResponse에 대한 의존성을 선언하는 방법으로 Endpoint method를 변경할 수 있습니다.
using MyWebApp.Services;
namespace MyWebApp.Middleware
{
public class WeatherEndpoint
{
public static async Task Endpoint(HttpContext context, IMyResponse response)
{
await response.Format(context, "Endpoint Class: It is cloudy.");
}
}
}
Project의 Services folder에 아래와 같이 EndPointExtensions.cs file을 추가합니다.
using MyWebApp.Middleware;
namespace MyWebApp.Services
{
public static class EndpointExtensions
{
public static void MapWeather(this IEndpointRouteBuilder app, string path)
{
IMyResponse formatter = app.ServiceProvider.GetRequiredService<IMyResponse>();
app.MapGet(path, context => WeatherEndpoint.Endpoint(context, formatter));
}
}
}
추가된 file은 IEndpointRouterBuilder Interface의 확장 Method를 생성하고 있는데 이것은 Program.cs에서 route를 생성하는 데 사용할 수 있습니다. Interface에는 가져올 수 있는 Serivce를 통해 IServiceProvider객체를 반환하는 ServiceProvider속성을 정의하고 있습니다. 확장 Method는 Service를 가져오고 MapGet Method를 사용하여 WeatherEndpoint로 HttpContext개체와 IMyResponse개체를 전달하는 questDelegate를 등록합니다. 위와 같이 작성한 확장 method는 아래와 같이 Program.cs에서 다음과 같이 사용할 수 있습니다.
//app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
app.MapWeather("endpoint/class");
MapWeather확장 Method는 route를 설정하고 Endpoint class주위에 adapter를 생성합니다. project를 실행하여 /endpoint/clsss로 URL을 요청하여 이전과 같은 결과가 나오는지를 확인합니다.
● Activation Utility Class
Service를 필요로 하는 Endpoint의 경우 serivce를 처리하기 위한 더 일반적인 접근방식을 가능하게 함으로써 instance화 될 수 있는 class를 사용하는 것이 더 쉬울 수 있습니다. 아래 예제는 WeatherEndpoint에 생성자를 추가하고 Endpoint method에서 static keyword를 제거하여 변경한 것입니다.
using MyWebApp.Services;
namespace MyWebApp.Middleware
{
public class WeatherEndpoint
{
private IMyResponse _response;
public WeatherEndpoint(IMyResponse response)
{
_response = response;
}
public async Task Endpoint(HttpContext context, IMyResponse response)
{
await response.Format(context, "Endpoint Class: It is cloudy.");
}
}
}
ASP.NET Core Application에서 의존성 주입을 사용하는 가장 일반적인 방법은 생성자를 사용하는 것입니다. middleware class에서 적용된 Method를 통한 주입방식은 다시 구현하기에는 복잡하지만 생성자를 확인하고 Service를 사용해 의존성을 해결하는 몇몇 내장된 유용한 도구들도 존재합니다.
using System.Reflection;
namespace MyWebApp.Services
{
public static class EndpointExtensions
{
public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
{
MethodInfo? methodInfo = typeof(T).GetMethod(methodName);
if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
throw new System.Exception("Method cannot be used");
T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
app.MapGet(path, (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), endpointInstance));
}
}
}
예제에서의 확장 method는 사용될 endpoint class를 특정하는 generic type 매개변수와 함께 route를 생성하는 데 사용되는 path와 요청을 처리할 endpoint class의 이름인 methodName을 매개변수로 사용하고 있습니다.
endpoint class의 새로운 instance가 생성되고 지정된 method의 delegate가 route를 생성하는데 사용됩니다. 다른 .NET reflection을 사용하는 어떤 code처럼 확장 Method 역시 읽기 어려울 수 있지만 특히 아래와 같은 code는 예제에서 가장 핵심적인 구문이라 할 수 있습니다.
T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
Microsoft.Extensions.DependencyInjection namespace에 정의된 ActivatorUtilities class는 생성자를 통해 의존성이 선언된 class의 instance화를 위한 Method를 제공하고 있는데 특히 아래 method는 ActivatorUtilities에서 가장 많이 사용되는 Method를 나열한 것입니다.
CreateInstance(services, args) | 이 Method는 Type매개변수에서 지정된 Class의 새로운 Instance를 생성하고 Service와 선택적으로 추가된 인수를 사용해 의존성을 해결합니다. |
CreateInstance(services, type, args) | 이 Method는 매개변수에서 지정된 Class의 새로운 Instance를 생성하고 Service와 선택적으로 추가된 인수를 사용해 의존성을 해결합니다. |
GetServiceOrCreateInstance (services, args) | 이 Method는 가능한 경우 지정된 Type의 Service를 반환하지만 그렇지 않은 경우 새로운 instance를 생성합니다. |
GetServiceOrCreateInstance(ser vices, type, args) | 이 Method는 가능한 경우 지정된 Type의 Service를 반환하지만 그렇지 않은 경우 새로운 instance를 생성합니다. |
두 Method모두 IServiceProvider객체를 통해 Service를 사용하여 생성자 의존성과 Service가 아닌 의존성에 사용되는 선택적 매개변수의 배열을 Resolve 합니다. 이들 Method는 사용자 class에 의존성 주입을 쉽게 적용할 수 있도록 만들며 CreateInstance Method를 사용한 결과 Service를 사용하는 Endpoint class를 통해 route를 생성할 수 있는 확장 Method가 생성됩니다. 아래 예제는 route를 생성하기 위해 새로운 확장 Method를 사용한 것입니다.
//app.MapGet("endpoint/class", WeatherEndpoint.Endpoint);
//app.MapWeather("endpoint/class");
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
이러한 확장 Method를 사용하는 방식은 endpoint class와의 작업을 쉽게 만들며 UseMiddleware Method에도 비슷한 경험을 제공합니다. Project를 실행하여 /endpoint/class로 URL을 요청하여 이전과 같은 결과가 표시되는지를 확인합니다.
4. Service 생명주기
위 예제에서 Service를 생성할 때는 AddSingleton이라는 확장 메서드를 사용하였습니다.
builder.Services.AddSingleton<IMyResponse, HTMLResponse>();
AddSingleton Method는 의존성 해결을 위해 처음 사용될 때만 instance화 된 Service를 생성하며 그다음 의존성부터는 해당 Service를 재사용하게 됩니다. 다시 말해 IMyResponse개체의 모든 의존성에서 같은 IMyResponse의 개체가 사용된다는 것입니다.
Singleton은 Service를 시작하는데 좋은 방법이기는 하지만 이것이 적합하지 않은 몇 가지 문제가 있기에 ASP.NET Core는 의존성 문제를 해결하기 위해 생성된 개체에 다른 생명주기를 제공하는 scoped와 transient Service를 지원하고 있습니다. 아래 표는 Service를 생성하는 데 사용되는 Method를 나열한 것이며 이러한 Method들에는 일반적인 인수로서 type을 받아들이는 Version이 존재합니다.
AddSingleton<T, U>() | 이 Method는 type U에 대한 단일 개체를 생성하고 type T에 대한 모든 의존성을 Resolve하기 위해 사용합니다. |
AddTransient<T, U>() | 이 Method는 모든 type T에 대한 각각의 의존성마다 새로운 type U의 개체를 생성합니다. |
AddScoped<T, U>() | 이 Method는 요청과 같은 단일 Scope에 한하여 type T에 대한 의존성 해결자에 type U를 사용하는 새로운 개체를 생성합니다. |
위 표에 있는 이러한 Method에는 단일 type인수를 갖는 Version이 있는데 이것은 Service위치에 대한 단점을 강력한 결합을 해결하지 않고도 Service가 생성될 수 있도록 허용합니다. 다시 말해 interface 없이도 Service가 생성될 수 있는 것입니다.
(1) Transient Services 생성
AddTransient Method는 AddSingleton Method와는 정반대의 Method이며 Resolve 되는 모든 의존성마다 새로운 구현 class의 Instance를 생성합니다. Service의 생명주기를 알아볼 Service를 생성하기 위해 GuidService.cs file을 Services folder에 아래와 같은 내용으로 생성합니다.
namespace MyWebApp.Services
{
public class GuidService : IMyResponse
{
private Guid guid = Guid.NewGuid();
public async Task Format(HttpContext context, string content)
{
await context.Response.WriteAsync($"Guid: {guid}\n{content}");
}
}
}
예제에서 Guid는 고유한 식별자 값을 생성하여 IMyResponse의 의존성 해결자에 다른 Instance가 사용되는 경우를 분명하게 구분할 수 있도록 해줄 것입니다. 아래 예제에서는 AddTransient Method와 GuidService 구현 class를 사용하여 Service가 생성되는 구문을 변경하였습니다.
var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddSingleton<IMyResponse, HTMLResponse>();
builder.Services.AddTransient<IMyResponse, GuidService>();
var app = builder.Build();
Project를 실행하여 /endpoint/class와 /middleware/class로 번갈아 URL을 요청하여 아래와 같이 각각의 응답이 생성되는지를 확인합니다. 이 동작은 각각 다른 GUID값을 생성함으로써 다른 IMyResponse Service가 endpoint와 middleware component의 의존성 해결자에 사용됨을 알 수 있게 합니다.
(2) Transient Service의 재사용 위험성 고려하기
위에서는 GUID값의 생성을 통해서 다른 Service의 객체가 생성되는 경우를 살펴봤는데 사실 예상했던 것만큼의 효과를 보기는 어렵습니다. 실제 Project를 실행시켜 GUID가 표시된 상태에서 다른 URL로의 요청이 아닌 Web Browser의 새로고침을 눌러보면 GUID값이 변하지 않음을 확인함으로써 그 사실을 명확히 알 수 있습니다.
새로운 Service 객체가 생성되는 경우는 해당 Service가 사용될 때가 아닌 오로지 의존성의 Resolve가 필요한 상황에서만 가능합니다. 즉, 예제에서의 Component와 Endpoint는 Application이 시작되고 Program.cs file의 구문이 실행되는 경우에만 의존성이 Resolve 되는 것입니다. 따라서 일반적인 경우 각각 별도의 서비스 개체를 수신한 후 처리되는 모든 요청에 재사용될 수 있습니다.
Middleware component에서 이러한 문제를 해결하려면 우선 Service의 의존성이 Invoke Method로 이동되어야 합니다.
using MyWebApp.Services;
namespace MyWebApp.Middleware
{
public class WeatherMiddleware
{
private RequestDelegate next;
//private IMyResponse _response;
public WeatherMiddleware(RequestDelegate nextDelegate)//, IMyResponse response)
{
next = nextDelegate;
//_response = response;
}
public async Task Invoke(HttpContext context, IMyResponse _response)
{
if (context.Request.Path == "/middleware/class")
await _response.Format(context, "Middleware Class: It is raining.");
else
await next(context);
}
}
}
이러한 방법은 ASP.NET Core platform이 발생하는 모든 요청에서 Invoke Method에 선언된 의존성을 새로운 Instance Service 개체를 통해 Resolve 하게 될 것임을 보장할 수 있게 됩니다.
ActivatorUtilities class는 Method에서 의존성 해결을 다루지 않으며 ASP.NET Core는 이 기능을 Middleware Component에 대해서만 포함하게 됩니다. endpoint에서 같은 문제를 해결하기 위한 간단한 방법으로는 각 요청이 처리될 때 Service를 명시적으로 요청하는 것입니다. 이것은 또한 endpoint를 대신하여 확장 Method가 Service를 요청하도록 개선할 수도 있습니다.
public static class EndpointExtensions
{
public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
{
MethodInfo? methodInfo = typeof(T).GetMethod(methodName);
if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
{
throw new System.Exception("Method cannot be used");
}
T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
ParameterInfo[] methodParams = methodInfo!.GetParameters();
app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => p.ParameterType == typeof(HttpContext) ? context : app.ServiceProvider.GetService(p.ParameterType)).ToArray()))!);
//app.MapGet(path, (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), endpointInstance));
}
}
그러나 예제는 Middleware component를 위해 ASP.NET Core Platform이 취한 접근법보다는 효율적이지 않습니다. 요청을 처리하는 Method에 의해 정의된 모든 매개변수는 HttpContext를 제외하고 Resolve 될 수 있는 Service로서 다뤄집니다. route는 모든 요청에 대한 Service를 Resolve 하는 Delegate를 통해 생성되며 요청을 처리하는 Method를 호출합니다.
아래 예제는 WeatherEndpoint class를 개선한 것으로 IMyResponse에 대한 의존성을 Endpoint Method로 이동시킴으로써 새로운 Service개체는 모든 요청에서 전달받게 될 것입니다.
public class WeatherEndpoint
{
private IMyResponse _response;
public WeatherEndpoint(IMyResponse response)
{
_response = response;
}
public async Task Endpoint(HttpContext context, IMyResponse response)
{
await response.Format(context, "Endpoint Class: It is cloudy.");
}
}
예제의 이러한 변경점은 transient service가 모든 요청에서 Resolve 되도록 함으로써 새로운 GuidService Service가 생성되고 모든 응답은 고유한 ID를 포함할 수 있도록 합니다.
Project를 실행하여 /Middleware/class로 URL을 요청한 뒤 Web Browser의 '새로고침'을 눌러 GUID값이 변경되는지 확인합니다.
(3) Scoped Service
Scoped Service는 singleton과 transiend service의 중간 정도 특징을 갖는 Service입니다. 같은 범위(scope) 안에서 의존성은 같은 개체를 통해 Resolve 되지만 각각의 HTTP 요청에서 새롭게 시작된 범위(scope)라면 Serivce개체는 요청을 처리하는 모든 개체를 통해 공유됩니다. Scoped Service를 직접 경험해 보기 위해 WeatherMiddleware class를 수정하여 같은 Service에서 3개의 의존성을 선언하였습니다.
public class WeatherMiddleware
{
private RequestDelegate next;
public WeatherMiddleware(RequestDelegate nextDelegate)
{
next = nextDelegate;
}
public async Task Invoke(HttpContext context, IMyResponse _response1, IMyResponse _response2, IMyResponse _response3)
{
if (context.Request.Path == "/middleware/class")
{
await _response1.Format(context, "Middleware Class: It is raining.");
await _response2.Format(context, "Middleware Class: It is raining.");
await _response3.Format(context, "Middleware Class: It is raining.");
}
else
await next(context);
}
}
위와 같은 예제는 사실 현실적으로는 사용되지 않을 것이지만 3개의 의존성이 각각 독립적으로 Resolve 되는 상황을 보기 위한 방법으로 충분할 것입니다. IMyResponse Service는 AddTransiend Service를 통해 생성되었으므로 각각의 의존성은 다른 개체를 통해 Resolve 됩니다.
Project를 다시 시작하고 이전과 동일한 URL을 요청하여 3개의 GUID값이 다르게 표시되는지를 확인합니다.
아래 예제는 IMyResponse Serivce를 변경하여 AddScoped Method를 통해 scoped lifecycle을 사용하도록 한 것입니다.
scope는 IServiceProvider Interface에 대한 CreateScope확장 Method를 통해 생성할 수도 있습니다. 그러면 IServiceProvider는 새로운 Scope와 연결되고 Scope Service를 위해 자체적인 구현 개체를 가지게 됩니다.
var builder = WebApplication.CreateBuilder(args);
//builder.Services.AddSingleton<IMyResponse, HTMLResponse>();
builder.Services.AddScoped<IMyResponse, GuidService>();
var app = builder.Build();
Project를 실행하고 이전과 동일한 URL로 요청을 시도하면 Middleware Componen에서 정의된 3개의 의존성 모두에서 Resolve를 위해 동일한 GUID값이 사용되고 있음을 볼 수 있습니다.
● Scoped Service의 유효성에 대한 예외상황
Service 사용자는 singleton과 transient service에서 선택한 생명주기를 알 수 없습니다. 그저 의존성을 선언하거나 Service를 요청하고 필요한 객체를 가져올 뿐입니다.
Scoped Service는 해당 scope안에서만 사용될 수 있는데 새로운 scope는 수신된 각 요청에 대해서만 자동적으로 생성됩니다. 이러한 특징 때문에 scope외부에서 scoped service를 요청하면 다음과 같이 예외를 일으키게 됩니다.
endpoint를 구성하는 확장 Method는 routing middleware로부터 가져온 IServiceProvider를 통해 다음과 같이 Service를 Resolve 합니다.
app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => p.ParameterType == typeof(HttpContext) ? context : app.ServiceProvider.GetService(p.ParameterType)).ToArray()))!);
● Context Object를 통한 Scoped Service 접근하기
HttpContext는 IServiceProvider개체를 반환하는 RequestServices속성을 정의하고 있어서 singleton과 transient service는 물론 이를 통해 scope service로도 접근할 수 있습니다. 또한 이것은 각 HTTP 요청에 대해 single service개체를 사용하는 scope service의 가장 일반적인 사용 방식과 잘 맞기도 합니다. 아래 예제는 기존 endpoint 확장 method에서 HttpContext에 의해 제공되는 service를 사용하여 의존성이 resolve 되도록 개선한 것입니다.
public static class EndpointExtensions
{
public static void MapEndpoint<T>(this IEndpointRouteBuilder app, string path, string methodName = "Endpoint")
{
MethodInfo? methodInfo = typeof(T).GetMethod(methodName);
if (methodInfo == null || methodInfo.ReturnType != typeof(Task))
{
throw new System.Exception("Method cannot be used");
}
T endpointInstance = ActivatorUtilities.CreateInstance<T>(app.ServiceProvider);
ParameterInfo[] methodParams = methodInfo!.GetParameters();
app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => p.ParameterType == typeof(HttpContext) ? context : context.RequestServices.GetService(p.ParameterType)).ToArray()))!);
}
}
예제에서와 같이 단지 요청을 처리하는 method에서 선언된 의존성만이 HttpContext.RequestServices 속성을 사용해 resolve 됩니다.생성자에서 선언된 Service는 여전히 IEndpointRouteBuilder.ServiceProvider속성을 사용해 resolve 되므로 endpoint가 scope service를 부적절하게 사용하는 경우는 발생하지 않도록 합니다.
※ 각 요청에 대한 새로운 처리기 생성
확장 method의 문제점은 의존하고 있는 Service의 생명주기를 알기 위해 endpoint class가 필요하다는 것입니다. 따라서 WeatherEndpoint class는 IMyResponse service에 의존하고 있으며 의존성은 생성자가 아닌 오로지 Endpoint method를 통해서만 선언될 수 있음을 알고 있어야 합니다.
하지만 이러한 번거로움을 제거하기 위해 각 요청을 처리하기 위한 새로운 endpoint class의 instance를 다음과 같은 같은 방법으로도 생성할 수도 있습니다. 아래 예제는 service가 scoped 되는 것에 대해 신경 쓸 필요 없이 생성자와 method에서 의존성을 resolve 할 수 있는 방법을 보여주고 있습니다.
//app.MapGet(path, context => (Task)(methodInfo.Invoke(endpointInstance, methodParams.Select(p => p.ParameterType == typeof(HttpContext) ? context : context.RequestServices.GetService(p.ParameterType)).ToArray()))!);
app.MapGet(path, context =>
{
T endpointInstance = ActivatorUtilities.CreateInstance<T>(context.RequestServices);
return (Task)methodInfo.Invoke(endpointInstance!, methodParams.Select(p => p.ParameterType == typeof(HttpContext) ? context : context.RequestServices.GetService(p.ParameterType)).ToArray())!;
});
이러한 방식은 각 요청을 처리하기 위해 endpoint class의 새로운 instance생성을 필요로 하지만 Service의 생명주기와 관련된 다른 것에 대해 신경 쓸 필요가 없습니다.
※ Lambda 식에서의 scope service사용
Middleware component와 endpoint에서 사용되는 HttpContext class는 또한 lambda 식을 통해서도 정의될 수 있습니다.
app.MapEndpoint<WeatherEndpoint>("endpoint/class");
app.MapGet("endpoint/function", async (HttpContext context) => {
IMyResponse formatter = context.RequestServices.GetRequiredService<IMyResponse>();
await formatter.Format(context, "Endpoint Function: It is sunny.");
});
app.Run();
Project를 시작하여 /endpoint/function으로 URL을 요청합니다. Scope service를context로부터 가져와 아래와 같은 응답을 생성할 것입니다.
만약 application의 program.cs를 구성하는 과정에서 scope service로의 접근이 필요하다면 아래와 같이 새로운 scope를 생성하고 service를 요청할 수 있습니다.
app.Services.CreateScope().ServiceProvider.GetRequiredService();
CreateScope method는 scope service로의 접근을 허용하는 scope를 생성합니다. 만약 scope를 생성하지 않고 scope service에 대한 접근을 시도한다면 예외가 발생할 것입니다.
5. 의존성 주입의 또 다른 방법
의존성 주입을 사용할 때 가능한 다른 방법은 모든 project에서 꼭 필요한 것은 아니지만 의존성 주입 기능이 어떻게 작동하는지에 관해 이해해볼 수 있으며 project에서 의존성 주입에 관한 온전한 기능을 꼭 필요로 하지 않는 경우 도움이 될 수 있기 때문에 한 번쯤 훑어볼 필요가 있을 것입니다.
(1) 의존성 Chain
Class가 Service의 의존성을 resolve 할 때 생성자를 확인하고 관련된 Service의 모든 의존성을 resolve 하게 됩니다. 이것은 하나의 Service가 다른 Serivce에 대한 의존성을 선언할 수 있게 함으로써 자동적으로 resolve 될 수 있는 chain을 생성하도록 하는 것입니다. 실험을 위해 project에 TimeStamp.cs라는 file을 Services folder에 아래 내용으로 추가합니다.
namespace MyWebApp.Services
{
public interface ITimeStamp
{
string NowTimeStamp { get; }
}
public class DefaultTimeStamp : ITimeStamp
{
public string NowTimeStamp
{
get
{
return DateTime.Now.ToShortTimeString();
}
}
}
}
해당 class는 ITimeStamp라는 interface와 DefaultTimeStamp라는 구현 class를 정의하고 있습니다. 다음으로 TimeResponse.cs라는 이름의 file 역시 아래 내용으로 Service에 folder에 추가합니다.
namespace MyWebApp.Services
{
public class TimeResponse : IMyResponse
{
private ITimeStamp stamper;
public TimeResponse(ITimeStamp timeStamp)
{
stamper = timeStamp;
}
public async Task Format(HttpContext context, string content)
{
await context.Response.WriteAsync($"{stamper.NowTimeStamp}: {content}");
}
}
}
TimeResponse class는 IMyResponse inserface를 구현하고 있는데 생성자 매개변수를 통해서는 ITimeStamp interface에 대한 의존성을 선언하고 있습니다. 그리고 Program.cs에서는 이 둘의 interface에 대한 Service를 정의하도록 합니다.
builder.Services.AddScoped<IMyResponse, TimeResponse>();
builder.Services.AddScoped<ITimeStamp, DefaultTimeStamp>();
var app = builder.Build();
위와 같이 Service를 정의하는 경우에는 굳이 같은 생명주기를 통해 정의할 필요는 없지만 다소 혼란스러운 상황에 처해질 수 있습니다. Service의 생명주기는 의존성이 resolve 되는 경우에만 적용되므로 예를 들어 scope service가 transient service에 의존하는 경우라면 transient객체는 마치 scoped 생명주기가 할당된 것처럼 동작할 것입니다.
예제에 따라 IMyResponse의존성이 resolve 될 때 TimeResponse 생성자를 확인함으로써 ITimeStamp의존성을 감지하게 됩니다. 그리고 곧 DefaultTimeStamp객체를 생성하여 TimeResponse생성자를 통해 주입함으로써 본래 의존성이 resolve 될 수 있도록 합니다. Project를 실행하여 /middleware/function으로 URL을 요청하고 MyResponse class에 의해 생성된 응답을 포함하고 있는 DefaultTimeStamp class의 timestamp값이 표시되는지를 확인합니다.
(2) Program.cs에서 Service로의 접근
Program.cs file에서 생성된 Service의 경우 설정을 변경하기 위해 필요한 일반적인 요구사항은 Application의 구성 설정을 사용하는 것입니다. 다만 이것은 문제를 일으킬 수 있는데 왜냐하면 구성 설정은 Service로서 제공되며 Service는 WebApplicationBuilder.Build method가 호출된 이후까지 접근할 수 있기 때문입니다.
이러한 문제를 해결하기 위해 WebApplication과 WebApplicationBuilder class는 Application 설정으로의 접근을 제공하는 기본 sevice를 가진 아래 표의 속성을 정의하고 있습니다.
Configuration | 이 속성은 IConfiguration interface의 구현체를 반환하여 Application에 대한 구성설정으로의 접근을 제공합니다. |
Environment | 이 속성은 IWebHostEnvironment interface의 구현체를 반환하여 Application이 실행된 환경정보를 제공함으로서 Application이 개발 혹은 배포용으로 구성되었는지를 확인하기 위해 주로 사용됩니다. |
이들 service는 아래와 같이 Program.cs에서 구성된 Service를 사용자customize 하는 데 사용될 수 있습니다.
IWebHostEnvironment env = builder.Environment;
if (env.IsDevelopment())
{
builder.Services.AddScoped<IMyResponse, TimeResponse>();
builder.Services.AddScoped<ITimeStamp, DefaultTimeStamp>();
}
else
{
builder.Services.AddScoped<IMyResponse, HTMLResponse>();
}
var app = builder.Build();
위 예제에서는 Environment속성을 사용해 IWebHostEnvironment interface의 구현체를 가져와 IsDevelopment method를 통해 Application에서 어떤 Service를 설정할지를 판단하고 있습니다.
(3) 서비스 factory 함수 사용
Factory 함수는 service구현 객체의 instance를 생성하는데 ASP.NET Core에 의존하기보다는 객체가 생성되는 방식 자체를 제어할 수 있도록 합니다. AddSingleton, AddTransient, AddScoped method 등등 다양한 factory version이 있으며 이들은 IServiceProvider객체를 전달받는 함수를 통해 사용되며 service의 구현 객체를 반환합니다.
Factory 함수의 용도중 하나는 IConfiguration Service를 통해 읽는 구성 설정으로서 Service에 대한 구성 class를 정의하는 것이며 이때 WebApplicationBuilder 속성을 필요로 합니다. 아래 예제는 Program.cs를 수정하여 설정 data로부터 구현 class를 얻는 IMyResponse service의 factory 함수를 추가하였습니다..
//IWebHostEnvironment env = builder.Environment;
IConfiguration config = builder.Configuration;
builder.Services.AddScoped<IMyResponse>(serviceProvider => {
string? typeName = config["services:IMyResponse"];
return (IMyResponse)ActivatorUtilities.CreateInstance(serviceProvider, typeName == null ? typeof(GuidService) : Type.GetType(typeName, true)!);
});
builder.Services.AddScoped<ITimeStamp, DefaultTimeStamp>();
var app = builder.Build();
factory 함수는 설정 data로부터 값을 읽어 이것을 type으로 변환한 뒤 ActivatorUtilities.CreateInstance method로 전달합니다. 위와 같은 수정사항에 따라 appsettings.Development.json file에 HTMLResponse class를 IMyResponse service의 구현체로 지정하는 설정을 다음과 같이 추가합니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"services": {
"IMyResponse": "MyWebApp.Services.HTMLResponse"
}
}
IMyResponse Service의 의존성이 Resolve 될 때 Factory 함수는 설정 file에 지정된 type의 instance를 생성하게 됩니다. Project를 실행하여 /middleware/function으로 URL을 요청하여 아래와 같은 응답이 반환되는지 확인합니다.
(4) 다중 구현체를 통한 Service생성
Service는 여러 개의 구현체를 통해서 정의될 수 있으며 이를 통해 사용자는 여러개의 구현체 중 특정한 상황에 대해 가장 적합하다고 판단되는 구현체를 선택할 수 있습니다. 이것은 각 구현 class의 기능을 잘 이해하고 있을 때 가장 효휼적으로 작동할 수 있는 기능이기도 합니다. IMyResponse class의 기능에 대한 정보를 제공하기 위해 아래와 같이 interface에 기본 속성을 추가합니다.
namespace MyWebApp.Services
{
public interface IMyResponse
{
Task Format(HttpContext context, string content);
public bool AdvencedOutput => false;
}
}
추가한 AdvencedOutput속성의 값은 구현 class에서 기본값을 override하지 않는다면 false값을 유지할 것입니다. 이 상태에서 아래와 같이 HTMLResponse class에 속성을 추가하여 AdvencedOutput값을 true로 지정합니다.
public class HTMLResponse : IMyResponse
{
public async Task Format(HttpContext context, string content)
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync($@"<h2>HTML Response</h2><br />{content}");
}
public bool AdvencedOutput => true;
}
그리고 Program.cs에서 아래와 같이 IMyResponse Service에 대한 여러 구현 class(예제에서 2개)를 등록합니다. 또한 기존에 존재하던 요청 pipeline을 Service가 어떻게 사용될지를 보여줄 2개의 route로 변경합니다.
var builder = WebApplication.CreateBuilder(args);
IConfiguration config = builder.Configuration;
builder.Services.AddScoped<IMyResponse, HTMLResponse>();
builder.Services.AddScoped<IMyResponse, GuidService>();
var app = builder.Build();
app.MapGet("single", async context => {
IMyResponse formatter = context.RequestServices.GetRequiredService<IMyResponse>();
await formatter.Format(context, "Single service");
});
app.MapGet("/", async context => {
IMyResponse formatter = context.RequestServices.GetServices<IMyResponse>().First(f => f.AdvencedOutput);
await formatter.Format(context, "Multiple services");
});
app.Run();
AddScoped Method는 IMyResponse interface에 대한 2개의 Service를 각각 다른 구현 class를 통해 등록하고 있으며 /single URL에 대한 route는 IServiceProvider. GetRequiredService<T> Method를 아래와 같이 Service를 요청하는 데 사용하고 있습니다.
IMyResponse formatter = context.RequestServices.GetRequiredService<IMyResponse>();
해당 Service는 사용 가능한 여러 구현체가 존재한다는 것을 인식할 수 없습니다. 따라서 Service는 가장 마지막에 등록된 구현 class인 GuidService class를 사용해 Resolve됩니다. Project를 실행하여 /single URL로 요청 후 아래와 같은 응답이 생성되는지를 확인합니다.
다른 endpoint의 Service는 사용가능한 여러 구현체가 있다는 것을 알고 있다는 가정하에서 수정한 것입니다. 따라서 IServiceProvider.GetServices<T> method를 사용하는 Service를 요청하도록 하였습니다.
IMyResponse formatter = context.RequestServices.GetServices<IMyResponse>().First(f => f.AdvencedOutput);
이 Method는 IEnumerable<IMyResponse>형식을 반환하여 사용 가능한 구현체를 열거하게 되는데 이때 LINQ의 First Method를 통해 AdvencedOutput속성이 true인 구현체를 선택하게 됩니다. 따라서 Project를 시작하면 곧장 아래와 같은 응답을 수신받게 됩니다.
(5) Service에서 Unbound Types사용
generic type 매개변수를 통해 정의될 수 있는 Service는 Service가 요청될 때 특정 type에 bound 될 수 있습니다.
var builder = WebApplication.CreateBuilder(args);
IConfiguration config = builder.Configuration;
builder.Services.AddSingleton(typeof(ICollection<>), typeof(List<>));
var app = builder.Build();
app.MapGet("string", async context => {
ICollection<string> collection = context.RequestServices.GetRequiredService<ICollection<string>>();
collection.Add("a");
collection.Add("b");
collection.Add("c");
foreach (string str in collection)
await context.Response.WriteAsync($"content: {str}\n");
});
app.MapGet("int", async context => {
ICollection<int> collection = context.RequestServices.GetRequiredService<ICollection<int>>();
collection.Add(10);
collection.Add(20);
collection.Add(30);
foreach (int val in collection)
await context.Response.WriteAsync($"Int: {val}\n");
});
app.Run();
이 기능은 AddSIngleton, AddScoped, AddTransient Method에 의존하게 되는데 이들 Method는 보통 기존 인수의 type을 적용할 뿐 generic type 인수를 사용해서는 수행될 수 없습니다. 따라서 Service는 다음과 같이 Unbound type을 통해 생성하도록 합니다.
builder.Services.AddSingleton(typeof(ICollection<>), typeof(List<>));
ICollection<T>의 의존성이 Resolve 될 때는 List<T>객체가 생성됩니다. 따라서 ICollection<string>에 대한 의존성은 곧 List<string>객체를 사용해 Resolve 됩니다. Unbound Service는 각 type에 각각 개별적인 Service를 요구하기보다는 생성될 수 있는 모든 generic type을 Mapping 하도록 합니다.
예제에서 2개의 Endpoint는 각각 ICollection<striing>과 ICollection<int> Service를 필요로 하며 서로 다른 List<T>객체를 통해 Resolve 될 것입니다. Project를 실행하여 /string과 /int로의 URL을 요청하여 아래와 같은 결과가 생성되는지 확인합니다. Service는 singleton을 사용해 정의되었으므로 ICollection<string>과 ICollection<int>에 대한 모든 요청에서 같은 List<string>와 List<int>객체를 사용해 Resolve 하게 되므로 page를 새로고침 하면 중복으로 collection에 새로운 item에 추가되는 것을 확인할 수 있습니다.