다음을 통해 공유


SignalR 1.x에서 종속성 주입

작성자 : Patrick Fletcher

경고

이 설명서는 최신 버전의 SignalR용이 아닙니다. ASP.NET Core SignalR을 살펴보세요.

종속성 주입은 개체 간에 하드 코딩된 종속성을 제거하여 테스트(모의 개체 사용) 또는 런타임 동작을 변경하기 위해 개체의 종속성을 더 쉽게 바꿀 수 있는 방법입니다. 이 자습서에서는 SignalR Hubs에서 종속성 주입을 수행하는 방법을 보여 줍니다. 또한 SignalR에서 IoC 컨테이너를 사용하는 방법도 보여 줍니다. IoC 컨테이너는 종속성 주입을 위한 일반적인 프레임워크입니다.

종속성 주입이란?

종속성 주입에 이미 익숙한 경우 이 섹션을 건너뜁니다.

DI(종속성 주입)는 개체가 자체 종속성을 만들 책임이 없는 패턴입니다. 다음은 DI에 동기를 부여하는 간단한 예입니다. 메시지를 기록해야 하는 개체가 있다고 가정해 보겠습니다. 로깅 인터페이스를 정의할 수 있습니다.

interface ILogger 
{
    void LogMessage(string message);
}

개체에서 을 ILogger 만들어 메시지를 기록할 수 있습니다.

// Without dependency injection.
class SomeComponent
{
    ILogger _logger = new FileLogger(@"C:\logs\log.txt");

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

이는 작동하지만 최상의 디자인은 아닙니다. 를 다른 ILogger 구현으로 바꾸려 FileLogger 면 를 수정SomeComponent해야 합니다. 다른 많은 개체가 를 사용 FileLogger한다고 가정하면 모든 개체를 변경해야 합니다. 또는 싱글톤을 만들기 FileLogger 로 결정한 경우 애플리케이션 전체에서 변경해야 합니다.

더 나은 방법은 예를 들어 생성자 인수를 사용하여 개체에 를 "삽입" ILogger 하는 것입니다.

// With dependency injection.
class SomeComponent
{
    ILogger _logger;

    // Inject ILogger into the object.
    public SomeComponent(ILogger logger)
    {
        if (logger == null)
        {
            throw new NullReferenceException("logger");
        }
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.LogMessage("DoSomething");
    }
}

이제 개체는 사용할 항목을 선택할 ILogger 책임이 없습니다. 구현에 의존하는 개체를 변경하지 않고도 구현을 전환 ILogger 할 수 있습니다.

var logger = new TraceLogger(@"C:\logs\log.etl");
var someComponent = new SomeComponent(logger);

이 패턴을 생성자 주입이라고 합니다. 또 다른 패턴은 setter 메서드 또는 속성을 통해 종속성을 설정하는 setter 삽입입니다.

SignalR의 단순 종속성 주입

SignalR을 사용하여 자습서 시작 채팅 애플리케이션을 고려합니다. 해당 애플리케이션의 허브 클래스는 다음과 같습니다.

public class ChatHub : Hub
{
    public void Send(string name, string message)
    {
        Clients.All.addMessage(name, message);
    }
}

메시지를 보내기 전에 서버에 채팅 메시지를 저장하려는 경우를 가정해 보겠습니다. 이 기능을 추상화하는 인터페이스를 정의하고 DI를 사용하여 인터페이스를 클래스에 ChatHub 삽입할 수 있습니다.

public interface IChatRepository
{
    void Add(string name, string message);
    // Other methods not shown.
}

public class ChatHub : Hub
{
    private IChatRepository _repository;

    public ChatHub(IChatRepository repository)
    {
        _repository = repository;
    }

