✅ 예외처리 기능을 갖춘 DAO
JDBC 코드에는 반드시 예외처리를 해야한다. 어떤 이유로든 예외가 발생했을 경우에 사용한 리소스를 반환해야한다. 서버는 제한된 개수의 DB 커넥션을 만들어 재사용이 가능한 풀(pool)로 관리한다. 만약 예외가 발생해서 리소스가 반환되지 않는다면 오류를 내며 중단될 것이다.
- UserDao
//== 테이블 정보 삭제 ==// public void deleteAll() throws SQLException { Connection con = dataSource.getConnection(); //여기서 예외가 발생하면 리소스를 반환하지 않는다. String sql = "DELETE FROM users"; PreparedStatement pst = con.prepareStatement(sql); pst.executeUpdate(); pst.close(); con.close(); }
이 메소드에서 Connection과 PreparedStatement라는 공유 리소스를 가져와 사용한다. 만약 PreparedStatement를 실행하는 중에 오류가 발생한다면 메소드를 끝까지 실행하지 못하고 종료되어, 사용하고 있던 리소스도 반환되지 않는다.
그래서 어떤 상황에서도 가져온 리소스를 반환하도록 try-catch-finally 구문을 사용해 반드시 리소스를 반환하게 만든다.
- UserDao, try-catch-finally 적용
//== 테이블 정보 삭제 ==// public void deleteAll() throws SQLException { Connection con = null; PreparedStatement pst = null; try { con = dataSource.getConnection(); String sql = "DELETE FROM users"; pst = con.prepareStatement(sql); pst.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } }
finally 구문은 try 구문을 수행 중, 정상처리 혹은 예외처리에 관계 없이 반드시 실행되는 구문이다. 위 코드에서는 Connection을 가져오다가 오류가 발생할 수도 있고, PreparedStatement를 실행하다가 오류가 발생할 수도 있다. 그렇기 때문에 if 문을 사용해서 con, pst가 null 인지 아닌지 반드시 확인해야한다.
또
.close()
메소드는SQLException
오류를 던지기 때문에 try-catch 문을 통해 해당 오류를 다시 던져야 한다. 오류를 던지면 해당 메소드에서 오류를 처리하지 않고, 메소드를 사용한 쪽에서 해당 오류를 처리해줘야한다..close()
메소드를 사용하여 리소스를 반환할 때는 생성 순서의 반대로 닫아주어야 한다.
- UserDao, getCount()
//== 테이블 정보 개수 조회 ==// public int getCount() throws SQLException { Connection con = null; PreparedStatement pst = null; ResultSet rs = null; try { con = dataSource.getConnection(); String sql = "SELECT COUNT(*) FROM users"; pst = con.prepareStatement(sql); rs = pst.executeQuery(); rs.next(); return rs.getInt(1); } catch (Exception e) { throw e; } finally { if (rs != null) { try {rs.close(); } catch (SQLException e) {} } if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } }
검색 기능을 수행할 때는 검색을 통해서 반환받는 ResultSet 객체도 try-catch 구문으로 감싸야 하며, 해당 리소스도
.close()
메소드로 반환해야 한다.
👉UserDaoTest를 수행하면 잘 작동하는 것을 확인할 수 있다. 그렇지만 너무 복잡한 코드, 반복되는 코드가 아쉽다.
✅ 분리와 재사용을 위한 디자인 패턴 적용
너무 복잡하고, 반복되는 코드는 작성하다보면 하나씩 빼먹는 실수를 야기할 수 있다. .close()
메소드가 제대로 실행되는지 확인하는 코드를 작성할 수도 있지만 근본적인 원인은 제거되지 않는다. 우선은 변하는 코드와 변하지 않는 코드를 메소드 추출 리팩토링, 템플릿 메소드 패턴, 전략패턴으로 적용해서 분리하고 재사용이 가능한 코드로 만들어보자.
- 변하는 코드와 변하지 않는 코드
Connection, PreparedStatement 객체를 호출하는 것과 해당 리소스를 반환하는 코드는 업데이트용 쿼리를 실행하는 것이라면 위의 부분과 같이 어디든 동일할 것이다. 쿼리를 실행하는 부분만 기능에 따라 달라진다. 변하지 않는다면 공통된 부분을 리팩토링할 수 있지 않을까?
- UserDao, deleteAll(), 메소드 추출 리팩토링
//== 테이블 정보 삭제 ==// public void deleteAll() throws SQLException { Connection con = null; PreparedStatement pst = null; try { con = dataSource.getConnection(); pst = makeStatement(con); pst.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } } //== 메소드 추출 리팩토링 적용 ==// private PreparedStatement makeStatement(Connection con) throws SQLException { String sql = "DELETE FROM users"; PreparedStatement pst = con.prepareStatement(sql); return pst; }
공통된 부분을 메소드 추출 리팩토링을 적용해보았다. 하지만 메소드 추출 리팩토링은 분리시킨 메소드를 다른 곳에 재활용할 수 있어야 하는데 위의 경우는 분리되고 남은 deleteAll() 메소드가 각각에 기능에 맞는 메소드에서 공통적으로 재사용되어야할 부분이고, makeStatement() 메소드가 여러가지 쿼리를 만들어가며 기능에 맞게 확장되어야하는 부분이 되서 반대로 되었다.
- 템플릿 메소드 패턴 적용
- UserDaoDeleteAll
package vol1.jhcode.ch3.user.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class UserDaoDeleteAll extends UserDao { protected PreparedStatement makeStatement(Connection con) throws SQLException { String sql = "DELETE FROM users"; PreparedStatement pst = con.prepareStatement(sql); return pst; } }
UserDao
클래스의 기능을 확장하고 싶을 때마다 상속을 통해 확장할 수 있고, 상위 클래스인 UserDao의 불필요한 변경이 없어 개방 폐쇄 원칙( OCP ), “확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다”를 그럭저럭 지킬 수 있다.하지만 아래 그림과 같이 DAO 로직마다 상속을 통해 새로운 클래스를 계속 만들어야 한다는 단점이 있다. 또한
UserDao
는 DAO 기능을 사용할 때마다 상속을 통해 구현한 어떤 클래스를 사용할 것인지 알아야 하는 관계가 결정되어 유연성이 떨어진다.
- UserDaoDeleteAll
- 전략 패턴 적용
전략 패턴의 핵심 아이디어는 컨텍스트가 직접적인 의존성을 가지지 않고, 대신 인터페이스를 통해 간접적으로 의존하게 되는 것이다.
PreparedStatement
객체를 만들어주는 외부 기능을 만들고, 이를 인터페이스를 통해서UserDao
가 사용하도록 하는 것이다.- StatementStrategy
package vol1.jhcode.ch3.user.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public interface StatementStrategy { //== 전략패턴에서 인터페이스로 사용할 공통된 기능인, PrerparedStatement 객체 생성 메소드 ==// PreparedStatement makePreparedStatement(Connection con) throws SQLException; }
인터페이스에서 공통적으로 사용할
PreparedStatement
객체 생성 메소드를 정의한다.
- DeleteAllStatement
package vol1.jhcode.ch3.user.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class DeleteAllStatement implements StatementStrategy { //== deleteAll() 메소드에서 사용될 구체적인 PreparedStatement 객체 생성 구현부 ==// @Override public PreparedStatement makePreparedStatement(Connection con) throws SQLException { String sql = "DELETE FROM users"; PreparedStatement pst = con.prepareStatement(sql); return pst; } }
StatementSrategy
인터페이스를 구현하여deleteAll()
메소드에서 사용될PreparedStatement
객체 생성 메소드를 구현한다.
- UserDao, deleteAll() 메소드에 적용
//== 테이블 정보 삭제 ==// public void deleteAll() throws SQLException { Connection con = null; PreparedStatement pst = null; try { con = dataSource.getConnection(); StatementStrategy strategy = new DeleteAllStatement(); pst = strategy.makePreparedStatement(con); pst.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } }
전략패턴을 통해서 인터페이스로 해당 기능을 접근하긴 하지만,
UserDao
클래스에서 인터페이스로 접근하는 기능을 실제적으로 구현한 클래스인DeleteAllStatement
에 대해서 직접적으로 알게 된다. 간접적으로 알게 되는 전략패턴에 적합하다고 볼 수도 없고, 구현체가 바뀔 때마다UserDao
코드의 구현체 생성 부분을 바꾸어야 하기 때문에 OCP 에도 잘 맞는다고 볼 수 없다.
- StatementStrategy
✅ DI 적용을 위한 클라이언트/컨텍스트 분리
어떤 구현 클래스를 쓸 것인지 자신이 직접 정하는 것이 아니라 client가 정한 구현 클래스를 주입받아 사용하는 것이 DI의 핵심이다.
컨텍스트인 UserDao
가 필요로 하는 전략인 Strategy
를 사용할 수 있게, Strategy
를 구현한 특정 클래스인 ConcretaStrategyA
를 Client가 생성하여 제공해주는 방법을 사용할 수 있다.
전략 객체 (ConcretaStrategy) 생성과, 컨텍스트 (UserDao)로 전달을 담당하는 책임을 분리한 것이 Factory라고 할 수 있다. 결국 앞에서 배웠던 DI란, 이러한 전략 패턴의 장점을 일반적으로 간단하게 사용할 수 있도록 만든 구조이다.
- UserDao, jdbcContextWithStatementStrategy()
//...생략 public void jdbcContextWithStatementsStrategy(StatementStrategy stmt) throws SQLException{ Connection con = null; PreparedStatement pst = null; try { con = dataSource.getConnection(); pst = stmt.makePreparedStatement(con); pst.executeUpdate(); } catch (SQLException e) { throw e; } finally { if (pst != null) { try {pst.close(); } catch (SQLException e) {} } if (con != null) { try {con.close(); } catch (SQLException e) {} } } } //...생략
StatementStrategy 구현 클래스를 인수로 받아서, PreparedStatement를 실행하는 메소드를 만들었다. 이 메소드에 의해서 DAO의 확장된 기능들은 StatementStrategy의 구현 클래스에 정의해두고, DAO에서는 각 메소드에 적절한 구현 클래스를 주입하여 사용하기만 하면 된다.
- UserDao, deleteAll()
//== 테이블 정보 삭제 ==// public void deleteAll() throws SQLException { StatementStrategy st = new DeleteAllStatement(); jdbcContextWithStatementsStrategy(st); }
코드가 정말 짧아졌지 않은가? 비록 여기서는 StatementStrategy의 구현 클래스에 대한 정보를 가지고 있어서 아쉽다. 클라이언트와 컨텍스트를 분리하지 않았기 때문인데 학습용으론 충분하다고 생각한다. 만약 분리하고자 한다면 1장에서와 같이 UserDao의 deleteAll() 메소드를 사용하는 Client인 UserDaoTest에서 인수로 deleteAll() 메소드에 StatementStrategy 타입의 객체를 주입하면 될 것이다.
👉이로써 전략 패턴의 장점을 일반적으로 사용하게 해주는 DI를 적용하는 부분까지 해보았다. 다소 어려울 수 있으나 여기서 핵심은 전략 패턴인 인터페이스를 통해 간접적으로 의존하게 하는 것과, DI는 이러한 전략 패턴의 장점을 일반적으로 사용하게 만들어주는 구조로 되어있다는 점을 기억하자.
📖토비 스프링 3.1 -p209~224
Uploaded by N2T