ASP.NET 핵심 종속성 주입 모범 사례, 팁 및 요령

이 기사에서는 ASP.NET Core 응용 프로그램에서 Dependency Injection 사용에 대한 경험과 제안을 공유합니다. 이러한 원칙의 동기는 다음과 같습니다.

  • 효과적으로 서비스 및 해당 종속성을 디자인합니다.
  • 멀티 스레딩 문제 방지
  • 메모리 누수 방지.
  • 잠재적 인 버그 예방.

이 문서에서는 기본 수준에서 Dependency Injection 및 ASP.NET Core에 이미 익숙하다고 가정합니다. 그렇지 않은 경우 먼저 ASP.NET Core Dependency Injection 설명서를 읽으십시오.

기초

생성자 주입

생성자 주입은 서비스 구성에 대한 서비스의 종속성을 선언하고 얻는 데 사용됩니다. 예:

공공 클래스 ProductService
{
    개인 읽기 전용 IProductRepository _productRepository;
    공개 ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    공공 무효 삭제 (INT ID)
    {
        _productRepository.Delete (id);
    }
}

ProductService는 IProductRepository를 생성자에 종속성으로 삽입 한 다음 Delete 메소드 내부에서이를 사용합니다.

좋은 습관:

  • 서비스 생성자에서 필수 종속성을 명시 적으로 정의하십시오. 따라서 서비스가 종속성없이 구성 될 수 없습니다.
  • 주입 된 종속성을 읽기 전용 필드 / 프로퍼티에 할당하십시오 (메소드 내에서 실수로 다른 값을 할당하지 않도록).

재산 주입

ASP.NET Core의 표준 종속성 주입 컨테이너는 속성 주입을 지원하지 않습니다. 그러나 속성 주입을 지원하는 다른 컨테이너를 사용할 수 있습니다. 예:

Microsoft.Extensions.Logging 사용;
Microsoft.Extensions.Logging.Abstractions 사용;
네임 스페이스 MyApp
{
    공공 클래스 ProductService
    {
        공개 ILogger  로거 {get; 세트; }
        개인 읽기 전용 IProductRepository _productRepository;
        공개 ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            로거 = NullLogger  .Instance;
        }
        공공 무효 삭제 (INT ID)
        {
            _productRepository.Delete (id);
            로거 로그 정보 (
                $ "id가 ​​{id} 인 제품을 삭제했습니다");
        }
    }
}

ProductService가 공용 setter를 사용하여 Logger 특성을 선언하고 있습니다. 사용 가능한 경우 종속성 주입 컨테이너가 로거를 설정할 수 있습니다 (이전에 DI 컨테이너에 등록됨).

좋은 습관:

  • 선택적 의존성에 대해서만 속성 주입을 사용하십시오. 즉, 이러한 종속성을 제공하지 않아도 서비스가 제대로 작동 할 수 있습니다.
  • 가능한 경우 Null Object Pattern (이 예와 같이)을 사용하십시오. 그렇지 않으면 종속성을 사용하는 동안 항상 null을 확인하십시오.

서비스 로케이터

서비스 로케이터 패턴은 종속성을 얻는 또 다른 방법입니다. 예:

공공 클래스 ProductService
{
    개인 읽기 전용 IProductRepository _productRepository;
    개인 전용 읽기 전용 ILogger  _logger;
    공개 ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger . 인스턴스;
    }
    공공 무효 삭제 (INT ID)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "id = {id}"인 제품을 삭제했습니다);
    }
}

ProductService가 IServiceProvider를 주입하고이를 사용하여 종속성을 해결합니다. 요청 된 종속성이 이전에 등록되지 않은 경우 GetRequiredService에서 예외가 발생합니다. 반면에 GetService는이 경우 null을 반환합니다.

생성자 내부의 서비스를 확인하면 서비스가 릴리스 될 때 서비스가 해제됩니다. 따라서 생성자 및 속성 삽입과 같이 생성자 내부에서 해결 된 서비스를 릴리스 / 배포하는 것에 대해서는 신경 쓰지 않습니다.

좋은 습관:

  • 가능하면 서비스 로케이터 패턴을 사용하지 마십시오 (개발 시간에 서비스 유형이 알려진 경우). 종속성을 암시 적으로 만들기 때문입니다. 즉, 서비스 인스턴스를 작성하는 동안 종속성을 쉽게 볼 수 없습니다. 이는 서비스의 일부 종속성을 조롱하려는 단위 테스트에 특히 중요합니다.
  • 가능하면 서비스 생성자의 종속성을 해결하십시오. 서비스 방법으로 해결하면 응용 프로그램이 더 복잡해지고 오류가 발생하기 쉽습니다. 다음 섹션에서 문제와 해결책을 다룰 것입니다.

수명

