이전 포스팅에서, UserDao의 코드에서 예외처리 구문을 추가하고, 디자인 패턴을 이용해 각 테스트 코드에서 쿼리문을 실행하는 PrepareStatement 객체를 생성하는 작업을 최적화하였다.
변하는 부분을 메소드로 추출한 메소드 추출 기법, 추상 메서드를 오버라이딩하는 템플릿 메소드 패턴, 그리고 인터페이스를 생성하여 각각의 PrepareStatement 생성 방법마다 구현 클래스를 만드는 전략 패턴을 고려하였다.
전략 패턴에서 테스트 메소드 안에 직접적으로 구현 클래스가 결정되는 현상을 막기 위해서 컨텍스트 코드를 분리하고, 테스트 메소드는 하나의 클라이언트로 작동하도록 하였다. 즉 DI를 사용해 클라이언트 (UserDao) 와 컨텍스트 (jdbcContextWithStatementStrategy) 를 분리한 것이라고 볼 수 있다.
이번 포스팅에서는 컨텍스트를 따로 분리하여 다른 DAO가 사용할 수 있도록 하는 실습을 해보겠다.
JdbcContext의 분리
이렇게 JdbcContext로 분리하고 나면 클라이언트인 UserDao에서는 이렇게 컨텍스트를 사용할 수 있다.
public class UserDao {
private JdbcContext jdbcContext;
...
public void setJdbcContext(JdbcContext jdbcContext){
this.jdbcContext = jdbcContext;
}
// 사용자 생성
public void add(final User user) throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { ... }
);
}
// 사용자 삭제
public void deleteAll() throws SQLException {
this.jdbcContext.workWithStatementStrategy(
new StatementStrategy() { ... }
);
}
}
JdbcContext가 의존하고 있는 DatatSource는 기존에는 UserDao가 의존하던 인터페이스이기 때문에, 빈 의존관계가 변경됨에 따라 JdbcContext 타입의 빈을 ApplicationContext에 등록한다.
JdbcContext의 특별한 DI
UserDao는 JdbcContext에 의존하고 있는데, 여타 인터페이스를 의존성에 추가하였던 다른 예제들과는 달리 JdbcContext는 하나의 구체 클래스이다.
DI에서는 구체적인 의존관계가 명시되지 않고 런타임 시점에서 의존 관계가 결정되는 것이 원칙이었다. 그러나 UserDao와 JdbcContext는 클래스 레벨에서 의존관계가 명시적으로 결정된다. 문제가 있지 않나 ?
물론 인터페이스를 사용하지 않았다면 엄밀히 말해서 DI라고 볼 수 없지만, DI는 넓은 관점에서 오브젝트를 생성하고 제어의 권한을 외부로 위임했다는 것에서 IoC의 개념을 포괄한다.
인터페이스를 사용하지 않더라도 JdbcContext를 빈으로 등록하여 UserDao와 DI 구조로 만들어야 하는 이유는 아래와 같다.
- JdbcContext가 스프링 컨테이너에서 관리되는 싱글톤 빈이기 때문이다. JdbcContext는 다른 오브젝트들이 공유해서 사용할 만한, DB에 요청을 보내는 일종의 서비스 오브젝트로서 의미가 있으므로 싱글톤으로 등록되는 것이 이상적이다.
- JdbcContext가 DI로 다른 빈 ( = DataSource)에 의존하고 있기 때문이다. DI를 위해서는 주입 되는 (DataSource), 주입 받는 (JdbcContext) 오브젝트 양쪽이 빈으로 등록되어 있어야 한다.
그럼 인터페이스를 굳이 사용하지 않은 이유는 뭘까?
UserDao는 항상 JdbcContext를 사용해야하는, 강한 응집도를 가지고 있는 관계이다. 또한, JdbcContext는 다른 구현으로 대체하여 사용할 이유가 없다. 그래서 인터페이스를 사용하지 않고, 스프링 빈으로 등록하여 DI하는 방식을 사용한다.
코드를 이용하는 수동 DI
UserDao 내부에서 JdbcContext를 직접 DI를 적용하는 방법도 있다.
이 방법을 사용하면 JdbcContext는 싱글톤 오브젝트가 될 수 없다. 왜냐하면 이 방법은 ApplicationContext에 빈을 등록하는 방식이 아니기 때문이다. 대신 DAO 하나마다 JdbcContext 오브젝트를 가지는 정도이기 때문에, 메모리에 부담이 가는 문제는 아니다.
JdbcContext가 스프링 빈이 아니기 때문에, UserDao가 JdbcContext 오브젝트의 생성과 초기화를 직접 담당하게 된다. 문제는 DataSource이다. JdbcContext는 DataSource를 빈으로 주입받고 있는데, DI에서는 양쪽 모두가 빈에 등록되어 있어야 가능하다고 하지 않았는가 ?
이 문제를 해결하기 위해 UserDao에게 이 DI까지 전담하도록 하는 것이다. 즉, UserDao가 임시로 두 오브젝트의 의존관계를 맺는 DI 컨테이너의 역할을 하는 것이다.
따라서, DataSource는 UserDao에서 직접 사용되지는 않지만, JdbcContext를 생성하고 초기화되는 것에 사용되고 버려지는 것이다.
빈 레벨에서 봤을 때 UserDao가 DataSource를 의존하고 있는 것이 맞지만, 실질적으로 JdbcContext가 DataSource를 의존하고 있고 이 관계를 UserDao에서 직접 맺어주고 있는 것이다. 코드로 살펴보자.
이 setDataSource() 메소드는 DI 컨테이너가 DataSource 오브젝트를 주입해줄 때 호출된다.이 메소드에서 jdbcContext 오브젝트와 dataSource 오브젝트의 수동 DI가 이루어진다.
이 방법의 장점은, 인터페이스를 둘 필요가 없는 긴밀한 관계를 가지는 DAO와 JdbcContext를 빈으로 분리하지 않고도 다른 오브젝트 (=DataSource)의 DI를 적용할 수 있다는 점이다. 이 장점을 통해, 기존에 jdbcContext를 빈으로 등록하여 사용한 방법의 문제 (DI를 사용하였을 때 인터페이스가 아닌 구체 클래스를 의존하기 때문에 의존관계가 클래스 레벨에서 명확히 드러남) 를 해결할 수 있게 되었다.
'DEV book > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] 4장 예외 - 예외 처리 전략과 DataAccessException을 사용한 JDBC 한계 극복 (0) | 2024.01.16 |
---|---|
[토비의 스프링 3.1] 3장 템플릿 (3) - 템플릿과 콜백 (0) | 2023.09.24 |
[토비의 스프링 3.1] 3장 템플릿 (1) - 디자인 패턴과 DI를 이용한 DAO 최적화 (0) | 2023.09.23 |
[토비의 스프링 3.1] 2장 테스트 - 단위 테스트와 테스트 코드 개선 (0) | 2023.09.20 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (3) - 싱글톤 레지스트리와 의존관계 주입 (0) | 2023.09.14 |