본문 바로가기

Spring/토비의 스프링 3.1

1.7 의존관계 주입(DI)


1.7.1 제어의 역전(IoC)와 의존관계 주입

IoC는 매우 폭 넓게 사용되는 용어이므로, 스프링이 제공하는 IoC 방식은 의존관계 주입(Dependency Injection)이라는 용어를 사용한다.

1.7.2 런타임 의존관계 설정

의존관계

A가 B에 의존한다.

  • B가 변하면 A에 영향을 미친다.
  • UML에서 점선 화살표로 표시함 (A ---> B)
  • 방향성을 가지고 있으므로 반대는 성립하지 않는다. 즉, A가 변해도 B는 영향을 받지 않는다.

UserDao의 의존관계

UserDao ---> ConnectionMaker

  • ConnectionMaker 인터페이스가 변하면 UserDao는 변해야한다.
  • 그러나, ConnectionMaker를 구현한 DConnectionMaker에는 의존하지 않으므로 DConnectionMaker의 변화는 UserDao에 영향을 미치지 않는다.

런타임 의존관계 / 오브젝트 의존관계

  • 설계 시점의 의존관계가 실체화 된 것
  • 모델링 시점의 의존관계와는 성격이 다르다.
  • UserDao 오브젝트와 DConnectionMaker 오브젝트의 관계
  • 프로그램 시작 전에는 알 수가 없다.

의존관계 주입의 충족 조건

  • 클래스 모델 또는 코드에 런타임 시의 의존관계가 드러나지 않는다. (이를 위해 인터페이스에만 의존하고 있어야 한다.)
  • 런타임 시의 의존관계는 컨테이너나 팩토리와 같은 제3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

UserDao의 의존관계 주입

Factory를 만들기 전 UserDao는 자신이 사용할 오브젝트의 클래스가 무엇인지 알아야만 했다.

public UserDao() {
    connectionMaker = new DConnectionMaker();
}

따라서 UserDao는 설계 시점에서 DConnectionMaker라는 구체적인 클래스의 존재를 알고 있으며,
UserDao는 ConnectionMaker 뿐만 아니라 DConnectionMaker에도 의존하고 있다.
따라서 이를 해결하기 위해 제 3의 존재인 DaoFactory를 만들어 런타임 의존관계 결정 권한을 위임하였다.

DaoFactory

  • UserDao와 ConnectionMaker 오브젝트 사이의 의존관계를 설정해주는 의존관계 주입을 주도
  • IoC 방식으로 오브젝트의 생성과 초기화, 제공 등의 작업을 수행하는 컨테이너
  • DI 컨테이너 라고도 한다.
  • 방식 : 생성자를 통해 DConnectionMaker의 오브젝트를 전달해주고, 전달받은 오브젝트는 인스턴스 변수에 저장해둔다.
public class UserDao {
    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

1.7.3 의존관계 검색과 주입

의존관계 검색

  • 의존관계를 맺는 방법이 외부로부터의 주입이 아니라 스스로 검색을 이용
  • 자신이 필요로 하는 의존 오브젝트를 능동적으로 찾음(어떤 클래스의 오브젝트를 이용할지를 결정하는 것은 아님)
  • 어떤 오브젝트를 이용할지 결정하는 것과 오브젝트의 생성은 외부 컨테이너에 맡기고, 이를 가져올 때 스스로 컨테이너에게 요청
public UserDao() {
    DaoFactory daoFactory = new DaoFactory();
    this.connectionMaker = daoFactory.connectionMaker();
}

코드를 위와 같이 수정하더라도 UserDao는 여전히 어떤 오브젝트를 사용할 지 알 수 없으며, 의존 대상 역시 ConnectionMaker 인터페이스 뿐이다.

getBean()

의존관계에 검색에 사용되는 대표적 메소드

public UserDao() {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFacory.class);
    this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}

의존관계 주입이 코드가 훨씬 깔끔한데 굳이 의존관계 검색을 사용하는 이유?

  • 대개는 의존관계 주입을 사용하는 것이 낫다.
  • 그러나 어플리케이션의 기동 시점에서 한 번은 의존관계 검색을 통해 오브젝트를 가져와야한다.
  • main() 메소드에서 오브젝트를 주입받을 방법이 없다.
  • 서버 역시 마찬가지
  • 사용자의 요청을 받을 때마다 한 번은 의존관계 검색을 하여야한다.

의존관계 주입 vs 의존관계 검색

  • 의존관계 주입 : 의존을 하는 오브젝트와 의존을 받는 오브젝트가 모두 스프링 빈이어야 한다.
  • 의존관계 검색 : 검색하는 오브젝트는 스프링 빈일 필요가 없다. 단, 의존을 받는 오브젝트는 스프링의 빈이어야 한다.

1.7.4 의존관계 주입의 응용

기능 구현의 교환