ASP.NET Core Dependency Injection에는 세 가지 서비스 수명이 있습니다.

  1. 일시적 서비스는 주입되거나 요청 될 때마다 작성됩니다.
  2. 범위별로 서비스가 작성됩니다. 웹 응용 프로그램에서 모든 웹 요청은 새로운 분리 된 서비스 범위를 만듭니다. 즉, 범위가 지정된 서비스는 일반적으로 웹 요청마다 생성됩니다.
  3. DI 컨테이너 당 싱글 톤 서비스가 생성됩니다. 이는 일반적으로 애플리케이션 당 한 번만 생성 된 후 전체 애플리케이션 수명 동안 사용됨을 의미합니다.

DI 컨테이너는 모든 해결 된 서비스를 추적합니다. 수명이 다하면 서비스가 해제되고 폐기됩니다.

  • 서비스에 종속성이있는 경우 자동으로 해제되어 처리됩니다.
  • 서비스가 IDisposable 인터페이스를 구현하면 서비스 릴리스시 Dispose 메소드가 자동으로 호출됩니다.

좋은 습관:

  • 가능하면 일시적으로 서비스를 등록하십시오. 일시적인 서비스를 디자인하는 것은 간단하기 때문입니다. 일반적으로 멀티 스레딩 및 메모리 누수에 신경 쓰지 않으며 서비스 수명이 짧다는 것을 알고 있습니다.
  • 하위 서비스 범위를 만들거나 웹 이외의 응용 프로그램에서 이러한 서비스를 사용하는 경우 까다로울 수 있으므로 범위가 지정된 서비스 수명을 신중하게 사용하십시오.
  • 멀티 스레딩 및 잠재적 인 메모리 누수 문제를 처리해야하므로 싱글 톤 수명을 신중하게 사용하십시오.
  • 싱글 톤 서비스의 일시적 또는 범위가 지정된 서비스에 의존하지 마십시오. 단일 서비스가 서비스를 주입 할 때 임시 서비스가 단일 인스턴스가되기 때문에 임시 서비스가 그러한 시나리오를 지원하도록 설계되지 않으면 문제가 발생할 수 있습니다. 이러한 경우 ASP.NET Core의 기본 DI 컨테이너는 이미 예외를 발생시킵니다.

분석법 본문에서 서비스 해결

경우에 따라 서비스 방법으로 다른 서비스를 해결해야 할 수도 있습니다. 이 경우 사용 후 서비스를 해제하십시오. 이를 보장하는 가장 좋은 방법은 서비스 범위를 만드는 것입니다. 예:

공공 클래스 PriceCalculator
{
    개인 읽기 전용 IServiceProvider _serviceProvider;
    공공 가격 계산기 (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculate (제품 제품, 정수,
      세금 전략 서비스 유형)
    {
        (var scope = _serviceProvider.CreateScope ()) 사용
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var price = product.Price * count;
            반품 가격 + taxStrategy.CalculateTax (price);
        }
    }
}

PriceCalculator는 생성자에 IServiceProvider를 주입하여 필드에 할당합니다. 그런 다음 PriceCalculator는 Calculate 메서드 내에서이를 사용하여 하위 서비스 범위를 만듭니다. 삽입 된 _serviceProvider 인스턴스 대신 scope.ServiceProvider를 사용하여 서비스를 해결합니다. 따라서 범위에서 확인 된 모든 서비스는 using 문이 끝날 때 자동으로 해제 / 처분됩니다.

좋은 습관:

  • 메소드 본문에서 서비스를 분석하는 경우 항상 분석 된 서비스가 올바르게 릴리스되도록 하위 서비스 범위를 작성하십시오.
  • 메소드가 IServiceProvider를 인수로 가져 오면 해제 / 처리에 신경 쓰지 않고 직접 서비스를 분석 할 수 있습니다. 서비스 범위 생성 / 관리는 메소드를 호출하는 코드의 책임입니다. 이 원칙을 따르면 코드가 더 깨끗해집니다.
  • 해결 된 서비스에 대한 참조를 보유하지 마십시오! 그렇지 않으면 메모리 누수가 발생할 수 있으며 나중에 오브젝트 참조를 사용할 때 (해결 된 서비스가 싱글 톤이 아닌 한) 폐기 된 서비스에 액세스하게됩니다.

싱글 톤 서비스

싱글 톤 서비스는 일반적으로 응용 프로그램 상태를 유지하도록 설계되었습니다. 캐시는 응용 프로그램 상태의 좋은 예입니다. 예:

공개 클래스 FileService
{
    개인 읽기 전용 ConcurrentDictionary  _cache;
    공개 FileService ()
    {
        _cache = 새로운 ConcurrentDictionary  ();
    }
    공개 바이트 [] GetFileContent (문자열 파일 경로)
    {
        _cache.GetOrAdd (filePath, _ => 반환
        {
            반환 File.ReadAllBytes (filePath);
        });
    }
}

FileService는 단순히 파일 내용을 캐시하여 디스크 읽기를 줄입니다. 이 서비스는 싱글 톤으로 등록해야합니다. 그렇지 않으면 캐싱이 예상대로 작동하지 않습니다.

