확장에는 자유롭게 열려있고, 변경에는 닫혀있어야 한다. OCP (개방-폐쇄의 원칙)


분리와 재사용을 위한 디자인 패턴 적용

로직에 따라서 변하는 부분을 변하지 않는 나머지 코드에서 분리하는 것. 이렇게 한다면 변하지 않는 부분을 재사용할 수 있는 방법이 있을 수 있다.

  • 메소드 분리
    - 변하는 부분을 메소드로 빼는 것.
    - 변하지 않는 부분이 변하는 부분을 감싸고 있어서 변하지 않는 부분을 추출하기 어렵다. 

    위의 방식으로 한다면 변하는 부분을 메소드로 독립시켰는데 분리시킨 메소드는 다른 곳에 재사용이 불가능하다. 왜냐? 변하는 부분이기 때문에. 

    변하지 않는 부분에서 변하는 부분을 메소드로 빼면 변하지 않는 부분을 사용하지 못한다.

  • 템플릿 메소드 패턴의 적용
    - 상속을 통해 기능을 확장해서 사용하는 부분
    - 변하지 않는 부분은 슈퍼클래스에 두고 변하는 부분은 추상 메소드로 정의하여 서브클래스에서 오버라이딩하여 새롭게 정의하도록 하는 것
    - 템플릿 메소드 패턴을 이용하면 동일한 로직을 이용하기 위해서는 매번 새로운 클래스를 만들어야 한다는 점이 존재. 또한 확장구조가 이미 클래스를 설계하는 시점에서 고정되어 버린다는 점.

  • 전략 패턴의 적용
    - OCP, 개방 폐쇄 원칙을 잘 지키는 구조이면서 템플릿 메소드 패턴보다 유연하고 확장성이 뛰어난 것은 오브젝트를 아예 둘로 분리하고 클래스 레벨에서 인터페이스를 통해서만 의존하도록 만드는 전략 패턴이다.
    - 전략 패턴은 OCP 관점에서 보면 확장에 해당하는 변하는 부분을 별도의 클래스로 만들어 추상회된 인터페이스를 통해 위임하는 방식 ( 나의 생각 : 인터페이스 구현체 느낌? )


  • DI 적용을 위한 클라이언트/컨텍스트 분리
    - 전략패턴에 따르면 Context 가 어떤 전략을 사용하게 할 것인가는 Context를 사용하는 앞단의 Client가 결정하는 게 일반적이다. Client 가 구체적인 전략의 하나를 선택하고 오브젝트를 만들어서 Context 에 전달하는 것이다. Context는 전달받은 그 Strategy 구현 클래스의 오브젝트를 사용한다.

    - 전략 오브젝트 생성과 컨텍스트로의 전달을 담당하는 책임을 분리시킨 것이 바로 ObjectFactory이며, 이를 일반화한 것이 앞에서 살펴보았던 의존관계 주입, DI 이다.


  • 익명 내부 클래스
    익명 내부 클래스(anonymous inner class)는 이름을 갖지 않는 클래스를 의미한다. 클래스 선언과 오브젝트 생성이 결합된 형태로 만들어지며 상속할 클래스나 구현할 인터페이스를 생성자 대신 사용해서 아래와 같은 형태로 만들어 사용한다. 클래스를 재사용할 필요가 없으며 구현한 인터페이스 타입으로만 사용할 경우에 유용하다.

    new 인터페이스 이름() { 클래스 본문; }


컨텍스트와 DI

의존관계 주입 DI라는 개념을 충실히 따르자면, 인터페이스를 사이에 둬서 클래스 레벨어서는 의존관계가 고정되지 않게 하고, 런타임 시에 의존할 오브젝트와의 관계를 다이내믹하게 주입해주는 것이 맞다. 따라서 인터페이스를 사용하지 않았다면 온전한 DI라고 말할 수 없다.


그러나 스프링의 DI는 넓게 보자면 객체의 생성과 관계설정에 대한 제어권한을 오브젝트에서 제거하고 외부로 위임했다는 IoC라는 개념을 포함한다. 그런 의미에서 (본문내용 생략) 


책에 있는 예제 확인하기.


