IoC 컨테이너와 의존성 삽입 패턴

Toggle Space Navigation Tree
Space Map

IoC 컨테이너와 의존성 삽입 패턴

Summary : Spring 프레임워크를 이해하기 위해서는 먼저 IoC 패턴에 대해 이해하고 있어야 한다. IoC 패턴을 이해할 경우 Spring이 가지고 있는 기본적인 Container 개념을 이해할 수 있다. 이번 강좌에서는 VSSH 프로젝트에서 진행한 IoC 패턴 강좌를 통하여 IoC 패턴에 대하여 이해할 수 있도록 한다.

들어가기에 앞서

이 글은 마틴 파울러(Martin Fowler)의 IoC 패턴에 대한 글을 토대로 스프링의 기반이 되는 IoC 패턴에 대해 소개할 목적으로 작성된 것입니다.
대부분의 내용은 아래 URL에 있는 마틴 파울러의 글을 편역한 것입니다.

참고자료 1. http://martinfowler.com/articles/injection.html : 중간 중간에 있는 IoC 컨테이너에 대한 설명은 TheServerSide.com에 등록된 Rod Johnson의 "Introducing the Spring Framework"에서 소개된 내용입니다

참고자료 2. http://www.theserverside.com/news/thread.tss?thread_id=21893

이 글타래는 마틴 파울러의 글을 순수하게 번역한 자료가 아닙니다. 번역한 원문을 읽어보실 분은 아래의 정보를 참고하세요.

원문 번역보기 1. 일본어 번역인 아래 링크를 파란 일본어 번역기를 이용하시면 전체 번역을 보실 수 있습니다.
http://www.kakutani.com/trans/fowler/injection.html

원문 번역보기 2. 네이버 "자바프레임워크 까페"에는 오버가이님이 번역하신 글도 있습니다. 번역한 글

IoC 컨테이너와 의존성 삽입 패턴(Inversion of Control Containers and the Dependency Injection pattern )

자바 커뮤니티에서는 다른 프로젝트에서 개발된 컴포넌트를 조립해서 응집력 있는 어플리케이션의 개발이 가능하도록 도와주는 경량급 컨테이너(Lightweight Container)에 대해 높은 관심을 보이고 있습니다.

경량급 컨테이너가 컴포넌트를 엮어주는 일을 수행하는 밑바탕에는 제어 역행화(Inversion of Control)라는 개념이 깔려 있습니다. 제어 역행화는 용어만으로 그 의미를 파악하는데 한계가 있기 때문에, 또 다른 말로 의존성 삽입(Dependency Injection) 이라고도 불려집니다. 이 글에서는 제어 역행화 패턴의 동작 원리를 설명하고, 대안이 되는 서비스 로케이터(Service Locator) 패턴과 비교해보도록 하겠습니다.

여기서 중요한 것은 제어 역행화 패턴과 서비스 로케이터 패턴 중 어떤 것을 선택할 것인지에 대한 고민 보다는, 컴포넌트의 설정을 그것의 사용에서 분리해야 한다는 원칙(The principle of separating configuration from use)을 이해하는 것입니다.

일반적 제어흐름을 갖는 작은 예제

우선 특정 기능을 가진 객체를 이용해서 어떤 로직을 처리하는 전형적인 프로그래밍 방식에 어떤 문제점이 있는지 살펴보기 위해 작은 예제를 하나 살펴보도록 합시다.

이 예제는 어느 특정 감독이 만든 영화를 리스트하는 프로그램(MovieLister)입니다.

MovieLister.java(moviesDirectedBy() 메소드 구현)
class MovieLister... 
  public Movie[] moviesDirectedBy(String arg) { 
    // 영화 정보를 검색하는 프로그램에게 모든 영화정보를 리스트로 가져오라고 요청 
    List allMovies = finder.findAll(); 
    for (Iterator it = allMovies.iterator(); it.hasNext();) { 
      // 각각의 영화를 Iterator를 이용해서 추출해 냄 
      Movie movie = (Movie) it.next(); 
      // 선택된 영화의 감독이 찾고자 하는 감독인지 비교 후 불일치하면 삭제 
      if (!movie.getDirector().equals(arg)) it.remove(); 
    } 
    // 해당 감독의 영화만 남겨진 리스트를 배열로 변환시켜 전달함 
    return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]); 
  }

