-
3장) 3.1 다시 보는 초난감 DAO ~ 3.3 JDBC 전략 패턴의 최적화Java & Spring/토비의 스프링 3.1 2021. 6. 11. 16:43반응형
3장 템플릿
- 개방 폐쇄 원칙
- 확장에는 자유롭게 열려 있고 변경에는 굳게 닫혀 있다.
3.1 다시 보는 초난감 DAO
2장에서 Test와 관련해서 DB 연결과 관련된 여러 개선 작업을 진행했지만, 예외상황 처리 부분이 미흡하다. 이를 개선해보자.
예외처리 기능을 갖춘 DAO
JDBC는 예외가 발생한 경우에도 리소스를 반드시 반환해야함
Connection Pool에서 리소스를 받아와 사용하기 때문에, 반환을 제대로 해주지 않은 리소스가 많아져서 connection pool에서 리소스가 부족하다는 치명적인 Exception이 발생할 수 있음
public void deleteAll() throws SQLException { try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement("delete from users");) { // .. logic } catch (SQLException e) { throw e; } }
JDBC 조회 기능의 예외 처리
public int getCount() throws SQLException { try (Connection c = dataSource.getConnection(); PreparedStatement ps = c.prepareStatement("select count(*) from users"); ResultSet rs = ps.executeQuery(); ) { // .. logic } catch (SQLException e) { throw e; } }
변하는 것과 변하지 않는 것
JDBC try / catch / finally 문제점
- 복잡한 try / catch / finally 블록이 2중으로 중첩되어 나오고, 모든 메소드마다 반복
- copy & paste에 한계도 있고, 잘못된 코드가 발생하면 전체적으로 추적하기도 어려움
- 복잡한 try / catch / finally 블록이 2중으로 중첩되어 나오고, 모든 메소드마다 반복
분리와 재사용을 위한 디자인 패턴 적용
메소드 추출
- 변하는 부분을 메소드로 빼는 방법을 먼저 떠올릴 수 있다.
connection.prepareStatement(queryString)
- 자주 바뀌는 부분을 메소드로 독립시켜도 별 이득이 없음 (위의 예시 코드 참고)
- 다른 곳에서 재사용할 수 있어야하는데, 분리시키고 남은 메소드가 재사용이 필요한 부분이고, 분리된 메소드가 DAO 로직마다 새롭게 만들어서 확장돼야하는 부분이 있기 때문
- 변하는 부분을 메소드로 빼는 방법을 먼저 떠올릴 수 있다.
템플릿 메소드 패턴 적용
public class UserDaoDeleteAll extends UserDao { protected PreparedStatment makeStatement(Connection c) throws SQLException { PreparedStatement ps = c.prepareStatement("delete from users"); return ps; } }
- 접근에 제한이 많음
- DAO 로직마다 상속을 통해 새로운 클래스를 만들어야하는 불편함 존재
- 컴파일 시점에 관계가 결정되어있음 ~> 유연성이 떨어짐
전략 패턴 적용
Context의 contextMethod()에서 일정 구조를 가지고 동작하다 특정 확장 기능은 Strategy 인터페이스를 통해 외부의 독립된 전략 클래스에 위임
deleteAll을 예로 들면, deleteAll 메소드가 contextMethod()에 해당
- DB 커넥션 가져오기
- PreparedStatement를 만들어줄 외부 기능 호출 <~ 전략 패턴의 전략
- 전달받은 PreparedStatement 실행
- 예외 발생 ~> 메소드 밖으로 던지기
- 모든 경우에 만들어진 PreparedStatement와 Connection을 적절히 닫아주기
public interface StatementStrategy { PreparedStatement makePreparedStatement(Connection c) throws SQLException; }
public class DeleteAllStatement implements StatementStrategy { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { PreparedStatement ps = c.prepareConnection("delete from users"); return ps; } }
public void deleteAll() throws SQLException { // ... try { c = dataSource.getConnection(); StatementStrategy strategy = new DeleteAllStatement(); ps = strategy.makePreparedStatement(c); // ... } }
전략 패턴은 필요에 따라 컨텍스트는 그대로 유지되면서 전략을 바꿔 쓸 수 있어야 함
- 하지만, 컨텍스트가 StatementStragy 인터페이스 뿐만 아니라 특정 구현 클래스인 DeleteAllStatement를 직접 알고 있음
폐쇄 원칙(OCP의 폐쇄 원칙)에 들어맞지 않음
DI 적용을 위한 Client / Context 분리
Context가 어떤 전략을 사용하게 할 것인지는 Client가 결정하는 것이 일반적
전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리하는 것이 ObjectFactory
- 이를 일반화한 것이 의존관계 주입(DI)
- 전략 패턴의 장점을 일반적으로 활용할 수 있도록 만든 구조
컨텍스트에 해당하는 부분을 별도의 메소드로 독립시켜보자.
public void jdbcContextWithStatementStrategy(StatementStrategy stmt) throws SQLException { try (Connection c = dataSource.getConnection(); PreparedStatement ps = stmt.makePreparedStatement(c);) { // .. logic } catch (SQLException e) { throw e; } }
- 제공받은 전략 오브젝트는 PreparedStatement 생성이 필요한 시점에서 호출해서 사용
클라이언트에 해당하는 부분(deleteAll 메소드)을 살펴보자
전략 오브젝트를 만들고 컨텍스트를 호출하는 책임이 있음
public void deleteAll() throws SQLException { StatementStrategy st = new DeleteAllStatement(); // 전략 오브젝트 생성 jdbcContextWithStatementStrategy(st); // 컨텍스트 호출, 전략 오브젝트 전달 }
- 클라이언트와 컨텍스트 클래스를 분리하지 않았지만, 의존관계와 책임으로 볼 때, 이상적인 관계를 가지고 있다.
JDBC 전략 패턴의 최적화
전략 클래스의 추가 정보
- 데이터를 add의 경우에는 PreparedStatement에 등록할 정보를 set해줘야한다.
이런 경우에는 생성 정보를 생성자를 통해 전달해주도록 해주어야한다.
- 데이터를 add의 경우에는 PreparedStatement에 등록할 정보를 set해줘야한다.
전략과 클라이언트의 동거
생성자로 전달하는 방법보다 개선할 수 있는 방법을 찾아보자.
- 위에 만들어진 구조에 두 가지 개선사항이 있다.
- DAO 메소드마다 새로운 StatementStrategy 구현 클래스를 만들어야 한다는 점
- DAO 메소드에서 StatementStrategy에 전달할 부가적인 정보가 있는 경우, 오브젝트를 전달받는 생성자와 이를 저장해둘 인스턴스 변수를 번거롭게 만들어야한다는 점
- 위에 만들어진 구조에 두 가지 개선사항이 있다.
로컬 클래스
특정 메소드에서만 사용되는 것이라면 로컬 클래스로 만들 수 있다.
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) { // ..ps 선언 및 값 설정 return ps; } } }
자신이 선언된 곳의 정보에 접근할 수 있다는 장점이 있음
- 다만, 내부 클래스에서 외부의 변수를 사용할 때는 외부 변수는 반드시 final로 선언
익명 내부 클래스
이름을 갖지 않는 클래스로 클래스 선언과 오브젝트 생성이 결합된 형태
구현하는 인터페이스를 생성자처럼 이용해서 오브젝트로 만듦
StatementStrategy st = new StatementStrategy() { public PreparedStatement makePreparedStatement(Connection c) throws SQLException { // ..ps 선언 및 값 설정 return ps; } }
- 코드를 줄일 수 있고 딱 한번만 사용될 오브젝트이므로 굳이 변수에 담아두지 않고 파라메터에서 바로 생성하는 것도 좋은 방법
용어 정리
- 마이크로 DI
- DI의 가장 중요한 개념은 제 3자의 도움을 통해 두 오브젝트 사이에 유연한 관계가 설정되도록 만든다는 것
- DI의 장점을 단순화해서 IoC 컨테이너의 도움 없이 코드 내에서 적용한 경우를 마이크로 DI라고 함
- 수동 DI라고도 부른다.
- 중첩 클래스 종류
- 중첩 클래스(nested class)
- 다른 클래스 내부에 정의되는 클래스
- 종류
- 스태틱 클래스(static class)
- 독립적으로 오브젝트로 만들어질 수 있는 클래스
- 내부 클래스(inner class)
- 자신이 정의된 클래스의 오브젝트 안에서만 만들어질 수 있는 클래스
- scope(범위)에 따라 세 분류로 구분됨
- 멤버 내부 클래스
- 멤버 변수처럼 오브젝트 레벨에 정의되는 클래스
- 로컬 클래스
- 메소드 레벨에 정의되는 클래스
- 익명 내부 클래스
- 이름을 갖지 않는 클래스
- 범위는 선언된 위치에 따라 다름
- 클래스를 재사용할 필요가 없음
- 멤버 내부 클래스
- 스태틱 클래스(static class)
- 중첩 클래스(nested class)
반응형'Java & Spring > 토비의 스프링 3.1' 카테고리의 다른 글
3장) 3.6 스프링의 JdbcTemplate~ 3.7 정리 (0) 2021.06.17 3장) 3.4 컨텍스트와 DI ~ 3.5 템플릿과 콜백 (0) 2021.06.11 2.4 스프링 테스트 적용 ~ 2.6 정리 (0) 2021.06.03 2장) 2.3 개발자를 위한 테스팅 프레임워크 JUnit (0) 2021.06.01 2장) 2.1 USERDAOTEST 다시보기 ~ 2.2 USERDAOTEST 개선 (0) 2021.05.27 - 개방 폐쇄 원칙