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장에서는 예외를 처리하는 방법에 대해 알아본다.