예외처리 기능을 갖춘 DAO
public class UserDao {
DataSource dataSource;
public UserDao(DataSource dataSource) throws SQLException {
this.dataSource = dataSource;
}
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) {
}
}
if (c != null) {
try {
c.close();
} catch (SQLException e) {
}
}
}
}
...add()
...getCount()
}
우리는 전편에 초난감 Dao중 예외 처리를 하지 않았다.
일반적으로 서버에서는 제한된 개수의 DB 커넥션을 만들어서 재사용을 할 수 있는 커넥션 풀로 관리한다. close 로 자원을 반납해야 재사용을 할 수 있는데 그러지 못하면 커넥션 풀에 여유가 없어지고 리소스가 모자라는 심각한 오류가 발생한다.
변하는 것과 변하지 않는 것
jdbc의 try catch finally 를 보면 한숨만 나온다. 반복적이 코드에다 try catch 의 2중으로 중첩까지 나온다.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users"); //1
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을 표시한 곳을 빼곤 나머지는 변하지 않는 부분이다.
먼저 생각 해 볼것은 변하는 부분을 추출해 내는 것이다.
메소드 부터 추출하는게 맞지만 간단하게 추출 할수 있으므로 생략하겠다.
public abstract class UserDao {
DataSource dataSource;
public UserDao(DataSource dataSource) throws SQLException {
this.dataSource = dataSource;
}
abstract public PreparedStatement makeStatement(Connection c) throws SQLException;
...deleteAll()
...getCount()
...add()
...etc
}
우리는 다시 템플릿 메소드 패턴을 적용하였다.
UserDao를 추상 클래스를 적용한 후 다음과 같이 UserDao를 상속받아 구현 하였다.
public class UserDaoDeleteAll extends UserDao{
public UserDaoDeleteAll(DataSource dataSource) throws SQLException {
super(dataSource);
}
public PreparedStatement makeStatement(Connection c) throws SQLException {
PreparedStatement ps = c.prepareStatement("delete from users");
return ps;
}
}
UserDao클래스의 기능을 확장하고 싶을 때마다 상속을 통해 자유롭게 확장할 수 있다. 또한 다중 상속을 못하지만 더 문제는 기능이 만들어 질때마다 클래스가 계
속적으로 늘어난다. 장점보다는 단점이 더 많은듯 싶다.
전략 패턴 적용
우리는 전략 패턴을 사용하여 변하지 않는 부분을 볼 것 이다.
여기서 말하는 전략 패턴이란 PreparedStatement를 만들어주는 외부 기능을 말한다.
일단 패턴을 사용하기 위해 인터페이스를 생성하자
public interface StatementStrategy {
PreparedStatement makePreparedStatement(Connection connection) throws SQLException;
}
그리고 deleteAll을 사용하기 위해 구현체를 만들자.
public class DeleteAllStatement implements StatementStrategy{
@Override
public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
PreparedStatement ps = connection.prepareStatement("delete from users");
return ps;
}
}
그리고 변하지 않는 부분을 메소드로 추출하자
private void jdbcContextWithStatementStrategy(StatementStrategy strategy) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
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) {
}
}
}
}
위의 메소드는 변하지 않는 부분만 메소드로 추출하고 StatementStrategy 구현체에 결정 된다.
public void deleteAll() throws SQLException {
StatementStrategy strategy = new DeleteAllStatement();
jdbcContextWithStatementStrategy(strategy);
}
DeleteAllStatement 를 선정하고 jdbcContextWithStatementStrategy를 호출 한다. 이 구조가 전략 패턴이라 할 수 있다.
jdbcContextWithStatementStrategy 메소드는 핵심적인 내용을 잘 담고 있다. 클라이언트부터 전략 오브젝트를 제공받고 만들어진 컨텍스트 내에서 작업을 수행하고 있다.
하지만 아직까지 문제는 기능마다 클래스가 계속 늘어난다는 점이다.
전략 패턴의 최적화
클래스가 늘어나는 문제를 해결 해보자
익명 내무 클래스 혹은 lambda로 해결 가능하다.
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(new StatementStrategy() {
@Override
public PreparedStatement makePreparedStatement(Connection connection) throws SQLException {
return connection.prepareStatement("delete from users");
}
});
}
//java8
public void deleteAll() throws SQLException {
jdbcContextWithStatementStrategy(connection -> connection.prepareStatement("delete from users"));
}
우리는 간단하게 늘어 나는 클래스를 막을 수 있다.
하지만 jdbcContextWithStatementStrategy 메서드는 userDao만 쓰는게 아니다. 여러 Dao에서 호출 해야된다.
클래스로 분리 시키자.
public class JdbcContext {
private DataSource dataSource;
public JdbcContext(DataSource dataSource) {
this.dataSource = dataSource;
}
public void withStatementStrategy(StatementStrategy strategy) throws SQLException {
Connection c = null;
PreparedStatement ps = null;
try {
c = dataSource.getConnection();
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) {
}
}
}
}
}
우리는 이렇게 외부에서도 쓸수 있도록 클래스로 분리 하였다.
빈 의존 관계 변경
새롭게 작성된 오브젝트 간의 의존관계를 살펴보고 이를 스프링 설정에 적용해보자. UserDao는 이제 JdbcContext 에 의존 하고 있다. 스프링의 DI는 기본적으로 인터페이스를 사이에 두고 의존 클래스를 바꾸어 사용하는게 목적이다. 하지만 이경우엔 JdbcContext는 그 자체로 독립적인 JDBC 컨텍스트를 제공해주는 서비스 오브젝트로 구현 방법이 바뀔 가능성은 극히 드물다. 따라서 인터페이스를 구현하도록 하지 않고 UserDao와 JdbcContext는 인터페이스를 사이에 두지 않고 DI를 적용하는 특별한 구조가 된다.
특별한 DI
UserDao와 JdbcContext 사이에는 인터페이스를 사용하지 않고 DI를 하고 있다.
이제까지 적용했던 DI에서는 클래스 레벨에서 구체적인 의존관계가 만들어지지 않도록 인터페이스를 사용했다. 그런데 UserDao는 인터페이스를 거치지 않고 코드에서 바로 JdbcContext 클래스를 사용하고 있다. JdbcContext의 메소드를 인터페이스로 뽑아내어 정의해두고 이를 UserDao 사용하게 해야 하지 않을까? 그렇게 해도 상관은 없지만 꼭 그럴필요는 없다. 토스(236p)
우리는 다음과 같이 변경 가능하다.
public class UserDao {
private JdbcContext jdbcContext;
private DataSource dataSource;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
jdbcContext = new JdbcContext(dataSource);
}
...
...
}
우리는 UserDao에게 JdbcContext 제어권을 넘겼다.
물론 스프링의 도움을 받아서 싱글톤으로 만드는 결 포기했다고 해서 DAO 메소드가 호출될 때마다 JdbcContext 오브젝트를 새로 만드는 무식한 방법을 시용해야 한다는 것은 아니다. 조금만 타협을해서 Dao마다 하나의 JdbcContext 오브젝트를 갖고 있게 하는 것이다. 기껏해야 대형 프로젝트라고 해도 수백개면 충분할 것이다.(토스 237p)
JdbcTemplate
public class UserDao {
private JdbcContext jdbcContext;
private DataSource dataSource;
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
jdbcContext = new JdbcContext(dataSource);
jdbcTemplate = new JdbcTemplate(dataSource);
}
}
우리는 JdbcTemplate 과 비슷한 JdbcContext구현 해봤다.
JdbcTemplate 도 이와같은 비슷한 역할을 하고 있다.
public void deleteAll() throws SQLException {
jdbcContext.executeSql("delete from users");
}
public int getCount() {
return jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
}
public List<User> getUser() {
return jdbcTemplate.query("select * from users", (rs, i) -> {
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
return user;
});
}
public void add(final User user) {
jdbcTemplate.update("insert into users(id, name, password) values(?,?,?)", user.getId(), user.getName(), user.getPassword());
}
deleteAll 만 jdbcContext사용 하였고 나머지는 jdbcTemplate을 사용 하였다.
이렇게 다시보는 초난감 DAO에 대해서 알아 봤다.