태그 : martinfowler

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 (4/4)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]
어떤 것을 쓸 것인가?

지금까지는 제가 이 패턴과 변종들을 어떻게 이해하는지를 집중적으로 설명했습니다. 이제, 언제 어떤 것을 쓸면 좋을지 판단하는 데 도움이 될 만한 장단점을 이야기할 수 있게 되었습니다.

서비스 위치탐색기 대 의존성 주입

서비스 위치탐색기와 의존성 주입 사이에서 어떤 것을 선택하느냐가 가장 근본적입니다. 이 두 구현체 모두 기본적으로 객체 간의 결합을 끊어 줍니다. 결합은 '빈약한 예제'의 결함이었습니다. 두 기술을 적용한 사례 모두 사용하는 쪽 코드가 서비스 인터페이스의 구현물에 의존하지 않았습니다. 서비스를 사용하는 응용 클래스에 구현물을 제공하는 방법이 두 패턴의 중요한 차이점입니다. 서비스 위치탐색기 방식에서는 응용 클래스가 명시적으로 위치탐색기를 호출하여 서비스를 요청합니다. 주입 방식에서는 명시적인 요청이 없습니다. 사용하기 원하는 서비스가 응용 클래스에 들어갑니다. 그래서 제어 역전이라고 합니다.

제어 역전은 프레임워크들의 일반적인 특징이지만 비용을 내해야 합니다. 프레임워크는 대체로 이해하기 어렵고 디버깅을 하려고 할 때 문제가 생기기도 합니다. 그래서 전 보통 필요하지 않으면 프레임워크를 쓰지 않는 편입니다. 그렇다고 프레임워크가 나쁘다는 뜻은 아닙니다. 단지 프레임워크들이 보다 직접적인 방식의 대안들에 대해 자신의 가치를 스스로 증명해야 할 필요가 있다는 생각이다.

서비스를 사용하는 모든 객체가 위치탐색기에 의존한다는 것이 서비스 위치탐색기의 가장 큰 차이점입니다. 위치탐색기로 다른 구현물들에 대한 의존성을 숨길 수 있지만 위치탐색기에는 의존해야 합니다. 그래서 위치탐색기와 주입 사이에서의 결정은 의존성이 문제가 되는지 여부에 따릅니다.

의존성 주입을 쓰면 컴포넌트가 무엇에 의존하고 있는지 파악하기 쉽습니다.  의존성 주입에서는 단순히 생성자 같은 주입 메커니즘만 살피면 의존성을 알 수 있습니다. 서비스 위치탐색기에서는 위치탐색기를 호출하는 부분의 소스코드를 찾아 읽어봐야 합니다. 최신 IDE의 참조 찾기 기능을 쓰면 이런 작업을 쉽게 할 수 있지만, 여전히 생성자나 변경 메소드를 보기 만큼 쉬운 건 아닙니다.

서비스를 사용하는 객체의 특징에 따라 대부분이 결정됩니다. 서비스 하나를 사용하는 여러 유형의 클래스로 된 응용 소프트웨어를 만들 때라면 응용 클래스가 위치탐색기에 의존하는 게 큰 문제가 안 됩니다. 친구들에게 준 예의 MovieLister에서는 서비스 위치탐색기가 아주 잘 작동했습니다. 친구들이 해야 할 일이라고는 올바른 서비스 구현물을 집어오도록 위치탐색기를 설정하는 것뿐입니다. 설정을 코드로 하든 아니면 파일로 하든 상관없이 말입니다. 이런 시나리오에서는 주입기의 역전이 그리 매력적으로 보이지 않습니다.

MovieLister가 컴포넌트로서 다른 사람이 작성하는 응용 프로그램에 사용된다면 좀 다릅니다. 이 경우에는 고객들이 사용하려는 서비스 위치탐색기의 API에 대해 그리 잘 알지 못합니다. 고객은 각각 서로 호환이 안 되는 자신만의 서비스 위치탐색기를 가지고 있을 것입니다. 격리된 인터페이스(segregated interface)를 쓰면 어느 정도 이 문제를 해결할 수 있습니다. 고객들이 직접 이 인터페이스로 자신들의 위치탐색기에 맞는 어댑터를 만들 수 있습니다. 그렇지만 어떤 경우라도 여전히 특정 격리 인터페이스를 찾으려면 첫 번째 위치탐색기를 살펴봐야 합니다. 그리고, 일단 어댑터가 등장하면 위치탐색기를 직접 다루는 단순성은 약해지기 시작합니다.

