지난 번에 번역한 YouCard Re-visited : Implementing the ViewModel pattern이라는 글에 이어지는 YouCard Re-visited: Implementing Dependency Injection in Silverlight라는 글을 번역한 것이다.
이번 주에 올린 글에서 나는 YouCard 어플레케이션에서 Model-View-ViewModel 패턴을 구현한 방법에 대해서 썼다. 이번 글에서 나는 Inversion of Control 컨테이너와 Dependency Injection 패턴을 구현하는 방법에 대해 쓰려고 한다. 의존성 주입에 관한 글은 매우 많이 있으므로, 이 글에서는 Silverlight에서 의존성 주입을 사용하는 방법에 대해 집중하려고 한다. 특히, Ninject 프레임워크를 사용하는 방법에 대해 쓸 것이다. 이 패턴에 대해서 아주 잘 설명하고 있는 마틴 파울러의 에세이를 읽어보기를 강력하게 추천한다.
의존성 주입을 써보려고 할 때, 여러분은 이 패턴을 구현하고 있는 수많은 프레임워크들에 질려버릴지도 모른다. 그래서 마치 의존성 주입이라는 개념이 아주 크고 복잡한, 여러분이 소프트웨어를 만드는 방식에 엄청난 변화를 가져오는 개념이라고 생각하게 될지도 모른다. 큰 변화가 생기는 것이 사실일 수는 있다. 그러나 그것은 프레임워크를 사용하기 때문에 생기는 변화가 아니라 의존성 주입이라는 패턴을 도입함으로써 얻을 수 있는 유연성 때문에 생기는 변화일 것이다. 간단히 표현하자면 의존성 주입은 외부의 의존성을 어떻게 여러분의 어플리케이션 클래스에 제공할 것인가 하는 내용이다. 이전의 블로그 글에서 나는 Twitter와 Flickr 연동 부분을, 인터페이스를 구현하는 클래스들로 리팩터링했다. 코드가 Blend에서 실행되는지 웹 브라우저에서 실행되는지에 따라 실제 구현코드가 사용되기도 하고, 디자인 타임에 몇몇 더미 데이터를 제공하는 가짜(mock) 구현코드가 사용되기도 한다.
위에 있는 코드의 문제점은 YouCardData 클래스와 IMicroBlog, IPhotoService 인터페이스를 구현하는 네 개의 클래스 사이에 결합 관계가 있다는 점이다. 뷰-모델(YouCardData)과 그것의 외부적인 의존물들(Twitter, Flickr) 사이에 있는 높은 결합 관계 때문에 코드의 유연성이 떨어진다. 인터페이스를 사용하도록 만들었기 때문에 Picasa 사진 서비스나 Tumblr 마이크로 블로그 서비스 같은 다른 서비스를 사용하는 기능을 구현할 수 있게 되기는 했다. 하지만 새롭게 구현한 서비스 클래스를 YouCardData 클래가 사용하도록 할 방법이 없다. 그리고 외부 서비스들을 가짜로 구현한 클래스를 제공할 수 없으므로 단위 테스트를 하기도 힘들다. 그래서 우리는 아래처럼 의존성 있는 개체를 생성자에 인자로 전달할 수 있도록 리팩터링할 수가 있다.
이렇게 함으로써 YouCardData를 사용하는 쪽에서는 생성자를 통해 의존성을 주입할 수 있게 된다. 이러한 방식을 '생성자를 이용한 의존성 주입' (constructor based dependency injection)이라고 부르는데, 여기서는 사용하는 코드 쪽에서 직접 의존성을 주입하는 식이다. 이렇게 변경함으로써, 다른 서비스 구현을 사용하도록 하거나, 단위 테스트를 하거나, 새로운 기능을 제공함에 있어서 아주 큰 유연성을 얻게 되었다. 하지만 이 코드에는 한 가지 문제가 있다. 데이터 컨텍스트를 생성하거나 하려면 XAML에서 이 클래스 인스턴스를 생성할 필요가 있는데, 이 때에는 파라미터가 없는 생성자가 필요하다. 아래와 같은 생성자를 하나 더 만들어서 이 문제를 해결할 수 있다.
이렇게 하여 XAML에서 선언적인(declarative) 방식으로 생성되는 인스턴스는 Twitter와 Flickr 서비스 구현을 사용하게 되고, 단위 테스트에서는 가짜 구현 개체를 사용할 수 있게 되었다. 하지만 여전히 완벽하지는 않다. 처음 만들었던 생성자에서는 YouCardData 클래스가 Blend에서 사용되고 있는 것인지 아닌지를 체크하도록 했었다. 현재 상태 그대로 유지할 수도 있겠지만 여기서 '제어 역전' (Inversion of Control) 컨테이너의 도움을 받아 보도록 하자.
Ninject
근래에 UI 프레임워크들과 패턴에 대해서 조사를 좀 해보았는데, 그들중 많은 수가 의존성 주입 프레임워크를 쓰고 있었다. Prism-AG 프레임워크는 Composite WPF 프레임워크를 Silverlight로 포팅한 것인데 DLLite라고 하는 작은 의존성 주입 프레임워크를 사용한다. Silverlight로 포팅된 또다른 의존성 주입 프레임워크에는 Unity가 있는데, Microsoft patterns and practices 그룹에서 내놓은 프레임워크 중 하나다. 하지만 내가 가장 좋아하는 것은 Ninject인데 의존성 주입 프레임워크 계의 닌자라고 할 수 있다. AltDotNet 팟캐스트의 5, 6번 에피소드에서 Nate Kohari의 이야기를 듣고 이 프레임워크에 관심을 가지게 되었다. Ninject는 스스로를 다음과 같이 설명하고 있다.
Ninject는 번개같이 빠르고, 엄청나게 가벼운 .NET 기반의 의존성 주입 프레임워크이다. 여러분의 어플리케이션을 느슷하게 결합된, 높은 응집도를 지닌 조각들로 나눈 후, 유연한 방법으로 다시 엮을 수 있는 방법을 제공해주는 프레임워크이다. Ninject를 사용함으로써 여러분의 소프트웨어 아키텍처와 코드는 작성하기 쉽고, 재사용하기 쉬우며, 테스트하고 수정하기에 용이하게 될 것이다.
이 프레임워크의 컴파일된 크기는 110KB 밖에 되지 않고, Ninject.Core.dll만 참조에 추가하면 대부분의 사용 시나리오를 만족시킨다. Ninject의 강점은 XML이 아니라 코드로 설정을 한다는 점이다. 대부분의 IoC 컨테이너는 개체간의 의존성을 설정하는데 XML을 사용한다는 것을 강조하고 있다. 여러분의 소프트웨어 배포 환경이 설치할 때마다 많이 바뀌는 경우에는 XML을 사용하는 것이 이점으로 작용하지만, 많은 경우 아래와 같은 설정상의 부담이 따른다.
- 모든 개체 타입에 대해서 전체 어셈블리 이름(fully qualified assembly name)을 써야하기 때문에 설정 내용이 쓸데없이 많아지고 복잡하게 된다.
- 단순한 오타 때문에 어플리케이션이 제대로 동작하지 않게 되기 십상이다.
- 개체를 구성하는 규칙이 복잡해질수록 XML을 통해서 규칙을 표현한다는 것이 힘들어질 것이다.
그에 반해서 Ninject는 가끔은 "내장된 특수 분야 언어(embedded domain-specific language)"라고 불리기도 하는 늘어놓는 인터페이스(Fluent interface)를 사용한다. 이런 방식은 어플리케이션을 구성하는 데에 프로그래밍 언어의 힘을 빌릴 수 있다는 장점을 갖는다. 복잡하게 얽힌 클래스 간의 연결 관계를 표현할 수 있고, 사용자의 설정에 영향을 받는 클래스 연관 관계가 있을 경우 설정 파일을 읽어들여서 적용하는 방식을 방해하는 것도 전혀 없다.
... 코드를 이용해서 어플리케이션을 조립하는 편이 더 쉬운 경우가 있다. 배포할 때마다 별로 변하는 것이 없는 단순한 어플리케이션을 만드는 경우가 한 가지 예다. 이런 경우에는 별도의 XML 파일보다 몇 줄의 코드가 더 명확할 수 있다.
이와 대조적인 경우가, 조립 방법이 매우 복잡하고 조건에 따른 선택적인 과정이 섞여있는 경우이다. 이렇게 프로그래밍 언어와 비슷해지기 시작하면 XML은 무너지기 시작한다. 명료한 프로그램을 작성하기 위한 문법을 가진 실제 언어를 사용하는 편이 좋다.
... 이런 경우의 내 충고는, 항상 코드를 통해서 모든 설정을 할 수 있는 방법을 제공하고 나서, 분리된 설정 파일은 선택적인 기능으로 생각하라는 것이다. - 마틴 파울러
"… There are cases where it’s easier to use program code to do the assembly. One case is where you have a simple application that’s not got a lot of deployment variation. In this case a bit of code can be clearer than separate XML file.
A contrasting case is where the assembly is quite complex, involving conditional steps. Once you start getting close to programming language then XML starts breaking down and it’s better to use a real language that has all the syntax to write a clear program.
…My advice here is to always provide a way to do all configurations easily with a programmatic interface, and then treat separate configuration file as an optional feature. – Martin Fowler" (Inversion of Control Containers and the Dependency Injection pattern)
Ninject의 또다른 멋진 부분이 시작부터 Silverlight를 위해서 만들어졌다는 점이다. 즉, 원래 존재하던 더 큰 프레임워크를 싹둑 잘라서 Silverlight로 포팅한 것이 아니라는 말이다. 일단 Ninject는 작고 가볍다.
Dependency Injection with Ninject
Ninject를 사용하는 일은 아주 쉽다. 우선 우리가 해야할 일은 의존성 주입을 받는 생성자를 찾아서 간단한 어트리뷰트를 추가하는 일이다.
또, 화면에 보이는 모든 카드를 제어하는 메인 페이지의 뷰-모델에도 영속성 서비스를 위해서 의존성 주입을 사용하고 싶다.
Twitter 서비스를 구현한 클래스를 쓸 때에는 프록시 URL을 주입시키고 싶을 것이다. Twitter는 크로스도메인 접근을 허용하지 않기 때문에 우리가 운영하는 서버를 통해서 요청을 보내도록 해야한다.
다음으로 우리가 해야할 일은 뷰-모델과 서비스들을 연결하는 작업이다. 많은 IoC 컨테이너에서 이 작업은 XML 설정을 통해 이뤄지지만 Ninject에서는 늘어놓은 인터페이스 스타일의 코드를 이용해서 작업이 이뤄진다. Ninject는 모듈이라는 개념을 사용해서 연결 작업을 수행한다. 하나의 커널(컨테이너)이 여러 모듈을 책임질 수 있다. YouCard 어플리케이션에서는 하나의 모듈이면 충분하다.
늘어놓는 인터페이스는 코드의 가독성을 높여준다. 그래서 굳이 위에 있는 클래스를 한 줄 한 줄 설명하는 데에 시간을 낭비할 필요는 없을 것 같다. 이 클래스는 StandardModule을 상속해서 Load 메서드를 재정의 하고 있다. Load 메서드에서는 서비스들과 그 서비스의 구체(concrete) 타입을 바인딩하고 있다. 바인딩 코드 한 줄 한 줄을 영어 문장이라고 생각하고 왼쪽에서 오른쪽으로 읽어보면 바인딩 규칙들이 아주 쉽게 이해된다. 또 이 코드가 Blend에서 실행되는 경우에 어떤 구현을 사용할 지 결정하는 부분에서 코드를 통한 설정 방식의 장점이 잘 드러나고 있음을 눈여겨 볼 필요가 있다. 웹 브라우저에서 실행되는 경우에는 Twitter 서비스를 사용하고 Blend에서 실행되는 경우에는 가짜로 구현된 서비스를 사용하도록 조건을 추가해두었다.
이 모듈을 사용하려면 간단히 Ninject 커널을 생성해서 모듈에 전달하면 된다.
YouCardData 개체를 어떻게 생성할 지 걱정할 필요가 없다는 사실을 알아두기 바란다. Ninject 커널이 개체 생성을 모두 책임지고 YouCardModule에 정의된 바인딩 규칙에 따라 의존성을 주입해 준다.
Data binding to objects built by Ninject
여기까지 우리는 Ninject 커널을 설정했다. 다음 과제는 커널에서 얻은 뷰-모델 개체를 뷰에 넣어주는 것이다. 물론 Blend에서의 디자인 타임 경험을 유지하면서 말이다. 코드를 이용해서 데이터 컨텍스트를 세팅하는 것은 아주 간단하다. Application 클래스에서 커널을 생성하도록 하여 아래와 같은 일을 할 수 있다.
이렇게 하면 어플리케이션에 단 하나의 커널만 존재하게 할 수 있다. 하지만 여전히 어떻게 하면 선언적인 XAML 코드를 사용해서 Ninject가 생성해준 뷰-모델을 사용자 컨트롤의 데이터 컨텍스트에 설정할 수 있는가 하는 문제는 풀지 못했다. 며칠 간을 고민하고, 여러 가지 접근 방법을 시험해보고, Nate Kohari와 의논도 해본 후에 얻은 해답이 Service Locator였다. 이것은 IoC를 구현하는 또다른 패턴이다. 아마 여러분은 서비스 로케이터를 쓸 것인지 의존성 주입을 쓸 것인지 결정할 일이 자주 있을 것이다. 이 패턴에 대해 잘 설명하고 있는 마틴 파울러의 에세이 중 Service Locator vs Dependency Injection 부분을 읽어 보기를 추천한다.
서비스 로케이터를 간단히 설명하자면, 어플리케이션에서 사용하는 모든 서비스들의 참조를 가지고 있는 등록부라고 할 수 있다. YouCard에서라면 뷰-모델 같은 클래스는 IPhotoService나 ITwitterService의 구현체를 얻기 위해서 서비스 로케이터에 문의할 수 있다는 말이다. 서비스 로케이터와 의존성 주입 사이의 큰 차이는, 서비스 로케이터를 쓸 때는 여러분의 클래스가 구현체를 요청 하지만 의존성 주입을 쓸 때는 구현체를 클래스에 넣어준다는 점이다(pull vs. push). Ninject와 서비스 로케이터 패턴을 어떤 식으로 잘 섞어 쓸 수 있는지 Nate가 블로그에 글을 올린 것이 있다.
나는 서비스 로케이터와 의존성 주입 패턴이 서로 배타적이라고 생각하지 않는다. 사실 나는 Ninject 같은 의존성 주입 프레임워크와 서비스 로케이터 패턴을 같이 썼을 때 아주 효과적일 수 있다는 것을 발견했다. - Nate Kohari
YouCard에서 내가 만든 서비스 로케이터는 아주 간단하다.
많은 경우 서비스 로케이터는 정적 클래스로 구현된다. 하지만 Silverlight 2에서는 정적 클래스에 대한 데이터 바인딩은 지원되지 않기 때문에 그 방식은 쓸 수 없다. 그래서 정적 생성자를 만들고, 사적인 정적 kernel 필드를 만드는 방식으로 처리했다. 정적 생성자는 다른 모든 생성자들 보다 먼저 클래스에서 가장 처음 실행된다. 정적 생성자에서는 커널이 있는지 확인해서 없으면 우리가 바인딩 설정을 하면서 만든 모듈을 사용하는 커널 인스턴스를 만든다. 커널 필드를 정적으로 만들었기 때문에, 어플리케이션에 커널 인스턴스가 하나만 존재함을 보장받을 수 있다. 클래스에는 뷰-모델을 커널로부터 얻어내는 두 개의 인스턴스 속성과, 모든 타입의 개체를 컨테이너로부터 얻을 수 있는 제네릭 인스턴스 메서드가 있다.
의존성 주입 개념을 도입하기 전에는 뷰와 뷰-모델을 이런 식으로 연결했다.
서비스 로케이터를 도입한 후에는 몇 가지 바꿀 것이 있다. 첫 번째로는 서비스 로케이터를 어플리케이션 수준의 리소스로 생성시킨다.
그 후에 각각의 뷰에서 데이터 컨텍스트를 얻는 방법을 변경한다.
각각의 뷰는 해당하는 뷰-모델을 서비스 로케이터의 속성에 데이터 바인딩하여 얻는다. (역자 첨언 : 위에서 YouCardViewModel은 더 위의 ServiceLocator 클래스 구현에서는 YouCardData라고 정의되어 있으므로, YouCardData라고 쓰는 게 맞는 것 같다.) 이 접근 방식의 가장 멋진 부분은, Blend의 디자인 타임을 완벽하게 지원하고 있다는 것과 디자인 타임에 가짜 구현 서비스를 통해 샘플 데이터를 제공한다는 것이다.
Conclusion
Silverlight에서 의존성 주입을 사용하는 것은 다른 .NET 어플리케이션에 쓰는 것보다 절대 어렵지 않다. 프레임워크를 가볍고 빠르게 유지하는 데 중점적인 노력을 쏟고 있다는 점 때문에 Ninject는 Silverlight에 잘 어울린다. 그러나 이렇게 간단한 샘플 프로젝트에서 Ninject를 사용하는 것은 쓸데없는 짓이라고 생각하는 사람도 있을 것이다. 의존성 주입 코드를 서비스 로케이터 안에 직접 작성할 수가 있고, 그렇게 해서 Ninject에 대한 의존성을 없애면서도 여전히 유연성은 유지할 수 있다. 반면, Ninject는 110KB (XAP 파일에 넣어서 압축하기 전 크기) 밖에 안되어서 어플리케이션 크기에 별로 영향을 주지 않는다. 100KB면 이 블로그 글에 사용한 이미지 크기의 절반도 되지 않는다.
의존성 주입을 사용함으로써 여러분은 코드를 더 유연하고 테스트하기 쉽게 유지할 수 있다. Silverlight 어플리케이션에서는 특히 이런 점들이 중요하다. 여러분이 Silverlight의 강력함을 알아감에 따라 UI 요구사항도 많이 바뀌어 갈 것이기 때문이다. 나는 Blend를 사용하는 디자이너에게 최상의 경험을 제공하는 것이 중요하다고 확신하고 있고, 의존성 주입이 그것을 바꿀 이유는 없다.
이 글은 스프링노트에서 작성되었습니다.