-
Notifications
You must be signed in to change notification settings - Fork 0
3.2: 변하는 것과 변하지 않는 것 #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JK-Kim4
wants to merge
1
commit into
main
Choose a base branch
from
jongwan
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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중으로 까지 중첩되어 나온다. | ||
| - 모든 메소드마다 반복적으로 작성된다. | ||
|
|
||
| 만약 공유 자원을 반환하는 코드가 특정 메소드를 기준으로 누락되었다고 하더라도 이는 **컴파일 오류로 인식되지 않고 기능 또한 정상적으로 작동**합니다. | ||
| 또한 자원 반납 여부는 테스트 코드로 검증하기 어렵기 때문에 잘못 작성된 코드가 운영 환경에 배포될 위험이 있습니다. | ||
| <br> | ||
| 반복적으로 작성된 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); | ||
| } | ||
| ``` | ||
|
|
||
| 이러한 개선 작업로 인한 장점이 크게 와닿지 않을 수도 있으나, 이러한 구조는 객체간 의존도를 줄이므로 앞으로 진행하게 된 추가적인 개선 작업의 기반이 됩니다. | ||
|
|
||
| ## 질문 | ||
|
|
||
| - 앞서 진행한 메소드를 추출하는 방식으로의 개선에 어떤 문제가 있고 어떻게 개선할 수 있나요? | ||
| - '변하지 않는 부분'과 '변하는 부분'을 구별하여 전략 패턴을 적용해 '변하는 부분'을 분리해내는 작업의 장점은 무엇인가요? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q. '변하지 않는 부분'과 '변하는 부분'을 구별하여 전략 패턴을 적용해 '변하는 부분'을 분리해내는 작업의 장점은 무엇인가요?
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Q. 앞서 진행한 메소드를 추출하는 방식으로의 개선에 어떤 문제가 있고 어떻게 개선할 수 있나요?