좋은 습관:

  • 서비스가 상태를 유지하는 경우 스레드 안전 방식으로 해당 상태에 액세스해야합니다. 모든 요청이 동시에 동일한 서비스 인스턴스를 사용하기 때문입니다. 스레드 안전성을 보장하기 위해 Dictionary 대신 ConcurrentDictionary를 사용했습니다.
  • 싱글 톤 서비스에서 범위가 있거나 일시적인 서비스를 사용하지 마십시오. 임시 서비스는 스레드 안전하도록 설계되지 않았을 수 있습니다. 이를 사용해야하는 경우 이러한 서비스를 사용하는 동안 멀티 스레딩을 관리하십시오 (예 : 잠금 사용).
  • 메모리 누수는 일반적으로 싱글 톤 서비스에 의해 발생합니다. 응용 프로그램이 끝날 때까지 릴리스 / 처분되지 않습니다. 따라서 클래스를 인스턴스화 (또는 인젝션)하지만 해제 / 폐기하지 않으면 응용 프로그램이 끝날 때까지 메모리에 남아 있습니다. 적시에 해제 / 폐기하십시오. 위의 메소드 본문에서 서비스 해결 섹션을 참조하십시오.
  • 데이터 (이 예제의 파일 내용)를 캐시하는 경우 원래 데이터 소스가 변경 될 때 (이 예제의 디스크에서 캐시 된 파일이 변경 될 때) 캐시 된 데이터를 업데이트 / 무효화하는 메커니즘을 만들어야합니다.

범위가 지정된 서비스

범위가 지정된 수명은 먼저 웹 요청 데이터 당 저장하기에 적합한 후보로 보입니다. ASP.NET Core는 웹 요청마다 서비스 범위를 생성하기 때문입니다. 따라서 서비스를 범위로 등록하면 웹 요청 중에 공유 할 수 있습니다. 예:

공개 클래스 RequestItemsService
{
    개인 읽기 전용 Dictionary  _items;
    공개 RequestItemsService ()
    {
        _items = 새로운 사전  ();
    }
    공공 무효 세트 (문자열 이름, 객체 값)
    {
        _items [이름] = 값;
    }
    공용 객체 Get (문자열 이름)
    {
        _items [이름]을 반환;
    }
}

RequestItemsService를 범위가 지정된 것으로 등록하고 두 개의 다른 서비스에 삽입하면 동일한 RequestItemsService 인스턴스를 공유하므로 다른 서비스에서 추가 된 항목을 가져올 수 있습니다. 그것이 우리가 범위 서비스에서 기대하는 것입니다.

그러나 .. 사실은 항상 그런 것은 아닙니다. 자식 서비스 범위를 만들고 자식 범위에서 RequestItemsService를 확인하면 RequestItemsService의 새 인스턴스가 생성되며 예상대로 작동하지 않습니다. 따라서 범위가 지정된 서비스가 항상 웹 요청 당 인스턴스를 의미하는 것은 아닙니다.

당신은 그러한 명백한 실수를하지 않는다고 생각할 수도 있습니다 (자식 범위 내에서 범위를 해결). 그러나 이것은 실수 (매우 일반적인 사용법)가 아니며 사건이 그렇게 간단하지 않을 수도 있습니다. 서비스간에 큰 종속성 그래프가있는 경우 다른 사람이 하위 범위를 생성하고 다른 서비스를 주입하는 서비스를 해결했는지 여부를 알 수 없습니다. 최종적으로 범위가 지정된 서비스를 주입합니다.

좋은 연습:

  • 범위가 지정된 서비스는 웹 요청에서 너무 많은 서비스에 의해 주입되는 최적화로 간주 될 수 있습니다. 따라서 이러한 모든 서비스는 동일한 웹 요청 동안 서비스의 단일 인스턴스를 사용합니다.
  • 범위가 지정된 서비스는 스레드로부터 안전하도록 설계 될 필요가 없습니다. 일반적으로 단일 웹 요청 / 스레드에서 사용해야합니다. 그러나 ...이 경우 다른 스레드간에 서비스 범위를 공유해서는 안됩니다!
  • 웹 요청에서 다른 서비스간에 데이터를 공유하도록 범위가 지정된 서비스를 설계 할 경우주의하십시오 (위에 설명). 웹 요청 당 데이터를 HttpContext에 저장하여 (IHttpContextAccessor에 액세스하여 액세스) 안전한 방법입니다. HttpContext의 수명은 범위가 없습니다. 실제로, 그것은 DI에 전혀 등록되지 않았습니다 (따라서 주입하지 않고 대신 IHttpContextAccessor를 주입합니다). HttpContextAccessor 구현은 AsyncLocal을 사용하여 웹 요청 중에 동일한 HttpContext를 공유합니다.

결론

의존성 주입은 처음에는 사용하기 간단 해 보이지만 엄격한 원칙을 따르지 않으면 멀티 스레딩 및 메모리 누수 문제가 발생할 수 있습니다. ASP.NET Boilerplate 프레임 워크를 개발하는 동안 내 경험을 바탕으로 몇 가지 좋은 원칙을 공유했습니다.