앞서 ConnectionMaker를 분리한 이유는 N사와 D사 등 확장에 유연하게 대처하기 위해서였지만, 좀 더 현실적인 예를 보자면 개발용과 운영용 DB를 사용하는 경우를 생각해볼 수 있다.

  • 개발 진행 시 실제 운영 DB에 접근하는 것은 무리
  • LocalDBConnection과 ProductionDBConnection을 만들어 사용한다고 가정
  • DI를 사용하지 않으면 개발 진행 시 모든 DBConnection을 LocalDBConnection으로 변경하고, 개발이 끝나면 다시 ProductionDBConnection으로 바꿔주어야 한다.
  • DI를 사용할 경우, 한 줄만 수정하면 해결이 된다.
@Bean
public ConnectionMaker connectionMaker() {
    // 개발용
    return new LocalDBConnectionMaker();

    // 운영용
    // return new ProductionDBConnectionMaker();
}

추가적으로 QA팀이 별도의 테스트 DB만들어 이용한다고 해도, DAO에는 전혀 손을 댈 필요가 없다.

부가기능추가

DB 연결횟수를 카운팅하고 싶을 때?

  • DI를 사용하지 않는다면 모든 DAO의 makeConnection() 메소드 호출부분에 카운터를 증가시키는 코드를 넣어야한다. 그리고 분석작업이 끝나면 다시 지워야한다....
  • 더욱이 카운터는 DAO의 관심사가 아니다 !
  • DI를 사용한다면 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결횟수를 카운팅하는 오브젝트를 하나 더 추가하면 된다.

CountingConnectionMaker.class

public class CountingConnectionMaker implements ConnectionMaker {
    int counter = 0;
    private ConnectionMaker realConnectionMaker;

    public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
        this.realConnectionMaker = realConnectionMaker;
    }

    @Override
    public Connection makeConnection() throws ClassNotFoundException, SQLException {
        this.counter ++;
        return realConnectionMaker.makeConnection();
    }

    public int getCounter() {
        return this.counter;
    }
}

ConnectionMaker 인터페이스를 구현하여 만들었지만, 내부에서 직접 DB 커넥션을 만들지는 않는다. 대신 makeConnecion()에서 카운터를 증가시킨다.

새로운 의존관계를 컨테이너가 사용할 설정정보 : CountingDaoFactory.class

public class DaoFactory {
    @Bean
    public UserDao userDao() {
        // UserDao는 여전히 connectionMaker()에서 생성되는 Object를 DI받는
        return new UserDao(connectionMaker());
    }

    @Bean
    public ConnectionMaker connectionMaker() {
        return new CountingConnectionMaker(realConnectionMaker());
    }

    @Bean
    public ConnectionMaker realConnectionMaker() {
        return new DConnectionMaker();
    }
}

실행코드 : UserDaoConnectionCountingTest.class

public class UserDaoConnectionCountingTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException{
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(CountingDaoFactory.class);
        UserDao dao = context.getBean("userDao", UserDao.class);

        // Dao 사용코드

        // DL을 사용하면 이름을 이용해 어떤 빈이든 가져올 수 있다.
        CountingConnectionMaker ccm = context.getBean("connectionMaker", CountingConnectionMaker.class);
        System.out.println("Connection counter" + ccm.getCounter());
    }
}

현재는 DAO가 하나 뿐이지만 수십, 수백개여도 문제 없다.

1.7.5 메소드를 이용한 의존관계 주입

의존 오브젝트와의 관계를 주입하는 방법

  1. 생성자
    • 현재의 UserDao
  2. 수정자 메소드(Setter)
    • 외부로부터 제공받은 오브젝트 레퍼런스를 저장해두었다가 내부의 메소드에서 사용
  3. 일반 메소드
    • 한 번에 여러 개의 파라미터를 받을 수 있다.

전통적으로 수정자 메소드를 이용하는 방법이 가장 보편적이다.

수정자 메소드 DI를 적용한 UserDao.class

public class UserDao {
    private ConnectionMaker connectionMaker;

    public void setConnectionMaker(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }
}

수정자 메소드 DI를 적용한 DaoFactory.class

public class DaoFactory {
    @Bean // 오브젝트 생성을 담담하는 IoC용 메소드라는 표시
    public UserDao userDao() {
        UserDao userDao = new UserDao();
        userDao.setConnectionMaker(connectionMaker());
        return userDao;
    }

    // ...
}


' Spring > 토비의 스프링 3.1' 카테고리의 다른 글

1.9 정리  (0) 2019.01.11
1.8 XML을 이용한 설정  (0) 2019.01.11
1.6 싱글톤 레지스트리와 오브젝트 스코프  (0) 2019.01.09
1.5 스프링의 IoC  (0) 2019.01.07
1.4 제어의 역전(IoC)  (0) 2019.01.07