앞서, 초난감 DAO 중에서 변경이 자주 일어나는 부분과 고정된 부분에 대해 효과적으로 활용하기 위한 리팩토링을 진행해보았다.
템플릿이란, 이렇게 서로 다른 부분에서 변경이 거의 일어나지 않는 부분을 자유롭게 변경되는 성질을 가진 부분으로 독립시켜 효과적으로 활용할 수 있게 하는 방법이다.
UserDao 예외 처리
기존에 작성하였던 UserDao는 예외처리 구문이 빠져있다. JDBC 코드에서는 어떤 상황에도 정상적으로 리소스를 반환하도록 try/catch/finally 구문을 사용할 것을 권장한다. 기존에 작성하였던 deleteAll 함수를 보자.
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
이 코드에서 예외가 발생할 수 있는 부분은 다음과 같다.
- Connection과 PreparedStatement를 처리하는 중에 예외가 발생하여 close() 메소드를 실행하지 않고 종료하는 경우
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 {
// null 상태인 ps, c를 close() 하는 일이 없도록 null check
if(ps != null){
try {
ps.close();
} catch (SQLException e){
// ps.close() 함수를 사용할 때도 SQLException이 발생할 수 있다.
}
}
if(c != null){
try{
c.close();
} catch (SQLException e){
}
}
이러한 설계의 문제점은 코드가 길어진다는 것이다. 많은 곳에서 중복되는 코드를 로직에 따라 잘 분리하는 것이 목적이다.
디자인 패턴을 이용한 개선
메소드 추출 기법
궁금한 점
원래 메소드 추출 기법에서는 변하는 부분을 메소드로 추출하나 ?
deleteAll() 함수에서 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출하도록 만들었다. 그러나 이 함수에서 변하는 부분은 PreparedStatement 객체를 생성하는 부분이고, 이것을 메소드로 뺐을 때 오히려 분리된 메소드를 DAO 마다 새롭게 만들어서 확장해야하는 일이 생기기 때문에 비효율적이다.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = makeStatement(c);
ps.executeUpdate();
}
...
}
private PreparedStatement makeStatement(Connection c) throws SQLException{
PreparedStatement ps;
ps = c.prepareStatement("delete from users");
return ps;
}
템플릿 메소드 패턴
변하지 않는 부분은 상위 클래스에 두고, 변하는 부분을 추상 메소드로 정의하여 서브클래스에서 오버라이드하는 방법이다. 우리는 위에서 만든 makeStatement(Connection c) 함수를 추상 메소드로 선언하여 템플릿 메소드 패턴을 적용할 수 있다.
abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;
..
public class UserDaoDeleteAll extends UserDao {
protected PreparedStatement makeStatement(Connection c) throws SQLException{
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
이 방법으로, 우리가 만들고자 하는 PreparedStatement에 맞추어 추상 클래스를 오버라이딩하여 서브 클래스를
생성할 수 있다. 문제는 DAO 로직을 하나 생성할 때마다 서브 클래스를 만들어야 한다는 것이다.
코드 상에서도 명확한 상속 클래스와 서브 클래스의 관계는 컴파일 시점에서 그 관계가 결정되는 것이기 때문에, 유연성이 떨어지는 설계가 된다.
전략 패턴
전략 패턴은 오브젝트를 둘로 분리하고, 클래스 레벨에서는 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다. OCP (개방 폐쇄 원칙)의 관점에서 봤을 때, 변하는 부분을 구현 클래스로 만들어 추상화된 인터페이스를 통해 위임 받는 방식이다.
deleteAll()에서 변하지 않는 부분이라고 명시한 것이 contextMethod()이고, "DB를 업데이트하는 작업"이라는 변하지 않는 맥락을 가진다. 그리고, deleteAll()의 컨텍스트 중에서 PreparedStatement를 만들어줄 외부 기능을 호출하는 기능이 바로 전략이다.
따라서, 이 기능을 인터페이스로 만들어두고 인터페이스의 메소드를 통해 PreparedStatment 생성 전략을 호출해주면 된다. PreparedStatement 객체를 생성할 때는 Connection 객체가 필요하므로, 파라미터에 Connection 객체를 넣어주면 인터페이스 메소드가 완성된다.
public void deleteAll() throws SQLException {
try {
c = dataSource.getConnection();
// 클라이언트에 들어가야 할 코드
StatementStrategy startegy = new DeleteAllStatement();
ps = startegy.makePreparedStatement(c);
ps.executeUpdate();
}
...
}
여기에서 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면, 인터페이스 뿐만 아니라 특정 구현 클래스를 직접 알고 있어야 하므로 OCP의 원칙에 잘 맞지 않는 것 같다.
궁금한 점
1. 맥락, 전략이라는 말이 코드에서 어떻게 사용되는 건지 와닿지 않는다.
함수가 구현하고자 하는 주요 기능이 맥락이고, 여러가지 맥락 (컨텍스트) 중에서 확장에 해당하는 변하는 부분을 골라서 전략이라고 이름을 붙여 사용 (인터페이스화) 하는 것인가 ?
2. 인터페이스 메소드 makePreparedStatement를 호출하는 deleteAll 메서드가 클라이언트의 역할을 하는 것인가?
DI 적용을 위한 클라이언트/컨텍스트 분리
어떤 전략을 사용할 지는 Context의 앞단에 있는 Client가 결정하는 것이 일반적이다. UserDao (컨텍스트) 에서 사용할 ConnectionMaker 인터페이스 (전략) 를 특정 구현 클래스를 선택해 오브젝트로 생성하고 컨텍스트로 전달하는 역할을 DaoFactory가 수행하였었다. 이를 일반화한 것이 DI라는 개념이며, deleteAll() 메서드에서 구현 클래스를 선택하고 컨텍스트에 전달하는 역할을 클라이언트에게 위임하자.
따라서 deleteAll() 에서 클라이언트 코드는 아래 코드이며, 나머지 코드는 컨텍스트 코드이므로 분리해야한다.
StatementStrategy strategy = new DeleteAllStatement();
p.222의 내용이 잘 이해가지 않는다.
"컨텍스트에 해당하는 부분은 별도의 메소드로 독립시킨다. 클라이언트는 전략 클래스의 오브젝트를 컨텍스트의 메소드로 호출하며 전달해야한다. 이를 위해 전략 인터페이스인 StatementStrategy를 컨텍스트 메소드 파라미터로 지정할 필요가 있다."
deleteAll() 에서 컨텍스트 코드를 메소드로 분리한 코드를 작성해보자.
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) {} }
}
}
위 코드에서 기존과 변경된 핵심적인 사항을 간추려보겠다.
- 클라이언트 (UserDao) 로부터 전략 인터페이스 (StatementStrategy) 를 제공받는다.
- 전략 오브젝트 (stmt) 는 PreparedStatement 생성이 필요한 시점에 내부 메서드를 호출해 사용한다.
그럼 이 함수를 사용하는 클라이언트인 deleteAll은 어떤 작업을 수행할까?
- 사용할 전략 클래스 (= 구현 클래스)로 오브젝트를 생성
- jdbcContextWithStatementStrategy 함수의 파라미터로 입력하여 호출
public void deleteAll() throw SQLException {
StatementStrategy st = new DeleteAllStatement(); // 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(st); // 컨텍스트 호출
}
이 방법으로, 다형성의 원리에 의해 자동으로 인터페이스의 구현 클래스가 지정되어 jdbcContextWithStatementStrategy 메소드에서 아래의 makePreparedStatement 메소드를 호출한다.
이로써 DAO 메소드들이 jdbcContextWithStatementStrategy 메소드를 이용해 공유할 수 있게 되었다 .
정리하자면, 컨텍스트는 PreparedStatement를 실행하는 JDBC 작업의 흐름이고, 전략은 PreparedStatement를 생성하는 것이다.
궁금한 점
1. 테스트 메소드 안에서 직접 구현 클래스가 결정되는 현상을 막기 위해서 컨텍스트 코드를 분리하였다. 그런데 여전히 테스트 메소드 (deleteAll)에는 구현 클래스를 결정하는 코드가 있는건데, 그냥 deleteAll을 클라이언트로 명명하고 그 이외의 변하지 않는 맥락 (jdbcContextWithStatementStrategy)을 따로 빼서 확장에 용이하게 한 것 뿐인건가 ?
2. 제목에서 DI를 사용해 컨텍스트와 클라이언트를 분리한다고 하였다. 내가 생각했을 때, 이 부분에서 DI가 사용된 부분은 파라미터 (인터페이스)에 구현 클래스가 들어간 부분이라고 짐작되는데, 맞는가? 아니라면 어떤 부분에서 DI가 사용된 것인가?
전략 클래스 추가
DeleteAllStatement 이외에도 AddStatement 클래스를 만들어 전략 클래스를 추가해보자.
deleteAll() 과는 다르게, 생성 메서드를 만들기 위해서는 쿼리에 사용할 user 정보가 필요하다. 이를 클라이언트가 제공하고, 생성자 주입을 이용해 AddStatement 구현 클래스를 만들 시 User 객체를 전달하도록 수정한다.
JDBC 전략 패턴의 최적화
위에서 전략 패턴을 사용한 방식의 문제점은 다음과 같다.
- DAO 메소드마다 구현 클래스를 추가해야한다.
- StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이 오브젝트를 전달받는 생성자와 파라미터를 번거롭게 만들어야 한다.
로컬 클래스
로컬 변수를 선언하듯이, UserDao 안에 로컬 클래스로서 전략 클래스를 만드는 방법이다. 로컬 클래스를 사용하면 User 오브젝트와 같은 부가적인 오브젝트를 전달할 필요가 없다는 장점이 있다. 전략 클래스 자체가 클라이언트와 동일한 클래스 안에 위치해있기 때문에 필요한 오브젝트를 공유하고 있기 때문이다.
또한, 이 방법을 사용하면 구현 클래스를 새로 생성할 필요가 없기 때문에 첫 번째 문제도 해결된다.
public void add(final User user) throws SQLException {
class AddStatement implements StatementStrategy { // add() 메소드 내부에 선언된 로컬 클래스
@Override
public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
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;
}
}
StatementStrategy st = new AddStatement();
jdbcContextWithStatementStrategy(st);
}
내부 클래스에서 외부 변수를 사용할 때는, 외부 변수를 반드시 final로 선언해주어야 한다.
익명 내부 클래스를 사용해서, 로컬 클래스의 이름을 제거한 형태로 더 간단하게 클래스를 선언할 수 있다.
조금 더 개선하여, 익명 내부 클래스의 오브젝트를 담아둘 필요 없이 jdbcContextWithStatementStrategy의 파라미터에서 바로 생성하는 방법이 있다.
다음 포스팅에서는 jdbcContextWithStatementStrategy()라는 함수를 UserDao 클래스 밖으로 독립시켜서 모든 DAO 가 사용할 수 있도록 하는 실습을 해보겠다.
'DEV book > 토비의 스프링 3.1' 카테고리의 다른 글
[토비의 스프링 3.1] 3장 템플릿 (3) - 템플릿과 콜백 (0) | 2023.09.24 |
---|---|
[토비의 스프링 3.1] 3장 템플릿 (2) - JdbcContext를 UserDao에서 사용하는 두 가지 방법 (0) | 2023.09.24 |
[토비의 스프링 3.1] 2장 테스트 - 단위 테스트와 테스트 코드 개선 (0) | 2023.09.20 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (3) - 싱글톤 레지스트리와 의존관계 주입 (0) | 2023.09.14 |
[토비의 스프링 3.1] 1장 오브젝트와 의존관계 (2) - 제어의 역전 (IoC)과 ApplicationContext (0) | 2023.09.13 |