1장 오브젝트와 의존관계 - 1.7 의존관계 주입(DI)
1장 오브젝트와 의존관계
1.7 의존관계 주입(DI)
1.7.1 제어의 역전(loC)과 의존관계 주입
스프링의 IoC 에 대해 깊이 알아본다.
IoC 는 소프트웨어에서 자주 발견되는 일반적인 개념이다.
DaoFactory 처럼 객체를 생성하고 관계를 맺어주는 등의 작업을 담당하는 기능을 일반화한 것이 스프링의 IoC 컨테이너이다.
사실 IoC 는 매우 느슨하게 정의되서 폭넓게 사용되는 용어이다.
IoC 컨테이너라는 용어로는 컨테이너인지 IoC 개념이 적용된 프레임워크인지 알기가 어려웠다.
그래서 스프링이 제공하는 IoC 방식을 의존관계 주입 Dependency Injection 이라는 명확한 이름으로 부르게 되었다.
이런 이유로 스프링 컨테이너는 예전에는 IoC 컨테이너라고 불렸지만 지금은 DI 컨테이너라고 더 많이 불리기도 한다.
용어는 동작방식보다는 의도를 가지고 이름을 짓는 편이 좋다.
그런 면에서 DI 를 ‘의존관계 주입’, ‘의존관계 설정’ 이라고 번역하는 것이 적절하다.
1.7.2 런타임 의존관계 설정
의존관계란 무엇일까? 의존관계는 방향성이 있다.
의존관계에서는 누가 누구에게 의존하고 있는지 보여야 한다.
그래서 UML 에서 의존관계를 표현할 때, A 가 B 에 의존하고 있음을 A —> B 라고 방향성을 포함한 기호로 표현한다.
A 가 B 에 의존한다는 말은 B 가 변하면 A 에 영향을 미친다는 뜻이다.
A 가 의존하는 B 의 메소드가 변하면 A 도 그에 따라서 수정되어야 한다.
UML 에서는 설계 모델의 관점에서 클래스와 인터페이스 간의 의존을 이야기한다.
하지만 런타임 시점에 오브젝트 간에 만들어지는 의존관계도 있다.
예컨데 코드 상 UserDao 클래스는 ConnectionMaker 인터페이스에 의존하고 있지만, 런타임 시점에 UserDao 오브젝트는 DConnectionMaker 오브젝트에 의존하게 된다.
런타임이 시점에 UserDao 오브젝트가 의존하게되는 오브젝트를 의존 오브젝트라고 한다.
의존관계 주입이란 이렇게 런타임 시에 의존 오브젝트와 클라이언트 오브젝트를 연결해주는 작업을 말한다.
의존관계 주입이란 아래 세 가지 조건을 충족하는 작업을 말한다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입) 해줌으로써 만들어진다.
의존관계 주입의 핵심은 설계 시점에는 알지 못했던 두 오브젝트의 관게를 맺도록 도와주는 제 3의 존재가 있다는 것이다.
제 3의 존재는 오브젝트간의 관계설정 책임을 가지는 오브젝트이다.
UserDao 에 적용된 의존관계를 살펴보면 최초에는 UserDao 가 생성자를 이용해 DConnectionMaker 를 직접 생성하였다.
public class UserDao() {
// ...
public UserDao() {
connectionMaker = new DConnectionMaker();
}
}
이 경우는 UserDao 가 어떤 ConnectionMaker 를 생성할지를 결정하고 관리하는 관계설정 책임을 가지고 있는 셈이다.
고쳐진 코드에서는 DaoFactory 를 만들고 DaoFactory 에서 ConnectionMaker 오브젝트를 생성하도록 하였다.
그렇게 만든 ConnectionMaker 오브젝트를 UserDao 의 생성자 파라미터로 주입하여 런타임에서 의존관계를 맺어주었다.
public class DaoFactory {
public UserDao userDaoO {
return new UserDao(new DConnectionMaker());
}
// ...
}
자바에서 주입이라고 하는 것은 메소드를 실행하면서 파라미터로 오브젝트의 레퍼런스를 전달해주는 방법뿐이다.
DaoFactory 오브젝트는 의존관계 주입역할을 맡았기 때문에 DI 컨테이너라고 불리어도 된다.
UserDao 오브젝트는 이제 주입받은 DConnectionMaker 오브젝트를 사용할 수 있다.
public class UserDao {
private ConnectionMaker ConnectionMaker;
public UserDao(ConnectionMaker ConnectionMaker) {
this.ConnectionMaker = ConnectionMaker;
}
// ...
}
스프링 컨테이너의 IoC 는 주로 의존관계 주입 또는 DI 에 초점이 맞추어져 있기 때문에 DI 컨테이너 혹은 DI 프레임워크라고 불린다.
1.7.3 의존관계 검색과 주입
스프링이 제공하는 IoC 방법에는 의존관계 주입만 있는 것이 아니다.
오브젝트가 필요로 하는 의존 오브젝트를 능동적으로 찾는 의존관계 검색 Dependency Lookup (DL) 이라고 불리는 것도 있다.
의존관계 검색은 런타임 시 의존관계를 맺을 오브젝트의 결정과 생성은 외부 컨테이너에 맡기는 것은 의존관계 주입과 동일하다.
하지만 메소드나 생성자를 통한 주입받는 대신 스스로 컨테이너에게 요청하는 방법을 사용한다.
스프링의 애플리케이션 컨텍스트는 getBean() 메소드를 제공해서 의존관계를 맺을 오브젝트를 검색하여 가져오게 할 수 있다.
public class UserDao {
private ConnectionMaker ConnectionMaker;
public UserDao {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
this.connectionMaker = context.getBean("connectionMaker", ConnectionMaker.class);
}
}
의존관계 주입과 의존관계 검색을 비교해보면 의존관계 주입이 코드상 훨씬 단순하고 깔끔하다.
DB 정보를 어떻게 가져올 것인가에 집중해야 하는 UserDao 에 context 와 getBean() 메소드로 오브젝트를 찾는 코드가 있는 것은 어색하다.
따라서 대게는 의존관계 주입 방식을 사용한다.
그래도 애플리케이션의 기동 시점에 적어도 한번은 의존관계 검색 방식을 이용해서 오브젝트를 가져와야 한다.
왜냐하면 기동 시점에는 DI 를 이용해서 생성한 오브젝트를 주입 받을 방법이 없기 때문이다.
사용자 요청을 처리하는 서블릿이나 프로그램의 main() 메소드는 오브젝트를 주입받을 방법이 없다.
의존관계 검색 방식을 사용할 때 검색하는 오브젝트는 빈 bean 일 필요가 없다.
의존관계 주입 방식에서는 검색하는 오브젝트가 반드시 빈 bean 오브젝트이어야 한다.
DI 에서 말하는 주입은 런타임에서 구현 클래스를 제공받을 수 있도록 인터페이스 타입의 파라미터를 통해 이뤄져야 한다.
1.7.4 의존관계 주입의 응용
런타임 시에 의존관계를 주입받는 DI 방식의 장점은 무엇인지 알아본다.
기능 구현의 교환이 가능하다.
만약 UserDao 에서 ConnectionMaker 를 직접 구현했다면 로컬 DB 에 붙을 때와 운영 DB 에 붙을 때마다 소스코드를 수정해줘야 한다.
DI 가 적용되어 있는 UserDao 클래스라면 생성 시점에 ConnectionMaker 인터페이스의 구현 오브젝트를 제공받기 때문에 개발/운영 상황에 따라 주입할 오브젝트만 변경해주면 된다.
부가기능 추가가 가능하다.
DB 연결횟수를 계산하는 기능을 추가한다고 생각해보자.
UserDao 의 makeConnection() 메소드의 앞에 DB 연결횟수를 계산하는 코드를 넣어야 할까?
DI 컨테이너에서라면 ConnectionMaker 인터페이스를 구현하고 있고, 연결횟수를 계산하는 오브젝트를 하나 더 추가하여 문제를 해결할 수 있다.
public class CountingConnectionMaker implements ConnectionMaker {
int counter = 0;
private ConnectionMaker realConnectionMaker;
public CountingConnectionMaker(ConnectionMaker realConnectionMaker) {
this.realConnectionMaker = realConnectionMaker;
}
public Connection makeConnection() throws ClassNotFoundException, SQLException {
this.counter++;
return realConnectionMaker.makeConnection();
}
public int getCounter() {
return this.counter;
}
}
DI 를 이용한다면 기존의 UserDao 나 ConnectionMaker 를 수정하지 않고, 새로운 기능의 CountingConnectionMaker 를 만들고, 런타임 시에 주입만 하면 된다.
중요한 것은 CountingConnectionMaker 는 UserDao 가 의존할 수 있도록 ConnectionMaker 인터페이스를 구현해서 만들어져야 한다는 것이다.
UserDao 는 ConnectionMaker 인터페이스에만 의존하고 있기 때문에 ConnectionMaker 인터페이스를 구현하고 있는 오브젝트라면 어떤 것이든 DI 가 가능하다.
그리고 CountingDaoFactory 클래스를 만들어서 의존관계에 대한 설정(설계도)을 제공한다.
public class CountingDaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(connectionMaker());
}
@Bean
public ConnectionMaker connectionMaker() {
return new CountingConnectionMaker(realConnectionMaker());
}
@Bean
public ConnectionMaker realConnectionMaker() {
return new DConnectionMaker();
}
}
CountingConnectionMaker 테스트용 클래스를 만들어보자.
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);
CountingConnectionMaker ccm = context.getBean("ConnectionMaker", CountingConnectionMaker.class);
System.out.println("Connection counter : " + ccm.getCounter());
}
}
DB 연결횟수를 계산하는 로직을 CountingConnectionMaker 에 집중시켰기 때문에 UserDao 말고 다른 몇 개의 DAO 에서도 사용이 가능하다.
이렇게 DI 의 장점은 관심사의 분리를 통해 얻어지는 높은 응집도에 있다.
스프링은 DI 를 편하게 사용할 수 있도록 도와주는 도구이자 DI 를 적극 활용한 프레임워크이다.
1.7.5 메소드를 이용한 의존관계 주입
생성자를 사용하는 것 말고 의존관계를 주입할 수 있는 방법이 있다.
- 수정자 setter 메소드를 이용한 주입
- 일반 메소드를 이용한 주입
스프링은 전통적으로 수정자 메소드를 이용한 DI 를 많이 사용해왔다.
XML 을 사용하는 경우에는 자바빈 규약을 따르는 수정자 메소드가 가장 사용하기 편리하다.
UserDao 를 생성자가 아닌 수정자 메소드로 DI 를 적용해보자.
public class UserDao {
private ConnectionMaker connectionMaker;
public void setConnectionMaker(ConnectionMaker connectionMaker) {
this.connectionMaker = connectionMaker;
}
// ...
}
UserDao 메소드가 변경되었기 때문에 DI 를 적용하는 DaoFactory 의 코드도 함께 수정한다.
public class DaoFactory {
// ...
@Bean
public UserDao userDao() {
UserDao userDao = new UserDao();
userDao.setConnectionMaker(connectionMaker());
return userDao;
}
// ...
}