    public void Send(string name, string message)
    {
        _repository.Add(name, message);
        Clients.All.addMessage(name, message);
    }

유일한 문제는 SignalR 애플리케이션이 허브를 직접 만들지 않는다는 것입니다. SignalR은 사용자를 위해 만듭니다. 기본적으로 SignalR은 허브 클래스에 매개 변수가 없는 생성자가 있어야 합니다. 그러나 함수를 쉽게 등록하여 허브 인스턴스를 만들고 이 함수를 사용하여 DI를 수행할 수 있습니다. GlobalHost.DependencyResolver.Register를 호출하여 함수를 등록합니다.

protected void Application_Start()
{
    GlobalHost.DependencyResolver.Register(
        typeof(ChatHub), 
        () => new ChatHub(new ChatMessageRepository()));

    RouteTable.Routes.MapHubs();

    // ...
}

이제 SignalR은 instance 만들어야 할 때마다 이 익명 함수를 ChatHub 호출합니다.

IoC 컨테이너

이전 코드는 간단한 경우에 적합합니다. 하지만 여전히 다음을 작성해야 했습니다.

... new ChatHub(new ChatMessageRepository()) ...

종속성이 많은 복잡한 애플리케이션에서는 이 "배선" 코드를 많이 작성해야 할 수 있습니다. 이 코드는 특히 종속성이 중첩된 경우 유지 관리가 어려울 수 있습니다. 단위 테스트도 어렵습니다.

한 가지 솔루션은 IoC 컨테이너를 사용하는 것입니다. IoC 컨테이너는 종속성 관리를 담당하는 소프트웨어 구성 요소입니다. 컨테이너에 형식을 등록한 다음 컨테이너를 사용하여 개체를 만듭니다. 컨테이너는 종속성 관계를 자동으로 파악합니다. 또한 많은 IoC 컨테이너를 사용하면 개체 수명 및 scope 같은 항목을 제어할 수 있습니다.

참고

"IoC"는 프레임워크가 애플리케이션 코드를 호출하는 일반적인 패턴인 "제어 반전"을 의미합니다. IoC 컨테이너는 일반적인 제어 흐름을 "반전"하는 개체를 생성합니다.

SignalR에서 IoC 컨테이너 사용

채팅 애플리케이션은 IoC 컨테이너의 이점을 활용하기에는 너무 간단합니다. 대신 StockTicker 샘플을 살펴보겠습니다.

StockTicker 샘플은 두 가지 기본 클래스를 정의합니다.

  • StockTickerHub: 클라이언트 연결을 관리하는 허브 클래스입니다.
  • StockTicker: 주가를 보유하고 주기적으로 업데이트하는 싱글톤입니다.

StockTickerHub 는 싱글톤에 대한 참조를 StockTicker 보유하지만 StockTicker 에 대한 IHubConnectionContext 에 대한 참조를 보유합니다 StockTickerHub. 이 인터페이스를 사용하여 인스턴스와 StockTickerHub 통신합니다. 자세한 내용은 ASP.NET SignalR을 사용하여 서버 브로드캐스트를 참조하세요.

IoC 컨테이너를 사용하여 이러한 종속성을 약간 풀 수 있습니다. 먼저 및 StockTicker 클래스를 StockTickerHub 간소화해 보겠습니다. 다음 코드에서는 필요하지 않은 부분을 주석으로 처리했습니다.

에서 매개 변수가 없는 생성자를 StockTicker제거합니다. 대신 항상 DI를 사용하여 허브를 만듭니다.

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly StockTicker _stockTicker;

    //public StockTickerHub() : this(StockTicker.Instance) { }