주입에서는 컴포넌트가 주입기에 의존하지 않기 때문에 컴포넌트가 한번 설정되고 나면 주입기에서 추가적인 서비스들을 얻을 수 없습니다.

사람들은 보통 테스트를 더 쉽게 할 수 있어서 의존성 주입을 선호합니다. 여기서 핵심은 테스트하려면 실 서비스 구현을 스텁(stub)이나 목(mock)으로 쉽게 바꿀 수 있어야 한다는 것입니다. 그렇지만 의존성 주입과 서비스 위치탐색기 사이에는 이런 면에서 진짜 어떤 차이도 없습니다. 양쪽 모두 스텁을 쓰기 아주 좋습니다. 서비스 위치탐색기를 쉽게 교체할 수 있도록 노력하지 않은 프로젝트에서 나온 관찰 결과는 의심스럽습니다. 이런 데는 지속적 테스트가 도움이 됩니다. 테스트를 하면서 서비스를 스텁으로 바꾸기 쉽지 않다면 설계에 심각한 문제가 있다는 뜻입니다.

물론 테스트 문제는 자바 EJB 프레임워크 같이 심하게 침투적인(intrusive) 컴포넌트 환경 때문에 악화됩니다. 이런 종류의 프레임워크는 응용 프로그램 코드에 주는 영향이 최소화되어야 한다고 봅니다. 특히, 코드를 수정하고 실행하는 주기가 느려지게 하는 일은 해서는 안 됩니다. 무거운 컴포넌트를 대체하는 플러그인을 쓰면 이런 작업에 큰 도움이 됩니다. 이는 테스트 주도 개발 같은 기법에는 성패가 달린 결정적 조건입니다.

이처럼 핵심은 외부의 응용 프로그램에 쓰일 것으로 예상하는 코드의 작성자와 관련되어 있습니다. 이런 프로그램에는 컴포넌트 제작자가 어떤 제어를 할 수가 없습니다. 이 경우에는 서비스 위치탐색기 도입을 아무리 최소한으로만 하려 한다 해도 문제가 됩니다.

생성자 주입 대 변경자 주입

서비스를 조합하는 데에는 서비스들을 서로 연결하는 항상 어떤 규약이 있어야 합니다. 주입은 매우 간단한 규약만 있으면 되는데 이것이 주입의 본질적인 강점입니다. 최소한 생성자 주입과 변경 주입은 그렇습니다. 컴포넌트에 잡스런 어떤 작업도 하지 않아도 되고 주입기 입장에서도 모든 설정 작업을 상당히 직접적이고 단순하게 할 수 있습니다.

인터페이스 주입은 설정 작업에 필요한 많은 인터페이스를 만들어야 하기 때문에 상대적으로 침투적입니다. 아발론의 방식처럼 몇몇 인터페이스만 있어도 된다면 그렇게 나쁘지만은 않습니다. 그렇지만 인터페이스 주입은 컴포넌트와 의존 객체들을 조립하는데 많은 작업을 해야 합니다. 이런 이유로 요즘 등장하는 경량 컨테이너들은 변경자와 생성자 주입을 합니다.

변경자와 생성자 주입 사이에서 선택하는 문제는 객체지향 프로그래밍의 일반적인 논쟁을 반영하기 때문에 흥미있습니다. 즉 객체의 필드에 값을 생성자에서 채울 것이냐 변경자에서 할 것이냐는 문제이다.

객체와 관련해서 오래전부터 전 기본적으로 가능한 생성 시에 객체를 유효하게 만듭니다. 이 조언은 켄트백의 "스몰톡 우수 기법 패턴(Smalltalk Best Practice Patterns)"에 실린 생성자 메소드와 생성자 매개변수 메소드 패턴까지 거슬러 올라갑니다. 매개변수가 있는 생성자는 적합한 장소에서 유효한 객체를 만들라는 뜻을 명백하게 표현합니다. 객체 초기화 방식이 하나 이상이라면  다른 조합을 표현하는 여러 생성자를 만듭니다.

단순히 변경자 없앰으로써 불변 필드를 확실히 숨길 수 있는 것도 생성자 초기화의 장점입니다. 이 점은 중요합니다. 무엇인가 바뀌지 말아야 할 때도 변경자는 변경사항을 고스란히 전달하는 결함이 있습니다. 초기화에 변경자를 쓰면 이런 특징 때문에 고민하게 됩니다. (실제로 이런 상황에 저는 변경자를 쓰는 방식을 배제하곤 합니다. 전 initFoo 같은 메소드를 써서 생성될 때에만 수행되어야 할 무엇이라는 것을 강조하는 편입니다.)

