MVVM+Command Pattern+Dependency Injection #1에 이어지는 Ninject를 사용한 Dependency Injection편이에요 :D

Ninject는 ‘번개처럼 빠른 인젝션’을 모토로 한 DI 프레임워크죠. Unity랑 비교해 보면 Unity가 전통적인 개발자에게 친숙한 인터페이스와 네이밍을 가진 반면 Ninject는 모던하고 약간은 팬시한 문법과 네이밍을 가졌고 다양한 조건에서 좀 더 자동화된 인젝션을 지원한다는 느낌이 들어요. 여튼 이 전에 만든 프로젝트를 정말로 약간만 수정하면 Unity를 Ninject로 바꿀 수 있어요. 이것이 바로 모듈화된 프로그래밍의 장점이죠 ;~)

우선 프로젝트 먼저 받아가시고요~


1. Ninject 라이브러리 참조

당연하겠지만 제일 먼저 라이브러리를 교체해야겠죠?

image

Ninject는 현재 5개의 어셈블리로 이루어져 있고 3개는 Extension, 2개는 거의 필수로 사용되는 어셈블리죠. 그 중 필수인 Ninject.Core와 Ninject.Conditions를 참조하면 돼요.
※Ninject.Condition 역시 사실 필수는 아니지만 프로젝트가 조금만 커져도 거의 필수로 사용됩니다.

2. ServiceConfiguration 수정

/// 
/// 서비스에서 사용할 인젝션 매핑을 설정하는 클래스
/// 
public class ServiceConfiguration : StandardModule
{
    public override void Load()
    {
        bool isBlend = App.IsBlend;
        bool isWeb = !isBlend;

        // IPhotoSerivce에 대한 바인딩 설정
        // 블렌드에서 실행할 때 바인딩만 다르게 표현
        if (isBlend)
        {
            Bind< IPhotoService>().To< MockPhotoService>();
        }
        else
        {
            Bind< IPhotoService>().To< LivePhotoService>().Only(When.Context.Variable("ServiceName").IsNotDefined);
            Bind< IPhotoService>().To< LivePhotoService>().Only(When.Context.Variable("ServiceName").EqualTo("Live"));
            Bind< IPhotoService>().To< FlickrPhotoService>().Only(When.Context.Variable("ServiceName").EqualTo("Flickr"));
            Bind< IPhotoService>().To< DaumPhotoService>().Only(When.Context.Variable("ServiceName").EqualTo("Daum"));
            Bind< IPhotoService>().To< NaverPhotoService>().Only(When.Context.Variable("ServiceName").EqualTo("Naver"));
        }

        // 공통으로 사용할 바인딩
        // 서비스 제공자의 목록을 싱글턴으로 바인딩
        Bind< PhotoServiceProviderList>().ToConstant< PhotoServiceProviderList>(
            new PhotoServiceProviderList
            {
                new PhotoServiceProvider { Name = "Live", DisplayName = "Live", LogoSource = "/MashupImageSearch;component/Resource/WindowsLive2_20.png" },
                new PhotoServiceProvider { Name = "Flickr", DisplayName = "Flickr", LogoSource = "/MashupImageSearch;component/Resource/flickr2_20.png" },
                new PhotoServiceProvider { Name = "Daum", DisplayName = "Daum", LogoSource = "/MashupImageSearch;component/Resource/daum_20.png" },
                new PhotoServiceProvider { Name = "Naver", DisplayName = "Naver", LogoSource = "/MashupImageSearch;component/Resource/naver_20.png" },
            }
        ).Using< SingletonBehavior>();
    }
}

Unity가 UnityContainer를 제공했던 것과 유사하게 Ninject는 StandardModule을 제공해요. Ninject에서 Module이란 하나의 서비스를 구성하는 DI들의 바인딩 구성을 정의하죠.

바인딩(매핑)을 등록하는 문장도 굉장히 다른데요, RegisterType()이라는 하나의 메서드로 인자의 위치나 배열에 따라 순서를 정했던 Unity와 달리 Ninject는 Bind<타입>().To<타입>().조건… 과 같은 명시적인 형식을 가지고 있어요. 마치 LINQ 표현식의 발전 과정을 보는 듯하죠.

