diff --git "a/chapter03/3.2_\353\263\200\355\225\230\353\212\224_\352\262\203\352\263\274_\353\263\200\355\225\230\354\247\200_\354\225\212\353\212\224_\352\262\203.md" "b/chapter03/3.2_\353\263\200\355\225\230\353\212\224_\352\262\203\352\263\274_\353\263\200\355\225\230\354\247\200_\354\225\212\353\212\224_\352\262\203.md" new file mode 100644 index 0000000..09ce32c --- /dev/null +++ "b/chapter03/3.2_\353\263\200\355\225\230\353\212\224_\352\262\203\352\263\274_\353\263\200\355\225\230\354\247\200_\354\225\212\353\212\224_\352\262\203.md" @@ -0,0 +1,214 @@ +# 3.2 변하는 것과 변하지 않는 것 + +앞선 3.1에서 공유 리소스(Connection, PreparedStatement 등)에 대하여 자원을 반납하는 코드를 try/catch/finally를 활용하여 작성하였습니다. + +## 3.2.1 JDBC try/catch/finally 코드의 문제점 + +```java + public void deleteAll() throws SQLException { + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try { + connection = dataSource.getConnection(); + + preparedStatement = connection.prepareStatement("delete from users"); + + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw e; + } finally { + if (resultSet != null) { + try{ + resultSet.close(); + } catch(SQLException e) { + throw e; + } + } + + if (preparedStatement != null) { + preparedStatement.close(); + } + + if (connection != null) { + connection.close(); + } + } + } +``` + +일반적으로 JDBC를 활용한 DAO의 로직은 위와같은 모습을 ㅂ보이게 됩니다. 이러한 코드 스타일은 몇가지 문제가 있습니다. + +- 복잡한 try/catch/finally 블록이 2중으로 까지 중첩되어 나온다. +- 모든 메소드마다 반복적으로 작성된다. + +만약 공유 자원을 반환하는 코드가 특정 메소드를 기준으로 누락되었다고 하더라도 이는 **컴파일 오류로 인식되지 않고 기능 또한 정상적으로 작동**합니다. +또한 자원 반납 여부는 테스트 코드로 검증하기 어렵기 때문에 잘못 작성된 코드가 운영 환경에 배포될 위험이 있습니다. +
+반복적으로 작성된 try/catch/finally 부분에 수정이 필요한 경우도 함께 생각해보겠습니다. DAO의 메소드가 증가할 수록 수정 범위는 예측하기 힘들게 되고, +수정 과정에서 누락이나 실수가 발생할 수도 있습니다. + +- **이러한 코드를 효과적으로 다룰 수 있는 방법은 없을까?** <== 개발자라면 당연히 이런 의문을 가져야 한다고합니다. + +## 3.2.2 분리와 재사용을 위한 디자인 패턴 적용 + +앞서 살펴본 예시 코드에서 변하는 부분과 변하지 않는 부분을 구분해보겠습니다. + +- 변하지 않는 부분 + - Connection과 같은 공유 자원을 획득 + - PreparedStatement를 실행 + - 실행이 완료된 자원에 대한 반납 +- 변하는 부분 + - 실행할 SQL을 전달받아 PreparedStatement를 생성 + +### 메소드 추출 + +현재 코드는 변하지 않는 부분이 변하는 부분을 앞뒤로 감싸고 있는 형태이기 때문에 변하지 않는 부분을 메소드로 추출하기는 어렵습니다. +때문에 변하는 부분을 별도의 메소드로 분리해보겠습니다. + +```java + public void deleteAll() throws SQLException { + Connection connection = null; + PreparedStatement preparedStatement = null; + ResultSet resultSet = null; + + try { + connection = dataSource.getConnection(); + + preparedStatement = makeStatement(c) + + preparedStatement.executeUpdate(); + } catch (SQLException e) { + throw e; + } //... 이후 자원 반납 부분 + } + + private PreparedStatement makeStatement(Connection c) throws SQLException { + PreparedStatement preparedStatement = null; + preparedStatement = c.preparedStatement("delete from users"); + return preparedStatement; + } +``` + +위와 같은 형태로의 리펙토링이 유효한지 한 번 생각해봅시다. 만약 유효하지 않다면 어느곳이 잘못되었는지도 생각해봅시다. + +### 템플릿 메소드 패턴의 적용 + +변하지 않는 부분을 슈퍼 클래스에 두고 변하는 부분은 추상 메소드로 정의해서 서브 클래스에서 오버라이드하여 새롭게 정의하는 템플릿 메서도 패턴을 적용해보겠습니다. + +`abstract protected PreparedStatement makeStatement(Connection c) throws SQLException;` + +try/catch/finally 블록을 가진 슈퍼 클래스 메소드와 위의 추상 메소드를 구체화한 메소드를 활용하여 앞선 개선 방식보다 적절하게 리펙토링 할 수 있습니다. + +```java +public class UserDaoDeleteAll extends UserDao { + + // try / catch / finally를 공통적으로 관리할 공통 메소드 + // public void exetuce() throws SQLExcetion + + protected PreparedSatement makeStatement(Connection c) throws SQLException { + PremaredStatement ps = c.prepareStatement("delete from users"); + return ps; + } +} +``` + +템플릿 메서드 패턴을 적용하였을 때의 단점에 대해 생각해봅시다. +만약 실행이 필요한 DAO 메소드가 추가될 경우 UserDao의 새로운 구현체를 추가하는 방식이기 때문에 관리가 필요한 서브 클래스의 양이 증가하게 됩니다. +또한 컴파일 시점에 이미 클래스간 연관 관계가 확정되어버려 추가적인 확장이 어렵다는 문제도 존재합니다. + +### 전략 패턴의 적용 + +전략 패턴은 연관 관계에 있는 두 오브젝트를 아예 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 만드는 패턴입니다. +확장에 해당하는 '변하는 부분'을 추상화된 인터페이스를 통하여 별도의 클래스로 만들기 때문에 보다 확장성 있는 구조를 확보할 수 있습니다. + +deleteAll()의 컨텍스트는 아래와 같은 흐름으로 수행됩니다. + +- DB 커넥션 가져오기 +- PreparedStatement를 만들어줄 외부 기능 호출 +- 전달받은 PreparedStatement 실행 +- 예외 발생 시 메소드 밖으로 던지기 +- 공유 자원에 대한 자원 반납 + +이 중 **PreparedStatement를 만들어줄 외부 기능 호출**이 변하는 부분에 해당하는 전략이라고 볼 수 있습니다. 이를 인터페이스로 분리하면 +Conneciton을 전달받아 PreparedStatement객체를 반환하는 아래와 같은 모습으로 분리할 수 있습니다. + +```java +public interface StatementStrategy { + PreparedStatement makePreparedStatement(Connection c) throws SQLException; +} +``` + +인터페이스를 상속해 실제 deleteAll에 해당하는 전략 클래스를 작성하면 아래와 같습니다. + +```java +public class DeleteAllStatement implements StatementStrategy { + public PreparedStatement makePreparedStatement(Connection c) throws SQLException { + PreparedStatement ps = c.prepareStatement("delete from users"); + return ps; + } +} + + +// 전략 패턴이 적용된 최종 deleteAll() 메소드 형식 +public void deleteAll() throws SQLException { + //... + try { + c = dataSource.getConnection(); + + StatementStrategy s = new DeleteAllStatement(); + ps = s.makePreparedStatement(c); + + ps.executeUpdate(); + } catch { + //... + } + +} +``` + +위와 같이 개선된 코드에서 어떤 부분이 문제이고 개선이 필요할까요? + +### DI 적용을 위한 클라이언트/컨텍스트 분리 + +Context가 어떤 전략을 사용할 것인가에 대한 결정은 Context가 아닌 이를 사용하는 앞단(Client)에서 결정해야합니다. + +- Client의 책임: 구체적인 전략을 선택하고 오브젝트를 생성하여 Context에 전달 +- Context의 책임: 전략을 실행 + +DI는 이러한 전략 패턴의 장점을 일반적으로 사용할 수 있도록 만든 구조 + +```java +public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { + Connection c = null; + PreparedStatement ps = null; + + try { + c = dataSource.getConnection(); + + ps = stmt.makePreparedStatement(c); + + ps.executeUpdate(); + } catch { + //... + } +} +``` + +jdbcContextWithStatementStrategy는 Client로부터 StatementStrategy를 전달받아 정해진 틀 안에서 필요한 기능을 수행합니다. +`deleteAll()` 메소드는 Client의 역할로 StatementStrategy를 생성하여 jdbcContextWithStatementStrategy를 실행하도록 아래와 같이 수정할 수 있습니다. + +```java +public void deleteAll() throws SQLException { + StatementStrategy st = new DeleteAllStatement(); + jdbcContextWithStatementStrategy(st); +} +``` + +이러한 개선 작업로 인한 장점이 크게 와닿지 않을 수도 있으나, 이러한 구조는 객체간 의존도를 줄이므로 앞으로 진행하게 된 추가적인 개선 작업의 기반이 됩니다. + +## 질문 + +- 앞서 진행한 메소드를 추출하는 방식으로의 개선에 어떤 문제가 있고 어떻게 개선할 수 있나요? +- '변하지 않는 부분'과 '변하는 부분'을 구별하여 전략 패턴을 적용해 '변하는 부분'을 분리해내는 작업의 장점은 무엇인가요?