템플릿과 콜백

복잡하지만 바뀌지 않는 패턴들은 작업흐름을 가지고 있으며 그 중 일부분만 자주 변경되어 사용해야 하는 경우 전략패턴을 이용해야 한다. 전략패턴의 기본 구조에 익명 내부 클래스를 활용하는 방식이다. 이러한 방식을 스프링에서는 템플릿 / 콜백 패턴이라고 부른다. 전략 패턴의 컨텍스트를 템플릿이라 부르고, 익명 내부 클래스로 만들어지는 오브젝트를 콜백이라고 부른다.


템플릿

템플릿은 어떤 목적을 위해 미리 만들어둔 모양이 있는 틀을 가리킨다. 프로그래밍에서는 고정된 틀 안에 바꿀 수 있는 부분을 넣어서 사용하는 경우에 템플릿이라고 부른다. JSP는 HTML이라는 고정된 부분에 EL과 스크립트릿이라는 변하는 부분을 넣어 일종의 템플릿 파일이다. 템플릿 메소드 패턴은 고정된 틀의 로직을 가진 템플릿 메소드를 슈퍼 클래스로 두고 바뀌는 부분을 서브 클래스의 메소드로 두는 구조를 가진다.


콜백

콜백(callback)은 실행되는 것을 목적으로 다른 오브젝트의 메소드에 전달되는 오브젝트를 말한다. 파라미터로 전달되지만 값을 참조하기 위한 것이 아니라 특정 로직을 담은 메소드를 실행시키기 위해 사용한다. 자바에서는 메소드 자체를 파라미터로 전달하는 방법이 없기 때문에 메소드가 담긴 오브젝트를 전달해야 한다. 그래서 functional object 라고도 한다.


템플릿 콜백의 동작원리


- 템플릿은 고정된 작업 흐름을 가진 코드를 재사용

- 콜백은 템플릿 안에서 호출되는 것을 목적으로 만들어진 오브젝트


스프링을 사용하는 개발자라면 당연히 스프링이 제공하는 템플릿/콜백 기능을 잘 사용할 수 있어야 한다. 동시에 템플릿/콜백이 필요한 곳이 있으면 직접 만들어서 사용할 줄 알아야 한다. 스프링에 내장된 것을 원리도 알지 못한 채로 기계적으로 사용하는 경우와 적용한 패턴을 이해하고 사용하는 것에는 큰 차이가 있다.

  • 고정된 작업 흐름을 갖고 있으면서 여기저기서 자주 반복되는 코드가 있다면, 중복되는 코드를 분리할 방법을 생각하는 습관 기르기
  • 중복된 코드는 먼제 메소드로 분리하는 간단한 시도 해보기
  • 일부 작업을 필요에 따라 바꾸어 사용한다면 인터페이스를 사이에 두고 분리해서 전략 패턴을 적용하고 DI 로 의존관계를 관리하도록 만들기
가장 전형적인 템플릿 / 콜백 패턴의 후보는 try / catch / finally 블록을 사용하는 코드이다. 일정한 리소스를 만들거나 가져와 작업하면서 예외가 발생할 가능성이 있는 코드는 보통 try / catch / finally 구조로 코드가 만들어질 가능성이 높다. 예외상황을 처리하기 위한 catch 와 리소스를 반납하거나 제거하는 finally 가 필요하기 때문이다. 이러한 코드가 자주 반복된다면 템플릿 / 콜백 패턴을 적용하기에 적당하다.


예제)

  • 텍스트 파일을 읽어들여서 해당 텍스트 파일에 쓰인 숫자 1 2 3 4 를 더하는 프로그램 작성
  • 파일에 모든 숫자의 곱을 계산하는 기능을 추가한다는 요구 발생
  • Calculator 라는 클래스의 이름에 걸맞게 앞으로 많은 파일에 담긴 숫자 데이터를 여러가지 방식으로 처리하는 기능이 계속 추가될 것이라는 예측 가능
  • 템플릿 / 콜백을 적용하는 경우 템플릿과 콜백의 경계를 정하고 템플릿이 콜백에게, 콜백이 템플릿에게 각각 전달하는 내용이 무엇인지 파악하는게 중요하며 그에 따라 인터페이스를 정의해야하기 때문이다.

    - 템플릿이 파일을 열고 각 라인을 읽어올 수 있는 BufferedReader를 만들어서 콜백에게 전달해주고, 콜백이 각 라인을 읽어서 알아서 처리한 후에 최종 결과만 템플릿에게 돌려주는 것
