이전의 방식을 전략패턴 구조로 보자면, 다음과 같다.
UserDao.deleteAll()
, UserDao.add()
: 클라이언트
익명 내부 클래스
: 전략
UserDao.jdbcContextWithStatementStrategy()
: 컨텍스트
PreparedStatement
를 실행하는 변하지 않는 부분JDBC의 일반적인 작업 흐름을 담고있는 jdbcContextWithStatementStrategy()
는 다른 DAO에서도 사용 가능하다. jdbcContextWithStatementStrategy()
를 UserDao
클래스 밖으로 독립시켜서 모든 DAO가 사용할 수 있게 해보자.
JdbcContext
라는 클래스를 생성하여 UserDao
에 있던 컨텍스트 메소드를 workWithStatementStrategy()
라는 이름으로 옮겨놓는다. DataSource
가 필요한 것은 UserDao
가 아니라 JdbcContext
가 된다.
JdbcContext
가 DataSource
에 의존하게 되므로 JdbcContext
에 DataSource
타입 빈을 DI 받을 수 있게 해줘야 한다.
JdbcContext.java
public class JdbcContext {
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = stmt.makePreparedStatement(c);
ps.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
if(ps != null) { try { ps.close(); } catch (SQLException e) { } }
if(c != null) { try { c.close(); } catch (SQLException e) { } }
}
}
}
UserDao.java
public class UserDao {
...
JdbcContext jdbcContext;
public void setJdbcContext(JdbcContext jdbcContext) {
this.jdbcContext = jdbcContext;
}
...
public void add(User user) throws SQLException {
StatementStrategy stmt = c -> {
PreparedStatement ps = c.prepareStatement(
"insert into users(id, name, password) values (?, ?, ?)"
);
ps.setString(1, user.getId());
ps.setString(2, user.getName());
ps.setString(3, user.getPassword());
return ps;
};
jdbcContext.workWithStatementStrategy(stmt);
}
public void deleteAll() throws SQLException {
StatementStrategy strategy = c -> c.prepareStatement("delete from users");
jdbcContext.workWithStatementStrategy(strategy);
}
...
UserDao
는 이제 JdbcContext
에 의존하고 있다. 그런데, JdbcContext
는 인터페이스인 DataSource
와 달리 구체 클래스이다.
스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꿔서 사용하도록 하는 게 목적이다. 하지만 이 경우 JdbcContext
는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로서 의미가 있을 뿐이고, 구현 방법이 바뀔 가능성은 없다.
따라서 인터페이스를 구현하도록 만들지 않았고, UserDao
와 JdbcContext
는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.
위 그림은 JdbcContext
를 적용한 UserDao
의 의존관계이다
위 그림은 JdbcContext
가 적용된 빈 오브젝트 관계이다.
xml파일은 아래와 같이 변경되면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="postgres" />
<property name="password" value="iwaz123!@#" />
<property name="driverClass" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost/toby_spring" />
</bean>
<bean id="jdbcContext" class="toby_spring.chapter1.user.jdbc_context.JdbcContext">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
<property name="jdbcContext" ref="jdbcContext" />
</bean>
</beans>
아직은 UserDao
클래스의 모든 메소드가 jdbcContext
를 사용하는 것은 아니라서 dataSource
에 대한 의존성도 필요하다.
이쯤에서 테스트를 한번 돌려보면 잘 동작한다.
UserDao
와 JdbcContext
사이에는 인터페이스를 사용하지 않고 DI를 적용했다. UserDao
와 JdbcContext
는 클래스 레벨에서 의존관계가 발생한다. 런타임 시에 DI 방식으로 외부에서 오브젝트를 주입해주는 방식을 사용하긴 했지만, 의존 오브젝트의 구현 클래스를 변경할 수는 없다.
인터페이스를 사용하지 않는다면 엄밀히 말해서 온전한 DI라고 볼 수는 없다. 하지만 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄한다.
JdbcContext
를 스프링을 이용해 UserDao
객체에서 사용하게 주입했다는 건 DI의 기본을 따르고 있다고 볼 수 있다.
JdbcContext
는 1개의 빈으로 관리 가능하다.JdbcContext
가 dataSource
라는 다른 빈에 의존해야 하기 때문이다.인터페이스가 없다는 건 UserDao
는 JdbcContext
클래스와 강한 결합을 갖고 있다는 의미이다. OOP의 설계 원칙에는 위배되지만, JdbcContext
는 테스트에서도 다른 구현으로 대체해서 사용할 이유가 없다.
이런 경우는 굳이 인터페이스를 두지 않아도 상관 없다.
단, 이런 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막 단계에서 고려해볼 사항임을 잊지 말자.
JdbcContext
를 빈으로 등록하지 않고, UserDao
내부에서 직접 DI를 적용할 수도 있다. 이 방법을 쓰려면 JdbcContext
를 스프링 빈으로 등록해서 사용했던 첫번째 이유인 싱글톤으로 만드려는 것은 포기해야 한다.
하지만 JdbcContext
자체는 싱글톤이 아니더라도, DAO 객체들은 빈으로 등록되어 싱글톤으로 관리될 것이기 때문에 JdbcContext
도 DAO와 1:1로 형성될 것이다. 웬만큼 대형 프로젝트라도 수백개면 충분할 것이다.
UserDao
가 직접 JdbcContext
에 DataSource
를 DI해주도록 코드를 변경해보자.
public class UserDao {
DataSource dataSource;
JdbcContext jdbcContext;
public UserDao() {
}
public UserDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
this.jdbcContext = new JdbcContext();
jdbcContext.setDataSource(dataSource);
}
...
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="connectionMaker" class="toby_spring.chapter1.user.connection_maker.DConnectionMaker" />
<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
<property name="username" value="postgres" />
<property name="password" value="iwaz123!@#" />
<property name="driverClass" value="org.postgresql.Driver" />
<property name="url" value="jdbc:postgresql://localhost/toby_spring" />
</bean>
<bean id="userDao" class="toby_spring.chapter1.user.dao.UserDao">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
이 방법의 장점은 굳이 인터페이스를 두지 않아도 될만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext
를 어색하게 따로 빈으로 분리하지 않고 내부에서 직접 만들어 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 점이다. 이렇게 한 오브젝트의 수정자 메소드에서 다른 오브젝트를 초기화하고 코드를 이용해 DI하는 것은 스프링에서도 종종 사용되는 기법이다.
지금까지 JdbcContext
와 같이 인터페이스를 사용하지 않고 DAO와 밀접한 관계를 갖는 클래스를 DI에 적용하는 방법 두가지를 알아보았다.
상황에 따라 적절한 방법을 선택해야 하며, 왜 그렇게 선택했는지에 대한 근거가 있어야 한다. 분명하게 설명할 자신이 없다면 차라리 인터페이스를 만들어 평범한 DI 구조로 만드는 게 나을 수도 있다.