하지만 모든 상황에는 예외가 있습니다. 생성자 매개변수가 너무 많으면 무척 보기 지저분합니다. 키워드 매개변수가 없는 언어에서는 특히 그렇습니다. 생성자가 길면 대개 객체가 너무 많은 일을 하고 있다는 것을 뜻하기 때문에 나눠야 하는 것이 맞지만 의도적으로 생성자를 길게 만들어야 할 때도 있습니다.

객체를 유효하게 생성하는 방법이 여러 가지 있다면 생성자가 매개변수의 숫자와 타입만으로 구분할 수 있는 이상 생성자로 구분해서 표현하기 어려울 수 있습니다. 이때가 팩토리 메서드가 활약할 때입니다. 팩토리 메서드는 private 생성자와 변경자의 조합으로 유효한 객체를 생성합니다. 컴포넌트 조합에 쓰는 전통적 팩토리 메서드는 보통 정적 메서드(static method) 형태이기 때문에 인터페이스에 둘 수가 없습니다. 팩토리 클래스를 만들 수도 있지만 그러면 주입해야 할 또 다른 서비스 인스턴스가 생길 뿐입니다. 팩토리 서비스가 가끔 좋은 전략일 수 있지만 팩토리를 생성하려면 이 글에서 다루는 IoC 기술 중 하나를 써야만 합니다.

문자열같이 단순한 매개변수를 가진 생성자도 골치입니다. 변경자 주입에서라면 각 변경자의 이름으로 문자열 매개변수가 무엇에 쓰는 것인지 나타낼 수 있습니다. 생성자는 매개변수의 위치에 의존할 뿐이어서 용도를 알기 어렵습니다.

생성자가 여러 개이고 상속을 했다면 특히 곤란해질 수 있습니다. 모든 것을 초기화를 하려면 자신만의 매개변수를 추가하면서도 부모 클래스의 생성자 하나하나를 호출하는 생성자들을 만들어야 합니다. 자칫하면 생성자가 폭발적으로 많아질 수도 있습니다.

이런 단점이 있어도 전 생성자 주입부터 시작하기 좋아합니다. 그렇지만 지금까지 지적한 문제가 진짜 문제가 되기 시작하면 즉시 변경자 주입으로 바꿀 수 있도록 준비를 합니다.

생성자 주입과 변경자 주입을 비교하는 문제 때문에 의존성 주입 기능이 있는 프레임워크의 제작팀 사이에서 많은 논쟁이 있었습니다. 그렇지만, 이들 프레임워크를 만드는 대부분의 사람은 비록 어느 한 쪽을 선호한다 하더라도 두 가지 방식을 모두 지원하는 것이 중요하다는 것을 이해하는 듯합니다.

코드 대 설정 파일

서비스들을 엮을 때 설정 파일을 쓸 것인지 아니면 코드로 API를 써서 할 것인지 선택하는 문제는 개별적이면서도 종종 혼용할 수도 있는 문제입니다. 여러 곳에 배포될 것으로 예상하는 애플리케이션 대부분은 별도 설정 파일 쪽이 대체로 타당합니다. 대부분의 경우 XML 파일일 것이고 XML은 이런 일에 잘 맞습니다. 하지만 프로그램 코드로 서비스를 조립하기가 더 쉬울 때도 있습니다. 한가지 사례로 배포 환경이 비슷비슷한 단순한 응용프로그램을 들 수 있습니다. 이 경우 분리된 XML 파일보다 코드가 조금 더 깨끗할 수 있습니다.

조건에 따라 분기하는 단계가 포함되어 조합이 매우 복잡한 경우에는 차이가 분명합니다. 한번 프로그래밍 언어에 친숙해지기 시작하면 XML은 서서히 쓰지 않을 것이고 진짜 언어를 쓰는 것이 더 나을 겁니다. 프로그래밍 언어는 깨끗한 프로그램을 짜는 데 필요한 모든 문법을 가지고 있습니다. 코드로 조립한다면 빌더 클래스를 만들 것입니다. 독특한 여러 구축 계획이 있다면 빌더 클래스들을 여러 개 마련하고 간단한 파일을 써서 어떤 것을 선택할지 설정할 수 있습니다.