MovieLister는 모든 영화 정보를 구해오기 위해 finder란 객체를 이용하고 있습니다. 코드에서 보여지는 finder 객체는 findAll()이란 메소드가 구현되어 있어야 합니다. MovieLister는 finder 객체가 어떤 방식으로 영화 정보를 가져오는지에는 관심이 없습니다. 그저 findAll()을 호출하면, 모든 영화 정보(Movie)를 담은 리스트가 넘어오면 되는 겁니다. 이를 위해 MovieFinder라는 인터페이스를 우선 만들도록 하겠습니다.

MovieFinder.java
public interface MovieFinder { 
  List findAll(); 
}

위에서 작성한 MovieLister가 제대로 동작하기 위해서는 MovieFinder 인터페이스를 구현할 구현 클래스가 필요합니다. 영화 정보가 콜론( : )으로 구별된 CSV형태의 텍스트 파일에 담겨 있다고 가정해 봅시다.

movies1.txt
#영화제목:감독:주연배우리스트:출시년도:장르 
80일간의 세계일주:프랭크 코라시:성룡, 스티브 쿠간:2004:액션 어드벤처 
귀신이 산다:김상진:차승원,장서희:2004:코미디 
꽃피는 봄이 오면:류장하:최민식,김호정:2004:드라마

그렇다면 MovieFinder의 구현 클래스로 ColonDelimitedMovieFinder란 클래스를 작성했겠죠? 그런 다음 MovieLister의 코드에 finder가 어떤 클래스의 인스턴스인지를 명시할 것입니다. 아래의 경우에는 MovieLister 클래스가 생성될 때 MovieFinder의 구현을 초기화하도록 했습니다. (또는 setMovieFinder() 같은 setter()를 만들어 초기화 할겁니다.)

이렇게 초기화해놓지 않으면.. 다시 말해 MovieFinder가 어떤 구현 클래스의 인스턴스 인지가 결정되지 않으면, moviesDirectedBy() 메소드 실행 중에 NullPointerException이 발생하기 때문에 어쩔 수 없는 일입니다.

MovieLister.java (finder 초기화)
class MovieLister... 
  private MovieFinder finder; 
  public MovieLister() { 
    // finder가 MoveFinder 인터페이스를 따르는 어떤 구현인지를 명시함 
    // 이로써 MovieFinder는 인터페이스와 그 구현 모두에 의존관계를 가짐. 
    finder = new ColonDelimitedMovieFinder("movies1.txt"); 
  }

이 예제는 아래의 클래스 다이어그램처럼 구성됩니다. MovieLister의 기능 구현에 MovieFinder 타입의 객체가 사용된다는 사실, 그리고 그 MovieFinder가 구체적으로 어떤 구현 클래스의 인스턴스인지가 아래와 같이 연결되어 있습니다.

여기에서 어떤 변화가 없다면, 이 프로그램은 충분히 만족할만 합니다. 그런데 만일 영화 정보를 기록하는 수단으로 파일이 아닌 DB를 이용해 달라는 새로운 요구 사항이 생긴다면? 또는 XML로 기록해달라거나, 영화 정보를 제공하는 또 다른 프로그램과 인터페이스 시켜달라거나 하는 요구 사항이 생긴다면?

다행히 이런 확장을 고려해서 미리 인터페이스를 만들어뒀기 때문에 XMLMovieFinder나 RDBMovieFinder 같은 기능 확장은 쉽게 처리됩니다. moviesDirectedBy() 메소드 역시 단 한 줄도 수정할 필요가 없구요. 그럼에도 불구하고 MovieLister.java의 코드는 재작성/재컴파일/재배포 과정을 거쳐야 합니다.