    public StockTickerHub(StockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

    // ...

StockTicker의 경우 싱글톤 instance 제거합니다. 나중에 IoC 컨테이너를 사용하여 StockTicker 수명을 제어합니다. 또한 생성자를 공용으로 만듭니다.

public class StockTicker
{
    //private readonly static Lazy<StockTicker> _instance = new Lazy<StockTicker>(
    //    () => new StockTicker(GlobalHost.ConnectionManager.GetHubContext<StockTickerHub>().Clients));

    // Important! Make this constructor public.
    public StockTicker(IHubConnectionContext clients)
    {
        if (clients == null)
        {
            throw new ArgumentNullException("clients");
        }

        Clients = clients;
        LoadDefaultStocks();
    }

    //public static StockTicker Instance
    //{
    //    get
    //    {
    //        return _instance.Value;
    //    }
    //}

다음으로, 에 대한 StockTicker인터페이스를 만들어 코드를 리팩터링할 수 있습니다. 이 인터페이스를 사용하여 클래스에서 를 StockTickerHub 분리합니다 StockTicker .

Visual Studio를 사용하면 이러한 종류의 리팩터링을 쉽게 수행할 수 있습니다. StockTicker.cs 파일을 열고 클래스 선언을 StockTicker 마우스 오른쪽 단추로 클릭한 다음 리팩터링 ...을 선택합니다. 인터페이스 추출.

리팩터링 및 인터페이스 추출 옵션이 강조 표시된 Visual Studio Code 위에 표시되는 오른쪽 클릭 드롭다운 메뉴의 스크린샷

인터페이스 추출 대화 상자에서 모두 선택을 클릭합니다. 다른 기본값을 그대로 둡니다. 확인을 클릭합니다.

모두 선택 옵션이 강조 표시되고 O K 옵션이 표시되는 인터페이스 추출 대화 상자의 스크린샷

Visual Studio는 라는 IStockTicker새 인터페이스를 만들고 에서 IStockTicker파생되도록 변경 StockTicker 합니다.

IStockTicker.cs 파일을 열고 인터페이스를 public으로 변경합니다.

public interface IStockTicker
{
    void CloseMarket();
    IEnumerable<Stock> GetAllStocks();
    MarketState MarketState { get; }
    void OpenMarket();
    void Reset();
}

클래스에서 StockTickerHub 의 두 인스턴스를 로 변경합니다.StockTickerIStockTicker

[HubName("stockTicker")]
public class StockTickerHub : Hub
{
    private readonly IStockTicker _stockTicker;

    public StockTickerHub(IStockTicker stockTicker)
    {
        if (stockTicker == null)
        {
            throw new ArgumentNullException("stockTicker");
        }
        _stockTicker = stockTicker;
    }

인터페이스를 IStockTicker 만드는 것은 반드시 필요한 것은 아니지만 DI가 애플리케이션의 구성 요소 간의 결합을 줄이는 데 어떻게 도움이 되는지 보여주고 싶었습니다.

Ninject 라이브러리 추가

.NET용 오픈 소스 IoC 컨테이너가 많이 있습니다. 이 자습서에서는 Ninject를 사용합니다. (기타 인기 있는 라이브러리로는 Castle Windsor, Spring.Net, Autofac, UnityStructureMap이 있습니다.

NuGet 패키지 관리자를 사용하여 Ninject 라이브러리를 설치합니다. Visual Studio의 도구 메뉴에서 NuGet 패키지 관리자 패키지 관리자>콘솔을 선택합니다. 패키지 관리자 콘솔 창에서 다음 명령을 입력합니다.

Install-Package Ninject -Version 3.0.1.10

SignalR 종속성 확인자 바꾸기

SignalR 내에서 Ninject를 사용하려면 DefaultDependencyResolver에서 파생되는 클래스를 만듭니다.

internal class NinjectSignalRDependencyResolver : DefaultDependencyResolver
{
    private readonly IKernel _kernel;
    public NinjectSignalRDependencyResolver(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override object GetService(Type serviceType)
    {
        return _kernel.TryGet(serviceType) ?? base.GetService(serviceType);
    }

    public override IEnumerable<object> GetServices(Type serviceType)
    {
        return _kernel.GetAll(serviceType).Concat(base.GetServices(serviceType));
    }
}

이 클래스는 DefaultDependencyResolverGetServiceGetServices 메서드를 재정의합니다. SignalR은 이러한 메서드를 호출하여 런타임에 허브 인스턴스를 비롯한 다양한 개체와 SignalR에서 내부적으로 사용되는 다양한 서비스를 만듭니다.

  • GetService 메서드는 형식의 단일 instance 만듭니다. 이 메서드를 재정의하여 Ninject 커널의 TryGet 메서드를 호출합니다. 해당 메서드가 null을 반환하는 경우 기본 확인자로 대체합니다.
  • GetServices 메서드는 지정된 형식의 개체 컬렉션을 만듭니다. 이 메서드를 재정의하여 Ninject의 결과를 기본 확인자의 결과와 연결합니다.

Ninject 바인딩 구성

이제 Ninject를 사용하여 형식 바인딩을 선언합니다.

RegisterHubs.cs 파일을 엽니다. 메서드에서 RegisterHubs.Start Ninject가 커널을 호출하는 Ninject 컨테이너를 만듭니다.

var kernel = new StandardKernel();

사용자 지정 종속성 확인자의 instance 만듭니다.

var resolver = new NinjectSignalRDependencyResolver(kernel);

다음과 같이 에 대한 IStockTicker 바인딩을 만듭니다.

kernel.Bind<IStockTicker>()
    .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()  // Bind to StockTicker.
    .InSingletonScope();  // Make it a singleton object.

이 코드는 두 가지를 말합니다. 먼저 애플리케이션에 가 필요할 IStockTicker때마다 커널은 의 StockTickerinstance 만들어야 합니다. 둘째, 클래스는 StockTicker 싱글톤 개체로 만들어져야 합니다. Ninject는 개체의 instance 만들고 각 요청에 대해 동일한 instance 반환합니다.

다음과 같이 IHubConnectionContext에 대한 바인딩을 만듭니다.

kernel.Bind<IHubConnectionContext>().ToMethod(context =>
    resolver.Resolve<IConnectionManager>().GetHubContext<StockTickerHub>().Clients
).WhenInjectedInto<IStockTicker>();

이 코드는 IHubConnection을 반환하는 익명 함수를 만듭니다. WhenInjectedInto 메서드는 인스턴스를 만들 IStockTicker 때만 이 함수를 사용하도록 Ninject에 지시합니다. 그 이유는 SignalR이 내부적으로 IHubConnectionContext 인스턴스를 만들고 SignalR이 인스턴스를 만드는 방법을 재정의하지 않기 때문입니다. 이 함수는 클래스 StockTicker 에만 적용됩니다.

종속성 확인자를 MapHubs 메서드에 전달합니다.

RouteTable.Routes.MapHubs(config);

이제 SignalR은 기본 확인자 대신 MapHubs에 지정된 확인자를 사용합니다.

다음은 에 대한 전체 코드 목록입니다 RegisterHubs.Start.

public static class RegisterHubs
{
    public static void Start()
    {
        var kernel = new StandardKernel();
        var resolver = new NinjectSignalRDependencyResolver(kernel);

        kernel.Bind<IStockTicker>()
            .To<Microsoft.AspNet.SignalR.StockTicker.StockTicker>()
            .InSingletonScope();

        kernel.Bind<IHubConnectionContext>().ToMethod(context =>
                resolver.Resolve<IConnectionManager>().
                    GetHubContext<StockTickerHub>().Clients
            ).WhenInjectedInto<IStockTicker>();

        var config = new HubConfiguration()
        {
            Resolver = resolver
        };

        // Register the default hubs route: ~/signalr/hubs
        RouteTable.Routes.MapHubs(config);
    }
}

Visual Studio에서 StockTicker 애플리케이션을 실행하려면 F5 키를 누릅니다. 브라우저 창에서 로 이동합니다 http://localhost:*port*/SignalR.Sample/StockTicker.html.

인터넷 Explorer 브라우저 창에 표시되는 SP dot NET Signal R Stock Ticker 샘플 화면의 스크린샷.

애플리케이션은 이전과 정확히 동일한 기능을 가지고 있습니다. 자세한 내용은 ASP.NET SignalR을 사용하여 서버 브로드캐스트를 참조하세요. 동작을 변경하지 않았습니다. 코드를 더 쉽게 테스트, 유지 관리 및 발전할 수 있도록 했습니다.