종종 사람들이 필요 이상으로 설정 파일을 정의하는데 열심인 것으로 보입니다. 프로그래밍 언어는 보통 직접적이고 강력한 설정 메커니즘을 만듭니다. 최신 언어들은 대규모 시스템의 일부로 조립해 쓸 수 있는 작은 조립기를 쉽게 컴파일할 수 있습니다. 컴파일하는 게 문제라면 같은 작업을 수행할 수 있는 스크립트 언어도 있습니다.

프로그래머가 아닌 사람이 수정할 수 있어야 하기 때문에 설정에 프로그래밍 언어를 쓰면 안 된다는 말을 듣곤 합니다. 그렇지만 이런 경우가 얼마나 될까요? 사람들이 진짜 복잡한 서버 응용 프로그램의 트렌젝션 격리 수준(transaction isolation level)을 비 프로그래머가 조정하기 원할까요? 프로그래밍 언어가 아닌 설정 파일을 써야만 단순하게 작업할 수 있는 걸까요? 설정 파일이 복잡해지면  적당한 프로그래밍 언어를 사용해볼 것을 생각하기 시작해야 할 시점입니다.

지금 이 순간 자바 세상에는 설정 파일 사이에 불협화음이 있습니다. 컴포넌트마다 다른 것들과 다른 자신만의 고유 설정파일을 가지고 있습니다. 만약 이런 컴포넌트 수십 개를 가지고 있다면 설정파일 수십 개를 서로 맞추느라 금방 질리고 말 것입니다.

여기서 저는 항상 프로그래밍 인터페이스를 써서 쉽게 모든 설정을 할 수 있도록 할 것을 제안합니다. 그러고 나면 별도 설정 파일을 다루는 일은 선택사항이 됩니다. 프로그래밍 인터페이스를 써서 설정 파일을 다루도록 하는 건 쉽습니다. 컴포넌트를 작성한다면 프로그래밍 인터페이스를 쓸지, 정해진 형태의 설정 파일을 쓸지, 자작 설정 양식을 만들고 프로그래밍 인터페이스에 이것을 붙일지 여부를 컴포넌트의 사용자에게 남겨두십시오.

사용과 설정의 분리

서비스를 사용하는 곳에서 설정을 분리하도록 하는 것이 그 무엇보다 중요합니다. 실제로 이는 구현에서 인터페이스를 분리하라는 기본적인 설계 원칙에 들어맞습니다. 이 원칙은 객체지향 프로그래밍에서 조건문으로 어떤 클래스를 생성할지 결정할 때에 볼 수 있습니다. 이렇게 생성을 한 후에는 중복된 조건문으로 조건을 평가하는 일이 다형성으로 대체됩니다.  

이런 분리가 단일 코드 베이스에서 유용하다면 서비스나 컴포넌트 같은 외래 요소를 쓸 때는 특히 절대적입니다. 먼저 특정 배포 환경에서 어떤 구현 클래스를 쓸지 선택하는 것을 나중으로 미루고 싶은지 질문해야 합니다. 만약 그렇다면 플러그인의 어떤 구현물을 사용해야 합니다. 일단 플러그인을 쓴다면 이 플러그인들을 조립하는 작업은 반드시 응용 프로그램의 다른 부분들과 분리해야 합니다. 이렇게 해야 다른 배포 환경에 맞는 설정을 쉽게 교체할 수 있습니다. 어떻게 이것을 하느냐는 것은 부차적인 문제입니다. 설정 메커니즘은 서비스 위치탐색기를 설정하는 것일 수도 있고 직접 객체를 설정하도록 주입을 사용하는 것일 수도 있습니다.

그 외 주제들

이 글에서는 의존성 주입과 서비스 위치탐색기로 서비스를 설정하는 기본 문제에 집중했습니다. 여전히 주목해서 다룰만한 주제가 더 있지만 아직 파고들 기회가 없었습니다. 특히 생애 주기 행태(life-cycle behavior)와 관련된 토론 주제가 있습니다. 어떤 컴포넌트는 (멈춤이나 시작 같은) 고유의 생애 주기 이벤트를 가지고 있습니다. 경량 컨테이너드에서 관점 지향을 사용하는 것도 점점 많은 관심을 받는 또 다른 토론 주제입니다. 비록 이 글에서는 관점 지향을 다루지 않았지만 이에 대해 내용을 추가하거나 따로 쓰거나 할 생각입니다.

