Chapter 03 본 내용은 토비의 스프링 3 책의 내용을 정리한 것입니다.
토비의 스프링은 스프링 뿐아니라 객체 지향의 기본 원리, 테스팅, 예외 처리, 디자인 패턴 등 Java 개발자라면 반드시 알아야 하는 내용을 스토리 전개 방식으로 점진적으로 친절하게 설명해주는 명저입니다.
똑같은 내용으로 미국에서 영어로 출간되었다면 Jolt상을 받기에도 충분한 책이라고 생각합니다.
책의 절대적인 가격은 높아보이지만 웬만한 책 두어권 보는 것보다 이 한 권의 책을 여러번 보는 것이 비용 대비 효과가 훨씬 좋을 것입니다. 이 책을 읽고 아낄 수 있는 많은 시간, 높아질 몸값을 생각하면 충분히 투자할 가치가 있는 책입니다.
http://www.acornpub.co.kr/book/toby-spring3-1-set
DB 관련 자원 반납 DAO에서 DB Connection을 가져오는 부분을 DataSource 인터페이스로 분리하고, DataSource 구현체를 DI 받아오도록 개선했지만, JDBC 리소스의 반납 관련 예외 처리 코드가 여전히 DAO에 남아있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class UserDao { private DataSource dataSource; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } private Connection getConnection () { dataSource.getConnection(); } public void deleteAll () throws SQLException { Connection c = null ; PreparedStatement ps = null ; try { c = 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) {} } } } public ResultSet getAll () throws SQLException { Connection c = null ; PreparedStatement ps = null ; ResultSet rs = null ; try { c = getConnection(); ps = c.prepareStatement("select from users" ); rs = ps.executeQuery(); return rs; } catch (SQLException e) { throw e; } finally { if (rs != null ) { try { rs.close(); } catch (SQLException e) {} } if (ps != null ) { try { ps.close(); } catch (SQLException e) {} } if (c != null ) { try { c.close(); } catch (SQLException e) {} } } } }
자원 반납 코드의 중복 위와 같이 DAO 메서드마다 자원 반납 코드가 계속 중복된다.
그리고 DAO 메서드에는 다음과 같은 공통 흐름이 있다.
dataSource에서 DB연결을 가져오고
쿼리 생성
쿼리 실행
예외 발생 시 던지기
모든 처리 후 자원 반납 처리
여기에서 변하는 부분(전략)은 쿼리 생성, 나머지는 변하지 않는다(컨텍스트).
전략 패턴을 통한 전략 분리 변하지 않는 부분을 하나의 메서드(여기에서는 jdbcContextWithStatementStrategy())로 빼내고, 변하는 부분을 jdbcContextWithStatementStrategy()에 인자로 주입하는 방식으로 컨텍스트(자원 반납 처리 부분)와 전략(쿼리 생성 부분)을 분리한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 public class UserDao { private DataSource dataSource; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } private Connection getConnection () { dataSource.getConnection(); } public deleteAll () throws SQLException { StatementStrategy strategy = new DeleteAllStatement(); jdbcContextWithStatementStrategy(strategy); } public add () throws SQLException { StatementStrategy strategy = new SelectAllStatement(); jdbcContextWithStatementStrategy(strategy); } public void jdbcContextWithStatementStrategy (StatementStrategy strategy) throws SQLException { Connection c = null ; PreparedStatement ps = null ; try { c = getConnection(); ps = strategy.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) {} } } } } public interface StatementStrategy { PreparedStatement makeStatement (Connection c) throws SQLException ; } public class DeleteAllStatement implements StatementStrategy { public PreparedStatement makeStatement (Connection c) throws SQLException { return c.prepareStatement("delete from users" ); } } public class SelectAllStatement implements StatementStrategy { public PreparedStatement makeStatement (Connection c) throws SQLException { return c.prepareStatement("select * from users" ); } }
조금 더 최적화해서 전략을 익명 클래스로 바꿔보자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public void deleteAll () throws SQLException { jdbcContextWithStatementStrategy( new StatementStrategy() { public PreparedStatement makeStatement (Connection c) throws SQLException { return c.prepareStatement("delete from users" ); } } } } public void add (final User user) throws SQLException { jdbcContextWithStatementStrategy( new StatementStrategy() { public PreparedStatement makeStatement (Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("inser into users(id, name, password) values (?, ?, ?)" ); ps.setString(1 , user.getId()); ps.setString(2 , user.getName()); ps.setString(3 , user.getPassword()); return ps; } } } }
메서드 였던 jdbcContextWithStatementStrategy를 JdbcContext 클래스로 분리 DB 연결을 가져오고 DB 연결 자원을 반납하는 jdbcContextWithStatementStrategy()는 UserDao 뿐 아니라 다른 DAO에서도 사용될 수 있다.
재사용할 수 있도록 별도의 클래스로 분리하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 public class JdbcContext { DataSource dataSource; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } private Connection getConnection () { dataSource.getConnection(); } public void processStatement (StatementStrategy strategy) throws SQLException { Connection c = null ; PreparedStatement ps = null ; try { c = getConnection(); ps = strategy.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) {} } } } } public class UserDao { JdbcContext context; public void setJdbcContext (JdbcContext context) { this .context = context; } public void deleteAll () throws SQLException { this .context.processStatement( new StatementStrategy() { public PreparedStatement makeStatement (Connection c) throws SQLException { return c.prepareStatement("delete from users" ); } } ); } public void add (final User user) throws SQLException { this .context.processStatement( new StatementStrategy() { public PreparedStatement makeStatement (Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("inser into users(id, name, password) values (?, ?, ?)" ); ps.setString(1 , user.getId()); ps.setString(2 , user.getName()); ps.setString(3 , user.getPassword()); return ps; } } ); } }
중복은 아직도 남아있다. 바로, 익명 클래스 생성 부분이다. 메서드로 빼서 최적화하자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class JdbcContext { DataSource dataSource; public void setDataSource (DataSource dataSource) { this .dataSource = dataSource; } private Connection getConnection () { dataSource.getConnection(); } public void executeSql (final String query) { processStatement( new StatementStrategy() { public PreparedStatement makeStatement (Connection c) throws SQLException { return c.prepareStatement(query); } ) ) } private void processStatement (StatementStrategy strategy) throws SQLException { Connection c = null ; PreparedStatement ps = null ; try { c = getConnection(); ps = strategy.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) {} } } } } public class UserDao { JdbcContext context; public void setJdbcContext (JdbcContext context) { this .context = context; } public void deleteAll () throws SQLException { this .context.executeSql("delete from users" ); } }
클래스 다이어그램 지금까지 개선한 내용을 클래스 다이어그램으로 표시하면 다음과 같다.
Spring의 JdbcTemplate Spring에서는 JdbcTemplate을 제공해서 앞에서 알아본 JdbcContext가 하는 역할을 편리하게 수행할 수 있게 해준다.
Spring JdbcTemplate은 update(), queryForInt(), queryForObject(), query() 등의 메서드를 지원하며, 실전에서는 JdbcTemplate를 확장한 NamedParameterJdbcTemplate을 주로 사용한다.
1 2 3 4 5 6 7 8 9 10 11 12 public class UserDao { JdbcTemplate jdbcTemplate; public void setJdbcTempate (JdbcTemplate template) { this .template = template; } public void deleteAll () throws SQLException { this .template.update("delete from users" ); } }
사실상 쿼리문만 명시해주면 DB 연결과 DB 연결 자원 반납은 더 이상 신경쓰지 않게 되었다. 초난감 DAO와 비교해보면 정말 많은 부분이 개선되었다.
하지만 곳곳에 있는 throws SQLException이 눈에 거슬린다.
4장에서는 예외를 처리하는 방법에 대해 알아본다.