두 부분을 전략 패턴을 이용해 분리해냈다. 독립된 JDBC 작업 흐름이 담긴 jdbcContextWithStatementStrategy()
는 DAO 메소드들이 공유할 수 있는 메소드다. 해당 메소드에 바뀌는 전략들에 대한 클래스를 추가하여 재활용할 수 있다.
여기서
컨텍스트
는 PreparedStatement
를 실행하는 JDBC의 작업 흐름이다.전략
은 PreparedStatement
를 생성하는 것이다. public void add(User user) throws SQLException {
jdbcContextWithStatementStrategy(new AddStatement(user));
}
public class AddStatement implements StatementStrategy{
private User user;
public AddStatement(User user) {
this.user = user;
}
@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;
}
}
add()
메소드에서는 deleteAll()
메소드와 다르게, User
라는 정보가 필요했다. 그래서 생성자에 User
정보를 받는 부분을 추가했다.
add()
메소드의 코드가 훨씬 개선되었다. 테스트를 통해 테스트해보자. 앞으로 비슷한 기능의 DAO 메소드가 필요할 때마다 이 Statement
전략과 jdbcContextWithStatementStrategy()
컨텍스트를 활용할 수 있으니 try/catch/finally
로 범벅된 코드를 만들다가 실수할 염려는 사라졌다. DAO 코드도 간결해졌다. DAO 코드의 양을 많게는 70~80%까지 줄일 수 있다.
미리 준비해둔 테스트덕에 마음껏 코드를 수정해도 정상적으로 동작한다는 확신을 가질 수 있다.
사실 현재 만들어진 구조에 두가지 불만이 있다.
StatementStrategy
구현 클래스를 만들어야 한다.StatementStrategy
에 전달할 부가 정보가 있는 경우, 클래스에 번거롭게 인스턴스 변수를 만들어줘야 한다.클래스를 매번 독립된 파일로 만들지 않고 UserDao
클래스 내부 클래스로 정의하면 클래스 파일이 많아지는 문제를 해결할 수 있따.
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy{
private User user;
public AddStatement(User user) {
this.user = user;
}
@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;
}
}
jdbcContextWithStatementStrategy(new AddStatement(user));
}
위처럼 로컬 클래스로 구성하면, 따로 클래스 파일을 만들지 않아도 된다.
AddStatement
가 사용될 곳이 add()
메소드 뿐이라면, 이렇게 사용하기 전에 바로 정의해서 쓰는 것도 나쁘지 않다. 덕분에 클래스 파일이 하나 줄고, add()
메소드 안에서 PreparedStatement
생성 로직을 함께 볼 수 있으니 코드를 이해하기도 좋다.
로컬클래스에는 또 한가지 장점이 있는데, 내부 클래스기 때문에 자신을 선언한 메소드의 로컬 변수에 접근할 수 있다는 점이다. 그래서 User
를 생성자로 받을 필요가 사라진다.
public void add(User user) throws SQLException {
class AddStatement implements StatementStrategy{
@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;
}
}
jdbcContextWithStatementStrategy(new AddStatement());
}
단, final
로 선언된 외부 로컬 변수에만 접근할 수 있다. 위 예제에서는 final
을 붙이지 않았지만, Effectively final
(참조 링크1, 참조 링크2) 때문에 자동으로 final
이 붙은 효과가 나타난다. 내부 클래스 선언 이후에 User
객체가 가리키는 곳을 다른데로 바꾸면 에러가 날 것이다.
AddStatement
는 여기 해당한다.한가지 더 욕심을 내서 AddStatement
클래스의 이름도 제거해보자. 익명 내부 클래스를 이용하면 된다.
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;
};
jdbcContextWithStatementStrategy(stmt);
}
메소드가 하나밖에 없는 인터페이스를 익명 내부 클래스로 구현하는 경우, 위와 같이 람다로 구현할 수 있다.
deleteAll()
메소드도 위와 같은 방식으로 간단히 정리해보면,
public void deleteAll() throws SQLException {
StatementStrategy strategy = c -> c.prepareStatement("delete from users"); // 선정한 전략 클래스의 오브젝트 생성
jdbcContextWithStatementStrategy(strategy); // 컨텍스트 호출, 전략 오브젝트 전달
}
위와 같이 줄일 수 있다.
이제, 많은 클래스 파일이 생기지도 않고 메소드의 로컬 변수를 직접 이용하는데에 문제도 없어졌다.