MovieLister.java (finder 초기화 정보 변경)
class MovieLister... 
  private MovieFinder finder; 
  public MovieLister() { 
    // 구현이 추가될 경우, 의존관계로 인해 초기화 루틴이 변경되어야 함. 
    finder = new XMLMovieFinder("movies1.xml"); 
  }

왜 이런 현상이 생긴걸까요? 그것은 MovieLister가 MovieFinder라는 인터페이스와 그 구현 모두에 의존하기 때문입니다.

변화에도 코드 수정 및 재배포가 필요없도록 의존성을 표현하는 방법은?

대부분의 경우 이를 해결하기 위해 팩토리 패턴을 적용하는 경우가 많습니다. 하지만 팩토리 패턴이 구현 객체에 대한 의존성 문제를 완전히 해결해주진 못합니다. 인터페이스 정보만을 참고해서 프로그램을 작성하는 것은 당연히 바람직한 현상입니다. 그렇다면, 인터페이스 구현 클래스의 인스턴스는 어떻게 생성하는 것이 좋을까요?
마틴 파울러는 그의 저서 "Patterns of Enterprise Application Architecture"에서 이를 해결하기 위해 "플러그인 (Plugin)"이라는 개념을 소개합니다. (참고 : http://martinfowler.com/eaaCatalog/plugin.html)

플러그인은 구현 객체에 대한 의존성 정보를 설정 파일에 담아 실시간으로 관리함으로써, 어플리케이션을 구성하는 각각의 컴포넌트들이 구체적인 구현 클래스를 모르고도 상호작용 할 수 있도록 합니다. (Plugin solves both problems by providing centralized, runtime configuration.) 그럼 어떻게 이러한 동작이 가능한 것일까? 그 기본 원리가 바로 *제어 역행화(Inversion of Control) 패턴*입니다.

제어 역행화 패턴과 IoC 컨테이너

IoC(Inversion of Control)을 제어 역행화라고 번역했으나 제어의 반전이라는 말로도 즐겨쓰는 듯 합니다. IoC는 직관적이지 못하기 때문에 Dependency Injection이라고도 불려집니다. Dependency Injection을 저는 의존성 삽입이라고 표현했는데요, 요즘은 의존성 주입이라는 말을 더 즐겨쓰는 듯 합니다. 다소 불편이 있더라도 양해해 주세요. 이 글은 제가 처음에 이해한대로 용어를 사용하고 있지만, 공론이 형성되면 거기에 따라 변경하겠습니다.

제어 역행화는 말 그대로 제어가 일반적인 흐름을 따르지 않고, 반대로 흘러간다는 뜻입니다. 각각의 프로그램이 가지고 있던 구현 객체에 대한 정보가 이젠 프레임워크에서 관리되는 것이죠. 제어 역행화(Inversion of Control)를 구현하는 프레임워크를 IoC 컨테이너라고 부릅니다. 일단 그림을 통해 간단히 개념부터 짚고 넘어가봅시다.

앞에서 살펴본 일반적인 제어흐름 그림과 비교해서 살펴보면 이해가 훨씬 쉬울겁니다. 이전의 MovieLister는 MovieFinder라는 인터페이스와 그 구현 MovieFinderImple에 모두 영향을 받았던 사실. 기억나죠? 하지만 이제 어떤 MovieFinderImpl이 만들어져야 하는지를 MovieLister는 전혀 알지 못합니다. 해당 객체의 생성을 Assembler라는 독립된 객체가 담당하기 때문입니다. 그럼 MovieLister는 어떻게 MovieFinderImpl을 이용할 수 있을까요? 그림에서 보이는 것처럼 Assembler에 의해서 생성된 MovieFinderImpl이 알아서 MovieLister를 찾아가서 삽입(Injection) 됩니다.

그림에서 보이는 Assembler 역할을 하는 것이 바로 IoC 컨테이너입니다. IoC 개념을 지지하는 많은 개발자들은 제어 역행화라는 용어가 직관적이지 않다는데 동의했습니다. 그래서 더 이해하기 쉬운 용어에 대한 논의를 거듭한 결과, 의존성 삽입이라는 용어를 만들어냈습니다. 의존성(Dependency)이란 어떤 기능을 구현하는데 있어 필요한 정보들을 전체적으로 표현하는 단어입니다.

예를 들면, 그 기능에서 사용되는 것이 구체적으로 어떤 객체인지.. 그리고 그 기능을 수행하는데 필요한 초기화 값은 무엇인지..

그런데 몇 가지 의문점이 생깁니다.

  • 질문-1) IoC 컨테이너는 무엇을 근거로 해당 인터페이스의 구현 객체 중 하나를 선택하는 것일까요?
  • 질문-2) 선택된 구현 객체를 생성시켜, 어느 클래스가 그 구현 객체를 사용하는지를 어떻게 판단할까요?
  • 질문-3) 사용자 클래스에 구현 객체를 연결시키는 방법은 무엇일까요?
    질문-1과 질문-2에 대한 답은 IoC 컨테이너의 설정 정보에 있습니다. IoC 컨테이너는 XML 방식의 설정 파일을 이용해서, 어떤 구현 객체가 생성되어야 하는지.. 그리고 생성된 구현 객체가 어떤 클래스에서 참조되는지에 대한 정보를 담도록 강제화합니다. (Spring의 예를 들어 설정 정보를 살펴보자면, 아래와 같은 형태가 되겠죠?)
    <bean id="movieLister" class="MovieLister"> 
        <property name="movieFinder"> 
            <ref bean="cdMovieFinder"/> 
        </property> 
    </bean> 
    <bean id="cdMovieFinder" class="ColonDelimitedMovieFinder"> 
        <property name="data"><value>movies1.txt</value></property> 
    </bean>

