diff --git "a/ \354\265\234\354\234\240\354\240\225/3\354\236\245_\355\205\234\355\224\214\353\246\277.md" "b/ \354\265\234\354\234\240\354\240\225/3\354\236\245_\355\205\234\355\224\214\353\246\277.md" new file mode 100644 index 0000000..fe9a606 --- /dev/null +++ "b/ \354\265\234\354\234\240\354\240\225/3\354\236\245_\355\205\234\355\224\214\353\246\277.md" @@ -0,0 +1,299 @@ +# 제목 없음 + +# 템플릿 + +## 기존 DAO의 문제 + +- 예외 상황 처리 문제 + + ```jsx + public void deleteAll() throws SQLException { + Connection c = dataSource.getConnection(); + + PreparedStatement ps = c.preparedStatement("delete from users"); + ps.executeUpdate(); // 여기서 예외가 발생하면 바로 메소드 실행이 중단되면서 DB 커넥션이 반환되지 못한다. + + ps.close(); + c.close(); + } + ``` + + DB 풀은 매번 getConnection()으로 가져간 커넥션을 명시적으로 close()해서 돌려줘야지만 다시 풀에 넣었다가 다음 커넥션 요청이 있을 때 재사용할 수 있다. 그런데 이런 식으로 오류가 날 때마다 미처 반환되지 못한 Connection이 계속 쌓이면 어느 순간에 커넥션 풀에 여유가 없어지고 리소스가 모자란다는 심각한 오류를 내며 서버가 중단될 수 있다. + +- try/catch/final 적용 + + ```jsx + public void deleteAll() throws SQLException { + Connection c = null; + PreparedStatement ps = null; + + try { + c = dataSource.getConnection(); + ps = c.prepareStatement("delete from users"); + ps.executeUpdate(); // 예외가 발생할 수 있는 코드를 모두 try 블록으로 묶어준다. + } catch (SQLException e) { + throw e; // 예외가 발생했을 때 부가적인 작업을 해줄 수 있도록 catch 블록을 둔다. 아직은 예외를 메소드 밖으로 던지는 것 밖에 없다. + } finally { // finally이므로 try 블록에서 예외가 발생했을 떄나 안 했을 때나 모두 실행된다. + if (ps != null) { + try { + ps.close(); + } catch (SQLException e) {} // ps.close() 메소드에서도 SQLException이 밣생할 수 있기 때문에 잡아줘야한다. + } + if (c != null) { + try { + c.close(); + } catch (SQLException e) {} + } + } + } + ``` + + 어느 시점에서 예외가 발생했는지에 따라서 close()를 사용할 수 있는 변수가 달라질 수 있기 때문에 finally에서는 반드시 c와 ps가 null이 아닌지 먼저 확인한 후에 close() 메소드를 호출해야 한다.문제는 이 close()도 SQLException이 발생할 수 있는 메소드라는 점이다.**** + + +## 디자인 패턴을 적용하여 분리 및 재사용 + +### 1. 변하는 부분을 메소드로 추출 + +```jsx +public void deleteAll() throws SQLException { + ... + try { + c = dataSource.getConnectin(); + ps = makeStatement(c); // 변하는 부분을 메소드로 추출하고 변하지 않는 부분에서 호출한다. + ps.executeUpdate(); + } catch (SQLException e) {...} +} + +private PreparedStatement makeStatement(Connection c) throws SQLException { + PreparedStatement ps; + ps = c.preparedStatement("delete from users"); + return ps; +} +``` + +### 2. 템플릿 메소드 패턴의 적용 + +```jsx +public class UserDaoDeleteAll extends UserDao { + protected PreparedStatement makeStatement(Connection c) throws SQLException { + PreparedStatement ps = c.preparedStatement("delete from users"); + return ps; + } +} +``` + +변하지 않는 부분을 슈퍼클래스에 두고 변하는 부분을 추상 메소드로 정의해서 서브클래스에서 새롭게 정의해 쓰는 것 + +⇒ 문제 + +1) 모든 DAO 로직(메소드)마다 상속을 통해 새로운 클래스를 만들어야 된다. + +2) 컴파일 시점에 클래스 간(슈퍼-서브) 관계가 결정되어 있어서 유연하지 못하다. + +### 3. 전략 패턴 적용 + +```jsx +public void deleteAll() throws SQLException { + ... + try { + c = dataSource.getConnection(); + + StatementStrategy strategy = new DeleteAllStatement(); // 전략 클래스가 DeleteAllStatement로 고정됨으로써 OCP 개방 원칙에 맞지 않게 된다. + ps = starategy.makePreparedStatement(c); + + ps.executeUpdate(); + } catch (SQLException e) {...} +} +``` + +전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있다는 것인데, 이렇게 컨텍스트 안에서 이미 구체적인 전략 클래스인 DeleteAllStatement를 사용하도록 고정되어 있다면 뭔가 이상하다 + +### 4. DI 적용을 위한 클라이언트/컨텍스트 분리 + +```jsx +public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { + Connection c = null; + PreparedStatement ps = null; + + try { + c = dataSource.getConnection(); + ps = stmt.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) {} + } +} +``` + +컨텍스트에 해당하는 JDBC try/catch/finally 코드를 클라이언트 코드인 StatementStrategy를 만드는 부분에서 독립시킨다 + +## JDBC 전략 패턴의 최적화 + +### 전략과 클라이언트의 동거 + +```jsx +public void add(final User user) throws SQLException { + class AddStatement implements StatementStrategy { // add() 메소드 내부에 선언된 로컬 클래 + 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(?,?,?)"); + ... + } + + StatementStrategy st = new AddStatement(user); + jdbcContextWithStatementStrategy(st); + } +} +``` + +- 이전의 개선된 코드의 문제점 + - DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점 + - DAO 메소드에서 StatementStrategy에 전달할 User와 같은 부가적인 정보가 있는 경우, 이를 전달하고 저장해 둘 생성자와 인스턴스 변수를 번거롭게 만들어야 한다. + +### 로컬 클래스 + +```jsx +public void add(final User user) throws SQLException { + class AddStatement implements StatementStrategy { // add() 메소드 내부에 선언된 로컬 클래 + 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(?,?,?)"); + ... + } + + StatementStrategy st = new AddStatement(user); + jdbcContextWithStatementStrategy(st); + } +} +``` + +StatementStrategy 전략 클래스를 매번 독립된 파일로 만들지 말고 UserDao 클래스 안에 내부 클래스로 정의해버리면 클래스 파일이 많아지는 문제는 해결할 수 있다. + +→ 장점 + +- AddStatement는 복잡한 클래스가 아니므로 메소드 안에서 정의해도 그다지 복잡해 보이지 않는다. +- 메소드마다 추가해야 했던 클래스 파일을 하나 줄일 수 있다 +- 내부 클래스의 특징을 이용해 로컬 변수를 바로 가져다 사용할 수 있다. + +### 익명 내부 클래스 + +```jsx +public void add(final User user) throws SQLException { + jdbcContextWithStatementStrategy( + new StatementStrategy() { + 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(); + ... + return ps; + } + } + ); +} +``` + +선언과 동시에 오브젝트 생성 + +## 컨텍스트와 DI + +### 컨텍스트 분리하고 DI + +JDBC 컨텍스트를 UserDao와 dI 구조로 + +```jsx +public class JdbcContext { + private DataSource dataSource; + + public void setDataSource(DataSource dataSource) { // DataSource 타입 빈을 DI 받을 수 있게 준비 + this.dataSource = dataSource; + } + + public void workWithStatementStrategy(StatementStrategy stmt) throws SQLException { + Connection c = null; + PreparedStatement ps = null; + + try {...} + catch (SQLException e) {...} + finally {...} + } +} +``` + +```jsx +public class UserDao { + ... + private JdbcContext jdbcContext; + + public void setJdbcContext(JdbcContext jdbcContext) { + this.jdbcContext = jdbcContext; // jdbcContext를 Di받도록 만든다. + } + + public void add(final User user) throws SQLException { + this.jdbcContext.workWithStatementStrategy( // DI 받은 JdbcContext의 컨텍스트 메소드를 사용하도록 변경한다. + new StatementStrategy() {...} + ); + } +} +``` + +→ 장점 + +- JdbcContext가 스프링 컨테이너의 싱글톤 레지스트리에서 관리되는 싱글톤 빈이기 되기 때문이다. +- JdbcContext가 DI를 통해 다른 빈에 의존하고 있디 때문이다. + +## 템플릿과 콜백 + +전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식 + +- 템플릿 : 전략 패턴의 컨텍스트 +- 콜백: 익명 내부 클래스로 만들어지는 오브젝트 + +### 콜백의 재활용 + +- 익명 내부 클래스를 사용 + + ```jsx + public void deleteAll() throws SQLException { + this.jdbcContext.workWithStatementStrategy( + new StatementStrategy() { // 변하지 않는 콜백 클래스 정의와 오브젝트 생성 + public PreparedStatement makePreparedStatement(Connection c) throws SQLException { + return c.preparedStatement("delete from users"); // 변하는 SQL 문장 + } + } + ); + } + ``` + +- 변하지 않는 부분을 분리 + + ```jsx + public void deleteAll() throws SQLException { + executeSql("delete from users"); + } + + private void executeSql(final String query) throws SQLException { + this.jdbcContext.workWithStatementStrategy( + new StatementStrategy() { // 변하지 않는 콜백 클래스 정의와 오브젝트 생성 + public PreparedStatement makePreparedStatement(Connection c) throws SQLException { + return c.preparedStatement(query); + } + } + ); + } + ``` \ No newline at end of file