calcSum 이라는 메소드가 존재. 해당 메소드는 파일의 경로를 받아 덧셈을 실시한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public Integer calcSum(String filePath) throws IOException{
    
    BufferedReader br = null;
    
    try{
        br = new BufferedReader(new FileReader(filePath));
        Integer sum = 0;
        String line = null;
        
        while((line = br.readLine()) != null){
            sum += Integer.parseInt(line);
        }
 
        return sum;
    }
    catch(IOException e){
        System.out.println(e.getMessage());
        throw e;
    }
    finally{
        if(br != null){
            try{
                br.close();
            }
            catch(IOException e){
                System.out.println(e.getMessage());
            }
        }
    }
}
cs


아래와 같이 변경


템플릿은 BufferedReader 를 만든다.

콜백은 작업을 수행한다. 

실질적인 작업은 콜백메소드에서 실시한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 인터페이스 파라미터
public Integer fileReadTemplate(String filePath, BufferedReaderCallback callback) throws IOException{
    
    BufferedReader br = null;
    
    try{
        br = new BufferedReader(new FileReader(filePath));
        
        /**
         * BufferedReader 를 만들어서 넘겨주는 것과 그 외의 작업들은 해당 [템플릿] 에서 진행
         * 준비된 BufferedReader를 이용해 작업을 수행하는 부분은 [콜백] 을 호출해서 처리.
         * **/
        
        // 콜백 메소드 호출, 콜백의 작업결과를 반환받는다.
        int ret = callback.doSomethingWithReader(br);    
        return ret;
    }// try()
    
    catch(IOException e){
        System.out.println(e.getMessage());
        throw e;
    }// catch()
    
    finally{
        if(br != null){
            try{
                br.close();
            }
            catch(IOException e){
                System.out.println(e.getMessage());
            }
        }
    }// finally()
}
 
// 덧셈 테스트 메소드
public Integer calcSum(String filePath) throws IOException{
    
    // 익명 내부 클래스를 해당 파라미터로 전달
    BufferedReaderCallback sumCallback = 
        new BufferedReaderCallback(){
            @Override
            public Integer doSomethingWithReader(BufferedReader br) throws IOException{
                Integer sum = 0;
                String line = null;
                
                while((line = br.readLine()) != null){
                    sum += Integer.valueOf(line);
                }
                
                return sum;
            }
    };
    
    return fileReadTemplate(filePath, sumCallback);
}
cs


콜백메소드는 인터페이스의 메소드로 표현되어있다. 그리고 try-catch-finally 구문을 여러번 써주지 않아도 구현이 되는 장점이 존재한다.


1
2
3
4
5
6
// BufferedReader 를 전달받은 콜백 인터페이스
public interface BufferedReaderCallback {
 
    Integer doSomethingWithReader(BufferedReader br) throws IOException;
 
}
cs


  • JDBC와 같은 예외가 발생할 가능성이 있으며 공유 리소스의 반환이 필요한 코드는 반드시 try/catch/finally 블록으로 관리해야 한다.
  • 일정한 작업흐름이 반복되면서 일부 기능만 바뀌는 코드가 존재한다면 전략패턴을 적용한다. 바뀌지 않는 부분은 컨텍스트, 바뀌는 부분은 전략으로 그리고 둘 사이는 인터페이스로 연결한다.
  • 컨텍스트가 하나 이상의 오브젝트에서 사용된다면 클래스를 분리해서 공유하도록 만든다.
  • 콜백의 코드에서도 일정한 패턴이 반복된다면 콜백을 템플릿에 넣고 재활용하는 것이 편리하다.
  • 템플릿과 콜백을 설계할 때에는 템플릿과 콜백 사이에 주고받는 정보에 관심을 두어야 한다.


Posted by doubler
,