동적 조건 식은 조금 복잡한데요, Unity가 단순히 이름만 가지고 바인딩을 결정했던 것과는 달리 Ninject는 굉장히 다양한 시나리오와 커스터마이징이 가능한 파라미터들로 바인딩을 결정할 수 있어요. Only(When.Context.Variable(“키”).조건…과 같이 “오직 컨텍스트에 특정 변수가 조건을 만족할 때만 바인딩”으로 글을 읽듯이 읽을 수 있죠. 굉장히 가독성이 좋은 문법이라고 봐요. 다만 지네릭과 람다 익스프레션 문법을 굉장히 많이 활용하기 때문에 익숙하지 않다면 어려울 수 있겠네요.

마지막으로 Unity의 RegisterInstance()메서드는 Ninject는 Bind<타입>().ToConstant<타입>() 으로 표현하는 걸 볼 수 있고 인젝션할 대상의 라이프 타임 관리를 .Using<라이프타임 동작>() 과 같이 할 수있어요. 여기에서 서비스 제공자의 목록은 항상 같으므로 단순한 SingletonBehavior로 지정했죠.

3. ServiceLocator 수정

/// 
/// 서비스에서 사용할 설정 모듈에서 타입을 제공하는 클래스
/// 
public class ServiceLocator
{
    #region Singlton 컨테이너
    private static IKernel _kernel;
    /// 
    /// 클래스 초기화.
    /// 서비스에서 사용할 DI 컨테이너를 초기화합니다.
    /// 
    static ServiceLocator()
    {
        if (_kernel == null)
        {
            _kernel = new StandardKernel(new ServiceConfiguration());
        }
    }

    /// 
    /// 주어진 타입에 대한 인젝션을 수행한 참조를 반환합니다.
    /// 
    /// 인젝션을 처리할 대상
    /// 인젝션이 처리된 결과
    public static T Get< T>()
    {
        return _kernel.Get< T>();
    }

    /// 
    /// 주어진 타입에 대한 인젝션을 수행한 참조를 반환합니다.
    /// 
    /// 인젝션을 처리할 대상
    /// 파라미터 이름
    /// 인젝션이 처리된 결과
    public static T Get< T>(string name)
    {
        return _kernel.Get< T>(With.Parameters.Variable("ServiceName", name));
    }
    #endregion Singlton 컨테이너

    /// 
    /// 설정에 따라 이미지 검색 뷰모델의 인스턴스를 반환합니다.
    /// 
    public ImageSearchViewModel ImageSearchViewModel
    {
        get { return Get< ImageSearchViewModel>(); }
    }

    /// 
    /// 설정에 따라 포토 서비스 제공자의 목록을 반환합니다.
    /// 
    public PhotoServiceProviderList PhotoServiceProviders
    {
        get { return Get< PhotoServiceProviderList>(); }
    }
}		

Unity에서는 UnityContainer가 인젝션 바인딩(매핑) 정보를 보관하고 관리하는 역할을 모두 수행하지만 Ninject는 Kernel이 관리하는 Module에서 인젝션 바인딩 정보를 이용하여 인젝션을 관리해요. 특별히 다른 점은 없고 다만 파라미터를 줄 때도 Kernel.Get<타입>(With.Parameters.Variable(“변수이름”, 변수값) 같은 식으로 명시적이고 가독성 있는 문법을 지원해요.

이 정도 수정만으로 Unity를 Ninject로 대체하는 마이그레이션이 끝났어요. 실행 결과는 당연히 동일하고요. 간단하죠?

DI 자체에 대해서는 언제 한번 자세히 다루고 싶지만 아직 내공이 딸려서 정확한 설명은 힘드네요. 시간도 부족하고요. 우선은 지금까지 했던 프로젝트를 기반으로 지식을 확장해가면서 경험을 쌓아도 충분할거라고 봐요.

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo

매쉬업 이미지 검색 + MVVM모델 + CommandPattern 예제에서 길버트님이 MVVM과 Command Pattern을 활용하여 화면 표시를 담당하는 View와 데이터 및 비즈니스 로직을 담당하는 Model, 그리고 View에 표시될 데이터와 View의 흐름 제어를 담당할 ViewModel이 적용될 수 있는 멋진 데모를 보여주셨죠.

사용된 패턴과 용어 정리

MVVM은 MVC 패밀리(?)의 일족으로 MVP(Model View Presentation)과 매우 유사해요. 사실 전 MVC/MVP/MVVM은 모두 같은 원리이고 구현의 관점이 약간씩 다를 뿐이라고 생각해요. 여튼 중요한 건 뷰와 데이터를 어떻게 분리한 상태에서 컨트롤 할 것이냐죠.

Commnad Pattern은 주로 애플리케이션 전역에서 동일하게 받을 수 있는 사용자의 제어 명령에 대한 이벤트를 효과적이고 표준적으로 받기 위한 구현 방법이에요. 커맨드 패턴은 특히 MVVM처럼 뷰와 모델이 분리된 환경에서 매우 효과적으로 코드를 짤 수 있게 도와주는 역할을 하죠.

Dependency Injection(DI) Pattern은 클래스의 독립성을 극한까지 끌어올리기 위해 고안된 방법 중 하나에요. 인젝션의 핵심은 어떤 클래스가 참조하거나 이용하는 인스턴스를 그 클래스가 생성하지 않고 외부에서 생성자, 프로퍼티 또는 메서드를 사용하여 ‘주입’inject시켜 보다 투명하고 분리된 클래스를 작성하는 데 있어요.

이 글에서는 DI에대한 설명은 접고 그냥 어떻게 활용되었는지를 보도록 하고 자세한 정보는 다음 링크들을 체크해 보세요.

길버트님의 예제 리모델링!

백문이 불여일타! 일단 샘플 받아서 열어보시고요~


동작은 길버트님의 예제와 완벽하게 동일하므로 동작 완구는 생략했어요. 리모델링 과정을 알아보죠.
※보통 프로그래밍에서 어떤 코드를 좀 더 적절하게 나누고 최적화하는 과정을 리팩터링이라고 합니다. 여기에서는 리모델링으로 표현했음에 주의하세요.

1. 문제점 분석

우선 기존 프로젝트에서 어떤 부분을 왜 고쳐야 하는지 생각해보죠. 기존 프로젝트에서는 각 포토 서비스를 IPhotoService라는 인터페이스만 상속받으면 되는 독립 클래스로 만들수 있었어요. 한 가지 문제는 이렇게 새 서비스를 만들어도 View와 ViewModel이 추가된 서비스를 알 수 없기 때문에 View와 ViewModel 모두를 고쳐야 한다는 점이죠. 이는 말하자면 Model의 독립성은 확보했지만 View와 ViewModel이 Model에 종속되어 있는 그림이 되죠. 특히 View는 ComboBox 컨트롤에 하드코딩으로 목록이 들어있는 형태라서 만약 디자이너 프로세스와 개발 프로세스가 나뉘어 있다면 Model이 추가될 때마다 디자이너가 View를 만져줘야 하는 상황이 발생하겠죠.

이를 해결하려면 View는 Model에 대해 전혀 알 필요가 없어야 하고 ViewModel은 Model의 추가/변경/삭제에 대해 코드의 수정 없이 대처할 수 있어야 해요.

2. View를 독립시키기

OOP의 기본 중에 기본이죠. 하나의 클래스는 그 이름으로 대표되는 최소한의 일만 수행하도록 쪼개고 또 쪼개는 것. 기존 프로젝트에서 콤보 박스에 들어가는 데이터를 생각해보면 각 서비스 제공자의 이름(Flickr, Live…)과 로고 이미지로 구성되어 있었죠? 그리고 이 부분이 새 Model이 추가될 때마다 함께 수정해야 할 부분이었고요. 이 부분을 분리해보죠.

image

포토 서비스 제공자의 이름과 로고를 처리하기 위해 IPhotoServiceProvider와 PhotoServiceProvider 클래스를 추가했고 내용은…

/// 
/// 기본적인 포토 서비스 제공자의 메타 정보를 관리하기 위한 클래스
/// 
public class PhotoServiceProvider : IPhotoServiceProvider
{
    /// 
    /// 서비스 제공자의 이름을 설정하거나 반환합니다.
    /// 
    public string Name { get; set; }

    /// 
    /// 화면에 표시할 이름을 설정하거나 반환합니다.
    /// 
    public string DisplayName { get; set; }

    /// 
    /// 서비스 제공자의 로고 이미지 소스를 설정하거나 반환합니다.
    /// 
    public string LogoSource { get; set; }
}

public class PhotoServiceProviderList : List< IPhotoServiceProvider>
{
}

간단하죠? 그리고 이 데이터는 하나가 아닌 여럿으로 구성될 것이므로 지네릭 타입의 List도 하나 선언해줬어요.

이제 View에서 데이터 템플릿을 사용하여 하드 코딩된 디자인을 걷어내보죠.

    
        
            
                
                
            
        
    

아직 데이터 바인딩의 소스를 설정하지 않았지만 DataTemplate을 사용해서 기존 디자인 형식을 그대로 가져왔고 대신 Image의 Source와 TextBlock의 Text 속성을 위에서 만든 IPhotoServiceProvider가 제공하는 속성으로 바인딩을 해줬어요. 데이터 바인딩의 소스는 나중에 추가하기로 하죠.

자 이제 View는 Model에 변경이 있더라도 영향을 받지 않도록 준비가 되었어요. 그러나 아직 데이터 소스를 어디에서 가져와서 언제 어떻게 설정할지는 미지수인데요, 이를 위해 드디어 Dependency Injection기법을 사용하여 몇 가지를 준비해보죠.

3. Dependency Injection 프레임워크 선택

DI는 개념이 그렇게까지 복잡한 것은 아니라 물론 직접 구현할 수도 있겠죠. 그러나 언제나 그렇지만 이런 표준적인 라이브러리는 다른 사람이 만든 훌륭한 구현체가 있기 마련이죠. 바로 제가 라이브러리 문서화의 중요성에서 언급한 Unity Application Block이나 Ninject처럼 말이죠. 이 중에서 좀 더 보편적이고 무엇보다 마이크로소프트가 운영하는 Pattern&Practice 프로젝트에서 만들어진 Unity를 사용할께요. 기회가 되면 Ninject도 사용해 볼 거에요.

imageimage

Unity Application Block for Silverlight를 다운로드 받으면 Microsoft.Practices.Unity.dll이란 어셈블리를 찾을 수 있는데요, 이걸 프로젝트에서 참조하면 돼요. 테스트 할 수 있도록 프로젝트의 DLL폴더에 어셈블리 파일을 포함시켜뒀으니 이걸 사용하셔도 되고요.

4. DI 설정 및 전역 서비스 제공 클래스 작성

DI는 외부에서 작성되어 애플리케이션 전역 혹은 일부에 영향을 주게 돼요. 저는 YouCard라는 짱멋진 프로젝트에 영향을 받아 DI 바인딩(또는 매핑) 정보를 저장하고 관리하는 ServiceConfiguration 클래스와 이 설정으로부터 애플리케이션이 사용할 인스턴스화된 오브젝트를 애플리케이션 전역에 제공할 ServiceLocator 클래스를 만들었어요.

먼저 ServiceConfiguration을 보면…

/// 
/// 서비스에서 사용할 인젝션 매핑을 설정하는 클래스
/// 
public class ServiceConfiguration : UnityContainer
{
    /// 
    /// 생성자.
    /// 여기에서 애플리케이션이 공통으로 사용할 컨테이너를 설정합니다.
    /// 
    public ServiceConfiguration()
    {
        // 디자인일 때와 그렇지 않을 때 서비스 바인딩을 달리 합니다.
        if (App.IsBlend)
        {
            RegisterType< IPhotoService, MockPhotoService>();
        }
        else
        {
            // 일반일 때에는 각 서비스의 바인딩을 이름으로 구분하여 등록합니다.
            RegisterType<  IPhotoService,  LivePhotoService>();    // 이름이 없을 때는 라이브를 사용
            RegisterType<  IPhotoService,  LivePhotoService>("Live");
            RegisterType<  IPhotoService,  FlickrPhotoService>("Flickr");
            RegisterType<  IPhotoService,  DaumPhotoService>("Daum");
            RegisterType<  IPhotoService,  NaverPhotoService>("Naver");
        }

        // 공통으로 사용할 바인딩
        // 서비스 제공자의 목록
        PhotoServiceProviderList providers = new PhotoServiceProviderList
        {
            new PhotoServiceProvider { Name = "Live", DisplayName = "Live", LogoSource = "/MashupImageSearch;component/Resource/WindowsLive2_20.png" },
            new PhotoServiceProvider { Name = "Flickr", DisplayName = "Flickr", LogoSource = "/MashupImageSearch;component/Resource/flickr2_20.png" },
            new PhotoServiceProvider { Name = "Daum", DisplayName = "Daum", LogoSource = "/MashupImageSearch;component/Resource/daum_20.png" },
            new PhotoServiceProvider { Name = "Naver", DisplayName = "Naver", LogoSource = "/MashupImageSearch;component/Resource/naver_20.png" },
        };
        // 인스턴스 자체를 등록
        RegisterInstance< PhotoServiceProviderList>(providers);
    }
}

가장 핵심적인 건 ServiceConfiguration 클래스가 UnityContainer를 상속받고 있다는 점이죠. UnityContainer는 바로 Unity에서 DI에 대한 매핑 정보를 저장하고 관리하는 기본 클래스에요. 기본적으로 RegisterType 메서드로 FromT 타입에 대해 ToT 타입으로 주입Injection하도록 매핑하고 또한 RegisterInstance 메서드로 FromT타입에 대해 등록한 인스턴스를 주입하도록 매핑하죠. 여기에서는 이 정도로만 설명하고 DI에 대한 건 기회가 되면 다른 글에서 소개할께요.

여기서 한 가지 센스있는 코드가 있는데요^^, 바로 실버라이트가 디자인 모드(블렌드)에서 동작중인지 아니면 정상적으로 실행되고 있는지에 따라 타입 매핑을 다르게 했어요. 이를 통해 디자이너가 블렌드에서도 여기에서 주어진 샘플 데이터를 볼 수 있죠.

이제 새로운 Model이 추가된다면 새 Model 클래스를 작성하고 ServiceConfiguration에 새로 만들어진 Model 타입을 매핑하고 새 Model을 설명하는 PhotoServiceProvider를 추가해주기만 하면 돼요. WOW~!

다음으로 ServiceLocator를 보죠.

/// 
/// 서비스에서 사용할 컨테이너를 초기화하고
/// 인젝션에 필요한 정보를 담고 있는 싱글턴 클래스
/// 
public class ServiceLocator
{
    #region Singlton 컨테이너
    private static ServiceConfiguration _container;
    /// 
    /// 클래스 초기화.
    /// 서비스에서 사용할 DI 컨테이너를 초기화합니다.
    /// 
    static ServiceLocator()
    {
        _container = new ServiceConfiguration();
    }

    /// 
    /// 주어진 타입에 대한 인젝션을 수행한 참조를 반환합니다.
    /// 
    /// 인젝션을 처리할 대상
    /// 인젝션이 처리된 결과
    public static T Get< T>()
    {
        return _container.Resolve< T>();
    }

    /// 
    /// 주어진 타입에 대한 인젝션을 수행한 참조를 반환합니다.
    /// 
    /// 인젝션을 처리할 대상
    /// 옵셔널 문자열
    /// 인젝션이 처리된 결과
    public static T Get< T>(string name)
    {
        return _container.Resolve< T>(name);
    }
    #endregion Singlton 컨테이너

    /// 
    /// 생성자.
    /// 
    public ServiceLocator()
    {
    }

    /// 
    /// 설정에 따라 이미지 검색 뷰모델의 인스턴스를 반환합니다.
    /// 
    public ImageSearchViewModel ImageSearchViewModel
    {
        get { return Get< ImageSearchViewModel >(); }
    }

    /// 
    /// 설정에 따라 포토 서비스 제공자의 목록을 반환합니다.
    /// 
    public PhotoServiceProviderList PhotoServiceProviders
    {
        get { return Get< PhotoServiceProviders>(); }
    }
}

ServiceLocator는 ServiceConfiguration에 대한 싱글턴 인스턴스를 하나 가지고 있으면서 애플리케이션에서 사용될 인젝션 대상 클래스의 인스턴스를 얻을 수(Get) 있도록 도와주죠. 그리고 바인딩의 편의를 위해 ImageSearchModelView와 PhotoServiceProviders 속성은 별도로 노출하고 있어요. 이 ServiceLocator는 말 그대로 애플리케이션 전역에서 접근할 수 있는 DI 컨테이너를 서비스하는 클래스이죠.

5. 애플리케이션 전역에 DI 컨테이너를 제공할 수 있도록 준비

애플리케이션 전역에서 ServiceLocator를 접근할 수 있도록 ServiceLocator의 인스턴스를 하나 만들어야죠. 여기에서 App.xaml의 진가가 나오는데요, 바로 App.xaml의 Resources에 ServiceLocator의 새 인스턴스를 하나 등록해두면 애플리케이션 전역에서 StaticResource로 접근할 수 있게 돼요. App.xaml을 보면…

    
        
    

View의 DataContext의 Source를 전역 리소스인 ServiceLocator로 지정했고 데이터의 경로Path는 ImageSearchViewModel로 지정했어요. 즉, ServiceLocator 클래스의 인스턴스에서 ImageSearchViewModel 속성을 읽어 오는거죠.

아주 간단하게 전역 리소스를 만들었죠? 다음으로 넘어가죠.

6. 전역 리소스에서 View에 데이터 바인딩

앞서 우리는 ServiceLocator를 전역에서 접근할 수 있도록 리소스로 만들었는데요, 이걸로 각 View의 ViewModel을 안전하게 접근하여 데이터 바인딩 할 수 있어요.

 

즉, ServiceLocator 클래스의 인스턴스에서 ImageSearchViewModel 속성으로 데이터 바인딩을 하는 거죠.

아직 하나 더 해줘야 할 게 있는데요, 앞서 DataTemplate만 설정하고 실제 데이터 소스를 설정하지 않은 ComboBox죠.

    
        
            
                
                
            
        
    

자, 이제 ViewModel을 독립시킬 차례에요.

7. ViewModel 리모델링

ViewModel은 View가 가질 데이터와 View를 표시하는 로직을 담고 Model로부터 데이터를 얻거나 넘겨주죠. 먼저 기존 ViewModel에서는 제공될 Model에 대한 정보를 이미 가지고 있었어요. 여러 개의 서비스를 사용할 수 있도록 각 서비스가 IPhotoService 인터페이스로 추상화가 되어있지만 결국 실체화된 인스턴스를 만들기 위해 Model의 Concrete 타입을 사용해요. 요컨대 ServiceTypes enum이 있었고 ComboBox에서 서비스를 변경하면 이 enum값에 따라 new LivePhotoService(); new FlickrPhotoService();와 같이 Model의 확정된 타입을 ‘생성’했었죠.

세부적인 코드는 첨부한 파일을 참고하시길 바라고 가장 핵심적으로 수정된 부분을 볼께요. 먼저 생성자 부분.

/// 
/// 기본 생성자
/// 
public ImageSearchViewModel(IPhotoService service)
{
    InitProperties(service);
    SearchCommands.Search.Executed += new EventHandler< HugeFlow.CommandPattern.ExecutedEventArgs>(Search_Executed);
    SearchCommands.Navigate.Executed += new EventHandler< HugeFlow.CommandPattern.ExecutedEventArgs>(Navigate_Executed);
    SearchCommands.ShowLargeImage.Executed += new EventHandler< HugeFlow.CommandPattern.ExecutedEventArgs>(ShowLargeImage_Executed);
    SearchCommands.ChangeService.Executed += new EventHandler< HugeFlow.CommandPattern.ExecutedEventArgs>(ChangeService_Executed);
    SearchCommands.NavigateOriginalImage.Executed += new EventHandler< HugeFlow.CommandPattern.ExecutedEventArgs>(NavigateOriginalImage_Executed);
}

생성자에서는 파라미터로 IPhotoService를 받도록 했고 InitProperties로 전달하고 InitProperties에서는 클래스의 멤버 변수인 _photoService에 전달 받은 IPhotoService의 인스턴스를 ‘주입’하게 되죠. 그리고 만약 블렌드에서 동작중이라면 타이머를 돌려 검색을 시뮬레이션 하고 있고요.

다음으로 ComboBox에서 서비스를 변경했을 때 처리를 보죠.

void ChangeService_Executed(object sender, HugeFlow.CommandPattern.ExecutedEventArgs e)
{
    PhotoServiceProvider provider = e.Parameter as PhotoServiceProvider;
    _photoService = ServiceLocator.Get< IPhotoService>(provider.Name);   // 컨테이너에서 서비스의 참조 얻기

    if (_photoService != null)
    {
        _photoService.SearchImageCompleted += new PhotoSearchEventHandler(_photoService_SearchImageCompleted);
    }

    InitSearch();
}

전에는 각 서비스 모델 별로 직접 생성을 했지만 이제는 ServiceLocator로부터 주어진 PhotoServiceProvider의 이름으로 생성해야 할 타입을 ServiceLocator가 유추하고 생성하여 반환하도록 했어요. 이로써 ViewModel은 외부 Model의 어떤 Concrete타입에도 의존하지 않고 단지 Model들의 추상화된 인터페이스만으로도 완벽하게 동작할 수 있게 되었어요.

꽤 길어졌네요. DI란게 짧은 글 하나로 완벽하게 정리가 될 만한 내용도 아니고 언제 어떻게 적용해야 하는지에 대한 것도 경험이 필요한거죠. 저역시 이제 막 이런 패턴들을 시도하고 익히고 있는 중이에요. 뭔가 설명할 수 있을 만큼 익히면 다시 정리하여 포스팅하기로 할께요. 그리고 다음 포스팅은 똑같은 프로젝트를 Unity가 아닌 Ninject를 사용하여 구현해보도록 하죠.

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo

요즘 실버라이트용 DI(Dependency Injection) 프레임워크로 Pattern&Practice 팀에서 제공하는 Unity Application Block과 독특한 특징을 가진 Ninject, 이 둘을 보고 있죠. 사실 DI의 원리나 구성은 대략 파악했다고 생각하는데 실제 구현은 참 난감한 부분이 많은 것 같아요. 그래서 우리는 이런 공개된 프레임워크의 덕분에 좀 더 쉬운 삶-바로 제가 추구하는 그것!-을 살 수 있어요.

여튼 이 둘은 서로 스타일이 꽤 달라요. 우선 네이밍 부터가 꽤 달라서 이게 과연 같은 패턴을 사용하는 그것이 맞나 싶을 정도. 아주 조금씩만 써보고 있어서 아직 확실하게 감은 못잡았지만 공부하면서 느끼는 차이는 바로 문서화의 차이에요.

Unity는 마이크로소프트의 P&P에서 나온 만큼 철저하게 MSDN 스타일로 구성되어 있죠. MSDN의 장점은 어떤 라이브러리를 매우 광범위하고 자세하게 기술하고 있다는 점인데요 반면 단점은 Getting started를 봐도 딱 그 정도까지만 쓸 수 있을 뿐 그 이상의 기능을 구현하려면 어떤 클래스를 어떻게 사용할지 찾기가 꽤 어렵다는 점이죠. 그래도 최근엔 HOWTO 토픽도 많이 생기고 해서 이런 점은 많이 해소 되었지만, 그래도 여전히 문서가 너무 딱딱해서 어느정도 기합을 넣고 보지 않으면 잘 읽히지가 않아요.

Ninject는 일단 메인 사이트 부터가 굉장히 팬시해서 둘러보는데 부담이 없어요. 그리고 Ninject는 MSDN처럼 완전히 정리된 문서따위는 없지만 Wiki를 통해 아주 깔끔하게 스텝별로 익힐 수 있도록 되어 있어요. 이게 중요한데요, Ninject는 Ninject를 설명할 때 기능 하나하나에 대한 사용법이나 설명을 독립적으로 하는게 아니라 단계별로 기본적인 사용법과 함께 Ninject가 어떤 부분에 주안점을 가지고 있는지 이해할 수 있도록 구성되어 있죠.

베스트 케이스라면 Getting started를 Ninject스타일로 친절하게 구성하고 세부적인 레퍼런스를 MSDN 스타일로 구성하는 거지만 둘 중 하나를 선택하라면 Ninject 스타일의 손을 들어주고 싶네요. 왜냐면 프레임워크나 라이브러리라는 건 일단 처음 사용하기가 굉장히 괴롭거든요. 일종의 진입 장벽이랄까요? 그런 걸 쉽게 덜어주고 그 라이브러리의 설계 철학이나 흐름을 잡는게 중요하다고 봐요.

왜 이런 소릴 하고 있냐면…

Ninject에서는 모듈(Unity의 컨테이너 개념)에 인터페이스와 클래스의 바인딩을 등록할 때 아주 간단하면서도 유용한 조건을 붙여서 하나의 인터페이스에 여러개의 클래스를 조건에 따라 자동으로 인젝션 되도록 되어 있어요. 예를 들면,

public class Swordsman { 
  [Inject] public IWeapon Weapon { get; set; } 
}

public class Ninja { 
  [Inject] public IWeapon MeleeWeapon { get; set; } 
  [Inject, Range] public IWeapon RangeWeapon { get; set; } 
}

Bind().To(); 
Bind().To().Only(When.Context.Target.HasAttribute());

이건 “IWeapon이라는 인터페이스를 Sword클래스와 Shuriken(수리검^^) 클래스에 바인딩을 하되 Context.Target(인젝션 대상, 여기에서는 Weapon, MeleeWeapon, RangeWeapon과 같은 프로퍼티)이 RangeAttribute라는 어트리뷰트를 가지고 있을 때만 Shuriken을 바인딩 해라”를 의미해요. 정말 기가 막히게 팬시하면서 읽기 쉬운 코드 아닌가요? 거의 영어 문장을 읽는 것과 비슷하게 느껴질 정도로요.

또한 예제 코드 역시 Foo나 Bar 따위가 아니라 검사(Swordman)과 닌자(Ninject를 떠올리는!)가 각각 자신의 역할에 맞게 사용할 무기들을 선택하는 과정이 머리속에 그려지도록 잘 선택되었고요.

다른 방법의 예제를 보면

public class Ninja { 
  [Inject] public IWeapon MeleeWeapon { get; set; } 
  [Inject, Tag("range")] public IWeapon RangeWeapon { get; set; } 
}

Bind().To(); 
Bind().To().Only(When.Context.Tag == "range");

비슷한데 Tag라는 미리 지원되는 어트리뷰트를 가지고 문자열 비교를 할 수 있게 한거에요. 이러면 매번 어트리뷰트를 새로 만들지 않아도 되니 편리하겠죠. 마찬가지로 문장은 정말 쉽게 읽히고요.

거기에 한 단계 더 생각해서, 단지 인젝션 대상의 이름 규칙(convention)만으로도 바인딩 할 수 있게 지원하죠.

public class Service { 
  public Service(ISource remoteSource, ISource localSource) { 
    //... 
  } 
}

Bind().To().Only(When.Context.Target.Name.BeginsWith("remote")); 
Bind().To().Only(When.Context.Target.Name.BeginsWith("local"));

Service라는 클래스는 생성될 때 두 개의 ISource를 받죠. 아래의 바인딩 문은 이 클래스에 인젝션할 때 “타겟이 remote라는 이름으로 시작할 때”, “타겟이 local이라는 이름으로 시작할 때”라는 조건을 붙인 걸 읽을 수 있어요. (진짜 볼 수록 감탄…)

참 쉽죠? (ㅋㅋ)

반면 Unity에서 비슷하게 하나의 컨테이너에 하나의 인터페이스를 여러 개의 클래스와 바인딩하고 싶은데 도무지 어디에서 그런 설정을 해야할지 난감하네요. 아마도 Configuration이라는 클래스를 상속받아 구현하는 것 같은데 실버라이트용에서는 그 Configuration이 빠졌다고 하는군요. 여튼 “동적인 조건부 인젝션”을 어떻게 할지 난감해요.

네, 솔직히 저는 Ninject의 손을 들어주고 있는데요, 다만 Unity for Silverlight는 단일 어셈블리에 용량도 조금이나마 작아서 딱 DI만 쓴다고 봤을 때 좀 더 가벼운데 Ninject는 3개의 어셈블리에 용량도 약간 더 커요. 그래서 Unity쪽도 해보고 둘 중에 하나를 선택하자..는 기분으로 써보고 있는데 이놈의 Unity 문서는 읽다보면 스르르 잠에 빠져서 진도가 안나가네요 ㅠ.ㅜ

그래서 오늘의 결론.

라이브러리 문서화를 잘 합시다. 이왕이면 단계별로 가장 핵심적인 내용을 익힐 수 있도록!
AND
Unity 써보신 분 조건적 인젝션 바인딩을 컨테이너에 어떻게 넣는지 좀 알려줍쇼 ㅠ.ㅜ 굽신굽신 oTL oTL

저작자 표시 동일 조건 변경 허락
신고
Posted by gongdo


티스토리 툴바