템플릿이란?
코드 블럭이 갖는 특성에 따라 코드를 분리하는 것이다. 다음과 같은 특징을 갖는다고 가정한다.
- 변경을 통해 그 기능이 다양해지고 확장되는 성질을 가진 코드가 있다.
- 고정되어 있고 변하지 않으려는 성질을 가진 코드가 있다.
일정한 패턴으로 유지되는 특성을 가지는 부분을 독립시킨 것이 템플릿(틀)이다.
책에서는
UserDAO
에서 비즈니스 로직을 담당하는 부분(변화)과 DB를 연결하는 부분(고정)을 나누었다.
deleteAll()
개선하기
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users"); // 변할 수 있는 부분
ps.executeUpdate();
} catch (SQLException e) {
throw e;
} finally {
if(ps != null) { try { ps.close(); } catch (SQLException e) { } } // ps 리소스 반환
if(c != null) { try { c.close(); } catch (SQLException e) { } } // c 리소스 반환
}
}
- 현재는 DB에 연결하는 부분과 커넥션을 닫는 부분 모두의 코드를 포함하고 있다.
- 사실 위의 코드 중 DB에 커넥션을 맺고 끊는 부분은 매번 동일할 것이고,
delete from users
만 유일하게 변하는 부분일 것이다.
개선1: 메서드 추출해보기
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = makeStatement(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) { } }
}
}
private PreparedStatement makeStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
- 보통 공통 로직을 메서드로 추출하여 공통 메서드를 여러 로직에서 쓰는 방식으로 리팩토링하는 경우가 흔하다.
- 그러나 위 경우에는 메서드로 뽑았을 때 별다른 이득이 없다.
- 우리의 첫 목적은 변하지 않는 부분을 템플릿으로 만들어 계속 재활용하고 싶어했다.
- 위의 형태대로라면,
selectAll()
등과 같은 메서드를 또 만들어야 하고 그 안에는 DB 커넥션 템플릿을 입력해줘야 할 것이다. - 변하지 않는 부분을 다시 작성할 필요가 없게 만들도록 고민해보자.
개선2: 템플릿 메서드 패턴 이용해보기
public abstract class UserDao {
...
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
...
}
public class UserDaoDeleteAll extends UserDao{
@Override
protected PreparedStatement makeStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
- 변화하는 부분을
@Override
할 수 있어 템플릿 코드의 재사용이 가능하다. - 개방 폐쇄 원칙(OCP)도 어느정도 지켰다.
@Override
를 통해 변화에는 열려있고, 변경에는 닫혀있다.
- 그러나 매번 클래스를 만들어서 상속받는 것은 아무래도 좀 무리가 있다.
- 컴파일 시점에 관계가 이미 형성되어 있어 관계에 대한 유연성도 떨어진다.
개선3: 전략 패턴 이용해보기
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection c) throws SQLException;
}
public class DeleteAllStatement implements StatementStrategy{
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
StatementStrategy strategy = new DeleteAllStatement();
ps = strategy.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) { } }
}
}
- 인터페이스를 통해 의존하게 만드는 방식이다.
- 인터페이스를 이용함으로써 클래스를 이용할 때보다 구현에 훨씬 자유로워졌다.
- 인터페이스를 상속함으로써 부모 클래스의 특성을 이어받을 필요 없이 단순히 메서드 1개만 구현하면 된다.
- 어떻게든
PreparedStatement
타입을 반환하는makePreparedStatement()
메서드만 구현하면 구현에 제약이 없다.
- 런타임에 관계설정이 되어 훨씬 유연해졌다.
- 인터페이스를 이용함으로써 클래스를 이용할 때보다 구현에 훨씬 자유로워졌다.
- 여전히 템플릿 코드가
deleteAll()
에 남았다는 점은 고쳐야 한다.- 들어오는 쿼리를
delete
로 한정할 것도 아니기 때문에 메서드 이름도deleteAll()
이 될 필요가 전혀 없다.
- 들어오는 쿼리를
개선4: DI 적용을 위해 클라이언트/컨텍스트 분리하기
public class DeleteAllStatement implements StatementStrategy{
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
return c.prepareStatement("delete from users");
}
}
public void jdbcContextWithStatementStrategy(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) { } }
}
}
public void deleteAll() throws SQLException {
StatementStrategy strategy = new DeleteAllStatement(); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
- 컨텍스트를 메서드 형태(
jdbcContextWithStatementStrategy
)로 분리시켰다.- 마침내 메서드 이름을 훨씬 범용성 있고 의도도 잘 표현하는 이름으로 변경하였다.
- 이제서야 올바르게 메서드에 변하지 않는 템플릿 부분이 들어갔다.
- 관심사 분리와 유연한 확장에 대해 매우 유리해졌다.
- 이제 위 메소드로 업데이트 등 다른 쿼리도 만들어낼 수 있다.
- 전략 클래스의 구현만 변경해주면 된다.
- 한번 쓰고 말 클래스라면 중첩 익명 클래스로 구현해도 무방하다.
- 매번 귀찮게
try/catch/finally
를 안해줘도 돼서 코드의 양도 줄고 실수를 할 이유도 줄었다.
스프링에서의 DI
- 스프링은 프레임워크가 작성한 인터페이스에 구현체를 주입해주는 방식으로 DI를 해준다.
- 클라이언트에서 직접 주입하는 것이 아닌 프레임워크에서 지원하는 것이라 IoC의 개념이 적용되었다고 볼 수 있다.
- DI는 사실 꼭 클래스 단위로 일어나는 것이 아니라, 매우 작은 단위의 코드와 메서드 사이에서도 일어날 수 있다.
전략패턴 구조 다시 살펴보기
전략
:DeleteAllStatement
클래스- 변하는 부분에 대한 구현
컨텍스트
:UserDao.jdbcContextWithStatementStrategy()
- 변하지 않는 부분
클라이언트
:UserDao.deleteAll()
- 전략과 컨텍스트가 합쳐져 최종적으로 클라이언트가 사용하는 메서드
위와 같이 3가지 부분으로 나뉘어져있다.
추가 개선1: 클래스 나누기
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) { } }
}
}
}
- 위의 코드처럼 전략패턴의 3가지 구성요소 중
컨텍스트
부분만 따로 빼내서 클래스로 만들면,UserDao
는 더이상DataSource
를 사용할 필요가 없어진다. - 클래스의 책임과 분리가 좀 더 명확해지고 있다.
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);
}
...
}
DataSource
의존성에서 해방되어서, 조금 더 깔끔한 코드가 되었다.
추가 개선2: 의존성 주입 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>
- 의존성 주입 xml 은 클래스에 대한 청사진이므로 이를 보고 구조를 이해하면 조금 쉽다.
JdbcContext
클래스가DataSource
의존성을 가져가므로,DataSource
의존성을 넣어주면 된다.JdbcContext
클래스 역시 스프링 컨테이너의 빈으로 올라가 다른 곳에 주입될 수 있게 되었다.
UserDao
의 경우 완전히DataSource
의존성에서 해방된 것은 아니므로 여전히 의존성이 필요하긴 하다.
템플릿과 콜백
- 전략 패턴에서
컨텍스트
를템플릿
이라고도 부르고,전략
을콜백
이라고도 부른다.- 자바스크립트와 같이 변수 안에 함수를 할당할 수 있는 언어에서는 위와 같은 콜백을 적극 활용한다.
JdbcContext
로 살펴보는 템플릿 콜백 동작 예
콜백 재활용해보기: 변경 전
public void deleteAll() throws SQLException {
StatementStrategy strategy = c -> c.prepareStatement("delete from users"); // 선정한 전략 클래스의 오브젝트 생성
jdbcContext.workWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
- 사실 위 코드에서
"delete from users"
를 제외한 나머지 부분들은 재사용이 가능하다.
콜백 재활용해보기: 변경 후
public void deleteAll() throws SQLException {
executeSql("delete from users");
}
public void executeSql(String sql) throws SQLException {
StatementStrategy strategy = c -> c.prepareStatement(sql); // 선정한 전략 클래스의 오브젝트 생성
jdbcContext.workWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
public void add(User user) throws SQLException {
this.jdbcContext.executeSql("insert into users(id, name, password) values (?, ?, ?)"
, user.getId()
, user.getName()
, user.getPassword()
);
}
public void executeSql(String sql, Object ...parameters) throws SQLException {
StatementStrategy stmt = c -> {
PreparedStatement ps = c.prepareStatement(sql);
for(int i=1; i<=parameters.length; i++) {
ps.setObject(i, parameters[i-1]);
}
return ps;
};
this.workWithStatementStrategy(stmt);
}
- 자주 쓰는 콜백을 메서드로 빼서 메서드만 공개하므로 더욱 캡슐화가 잘되어있고, 응집도도 높아졌다.
정리
- 맨 처음
deleteAll()
메서드에는 변화하는 부분과 변화하지 않는 부분이 마구잡이로 섞여있었다.- 크게 나누자면 계속 반복되는 DB 연결 부분과 변화가 가능한
PreparedStatement
를 만드는 부분이 있었다.
- 크게 나누자면 계속 반복되는 DB 연결 부분과 변화가 가능한
- DB 연결하는 부분의 코드는 고정적으로 보고 해당 부분을
템플릿
이라고 칭했다. 개선1
에서 메서드로 추출해보았는데, 분리를 이용해 적절히 재사용할 수 있는 형태가 아니었다.개선2
에서 추상 메서드를 이용한 클래스 상속을 이용하여 드디어 분리를 이용해 적절히 재사용할 수는 있었지만, 클래스 상속이라는 불편함이 있었다.개선3
에서 인터페이스를 도입한 전략 패턴으로 이전보다 훨씬 유연하게StatementStrategy
인터페이스만 구현하면 되는 방식으로 변했다.개선4
에서 실제 클라이언트가 호출하게 되는 클라이언트 부분과 실제 동작을 수행하는 컨텍스트 부분이 나뉘었다.- 이후 나머지 개선에서
Jdbc
의DataSource
와 관련된 부분을JdbcContext
클래스로 나누고,UserDao
에는 설정된JdbcContext
를 이용해 데이터를 주고받는 부분만 남겨두었다.
추후에는 DB 와 소통하기 위한 JDBC API 를 사용하는 부분은 단계적으로
JdbcContext
로 모두 옮기고UserDao
는 클라이언트와 DB 의 소통창구 정도의 역할을 해주는 것이 설계상 바람직할 것 같다.
반응형
'프레임워크 > 토비의 스프링' 카테고리의 다른 글
토비의 스프링 5장 요약 정리 - 서비스 추상화 (0) | 2022.09.06 |
---|---|
토비의 스프링 4장 요약 정리 - 예외 처리 (0) | 2022.06.21 |
토비의 스프링 2장 요약 정리 - 테스트 (2) | 2021.12.26 |
토비의 스프링 1장 요약 정리 - 오브젝트와 의존관계 (3) | 2021.12.26 |
토비의 스프링 0장 정리 (0) | 2021.12.14 |