Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- springflow
- jQuery값전송
- 스프링데이터흐름
- 제너릭
- 페치조인
- fullcalendar
- Hibernate
- values()
- LIST
- 엔티티직접사용
- fetchjoin
- JQuery
- joinfetch
- javaservlet
- jscalendar
- 제네릭
- 대량쿼리
- calendar
- javascriptcalendar
- namedQuery
- Generic
- jQueryUI
- jQuery값전달
- paging
- 프로젝트생성
- JPQL
- 자바서블릿
- 페이징
- 벌크연산
- JPA
Archives
- Today
- Total
가자공부하러!
토비의스프링(3) - 템플릿 본문
#3장. 템플릿
요약 및 결론
템플릿/콜백 : 콜백이라는 이름의 의미처럼 다시 불려지는 기능을 만들어서 보낸다.
배운 내용 정리
- 이 책이 나온 당시에는 JdbcTemplate이 많이 사용됐었나 보다. - 예외처리, 자원관리 등 변하지 않는 부분을 메서드(jdbcContextWithStatementStrategy)로 빼고 - 메서드로 추출한 내용을 다른 클래스에서도 쓸 수 있도록 별도 클래스(JdbcContext)로 분리하는 과정을 보면서 - 중복된 내용들을 어떻게 제거해서 깔끔하고 재사용하기 좋은 코드를 만든 결과를 확인하고 - 그 결과를 스프링이 어떻게 제공하는지 봤다. - 비록 JdbcTemplate은 사용할 일이 없을 것 같지만, 스프링에 녹아있는 아이디어를 알게됐다.
책 내용
템플릿 기법 : 변경이 거의 없는 부분을 독립시켜서 활용성을 높이는 방법
전략 패턴을 활용한 템플릿 기법
1. 다시 보는 초난감 DAO
- 지금까지의 DAO에는 예외상황에 대한 처리가 없음
- 예외처리 기능을 갖춘 DAO
- 예외가 발생한 경우 사용한 리소스를 반드시 반환하도록 만들어야 한다.(심각한 문제를 일으킬 수 있기 때문)
- close() 메서드를 통해 자원을 반환하지 않으면 리소스가 모자란다는 오류를 내며 서버가 중단될 수 있음
- JDBC 수정 기능의 예외처리 코드
// 예외처리 전 public void deleteAll() throws SQLException { Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement("delete from users"); ps.executeUpdate(); // 위에서 예외가 발생하면 아래 close메서드가 실행되지 않는다. ps.close(); c.close(); } // 예외처리 후 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()에서 예외가 발생하면 아래 c.close()가 실행되지 않기 때문에 예외처리 ps.close(); // 예외가 발생하면 로그처리 등 } catch (SQLException ee) { } } if (ps != null) { try { // 그럼 얘는 왜 try-catch? c.close(); } catch (SQLException ee) { } } } }
- JDBC 조회 기능의 예외처리
public int getCount() throws SQLException { Connection c = null; PreparedStatement ps = null; ResultSet rs = null try { c = dataSource.getConnection(); ps = c.prepareStatement("select count(*) from users"); rs = ps.executeQuery(); rs.next(); return rs.getInt(1); } catch (SQLException e) { throw e; } finally { if (rs != null) { try { rs.close(); } catch (SQLException ee) { } } if (ps != null) { try { ps.close(); } catch (SQLException ee) { } } if (c != null) { try { c.close(); } catch (SQLException ee) { } } } }
2. 변하는 것과 변하지 않는 것
- JDBC try-catch-finally 코드의 문제점
- 지저분하고 중복코드가 많다.
- close() 메서드가 빠질 가능성이 높기 때문에 위험하다.
- 분리와 재사용을 위한 디자인 패턴 적용
- 변하는 부분과 변하지 않는 부분 구분
- 변하는 부분 : 쿼리 수행
- 변하지 않는 부분 : 예외처리 코드와 close()메서드
- 방법1. 메소드 추출 : 재사용성에 이득이 없으므로 부적절
- 방법2. 템플릿 메소드 패턴 적용
- 상속을 통해 기능을 확장해서 사용
- 변하지 않는 부분을 슈퍼클래스에 두고 변하는 부분은 추상메소드로 정의
- 단점이 많아서 부적절
- DAO 로직마다 새로운 클래스 필요
- 관계 유연성이 없음
- 방법3. 전략 패턴의 적용
- 일정한 구조를 갖는 Context와 특정 확장 기능인 Starategy의 관계
- Context가 변하지 않는 부분, Strategy가 변하는 부분
// Context가 만들어둔 Connection을 전달받아서 PreparedStatement를 만들고 반환 public interface StatementStrategy { PreparedStatement makePreparedStatement(Connection c) throws SQLException; } // 실제 젼략 public class DeleteAllStatement implements StatementStrategy { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("delete from users"); return ps; } } // 실제 전략을 사용하는 부분 public class UserDao { ... public void deleteAll() throws SQLException { ... try { c = dataSource.getConnection(); // 전략 패턴을 적용했다는건 알겠다. // 그런데 장점이 뭔지 전혀 모르겠는데... StatementStrategy strategy = new DeleteAllStatement(); ps = strategy.makePrepraredStatement(c); ps.executeUpdate(); } catch (SQLException e) { throw e; } }
- DI 적용을 위한 클라이언트/컨텍스트 분리
- 전략 패턴에서 어떤 전략을 사용할것인가에 대한 결정을 내리는 주체는 Client다.
// 컨텍스트를 매개변수로 받아서 컨텍스트가 갖는 쿼리를 수행하는 메서드 // executeUpdate만 사용하니까 메서드이름을 바꾸는게 좋겠다. // executeUpdateWithStrategy()? public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { Connection c = null; PreparesStatement ps = null; try { c = dataSource.getConnection(); ps = stmt.makePrepraredStatement(c); ps.executeUpdate(); } catch (SQLException e) { ... } finally { if (ps != null) { try { ps.close(); } catch (SQLException ee) { } } if (c != null) { try { c.close(); } catch (SQLException ee) { } } } // 클라이언트 책임을 담당할 deleteAll() 메서드 public void deleteAll() throws SQLException { // 클라이언트가 전략 클래스를 결정하고 객체 생성 StatementStrategy strategy = new DeleteAllStatement(); // 컨텍스트를 호출하고 전략 객체 전달 jdbcContextWithStatementStrategy(strategy); }
- 변하는 부분과 변하지 않는 부분 구분
- 마이크로 DI
- DI의 가장 중요한 개념 : 제3자의 도움을 통해 두 객체 사이의 유연한 관계가 설정되도록 만든다.
- DI는 매우 작은 단위의 코드와 메소드 사이에서 일어나기도 한다.
3. JDBC 전략 패턴의 최적화
- 전략 : 변하는 부분, 바뀌는 로직
- 컨텍스트 : 변하지 않는 부분
- 전략 클래스의 추가 정보
- add()메서드에 전략패턴 적용
public class AddStatement implements StatementStrategy { User user; public AddStatement(User user) { this.user = user; } 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; } } // UserDao에서 사용 public class UserDao { ... public void add(User user) thrwos SQLException { StatementStrategy strategy = new AddStatement(user); jdbcContextWithStatementStrategy(st); } ... }
- 전략과 클라이언트의 동거
- 남은 문제들
- DAO 메서드 마다 새로운 StatementStrategy 구현 클래스를 만들어야 함
- 부가적인 정보가 있을 때 전달만을 위한 객체를 사용해야함
- 로컬 클래스
- 클래스 파일이 많아지는 문제를 해결하기 위한 방법
- UserDao 안에 내부 클래스로 정의하기
- 클래스 안에 있기 때문에 User 객체를 전달해 줄 필요도 없어졌다.
public void add(final User user) throws SQLException { class AddStatement implements StatementStrategy { User user; public AddStatement(User user) { this.user = user; } 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; } } // 로컬 클래스 끝 StatementStrategy strategy = new AddStatement(user); jdbcContextWithStatementStrategy(strategy); }
- 익명 내부 클래스
- 선언과 동시에 객체를 생성
public void add(final User user) throws SQLException { StatementStrategy strategy = new AddStatement() { 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(strategy); }
public void add(final User user) throws SQLException { // 메서드 파라미터 위치에서 바로 생성 jdbcContextWithStatementStrategy(new AddStatement() { 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; } }); }
- 남은 문제들
- 여기까지 정리
- 예외처리 적용
- 예외처리 중 중복되는 부분에 전략 패턴 적용(변하지 않는 부분 - 변하는 부분)
- 전략 선언 방법을 일반 클래스 -> 내부클래스 -> 익명클래스로 변경
4. 컨텍스트와 DI
- JdbcContext의 분리
- 전략패턴의 구조
- 클라이언트 : UserDao의 메서드
- 개별적인 전략 : 메서드 안에있는 익명 내부클래스
- 컨텍스트 : jdbcContextWithStatementStrategy()
- 이 상태에서 컨텍스트를 다른 DAO에서도 사용할 수 있도록 독립시킨다.
- 클래스 분리
- UserDao에 있던 jdbcContextWithStatementStrategy 메서드를 JdbcContext클래스로 분리
- 그 다음 UserDao에서 jdbcContext를 받아주고
- 빈 의존관계를 수정해준다
- 빈 의존관계는 기존에 UserDao와 DataSource 사이에 JdbcContext가 끼도록 바뀐다
- 그러나 아직 모든 UserDao의 메서드가 JdbcContext를 사용하는게 아니기 때문에 우선 DataSource는 그대로 둔다.(234p)
// JdbcContext public class JdbcContext { private DataSource dataSource; // DataSource타입 빈을 DI받을 수 있도록 준비 public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } // UserDao에 있던 jdbcContextWithStatementStrategy를 이쪽으로 가져왔음(232p) public void workWithStatementStrategy(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 (Exception e) { } } if(c != null) { try { c.close() } catch (Exception e) { } } } } }
public class UserDao { private DataSource dataSource; private JdbcContext jdbcContext; public UserDao(DataSource dataSource, JdbcContext jdbcContext) { this.dataSource = dataSource; this.jdbcContext = jdbcContext; } public void add(User user) throws ClassNotFoundException, SQLException { this.jdbcContext.workWithStatementStrategy(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; }); } public void deleteAll() throws SQLException { this.jdbcContext.workWithStatementStrategy(c -> c.prepareStatement("delete from users")); } }
- 전략패턴의 구조
- JdbcContext의 특별한 DI
- 예제코드에서 JdbcContext는 인터페이스 없이 DI를 적용했다.
- 이전까지는 클래스 레벨에서 의존관계가 만들어지지 않도록 인터페이스를 사용했는데?
- 스프링 빈으로 DI
- 인터페이스 없이 DI를 적용하는건 문제가 있지 않나?
- 요약 : UserDao는 항상 JdbcContext 클래스와 함께 사용돼야 하기 때문에 괜찮다. 그래도 인터페이스 두고싶으면 그래도 된다.
- 예제코드에서 JdbcContext는 인터페이스 없이 DI를 적용했다.
- 코드를 이용하는 수동 DI
- jdbcContext 빈 없애고 UserDao에서 직접 jdbcContext 생성해서 사용
public class UserDao { private DataSource dataSource; private JdbcContext jdbcContext; public UserDao(DataSource dataSource) { this.dataSource = dataSource; // 의존 객체 생성 및 주입 this.jdbcContext = new JdbcContext(dataSource); } }
- 두 방법의 장단점
- JdbcContext를 빈으로 등록
- 장점 : 객체 사이의 실제 의존관계가 설정 파일에 명확하게 드러남
- 단점 : DI 근본 원칙에 부합하지 않는(no interface) 구체적인 클래스와의 관계가 설정에 직접 노출된다.
- DAO 내부에서 수동으로 DI
- 장점 : 관계를 외부에 드러내지 않음
- 단점 : 싱글톤 사용 어려움, DI를 위한 부가적인 코드 필요
- JdbcContext를 빈으로 등록
5. 템플릿과 콜백
전략패턴 같은 방식을 스프링에서는 템플릿/콜백 패턴이라고 한다.
템플릿 :
어떤 목적을 위해 미리 만들어둔 모양이 있는 틀 전략패턴에서의 컨텍스트
콜백 :
실행되는 것을 목적으로 다른 객체의 메서드에 전달되는 객체 파라미터로 전달되지만 값을 참조하기 위함이 아니라 특정 로직을 담은 메서드를 실행시키기 위함 functional object 라고도 함 전략패턴에서의 전략
- 템플릿/콜백의 동작 원리
- 템플릿/콜백의 특징
- 콜백은 보통 단일 메서드 인터페이스를 사용한다. (여러 개의 메서드를 갖는 일반적인 인터페이스를 사용할 수 있는 전략 패턴의 전략과 다름)
- 특정 기능을 위해 한 번 호출되는 경우가 일반적이기 때문
- 일반적인 흐름
- 클라이언트에서 콜백 생성
- 템플릿 호출하면서 콜백 전달
- 템플릿에서 workflow 시작
- 템플릿에서 참조정보 생성
- 템플릿에서 콜백 호출하면서 참조정보 전달
- 콜백에서 Client final 변수 참조
- 콜백에서 작업 수행
- 콜백 작업 결과 템플릿으로 전달
- 템플릿에서 workflow 진행 및 마무리
- 템플릿에서 클라이언트로 작업 결과 전달
- JdbcContext에 적용된 템플릿/콜백
- 클라이언트 : UserDao.add()
- 템플릿 : JdbcContext.workWithStatementStrategy()
- 콜백 : 익명 StatementStrategy 객체
- 템플릿/콜백의 특징
- 편리한 콜백의 재활용
- 콜백의 분리와 재활용
- 복잡한 익명 내부 클래스의 사용을 최소화
// 익명 내부 클래스를 사용한 클라이언트 코드 public void deleteAll() throws SQLException { jdbcContextWithStatementStrategy(new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement("delete from users"); } }); } // 변하지 않는 부분을 분리시킨 코드 public void deleteAll() throws SQLException { executeSql("delete from users"); } public void executeSql(final String query) throws SQLException { jdbcContextWithStatementStrategy(new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } }); }
- 콜백과 템플릿의 결합
- executeSql()메서드는 재사용성이 높기 때문에 템플릿으로 옮겨도된다.
// JdbcContext로 옮긴 executeSql 메서드 public class JdbcContext { ... public void executeSql(final String query) throws SQLException { workWithStatementStrategy(new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { return c.prepareStatement(query); } }); } public void workWithStatementStrategy(StatementStrategy strategy) throws SQLException { ... } }
// JdbcContext로 옮긴 executeSql 메서드를 사용하는 deleteAll 메서드 public class UserDao { public void deleteAll() throws SQLException { this.jdbcContext.executeSql("delete from users"); } }
- 콜백의 분리와 재활용
- 템플릿/콜백의 응용
- 어떻게 써야될까?
- 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있따면, 중복코드를 분리할 방법을 생각하는 습관을 기르자.
- 중복된 코드는 메서드로 분리해보고
- 필요에 따라 바꿔야 한다면 인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI로 의존관계를 관리하도록 만든다.
- 거기에 바뀌는 부분이 한 애플리케이션 안에서 동시에 여러종류가 만들어 질 수 있다면 템플릿/콜백 패턴을 적용해보자
- 테스트와 try/catch/finally
- 간단한 템플릿/콜백 예제
public class CalcSumTest { //250p @Test public void sumOfNumbers() throws IOException { String filePath = "src\\test\\resources\\numbers.txt"; Integer result = Calculator.calcSum(filePath); assertThat(result.equals(10)); } } public class Calculator { public static Integer calcSum(String filePath) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filePath)); Integer sum = 0; String line = null; while((line = br.readLine()) != null) { sum += Integer.valueOf(line); } return sum; } catch (IOException e) { throw e; } finally { if (br != null) { try { br.close(); } catch (Exception e) { throw e; } } } } }
- 중복의 제거와 템플릿/콜백 설계
- 반복되는 작업 흐름은 템플릿으로 독립
- 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는 것이 중요하다.
- 구조
- fileReadTemplate(String filePath, BufferedReaderCallback) : 콜백 객체를 받아서 적절한 시점에 실행
- BufferedReaderCallback : 각 라인을 읽어서 처리한 후에 최종결과를 템플릿에 전달
// 콜백 public interface BufferedReaderCallback { Integer doSomethingWithReader(BufferedReader br) throws IOException; } public class Calculator { // BufferedReaderCallback을 사용하는 템플릿 메서드 public Integer fileReadTemplate(String filePath, BufferedReaderCallback callback) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filePath)); return callback.doSomethingWithReader(br); } catch (IOException e) { throw e; } finally { if (br != null) { try { br.close(); } catch (Exception e) { throw e; } } } } // 템플릿/콜백을 적용한 calcSum 메서드 public Integer calcSum(String filePath) throws IOException { BufferedReaderCallback callback = new BufferedReaderCallback() { @Override public Integer doSomethingWithReader(BufferedReader br) throws IOException { Integer sum = 0; String line = null; while((line = br.readLine()) != null) { sum += Integer.valueOf(line); } return sum; } }; return fileReadTemplate(filePath, callback); } }
- 템플릿/콜백 재설계
- 남아있는 중복되는 부분을 정리한다.
// 라인 별 작업을 정리한 콜백 인터페이스 public interface LineCallback { Integer doSomethingWithLine(String line, Integer value); } public class Calculator { // LineCallback을 사용하는 템플릿 메서드 public Integer lineReadTemplate(String filePath, LineCallback lineCallback, int initValue) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filePath)); Integer result = initValue; String line = null; while ((line = br.readLine()) != null) { result = lineCallback.doSomethingWithLine(line, result); } return result; } catch (IOException e) { throw e; } finally { if (br != null) { try { br.close(); } catch (Exception e) { throw e; } } } } // lineReadTemplate을 사용하도록 수정한 합, 곱 계산 메서드 public Integer calcMultiply(String filePath) throws IOException { LineCallback sumCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value * Integer.valueOf(line); } }; return lineReadTemplate(filePath, sumCallback, 1); } public Integer calcSum(String filePath) throws IOException { LineCallback sumCallback = new LineCallback() { public Integer doSomethingWithLine(String line, Integer value) { return value + Integer.valueOf(line); } }; return lineReadTemplate(filePath, sumCallback, 0); } }
- 제네릭스를 이용한 콜백 인터페이스
- 파일을 라인 단위로 처리한 결과의 타입을 다양하게 가져가고 싶으면 제너릭 활용
// 제너릭을 적용한 라인 별 작업을 정리한 콜백 인터페이스 public interface LineCallback<T> { T doSomethingWithLine(String line, T value); } public class Calculator { // 제너릭을 적용한 템플릿 메서드 public <T> T lineReadTemplate(String filePath, LineCallback<T> lineCallback, T initValue) throws IOException { BufferedReader br = null; try { br = new BufferedReader(new FileReader(filePath)); T result = initValue; String line = null; while ((line = br.readLine()) != null) { result = lineCallback.doSomethingWithLine(line, result); } return result; } catch (IOException e) { throw e; } finally { if (br != null) { try { br.close(); } catch (Exception e) { throw e; } } } } public String concatenate(String filePath) throws IOException { LineCallback<String> concatenateCallback = new LineCallback<String>() { public String doSomethingWithLine(String line, String value) { return value + line; } }; return lineReadTemplate(filePath, concatenateCallback, ""); } public Integer calcMultiply(String filePath) throws IOException { LineCallback<Integer> sumCallback = new LineCallback<Integer>() { public Integer doSomethingWithLine(String line, Integer value) { return value * Integer.valueOf(line); } }; return lineReadTemplate(filePath, sumCallback, 1); } public Integer calcSum(String filePath) throws IOException { LineCallback<Integer> sumCallback = new LineCallback<Integer>() { public Integer doSomethingWithLine(String line, Integer value) { return value + Integer.valueOf(line); } }; return lineReadTemplate(filePath, sumCallback, 0); } }
- 어떻게 써야될까?
6. 스프링의 JdbcTemplate
스프링은 다양한 템플릿/콜백 기술을 제공한다.
그 중 JdbdTemplate을 사용해본다.
public class UserDao { private JdbcTemplate jdbcTemplate; public void setDataSource(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); this.dataSource = dataSource; }
- update()
- deleteAll()에 적용
public void deleteAll() throws SQLException { // PreparedStatementCreator를 활용한 deleteAll() this.jdbcTemplate.update(connection -> connection.prepareStatement("delete from users")); // this.jdbcTemplate.update( // new PreparedStatementCreator() { // @Override // public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { // return connection.prepareStatement("delete from users"); // } // } // ); // 내장 콜백을 사용하는 update()로 변경한 deleteAll() this.jdbcTemplate.update("delete from users"); }
- add()에 적용
public void add(User user) throws ClassNotFoundException, SQLException { this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)" , user.getId(), user.getName(), user.getPassword()); }
- queryForInt() - 3.2.2 deprecated
- 콜백이 2개인 .query() 메서드 활용
- 첫 번째 콜백(PreparedStatementCreator) : statement 생성
- 두 번째 콜백(ResultSetExtractor) : ResultSet으로 부터 값 추출(제너릭)
public int getCount() { return this.jdbcTemplate.query(new PreparedStatementCreator() { @Override public PreparedStatement createPreparedStatement(Connection connection) throws SQLException { return connection.prepareStatement("select count(*) from users"); } }, new ResultSetExtractor<Integer>() { @Override public Integer extractData(ResultSet resultSet) throws SQLException, DataAccessException { resultSet.next(); return resultSet.getInt(1); } }); }
- queryForInt() 활용
// queryForInt는 spring 3.2.2에서 deprecated. 현재는 queryForObject 뿐 public int getCount() { return jdbcTemplate.queryForObject("select count(*) from users", Integer.class); }
- 콜백이 2개인 .query() 메서드 활용
- queryForObject()
- RowMapper : ResultSet의 로우 하나를 매핑하기 위해 사용
- 한 개의 결과만 얻을 것으로 기대한다.
- single일까 unique일까?
- unique!
- 2개 이상이면? IncorrectResultSizeDataAccessException 발생 하면서 실패
- 0개면? EmptyResultDataAccessException 발생 하면서 실패
public User get(String id) throws SQLException { return jdbcTemplate.queryForObject("select * from users where id = ?", // SQL에 바인딩 할 파라미터 값. 가변인자 대신 배열 사용 new Object[] { id }, // ResultSet 한 로우의 결과를 오브젝트에 매핑해주는 RowMapper콜백 new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; } }); }
- query()
- public <T> List<T> query(String sql, RowMapper
rowMapper) - 기능 정의와 테스트 작성
- 모든 사용자 정보를 가져오기 위한 getAll은 어떻게?
- id순으로 정렬해서 가져오도록.
- 테스트코드
@Test public void getAll() throws Exception { userDao.deleteAll(); User user1 = new User("001", "name1", "password1"); User user2 = new User("002", "name2", "password2"); User user3 = new User("003", "name3", "password3"); List<User> originUserList = Arrays.asList(user1, user2, user3); for(User user : originUserList) { userDao.add(user); } List<User> userList = userDao.getAll(); assertThat(userList.size()).isEqualTo(originUserList.size()); for(int i = 0; i < userList.size(); i++) { checkSameUser(originUserList.get(i), userList.get(i)); } } private void checkSameUser(User originUser, User resultUser) { assertThat(originUser.getId()).isEqualTo(resultUser.getId()); assertThat(originUser.getName()).isEqualTo(resultUser.getName()); assertThat(originUser.getPassword()).isEqualTo(resultUser.getPassword()); }
- UserDao 코드
public List<User> getAll() { return jdbcTemplate.query("select * from users order by id", (rs, rowNum) -> { User user = new User(); user.setId(rs.getString("id")); user.setName(rs.getString("name")); user.setPassword(rs.getString("password")); return user; }); }
- 테스트 보완
- 네거티브 테스트 필요
- 아래처럼 빈 테이블의 결과는 size가 0인 List이다 라고 알 수 있는 테스트가 있으면, getAll메서드가 내부적으로 어떻게 작동하는지 알 필요도 없어지는 장점이 있다.
@Test public void getAllFromEmptyTable() throws Exception { userDao.deleteAll(); List<User> userList = userDao.getAll(); assertThat(userList.size()).isEqualTo(0); }
- public <T> List<T> query(String sql, RowMapper
- 재사용 가능한 콜백의 분리
- DI를 위한 코드 정리
// 불필요한 DataSource, JdbcContext 제거 public class UserDao { private JdbcTemplate jdbcTemplate; public UserDao(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); }
- 중복 제거
- get(), getAll()에 RowMapper의 내용이 똑같음
- 단 두개의 중복이라 해도 언제 어떻게 확장이 필요해질지 모르니 제거하는게 좋다.
// 중복 제거한 rowMapper와 get()메서드 public User get(String id) { return jdbcTemplate.queryForObject("select * from users where id = ?", new Object[] { id }, getUserMapper()); } private RowMapper<User> getUserMapper() { return (resultSet, i) -> { User user = new User(); user.setId(resultSet.getString("id")); user.setName(resultSet.getString("name")); user.setPassword(resultSet.getString("password")); return user; }; }
- 템플릿/콜백 패턴과 UserDao
- 템플릿/콜백 패턴과 DI를 이용해 깔끔해진 UserDao 클래스
- 응집도 높다 : 테이블과 필드정보가 바뀌면 UserDao의 거의 모든 코드가 함께 바뀐다
- 결합도 낮다 : JDBC API 활용방식, 예외처리, 리소스 반납, DB 연결 등에 대한 책임과 관심은 JdbcTemplate에 있기 때문에 변경이 일어난다 해도 UserDao의 코드에는 영향을 주지 않는다.
- 추가 개선 사항
- userMapper를 독립된 빈으로 만들어서 분리한다.
- SQL문장을 외부 리소스에 담고 읽어와서 사용한다. => 쿼리를 최적화하는 경우에 UserDao 코드에 손 댈 필요 없다.
public class UserDao { private JdbcTemplate jdbcTemplate; public UserDao(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } public void add(final User user) { this.jdbcTemplate.update("insert into users(id, name, password) values(?, ?, ?)" , user.getId(), user.getName(), user.getPassword()); } public User get(String id) { return jdbcTemplate.queryForObject("select * from users where id = ?" ,new Object[] { id }, getUserMapper()); } private RowMapper<User> getUserMapper() { return (resultSet, i) -> { User user = new User(); user.setId(resultSet.getString("id")); user.setName(resultSet.getString("name")); user.setPassword(resultSet.getString("password")); return user; }; } public User getUserByName(String name) { return jdbcTemplate.queryForObject("select * from users where name = ?" , new Object[] { name }, getUserMapper()); } public void deleteAll() { this.jdbcTemplate.update("delete from users"); } public int getCount() { return jdbcTemplate.queryForObject("select count(*) from users", Integer.class); } public List<User> getAll() { return jdbcTemplate.query("select * from users order by id", getUserMapper()); } }
- DI를 위한 코드 정리
7. 정리
- 예외처리와 안전한 리소스 반환을 보장해주는 DAO 코드를 만들었다.
- 객체지향 설계 원리, 디자인 패턴, DI 등을 적ㅇ요해서 깔끔하고 유연하며 단순한 코드로 만들었다.
- JDBC 처럼 예외발생 가능성이 있으며 공유 리소스 반환이 필요한 코드는 반드시 try-catch-finally 블록으로 관리해야 한다.
- 전략 패턴
- 로직에 반복이 있으면서 그 중 일부(전략)만 바뀌고 일부(컨텍스트)는 바뀌지 않는다면 전략 패턴을 적용한다.
```
- 한 애플리케이션 안에서 여러 전략을 동적으로 구성하고 사용해야 한다면 컨텍스트를 이용하는 클라이언트 메서드에서 직접 전략을 정의하고 제공하도록 만든다.
- 익명 내부 클래스를 사용해서 전략 오브젝트를 구현하면 편리하다.
- 컨텍스트가 하나 이상의 클라이언트 객체에서 사용된다면 클래스를 분리해서 공유하도록 만든다.
- 컨텍스트는 별도 빈으로 등록해서 DI 받거나 클라이언트 클래스에서 직접 생성해 사용한다.
```
- 로직에 반복이 있으면서 그 중 일부(전략)만 바뀌고 일부(컨텍스트)는 바뀌지 않는다면 전략 패턴을 적용한다.
- 템플릿 콜백 패턴
- 단일 전략 메서드를 갖는 전략 패턴이면서, 익명 내부 클래스를 사용해서 매번 전략을 새로 만들어 사용하고, 컨텍스트 호출과 동시에 전략 DI를 수행하는 방식
- 콜백 : 다시 불려지는 기능 이라는 의미
```
- 콜백 코드에도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용 하는 것이 편리하다.
- 템플릿과 콜백의 타입이 다양하게 바뀔 수 있다면 제너릭을 이용한다.
- 스프링은 JDBC 코드 작성을 위해 JdbcTemplate을 기반으로 하는 다양한 템플릿과 콜백을 제공한다.
- 템플릿은 한 번에 하나 이상의 콜백을 사용할 수도 있고, 하나의 콜백을 여러번 호출할 수 있다.
- 템플릿/콜백을 설계할 때에는 템플릿과 콜백 사이에 주고받는 정보에 관심을 두어야 한다.
```
- 템플릿/콜백은 스프링이 객체지향 설계와 프로그래밍에 얼마나 가치를 두고 있는지를 잘 보여주는 예다.
- 얼마나 유연하고 변경이 용이하게 만들고자 하는지를 잘 보여주는 예다.
- 이 챕터에서는 추상화(템플릿), 캡슐화(DI)를 통해서
- DI에 녹아있는 캡슐화? -> DataSource가 갖는 세세한 정보를 DataSource를 직접 사용하는 UserDao는 알 필요가 없기 때문
중첩 클래스의 종류
- 스태틱 클래스(static class)
- 독립적으로 오브젝트로 만들어질 수 있는 클래스
- 내부 클래스(inner class)
- 자신이 정의된 클래스와 오브젝트 안에서만 만들어 질 수 있는 클래스
- 멤버 내부 클래스 : 멤버 필드처럼 오브젝트 레벨에 정의됨
- 로컬 클래스 : 메서드 레벨에 정의됨
- 익명 내부 클래스 : 이름을 갖지 않으며 선언된 위치에 따라 범위가 다름
'공부 > Spring' 카테고리의 다른 글
토비의스프링(5) - 서비스 추상화 (0) | 2021.02.15 |
---|---|
토비의스프링(4) - 예외 (0) | 2021.01.26 |
토비의스프링(2) - 테스트 (0) | 2020.12.30 |
토비의스프링(1) - 오브젝트와 의존관계 (0) | 2020.11.04 |
@Async를 JPA/Hibernate와 함께 사용할 때 주의할 점 (0) | 2020.02.25 |
Comments