이제 질문-3에 대한 답을 구해봅시다. 어떻게 생성된 구현 객체를 사용자 클래스에 삽입시키는 걸까요?

이 방법은 클래스에서 사용되는 객체 초기화를 어떻게 했었는지를 떠올려보면 쉽게 이해할 수 있습니다. 객체지향에서 상속보다 위임이 더 나은 설계임은 이미 널리 알려진 사실입니다.

대부분의 자바 코드는 아래와 같이 특정 컴포넌트를 사용해서 자신의 기능을 구현합니다. 이때 차후에 발생할 기능확장을 고려해서 타입 정보를 분리해내는 것이 일반적입니다. 객체지향에서 순수한 타입(ADT)이란 결국 어떤 기능을 수행할 수 있는가, 즉 오퍼레이션(Operation)들의 집합이죠. 그래서 What을 표현하기 위한 인터페이스를 위에 만들고, How는 What의 한 사례이기 때문에 인터페이스를 따르는 구현 클래스를 만들게 되죠. 이러한 원리를 누구나 다 알고 있기 때문에, 대개의 경우 코드가 아래와 같은 형태가 될 것입니다.

Class UserClass { 
    private IComponent instance; 
    public void userClassMethod() { 
        instance.componentMethod();   
        doSomeThing(); 
    } 
}

여러분은 instance를 어떻게 초기화하시나요? 일반적으로 생성자에서 초기화를 시켜주거나 setter를 이용하죠?

Class UserClass { 
    private IComponent instance; 

    public UserClass() { // (1) 이렇게 생성자에서 초기화를 시켜주기도 합니다. 
        this.instance = new ComponentImpl(); 
    } 

    public void userClassMethod() { 
        instance.componentMethod(); 
        doSomeThing(); 
    } // (2) 아니면 이렇게 setter를 만들고 호출하던지요.. 

    public void setIComponent(IComponent instance) { 
        this.instance = instance; 
    } 
}