경량 컨테이너 관련 웹 사이트를 찾아보면 지금까지 다룬 주제에 대해서는 더 많이 찾을 수 있을 것입니다. 피코 컨테이너와 스프링 웹 사이트를 방문하면 이들 주제와 관련된 더 많은 논의와 새로운 주제의 시작을 접할 수 있습니다.

마치면서...

요즘 쇄도하는 경량 컨테이너는 모두 서비스를 조립하는 방법과 관련하여 의존성 주입이라는 공통 패턴에 근거를 두고 있습니다. 의존성 주입은 서비스 위치탐색기의 유용한 대안입니다. 응용 클래스를 만들 때 이 둘은 거의 같습니다. 그렇지만 서비스 위치탐색기가 더 직접적이기 때문에 약간은 앞선다고 생각합니다. 그렇지만 여러 응용 프로그램에서 쓰이는 클래스를 만든다면 의존성 주입을 선택하는 것이 더 낫습니다.

서비스 위치탐색기와 의존성 주입 간의 선택은 응용 프로그램 안에서 서비스를 사용하는 부분과 서비스를 설정하는 부분을 분리한다는 원칙보다 중요하지는 않습니다.
[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]

by 박성철 | 2009/10/31 16:38 | 프로그래밍 이야기 | 트랙백(1) | 덧글(0)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 (3/4)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]


서비스 위치탐색기(Service Locator) 사용

MovieLister 클래스가 MovieFinder의 구현물에 종속되지 않게 하는 것이 의존성 주입으로 얻을 수 있는 주요 이점 입니다. 이로써 영화 목록 프로그램을 친구들에게 줘서 각자가 자기 환경에 적합한 구현물을 선택하도록 할 수 있게 되었습니다. 그러나 주입이 이 의존성을 끊는 유일한 방법은 아닙니다. 또 다른 방법은 서비스 위치탐색기를 쓰는 것입니다.

