1장. 오브젝트와 의존관계
- 초난감 DAO
- DAO의 분리
- DAO의 확장
- 제어의 역전(IoC)
- 스프링의 IoC
- 싱글톤 레지스트리와 오브젝트 스코프
- 의존관계 주입(DI)
- 정리
오브젝트와 의존관계(3)에 이어서 IoC와 단짝인 의존관계 주입(DI)에 대해 살펴봅시다.
의존관계 주입(DI)
앞선 글에서 스프링 IoC 기능을 굉장히 강조했습니다.
지금부터 알아볼 의존관계 주입은 스프링 IoC 기능의 대표적인 동작원리입니다. 사실 지금까지 외부에서 오브젝트를 생성하여 생성자로 오브젝트를 주입했는데 이 과정도 의존관계 주입이라 볼 수 있습니다.
먼저 의존관계에 대해 이야기해 봅시다.

의존관계란 A 클래스의 인스턴스 변수로 다른 B 클래스 타입을 가지는 것을 말합니다. 이때 A 클래스가 B 클래스에 의존한다고 볼 수 있습니다. 그리고 두 개의 클래스 또는 모듈이 의존관계에 있을 때 항상 방향성을 부여해야 합니다. A 클래스가 B 클래스에 의존하는 것도 A에서 B로의 방향성이 있는 것입니다.
의존한다는 것은 의존대상이 변하면 그것이 본인에 영향을 미친다는 뜻이기도 합니다.
만약 B에 새로운 메소드가 추가되거나 기존 메소드가 바뀌면 B에 의존하는 A도 그에 따라 수정되어야 합니다.
A가 B에 의존한다면 의존관계에는 방향성이 있기 때문에 B가 A에 의존하는 상황은 아닌 것입니다. 따라서 B는 A의 변화와 무관합니다.
지금까지 작업했던 UserDao를 떠올려봅시다. UserDao는 ConnectionMaker 인터페이스에 의존하고 있습니다. 따라서 ConnectionMaker 인터페이스가 변한다면 그 영향은 UserDao에 미치게 됩니다. 하지만 UserDao는 ConnectionMaker 인터페이스의 구현 클래스와는 의존하고 있지 않습니다. 따라서 구현 클래스의 변화는 UserDao에 영향을 미치지 않습니다.

위 그림처럼 인터페이스에 대해서만 의존관계를 만들어두면 인터페이스 구현 클래스와의 관계는 느슨해지면서 변화에 영향을 덜 받는 상태가 됩니다. 이것이 결합도가 낮은 관계입니다.
의존관계란 한쪽의 변화가 다른 쪽에 영향을 주는 것이라 했으니, 인터페이스를 통해 의존관계를 제한해 주면 그만큼 변경에서 자유로워지는 것입니다.
인터페이스를 통해 설계 시점에 느슨한 의존관계를 갖는 경우에는 UserDao의 오브젝트가 런타임 시에 사용할 오브젝트가 어떤 클래스로 만든 것인지 미리 알 수가 없습니다. 그래서 프로그램이 시작되고 UserDao 오브젝트가 만들어지고 나서 런타임 시에 의존관계를 맺는 대상을 외부에서 주입해야 합니다. 이를 의존관계 주입이라 합니다.
다시 말해, 의존관계 주입은 구체적인 의존 오브젝트와 그것을 사용할 주체(클라이언트) 오브젝트를 런타임 시에 연결해 주는 작업입니다.
정리하면 의존관계 주입이란 다음과 같은 세 가지 조건을 충족하는 작업을 말합니다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않습니다. 그러기 위해서는 인터페이스에만 의존하고 있어야 합니다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정합니다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어집니다.
우리가 DaoFactory를 만들어 관계설정 책임을 별도의 클래스로 분리했듯이 의존관계 주입의 핵심 역시 설계 시점에서는 드러나지 않는 의존관계를 맺도록 돕는 제3의 존재가 있다는 것입니다.
그리고 앞에서 이야기했던 애플리케이션 컨텍스트, 빈 팩토리, IoC 컨테이너 등이 모두 외부에서 오브젝트 사이의 런타임 관계를 맺어주는 책임을 가진 제3의 존재입니다.
DI는 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 수동적으로 주입받은 오브젝트를 사용한다는 점에서 IoC의 개념에 잘 들어맞습니다.
여기서 한 가지 주의할 것은 DI를 원하는 오브젝트는 먼저 자기 자신이 컨테이너가 관리하는 빈(스프링 빈)이 돼야 합니다.
UserDao와 ConnectionMaker 사이에 DI가 적용되려면 DI를 원하는 UserDao도 반드시 컨테이너가 만드는 스프링 빈 오브젝트여야 합니다.
이번에는 DI 개념을 이용하여 UserDao를 사용하며 DB 연결 횟수를 카운팅 하는 기능을 추가한다고 생각해 봅시다.
무식하게 모든 DAO의 makeConnection() 메소드에 카운터를 증가시키는 코드를 넣을 수 있지만... 너무 노가다입니다.
그리고 DB 연결 횟수를 세는 일은 DAO의 관심사항이 아니기 때문에 DAO와는 분리해야 합니다.
그렇다면 아래 그림처럼 DAO와 DB 커넥션을 만드는 오브젝트 사이에 연결 횟수를 카운팅 하는 오브젝트를 하나 추가한다면 어떨까요?