또 다른 방법이 있다면 객체 초기화를 담당할 인터페이스를 만들어서, 모든 클래스가 반드시 해당 인터페이스를 구현하도록 할 수도 있을 겁니다. 예를 들어 initialize() 같은 메소드가 포함된 인터페이스를 생각해볼 수 있겠네요..

IoC 컨테이너는 자신의 표준 설정 방법에 따라 의존성 정보를 확인하고, 그에 따라 객체 생성 및 초기화를 대신해주는 녀석입니다. 위에서 언급한 생성자, Setter, 인터페이스 이용 중 어떤 방식으로 구현 객체(의존성)을 사용자 객체에 삽입하느냐에 따라 의존성 삽입의 유형이 결정됩니다.

의존성 삽입의 3가지 유형

  • 유형 1. 생성자를 이용한 의존성 삽입 (Constructor Injection : type 1 IoC)
  • 유형 2. Setter() 메소드를 이용한 의존성 삽입 (Setter Injection : type 2 IoC)
  • 유형 3. 초기화 인터페이스를 이용한 의존성 삽입 (Interface Injection : type 3 IoC)

의존성 삽입이 구현되어 컴포넌트를 조립(플러그인)해서 응집력 있는 어플리케이션의 개발이 가능하도록 도와주는 어셈블러의 구현이 IoC 컨테이너이며, 다른 말로 경량급 컨테이너(Lightweight Container)라고도 합니다.

스프링이 바로 IoC 컨테이너이구요, 또 다른 종류로는 PicoContainer와 아파치의 Avalon, 그리고 HiveMind 등이 있습니다. 그 중에서 현재 가장 널리 사용되고 있는 것은 스프링(Spring)과 피코 컨테이너(PicoContainer)입니다.

피코 컨테이너는 type 1 IoC, 즉 생성자를 이용한 의존성 삽입이 일반적이구요, 스프링 프레임워크는 type 2 IoC, 즉 Setter() 메소드를 이용한 의존성 삽입이 일반적입니다.

글을 마무리하며..

저는 스프링을 접하면서 IoC 개념이 쉽게 이해되지 않아 고생이 심했습니다. Inversion of Control? 마틴 파울러의 글을 다섯 번이 넘게 반복해서 읽고 나서야 겨우 감이 잡히더군요..

기본 개념을 잡는데 저처럼 힘들어하실 분들을 위해 개념위주로 간단하게 기록을 남겨봅니다.

글에 실수가 있더라도 너그럽게 이해해주세요..

뭐든 기본 개념이 제일 중요한데, 사실 재미도 없고.. 제대로 설명하기도 어렵고...

그래서인지 이번 글은 진도가 안나가네요.. 그냥 이쯤에서 만족하고 접겠습니다.

더 자세한 설명이 필요한 분은 마틴 파울러의 원본글과 앞서 소개한 번역 자료들을 이용하시길..

이 강좌의 출처는 OpenSeed 프로젝트입니다. 강좌를 공유해주신 김승권님께 감사 드립니다.

강좌에 대하여

작성자 : 김승권
작성일 : 2005년 2월 23일

문서이력 :

  • 2005년 2월 23일 김승권 문서 최초 생성

참고 자료

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. 5월 08, 2007

    kang byeong kwon says:

    Martin Fowle (마틴파울러)의 IoC 컨테이너와 의존성 삽입패턴에 대한 번역문서는 아래에서 보실 수 있습니다. 위 링크가 연결이 되어있...

    Martin Fowle (마틴파울러)의 IoC 컨테이너와 의존성 삽입패턴에 대한 번역문서는 아래에서 보실 수 있습니다.
    위 링크가 연결이 되어있지 않네엽~
    1/3 정도 번역이 되어진 상태에서 멈춰졌습니다. 충분히 필요한 정보를 얻을 수 있다고 판단이 된다고 하네엽~ ^^

    http://cafe.naver.com/ArticleRead.nhn?clubid=10028767&menuid=6&boardtype=L&page=3&articleid=350

  2. 1월 04, 2010

    Anonymous says:

    http://cafe.naver.com/deve/350