서비스 위치탐색기 뒤에 있는 기본적인 생각은 응용 프로그램이 필요로 할 서비스들의 전체 소유권을 어떻게 취할지 알고 있는 어떤 객체를 두자는 것입니다. 따라서 이 응용 프로그램의 서비스 위치탐색기에는 필요시 영화 찾기 객체를 반환하는 메소드가 하나 있을 것입니다. 물론 이는 부담을 살짝 미룬 것 뿐입니다. 여전히 목록 객체에게 위치탐색기를 줘야 합니다.`\ 결과적인 의존성은 그림 3과 같습니다.

그림 3 : 서비스 위치탐색기에서 의존성

이 경우에는 ServiceLocator을 싱글톤 등기소(Registry)로 쓰려합니다. 그리고 목록 객체는 인스턴스화 할 때 이 저장소를 써서 찾기 객체를 얻습니다.
class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
주입 방식에서처럼 서비스 위치탐색기를 설정해야만 합니다. 여기서는 코드로 설정했지만 설정 파일에서 적절한 데이터를 읽는 방식을 쓰는 게 어렵지는 않습니다.
class Tester...
private void configure() {
ServiceLocator.load(new ServiceLocator(new ColonMovieFinder("movies1.txt")));
}
class ServiceLocator...
public static void load(ServiceLocator arg) {
soleInstance = arg;
}

public ServiceLocator(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
테스트 코드는 이렇습니다.
class Tester...
public void testSimple() {
configure();
MovieLister lister = new MovieLister();
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
종종 서비스 위치탐색기 같은 방식은 구현물을 교체할 수 없기 때문에 테스트하기 힘들어 좋지 않다는 듣습니다. 분명히 이런 종류의 문제에 빠지만한 나쁜 설계를 할 수 있습니다. 그렇지만 반드시 그렇게 되는 것은 아닙니다. 이 경우 서비스 위치탐색기 예는 단순히 데이터를 담고 있있기만 합니다. 테스트용으로 만든 서비스로 위치탐색기를 쉽게 만들 수 있습니다.

보다 정교한 위치탐색기를 쓰려면 서비스 위치탐색기의 서브클래스를 만들고 그 서브클래스를 등기소의 클래스 변수에 전달하도록 할 수 있습니다. 직접 인스턴스 변수에 접근하지 않고 인스턴스의 메소드를 호출하도록 정적 메소드(static method)를 바꾸도 좋습니다. 쓰레드에서만 쓰는 저장소를 써서 쓰레드 전용 위치탐색기를 제공할 수도 있습니다. 이 모든 것을 서비스 위치탐색기를 사용하는 쪽 코드를 바꾸지 않고도 할 수 있습니다.

서비스 위치탐색기는 싱글톤이 아니라 등기소로 봐야합니다. 싱글톤은 등기소를 구현하는 손 쉬운 방법이기는 하지만 이 구현 방식은 쉽게 바뀔 수 있습니다.

위치탐색기용 격리 인터페이스 사용하기

지금까지의 단순한 접근법은 MovieLister가 전체 서비스 위치탐색기 클래스에 의존한다는 문제가 있습니다. 심지어 필요한 서비스가 하나 뿐인데도 말입니다. 격리 인터페이스(segregated interface)를 써서 이 문제를 완화 할 수 있습니다. 전체 서비스 위치탐색기 인터페이스를 쓰는 대신 목록 객체는 필요한 일부 인터페이스만 선언하도록 할 수 있습니다.

이 상황에서 lister 인터페이스를 제공하는 측에서는 finder의 소유권을 얻는 데 필요한 위치탐색기 인터페이스도 제공해야 할 것입니다.
public interface MovieFinderLocator {
public MovieFinder movieFinder();
이제 찾기 객체에 접근하도록 하려면 위치탐색기가 이 인터페이스를 구현해야 합니다.
    MovieFinderLocator locator = ServiceLocator.locator();
MovieFinder finder = locator.movieFinder();
   public static ServiceLocator locator() {
return soleInstance;
}
public MovieFinder movieFinder() {
return movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;
눈치챘겠지만 인터페이스를 사용하기로 한 이상 더는 정적 메소드로 서비스에 접근할 수 없습니다. 위치탐색기의 인스턴스를 얻고 이것을 써서 필요로 하는 것을 얻으려면 클래스를 사용해야 합니다.

동적 서비스 위치탐색기

위의 예제는 정적이었습니다. 서비스 위치탐색기는 외부에서 필요로하는 서비스 마다 메소드를 하나씩 가지고 있습니다. 이것만이 유일한 방법은 아닙니다. 동적 서비스 위치탐색기를 만들어 원하는 어떤 서비스도 보관할 수 있고 실행시에 선택하도록 할 수 있습니다.

이번에는 서비스 위치탐색기가 서비스를 하나씩 저장하는 필드를 가지고 있지 않고 맵(map)을 사용하며 서비스를 얻고 등록하는 범용 메소드를 가지고 있습니다.
class ServiceLocator...
private static ServiceLocator soleInstance;
public static void load(ServiceLocator arg) {
soleInstance = arg;
}
private Map services = new HashMap();
public static Object getService(String key){
return soleInstance.services.get(key);
}
public void loadService (String key, Object service) {
services.put(key, service);
}
설정할 때에는 적당한 키로 서비스를 등록합니다.
class Tester...
private void configure() {
ServiceLocator locator = new ServiceLocator();
locator.loadService("MovieFinder", new ColonMovieFinder("movies1.txt"));
ServiceLocator.load(locator);
}
같은 키 값을 써서 이 서비스를 사용합니다.
class MovieLister...
MovieFinder finder = (MovieFinder) ServiceLocator.getService("MovieFinder");
저는 전적으로 이 방식을 좋아하지 않습니다. 확실히 유연하기는 하지만 그다지 명시적이지 않습니다. 서비스를 찾을 수 있는 유일한 방법은 문자적인 키 뿐입니다. 전 명시적인 방법을 좋아합니다. 인터페이스 선언을 보면 찾고자 하는 것이 어디에 있는지 알 수 있기 때문입니다.

아발론(Avalon)으로 위치탐색기와 주입을 동시에 사용하기

의존성 주입과 서비스 위치탐색기는 꼭 상호 배타적인 개념인 건 아닙니다. 둘을 동시에 사용하는 좋은 예가 아발론 프레임워크입니다. 아발론은 서비스 위치탐색기를 사용합니다. 하지만, 위치탐색기가 어디에서 있는지 알려줄 때는 주입을 사용합니다.

Berin Loritsch가 아발론을 사용한 우리 예제의 단순한 버전을 보내왔습니다.
public class MyMovieLister implements MovieLister, Serviceable {
private MovieFinder finder;

public void service( ServiceManager manager ) throws ServiceException {
finder = (MovieFinder)manager.lookup("finder");
}
이 service 메소드는 컨테이너가 MyMovieLister에게 서비스 메니저를 알려줄 수 있도록 하는 인터페이스 주입의 예입니다. 이 서비스 메니저는 서비스 위치탐색기의 한 예입니다. 이 예에서 목록 객체는 메니저를 필드에 보관하지 않습니다. 대신, 메니저를 써서 바로 찾기 객체를 찾아 저장합니다.

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]


by 박성철 | 2009/10/11 13:37 | 프로그래밍 이야기 | 트랙백 | 덧글(0)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 (2/4)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]


의존성 주입의 여러 유형

의존성 주입의 기본 착상은 조립을 담당하는 객체를 따로 두는 것입니다. 이 객체는 MovieLister 클래스의 필드에 MovieFineter 인터페이스의 구현물 중 적당한 것을 넣어 줍니다. 결국, 의존 관계는 그림 2처럼 됩니다.


그림 2 : 의존성 주입기에서의 의존 관계

의존성 주입은 크게 세 가지 유형으로 구분할 수 있습니다. 이 세 가지를 생성자 주입, 변경자(setter) 주입,  인터페이스 주입으로 부르겠습니다. 제어 역전에 대해 요즘 진행되는 토론을 읽어보면 사람들은 이 유형들에 대해서 인터페이스 주입은 유형 1, 변경자 주입은 유형 2, 생성자 주입은 유형 3이라고 칭합니다. 숫자로 된 이름은 기억하기 어려워서 저는 늘 이 이름을 사용합니다.

피코 컨테이너(PicoContainer)의 생성자 주입

피코 컨테이너라고 하는 경량 컨테이너를 써서 주입이 어떻게 되는지 보는 것으로 시작하겠습니다. 대부분 전 피코 컨테이너부터 시작하는데 쏘트웍스(ThoughtWorks)의 제 동료 중 몇몇이 피코 컨테이너의 개발에 무척 활동적으로 참여하고 있기 때문입니다. (예. 일종의 회사 연고주의가 맞습니다.)

피코 컨테이너는 목록 클래스에 MovieFinder 구현을 어떻게 주입할지 결정하기 위해 생성자를 사용합니다. 이것이 작동하려면 영화 MovieLister 클래스는 주입 받아야 할 모든 것이 포함된 생성자를 선언해야 합니다.
class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
MovieFinder 구현물도 피코 컨테이너로 관리 될 것이며 역시 컨테이너에 의해 텍스트 파일의 이름을 주입 받을 것입니다.
class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}
이제는 피코 컨테이너에 각 인터페이스마다 어떤 구현 클래스를 관련지을지, 그리고 어떤 문자열을 찾기 객체에 주입할지 알려줘야 합니다.
    private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
이 설정 코드는 보통 별도 클래스에 만들어 넣습니다. 우리 예로 보면 MovieLister를 쓰기 원하는 친구들 각각 자신의 설정 클래스에 적당한 설정 코드를 만들어 넣을 겁니다. 물론, 보통은 이런 설정 정보 같은 것을 소스 코드 보다는 별도 설정 파일에 보관합니다. 클래스 하나를 만들어서 설정 파일을 읽고 그에 따라 컨테이너를 구성하도록 할 수도 있습니다. 피코 컨테이너가 비록 이런 기능을 가지고 있지는 않지만 나노 컨테이너(NanoContainer)라는 이웃 프로젝트가 있습니다. 나노 컨테이너는 피코 컨테이너를 확장해서 설정을 XML 파일로 할 수 있도록 합니다. 나노 컨테이너는 XML을 읽어서 내장하고 있는 피코 컨테이너를 설정합니다. 이 프로젝트는 메카니즘과 설정 파일 포멧을 분리하자는 철학을 가지고 있습니다.

컨테이너를 쓰려면 다음과 같은 코드를 작성하십시오.
    public void testWithPico() {
MutablePicoContainer pico = configureContainer();
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
비록 예제에서 생성자 주입을 사용했지만 피코 컨테이너는 변경자 주입도 지원합니다. 개발자들은 생성자 주입을 선호하지만 말입니다.

스프링의 변경자(setter) 주입

스프링 프레임워크는 폭넓은 기업용 자바 개발 프레임워크입니다. 스프링은 트랜잭션과 영속화 프레임워크들과 웹 애플리케이션 개발과 JDBC에 해당하는 추상화 계층을 가지고 있습니다. 피코 컨테이너처럼 스프링도 생성자 주입과 변경자 주입을 모두 지원하지만, 스프링 개발자들이 변경자 주입을 선호하는 경향이 있기 때문에 우리 예제에서는 변경자 주입의 적당한 예로 스프링을 선택하였습니다.

영화 목록 객체가 주입을 받으려면 서비스를 받는 설정 메소드를 만들어야 합니다.
class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
마찮가지로 파일명을 설정하는 메소드도 만듭니다.
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}
세 번째로 이 파일들을 묶는 설정을 구성합니다. 스프링은 XML 파일로 설정을 할 수도 있고 코드로 할 수도 있지만 보통 XML을 사용합니다.
    <beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
테스트 코드는 다음과 같습니다.
    public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
인터페이스 주입

세 번째 주입 기술은 주입에 쓸 인터페이스를 정의하고 사용합니다. 아발론이 이 기술을 적절히 사용하는 프레임워크의 실례입니다. 나중에 아발론을 더 얘기할 것이고 여기서는 좀 단순한 예제 코드로 주입 방법을 사용해보려고 합니다.

이 기법에서는 주입하는 동안 사용할 인터페이스를 먼저 정의합니다. 여기에 영화 찾기 객체를 다른 객체에 주입하는 인터페이스가 있습니다.
public interface InjectFinder {
void injectFinder(MovieFinder finder);
}
이 인터페이스는 MovieFinder 인터페이스를 제공하는 누군가가 정의할 것입니다. 어떤 클래스라도 (예로 든 MovieLister 같이) MovieFinder를 쓰기 원한다면 이 인터페이스를 구현해야 합니다.
class MovieLister implements InjectFinder...
public void injectFinder(MovieFinder finder) {
this.finder = finder;
}
MovieFinder 구현물에도 파일 이름을 주입할 때에 같은 방식을 씁니다.
public interface InjectFinderFilename {
void injectFilename (String filename);
}
class ColonMovieFinder implements MovieFinder, InjectFinderFilename......
public void injectFilename(String filename) {
this.filename = filename;
}
이제 구현물들을 하나로 엮을 설정 코드를 만들 차례입니다. 단순성을 유지하려고 코드로 작성하겠습니다.
class Tester...
private Container container;

private void configureContainer() {
container = new Container();
registerComponents();
registerInjectors();
container.start();
}
설정은 두 단계로 되어 있습니다. 검색용 키로 컴포넌트를 등록하는 건 다른 예제들과 아주 비슷합니다.
class Tester...
private void registerComponents() {
container.registerComponent("MovieLister", MovieLister.class);
container.registerComponent("MovieFinder", ColonMovieFinder.class);
}
여기에 의존하는 컴포넌트를 주입할 주입기를 등록하는 단계가 추가됩니다. 주입 인터페이스마다 의존하는 객체를 주입하는 코드가 뭔가 있어야 합니다. 여기서는 컨테이너에 주입기 객체를 등록하는 것으로 처리해보겠습니다. 각 주입기 객체는 injector 인터페이스를 구현합니다.
class Tester...
private void registerInjectors() {
container.registerInjector(InjectFinder.class, container.lookup("MovieFinder"));
container.registerInjector(InjectFinderFilename.class, new FinderFilenameInjector());
}
public interface Injector {
public void inject(Object target);

}
의존 대상 객체가 이 컨테이너에 쓰도록 만들어진 클래스라면 이 컴포넌트가 injector 인터페이스를 스스로 구현하는 것이 타당합니다. 여기서는 영화 찾기 객체에 이것을 적용하려 합니다. 문자열 같은 범용 클래스는 설정 코드 안에서 내부 클래스(inner class)를 씁니다.
class ColonMovieFinder implements Injector......
public void inject(Object target) {
((InjectFinder) target).injectFinder(this);
}
class Tester...
public static class FinderFilenameInjector implements Injector {
public void inject(Object target) {
((InjectFinderFilename)target).injectFilename("movies1.txt");
}
}
테스트는 이 컨테이너를 씁니다.
class IfaceTester...
public void testIface() {
configureContainer();
MovieLister lister = (MovieLister)container.lookup("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
이 컨테이너는 선언된 인터페이스를 써서 의존성을 파악하며 해당하는 의존성을 주입할 때에는 주입기를 씁니다. (여기에서 사용한 컨테이너가 어떻게 구현되었는지는 이 기술에서 중요한 것이 아니 기도하고 조롱받기만 할 것이라 보여주지 않겠습니다.)

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]


by 박성철 | 2009/10/10 17:14 | 프로그래밍 이야기 | 트랙백 | 덧글(0)

◀ 이전 페이지다음 페이지 ▶