Java & Spring/토비의 스프링 3.1

3장) 3.4 컨텍스트와 DI ~ 3.5 템플릿과 콜백

Zin0_0 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) 라고도 함
반응형