ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 3장) 3.4 컨텍스트와 DI ~ 3.5 템플릿과 콜백
    Java & Spring/토비의 스프링 3.1 2021. 6. 11. 16:44
    반응형

    3장 템플릿

    3.4 컨텍스트와 DI

    • JdbcContext의 분리

      • 전략 패턴 구조
        • UserDao의 메소드가 클라이언트
        • 익명 내부 클래스로 만들어진 것이 개별적인 전략
        • jdbcContextWithStatementStrategy 메소드가 Context
      • JDBC의 일반적인 작업 흐름을 가지고 있는 컨텍스는 다른 DAO에서도 사용 가능
        ~> 분리 시켜보자
      • 클래스 분리
        • JdbcContext가 DataSource에 의존 ~> DataSource 타입 빈을 DI 받게 변경
          • 생성자를 통해 DataSource 주입
      • 빈 의존관계 변경
        • 클래스 분리로 인해 UserDao는 JdbcContext에 의존하고 있지만, JdbcContext는 구체 클래스
        • 스프링 DI는 인터페이스를 사이에 두고 의존 클래스를 바꿔 사용하는게 목적
        • 하지만, 이 경우 서비스 오브젝트로서 의미가 있을 뿐이고 구현 방법이 바뀔 가능성이 없기 때문에 인터페이스를 사용하지 않아도 괜찮다.
    • jdbcContext의 특별한 DI

      • 지금까지 적용했던 DI는 클래스 레벨에서 구체적인 의존관계가 만들어지지 않도록 인터페이스를 사용했지만, UserDao는 바로 JdbcContext 클래스를 사용

        • 인터페이스를 사용하지 않았기 때문에 온전한 DI라고 볼 수는 없음
        • 하지만, DI의 기본을 따름
          • 객체의 생성과 관계설정에 대한 제어 권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포괄
      • 스프링 빈으로 DI

        • JdbcContext를 UserDao와 DI 구조로 만들어야하는 이유
          • JdbcContext가 스프링 컨테이너 싱글톤 레지스트리에서 관리되는 싱글톤 빈임
            • 그 자체로 변경되는 상태정보를 가지고 있지 않음
          • JdbcContext가 DI를 통해 다른 빈에 의존하고 있음
            • 다른 빈을 DI 받기 위해서라도 스프링 빈으로 등록돼야함
        • 스프링에는 드물지만 이렇게 인터페이스를 사용하지 않는 클래스를 직접 의존하는 DI가 있음
          • 그 이유는, 책임과 목적을 따졌을 때, 강한 응집도를 가지고 있기 때문
          • 단, 클래스를 바로 사용하는 코드 구성을 DI에 적용하는 것은 가장 마지막에 고려해야함
      • 코드를 이용하는 수동 DI

        • UserDao 내부에서 직접 DI

          • JdbcContext를 스프링 빈으로 등록해서 사용했던 첫 번째 이유인 싱글톤으로 만드려는 것을 포기해야함
        • JdbcContext를 스프링 빈으로 등록하지 않았으므로 누군가 JdbcContext의 생성과 초기화를 책임져야함

          • JdbcContext의 제어권은 UserDao가 갖는 것이 적당
        • JdbcContext를 스프링 빈으로 등록해서 사용했던 문제를 해결해야함

          • JdbcContext는 다른 빈을 인터페이스를 통해 간접적으로 의존 ~> 자신도 빈으로 등록되어 있었어야함
          • JdbcContext에 대한 제어권을 갖고 생성과 관리를 담당하는 UserDao에게 DI 까지 맡기며 해결 가능
          • UserDao가 임시로 DI 컨테이너처럼 동작하게 만들면 해결
        • jdbcContext 빈을 제거한 설정파일만 보면 UserDao가 직접 DataSource를 의존하고 있는 것 같지만, 내부적으로는 JdbcContext를 통해 간접적으로 DataSource를 사용

          • 빈 레벨에서는 userDao 빈이 dataSource 빈에 의존하고 있다고 말할 수도 있음
        • public class UserDao {
              // ...
              private JdbcContext jdbcContext;
          
              public void setDataSource(DataSource dataSource) {
                  this.jdbcContext = new JdbcContext();
                  this.jdbcContext.setDataSource(dataSource);
                  this.dataSource = dataSource;
              }
          }
        • 인터페이스를 두지 않아도 될 만큼 긴밀한 관계를 갖는 DAO 클래스와 JdbcContext를 어색하게 따로 빈으로 분리하지 않고, 내부에서 직접 사용하면서도 다른 오브젝트에 대한 DI를 적용할 수 있다는 장점이 있음

          • 최근 들어 수정자 주입을 하는 코드를 많이 보지는 못함 (개인 의견)
      • 스프링 DI를 위한 빈 등록 vs 수동 DI

        • 스프링 DI를 위한 빈 등록
          • 장점) 의존관계가 설정파일에 명확하게 드러남
          • 단점) DI의 근본 원칙에 부합하지 않는 구체적 클래스와의 관계가 설정에 집적 노출됨
        • 수동 DI
          • 장점) 관계를 외부에 드러내지 않음
          • 단점) 싱글톤으로 만들 수 없고, DI를 위한 부가적인 코드가 필요함

    3.5 템플릿과 콜백

    • 템플릿/콜백 패턴

      • 전략 패턴의 기본 구조에 익명 내부 클래스를 활용한 방식
      • 전략 패턴의 컨텍스트 ~> 템플릿
        익명 내부 클래스 오브젝트 ~> 콜백
    • 템플릿/콜백의 동작 원리

      • 템플릿/콜백의 특징
        • 단일 메소드 인터페이스를 사용 ~> 전략 패턴은 여러 메소드를 가진 일반 인터페이스 사용 가능
        • 콜백은 일반적으로 하나의 메소드를 가진 인터페이스를 구현한 익명 내부 클래스로 만들어진다.
        • 클라이언트 역할
          • 템플릿 안에 실행될 로직을 담은 콜백 오브젝트를 생성, 참조할 정보 제공
          • 콜백은 템플릿 메소드를 호출할 때 파라미터로 전달
        • 템플릿
          • 정해진 작업 흐름을 따라 작업 진행 ~> 콜백 오브젝트의 메소드 호출
          • 콜백은 클라이언트 메소드에 있는 정보와 템플릿의 참조정보를 이용해 작업 수행 ~> 다시 템플릿에게 결과를 리턴
          • 템플릿은 콜백이 돌려준 정보로 작업을 마저 수행 ~> 최종 결과를 클라이언트에 리턴
        • DI 방식의 전략 패턴 구조라고 생각하면 이해하기 수월
          • 일반적인 DI ~> 템플릿에 인스턴수 변수 생성 ~> 주입
          • 템플릿/콜백 패턴 ~> 매번 새롭게 오브젝트를 전달받음, 콜백 오브젝트가 자신을 생성한 클라이언트 메소드 내의 정보를 직접 참조
          • 전략 패턴과 DI 장점을 익명 내부 클래스 사용 전략과 결합한 활용법
      • JdbcContext에 적용된 템플릿/콜백
        • UserDao, JdbcContext를 템플릿/콜백의 구조로 살펴보자
          • 템플릿과 클라이언트가 메소드 단위인 것이 특징
    • 편리한 콜백의 재활용

      • 템플릿/콜백의 장단점

        • 장점
          • 클라이언트 DAO의 메소드가 간결해짐
          • 최소한의 데이터 엑세스 로직만 가짐
        • 단점
          • DAO 메소드에서 매번 익명 내부 클래스를 사용 ~> 코드를 작성하고 읽기가 불편
      • 콜백의 분리와 재활용

        • deleteAll() 메소드의 내용을 통틀어 바뀔 수 있는 것은 오직 쿼리 뿐

          • 파라미터로 SQL 문장을 받아 바꾸도록 설정하면 분리 가능
        • public void deleteAll() throws SQLException {
              executeSql("delete from users");
          }
          
          private void excuteSql(final String query) throws SQLException {
              this.jdbcContext.workWithStatementStrategy(
                  new StatementStrategy() {
                      public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                          return c.prepareStatement(query);
                      }
                  }
              );
          }
        • 바뀌지 않는 모든 부분을 빼내 executeSql() 메소드로 만들고, 바뀌는 부분인 SQL 문장만 파라미터로 받아 사용하게 설정

          • 파라미터는 final로 선언
      • 콜백과 템플릿의 결합

        • excuteSql을 UserDao만 사용하기는 아까움 ~> 재활용 해보자

        • 템플릿은 JdbcContext 클래스가 아니라 workWithStatementStrategy() 메소드 ~> JdbcContext 클래스로 콜백 생성과 템플릿 호출이 담긴 executeSql() 메소드를 옮겨도 문제 X

        • public class JdbcContext {
              //...
              public void excuteSql(final String query) throws SQLException {
                  workWithStatementStrategy(
                      new StatementStrategy() {
                          public PreparedStatement makePreparedStatement(Connection c) throws SQLException {
                              return c.prepareStatement(query);
                          }
                      }
                  );
              }
          }
          • deleteAll은 this.jdbcContext.executeSql("delete from users"); 로 바꿔주기만 하면 사용 가능
        • 수정으로 인해 JdbcContext 안에 클라이언트와 템플릿, 콜백이 모두 함께 공존하면서 동작하는 구조로 바뀜

        • 성격이 다른 코드들은 가능한 한 분리하는 편이 낫지만, 이 경우는 하나의 목적을 가지고, 응집력이 강한 코드기 때문에 한 군데 모여있는게 유리

    • 템플릿/콜백 응용

      • 스프링은 기본적으로 OCP를 지키고, 전략 패턴과 DI를 바탕에 깔고 있으니, 언제든 확장해서 편리한 방법으로 사용 가능

        • 중복 코드를 분리할 방법을 생각해보는 습관을 기르자
      • 템플릿/콜백 응용 예시

        • Calculator라는 클래스를 만들어보자.

          • 초기에는 덧셈, 뺄셈 등과 같은 수식을 만든다고 가정해보자.

          • 이후에, 곱셈 및 나눗셈 등이 추가될 수 있다.

          • 수식의 공통점을 생각해보면 계산 값들이 있고, 해당 값을 저장할 초기 값(결과 값을 담을 변수), 수식 등이 있다.

          • public interface LineCallback {
                Integer doSomthingWithLine(String line, Integer value);
            }
          • public Integer lineReadTemplate(String filepath, LineCallback callback, int initVal) throws IOException {
                try (BuffredReader br = new BufferedReader(new FileReader(filepath));) {
                    Integer res = initVal;
                    String line = null;
                    while(line = br.readLine() != null) {
                        res = callback.doSomethingWithLine(line, res);
                    }
                    return res;
                } catch (IOException e) { //... }
            }
            • 익명 내부 클래스 때문에 라인 수가 많아보이지만 핵심 코드는 딱 한줄 뿐이다.
          • public Integer calcSum(String filepath) throws IOException {
                LineCallback sumCallback = new LineCallback() {
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value + Integer.valueOf(line);
                    }};
                return lineReadTemplate(filepath, sumCallback, 0);
            }
            
            public Integer calcMultiply(String filepath) throws IOException {
                LineCallback muliplyCallback = new LineCallback() {
                    public Integer doSomethingWithLine(String line, Integer value) {
                        return value * Integer.valueOf(line);
                    }};
                return lineReadTemplate(filepath, muliplyCallback, 1);
            }
          • 위와 같이 재사용을 수월하게 할 수 있음

        • 코드의 특성이 바뀌는 경계를 잘 살피고, 인터페이스를 사용해 분리한다는 객체지향 원칙만 충실하면, 어렵지 않게 템플릿/콜백 패턴을 만들어 활용할 수 있음

      • 제네릭스를 이용한 콜백 인터페이스

        • 파일 각 라인의 문자를 모두 연결해서 하나의 스트링으로 돌려주는 기능을 추가한다고 생각해보자.

        • public interface LineCallback<T> {
              T doSomethingWithLine(String line, T value);
          }
        • public <T> T lineReadTemplate(String filepath, LineCallback<T> callback, T initVal) throws IOException{
              try (BuffredReader br = new BufferedReader(new FileReader(filepath));) {
                  T res = initVal;
                  String line = null;
                  while((line = br.readLine()) != null) {
                      res = callback.doSomethingWithLine(line, res);
                  }
                  return res;
              } catch (IOException e) { //... }
          }
        • public String concatenate(String filepath) throws IOException {
              LineCallBack<String> concatenateCallback = new LineCallback<String>() {
                  public String doSomthingWithLine(String line, String value) {
                      return value + line;
                  }};
              return lineReadTemplate(filepath, concatenateallback, "");
          }
          
          public Integer calcSum(String filepath) throws IOException {
              LineCallback<Integer> sumCallback = new LineCallback() {
                   // ...
              }
          }
        • 위와 같이 제네릭으로 인터페이스를 설정해주고, 콜백 함수에서 타입을 지정해주면 확장성을 고려하고, 코드 중복을 줄여줄 수 있음

    용어 정리

    • 템플릿/콜백

      • 템플릿
        • 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀
      • 콜백
        • 다른 오브젝트 메소드에 전달되는 오브젝트
        • 펑셔널 오브젝트(functional object) 라고도 함
    반응형

    댓글

Designed by Tistory.