아래 코드를 살펴봅시다.
package springbook.user.dao;
...
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;
}
}
CountingConnectionMaker는 ConnectionMaker 인터페이스를 구현해서 만들었기 때문에 DAO가 의존할 대상이 될 수 있습니다. 만약 DB 연결 횟수를 카운팅해야 할 때는 아래 코드와 같이 설정정보(@Configuration Class)에서 런타임 의존관계를 DConnectionMaker가 아닌 CountingConnectionMaker로 수정하고 다시 CountingConnectionMaker가 DConnectionMaker에 의존하도록 만들면 됩니다. 기존 DAO 설정 부분은 바꾸지 않아도 됩니다.
@Configuration
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를 이용한 분석 작업이 모두 끝나면, 다시 CountingDaoFactory 설정 클래스를 DaoFactory로 변경하거나 connectionMaker() 메소드를 수정하는 것만으로 DAO의 런타임 의존관계는 이전 상태로 복구됩니다.
지금까지 DI를 활용하는 방법을 살펴봤고 이외에도 다양합니다.
스프링을 공부하는 건 DI를 어떻게 활용해야 할지를 공부하는 것도 비슷하다고 합니다.
생성자를 통해 의존관계를 주입받는 방법 이외에도 두 가지 방법이 더 있습니다. 하지만 보통 생성자를 통한 주입을 권장합니다. 생성자 주입은 불변, 필수라는 성격을 가지고 있기 때문에 권장됩니다.
DataSource 인터페이스로 변환
지금까지 확장했던 ConnectionMaker 인터페이스를 다시 살펴봅시다.
public interface ConnectionMaker {
public Connection makeConnection() throws ClassNotFoundException, SQLException;
}
ConnectionMaker 인터페이스는 DB 커넥션을 생성해주는 기능 하나만을 정의하는 매우 단순한 인터페이스입니다.
사실 자바에서는 DB 커넥션을 가져오는 오브젝트의 기능을 추상화해서 비슷한 용도로 사용할 수 있게 만들어진 DataSource라는 인터페이스가 이미 존재합니다.
따라서 실전에서는 ConnectionMaker와 같은 인터페이스를 직접 만들어서 사용할 일은 없을 것입니다.
이미 다양한 방법으로 DB 연결과 풀링(pooling) 기능을 갖춘 많은 DataSource 구현 클래스가 존재하고, 이를 가져다 사용하면 충분하기 때문입니다. 대부분의 DataSource 구현 클래스는 DB의 종류나 아이디, 비밀번호 정도는 DataSource 구현 클래스를 다시 만들지 않아도 지정할 수 있는 방법을 제공합니다.
DataSource 인터페이스가 제공하는 여러 메소드들이 있지만, DB 커넥션을 가져오는 getConnection 메소드를 살펴봅시다.
package javax.sql
public interface DataSource extends CommonDataSource, Wrapper {
Connection getConnection() throws SQLException;
...
}
이제 우리가 직접 만든 ConnectionMaker 인터페이스가 아니라 DataSource 인터페이스를 이용하도록 UserDao를 수정하겠습니다. 먼저 UserDao에 주입될 의존 오브젝트의 타입을 ConnectionMaker에서 DataSource로 변경합니다.
import javax.sql.DataSource;
public class UserDao {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void add(User user) throws SQLException {
Connection c = dataSource.getConnection();
...
}
...
}
이번에는 DataSource 인터페이스의 구현 클래스가 필요합니다. 스프링이 제공하는 DataSource 구현 클래스 중에 테스트환경에서 간단히 사용할 수 있는 SimpleDriverDataSource라는 구현 클래스를 사용하도록 DI를 재구성하겠습니다.
@Configuration
public class DaoFactory {
@Bean
public UserDao userDao() {
return new UserDao(dataSource());
}
@Bean
public DataSource dataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
dataSource.setUrl("jdbc:mysql://localhost/springbook");
dataSource.setUsername("spring");
dataSource.setPassword("book");
return dataSource;
}
}
이렇게 해서 UserDao에 DataSource 인터페이스를 적용하고 SimpleDriverDataSource의 오브젝트를 DI로 주입해서 사용할 수 있도록 고쳤습니다.