ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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에 한계도 있고, 잘못된 코드가 발생하면 전체적으로 추적하기도 어려움
      • 분리와 재사용을 위한 디자인 패턴 적용

        • 메소드 추출

          • 변하는 부분을 메소드로 빼는 방법을 먼저 떠올릴 수 있다.
            • 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해줘야한다.
          이런 경우에는 생성 정보를 생성자를 통해 전달해주도록 해주어야한다.
      • 전략과 클라이언트의 동거

        • 생성자로 전달하는 방법보다 개선할 수 있는 방법을 찾아보자.

          • 위에 만들어진 구조에 두 가지 개선사항이 있다.
            • 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(범위)에 따라 세 분류로 구분됨
            • 멤버 내부 클래스
              • 멤버 변수처럼 오브젝트 레벨에 정의되는 클래스
            • 로컬 클래스
              • 메소드 레벨에 정의되는 클래스
            • 익명 내부 클래스
              • 이름을 갖지 않는 클래스
              • 범위는 선언된 위치에 따라 다름
              • 클래스를 재사용할 필요가 없음
    반응형

    댓글

Designed by Tistory.