6. 리팩토링.pdf



마지막 스터디에서 잠시 진행했던, Extra 챕터입니다. 


모두들 수고하셨습니다. @.@ [꾸벅][꾸벅]



참고자료 

- 한국기술교육대학교 - 객체지향개발론및실습 (김상진 교수님)

- Refactoring


추가내용!


Refactoring.zip


이 곳의 내용을 보충할 실습 코드를 첨부합니다.




반응형
Posted by N'

Observer 패턴에 다루는 마지막 포스팅입니다. 


정말, 이 패턴에 대해 할 말이 많군요...

그만큼 많이 사용하고 유명한 패턴이기 때문에 그런 듯 합니다.

(그렇다고, Observer 패턴은 사랑은 아닙니다...     ㅡㅡ^)


Observer 패턴과 관련된 정보는 아래에서 참고!



앞써, 언급한 바와 같이 Observer 패턴은 매우 많이 사용하는 패턴입니다.

그렇기 때문에 JAVA 를 만든 개발자들은 이 패턴을 보다 적극적으로 활용할 수 있도록, JDK1.0 부터 모듈을 배포하였습니다.


오늘 포스팅에서는 지난 포스팅에서 만든 예제를 자바의 Observer 패턴 관련 클래스를 이용하여 리팩토링해보려 합니다. 대상 프로젝트는 "[CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용2" 하단에서 다운로드 할 수 있습니다.


일단, 자바에서 제공하는 내장 모듈인 Observable 과 Observer 부터 소개를 해보겠습니다.



1. 자바 내장 모듈 [Observable 과 Observer]


앞써, 알아봤던 Observer 패턴에서는 크게 두 개의 컴포넌트가 있음을 확인했습니다.

바로 주제(Subject)와 관찰자(Observer) 입니다. 기억이 나시나요?


자바 내장 모듈은 이 [두 개의 컴포넌트]들을 쉽게 제작할 수 있도록 지원하고 있습니다.


- Observable [java.util.Observable]


주제(Subject)에 해당하는 컴포넌트를 만들 수 있도록 지원하는 클래스입니다. 

변화를 통보할 대상인 [Observer 인터페이스]목록을 Thread-safe 하게 관리하며, 통보할 수 있는 메소드를 지원하고 있습니다.


통보하는 메소드는 물론 PUSH, PULL 방식 모두 지원하도록 설계되어 있습니다.


1
2
3
4
5
// PULL 방식 notify. (아무것도 보내지 않는 방식)
observable.notifyObservers();
        
// PUSH 방식 notify. (아쉽게도, 통보할 때 보내는 데이터 타입은 한 개로 제한..)
observable.notifyObservers(memberList);
cs


편리해보이지만, 주의해볼만한 사항은 Observable 이 class 라는 점입니다.

Observer 인터페이스들을 관리해야하는 로직을 담아야하기 때문에 interface 로 제작할 수는 없었던 듯 합니다.


- Observer [java.util.Observer]


관찰자(Observer)에 해당하는 컴포넌트를 만들 수 있도록 지원하는 인터페이스입니다.

이 전 예제의 IObserver 를 대체할 수 있겠군요.


Observer 의 내부를 보면, 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Observer {
    /**
     * This method is called whenever the observed object is changed. An
     * application calls an <tt>Observable</tt> object's
     * <code>notifyObservers</code> method to have all the object's
     * observers notified of the change.
     *
     * @param   o     the observable object.
     * @param   arg   an argument passed to the <code>notifyObservers</code>
     *                 method.
     */
    void update(Observable o, Object arg);
}
cs


파라미터로는 [주제에 해당하는 Observable o] 과 [Push 방식을 통해 전달되는 데이터인 Object args] 이 존재 합니다. 


[Object args]의 주석을 보면, Observable::notifyObservers 를 통해 넘겨지는 데이터가 온다고 써있습니다. 즉, PUSH 방식으로 통보를 하면 통보 시 전달한 데이터가 넘어오고, PULL 방식을 사용하면 NULL 임을 짐작할 수 있을 것 같습니다.



쉽게 사용할 수 있도록 제작된 모듈이니, 이 정도면 한 번 모듈을 사용해서 Observer 패턴을 구현해 볼 수 있을 것 같군요. 


한번, 지난 포스팅에서 작성한 예제를 바탕으로 리팩토링을 한번 해볼까요? 



2. 내장 모듈을 사용한 리팩토링


이미 우리는 Observer 패턴을 구현하였기 때문에 JDK 의 내장모듈을 쉽게 부착할 수 있을 지도 모릅니다. 


먼저 우리가 작성했던 주제(Subject) 컴포넌트인 DataManager 부터 손을 보도록 하죠.


DataManager 클래스는 Observable 클래스를 상속받을 예정이며, 이를 통해 Observer 를 관리하던 메소드 및 멤버변수도 삭제할 것입니다. 


저는 아래와 같이 만들어 봤습니다. (정확히 말하면, 기능 삭제만 했습니다. @.@)


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
 * 메신저에서 사용하는 데이터를 총괄적으로 관리하는 데이터 매니저.
 *
 * <pre>
 *     Observable 를 상속!
 *     그러나, 추 후 DataManager 가 상속받아야 하는 클래스가 있으면 문제 발생.
 *     (JAVA 는 다중상속을 할 수 없음..)
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 16..
 */
public class DataManager extends Observable{
 
    // 통보대상 집합 (삭제. Observable 클래스가 지원)
    // private HashSet<IObserver> iObserverSet = new HashSet<>();
 
    /**
     * 옵저버 추가
     *
     * <pre>
     *     Deprecated 사유 : Observable::addObserver 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     * @param observer
     */
    @Deprecated
    public void registerObserver(IObserver observer) {
      //  iObserverSet.add(observer);
    }
 
    /**
     * 옵저버 삭제
     *
     * <pre>
     *     Deprecated 사유 : Observable::deleteObserver 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     * @param observer
     */
    @Deprecated
    public void deleteObserver(IObserver observer) {
     //   iObserverSet.remove(observer);
    }
 
    /**
     * 각 옵저버에게 현재 상태 데이터를 넘기면서, 변경사실을 알린다.
     *
     * <pre>
     *     Deprecated 사유 : Observable::notifyObservers 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     */
    @Deprecated
    public void notifyObserver() {
     /*   for (IObserver observer : iObserverSet) {
           observer.update(this);
        }*/
    }
 
   /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        // something work
 
        // 변화가 일어났음을 알림. (이 메소드 호출을 통해 자주 통보하는 것을 막을 수 있음)
        setChanged();
 
        // Observable::notifyObservers 기능 사용. PULL 방식
        notifyObservers();
    }
}
cs


Observer 를 관리하는 서명들은 위에서 모두 주석 혹은 사장(Deprecated) 처리가 되었지만, 최종적으로 삭제할 것입니다. 


Observable 을 상속받음으로써, DataManager 는 서버로부터 전달된 데이터만 업데이트 하고,  Observable::setChanged와 Observable::notifyObservers 만 사용해주면 되는군요. 


여기서, 주목할 것은 Observable::setChanged로 상태 변화가 일어났음을 나타내는 메소드 입니다. 

이 메소드를 통해 너무 자주 통보되는 것을 막을 수 있으며, 이를 호출하지 않고 Observable::notifyObservers 만 호출하면 아무 일도 일어나지 않습니다. 

적절한 비지니스 로직으로 Observable::setChanged 를 제어할 수 있을 것 입니다.


즉 구현에 있어서 Observer 관련 내용을 신경쓰지 않아도 됩니다.



이제 관찰자(Observer) 컴포넌트를 수정할 차례입니다. 


우리는 이전에 IObserver 를 구현하는 데모 View 컴포넌트를 만든 적이 있습니다. 

이제는 Observer 를 구현하도록 수정하고, IObserver 와 작별을 할 것입니다. :-)


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
public class DemoViewComponent_V1 implements Observer {
 
    /**
     * 화면 갱신
     *
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
        if (instanceof DataManager) {
            final DataManager dataManager = (DataManager) o;
 
            // 구성원 정보 업데이트.
            {
                // 유연한 구성원 데이터 조회
                final List<MemberVo> memberVoList = dataManager.getMemberList();
                viewUpdateByMember(memberVoList);
            }
 
            // 전자결재 정보 업데이트.
            {
                // 유연한 전자결재 데이터 조회
                final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
                viewUpdateByApprovalDoc(approvalDocList);
            }
        }
    }
}
cs


일단, DataManager 의 서명이 변경되었으니 테스트 코드 쪽에서도 불이 났을 것입니다. 

테스트 코드 역시 조금 수정을 할 필요가 있겠군요. 


새 메소드 서명에 따라 다음과 같이 변경할 수 있어 보입니다.


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
// 데이터 매니저 생성
DataManager dataManager = new DataManager();
 
// 데모 View 컴포넌트 생성.
DemoViewComponent_V1 demoViewComponent_v1 = new DemoViewComponent_V1();
DemoViewComponent_V2 demoViewComponent_v2 = new DemoViewComponent_V2();
 
// 각 컴포넌트들이 Data 변경알림에 대해 구독하겠다고 등록.
dataManager.addObserver(demoViewComponent_v1);
dataManager.addObserver(demoViewComponent_v2);
 
// something work
dataManager.loadByServer(
        Arrays.asList(new MemberVo("강현지"), new MemberVo("유덕형"), new MemberVo("유형주"))
        , Arrays.asList(new ApprovalDoc("통과 - 스터디 신청"))
        , Arrays.asList(new MessageVo("모두, 스터디 참여 잘해줘서 감사합니다."), new MessageVo("그동안 수고하셨습니다.")));
 
 
// CONSOLE LOG
// 데모2 메시지 View 화면 갱신 중..
// [모두, 스터디 참여 잘해줘서 감사합니다.] 갱신 중
// [그동안 수고하셨습니다.] 갱신 중
// 데모2 메시지 View 화면 갱신 완료!!
// 
// 데모1 구성원 View 화면 갱신 중..
// [강현지] 갱신 중
// [유덕형] 갱신 중
// [유형주] 갱신 중
// 데모1 구성원 View 화면 갱신 완료!!
// 
// 데모1 전자결재 View 화면 갱신 중..
// [통과 - 스터디 신청] 갱신 중
// 데모1 전자결재 View 화면 갱신 완료!!
cs


다행히 DataManager 의 변화에 따라 View 컴포넌트들이 적절한 행위를 잘 해주는 군요. :-)



3. 자바 내장 모듈의 한계와 극복


Observable 과 Observer 를 사용하면 편리하게 Observer 패턴을 사용할 수 있음을 확인하였습니다. 

하지만, 이 내장 모듈에는 한계가 존재합니다. Observable 이 클래스라는 것이죠. 


Observable 을 개발한 개발자의 의도는 Observable 을 상속받길 원했겠지만, 아쉽게도 DataManager 에 상속을 해야할 일이 있다고 가정해봅시다.


Java 에서는 다중상속을 지원하지 않으니, Observable 을 상속하는 것을 포기해야 합니다.

또한 상태의 변화가 있다고 체크하는 Observable::setChanged 는 protected 입니다. 상속이 힘들다고, DataManager 에 Observable 인스턴스를 만들어 관리한다 하더라도 Observer 들에게 통보를 할 수 없습니다. 


이는, 설계원칙 Favor has-a over is-a (상속해야하는 것을 has-a 로 바꿀 수 있어야 한다.)에 위배됩니다. 

안타깝군요. ㅜㅡㅜ


하지만 조금 돌아가면 방법이 없지는 않습니다.

Observable 을 상속한 다른 클래스(이를테면, MessengerObservable) 을 만들고, DataManager 가 MessengerObservable 을 has-a 관계로 취할 수 있을 것 같습니다.


저는 이렇게 작성을 해보았습니다.


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
 * 메신저에서 사용할 Observable 클래스
 *
 * <pre>
 *     DataManager 에 직접 상속을 할 수 없기 때문에, 해당 클래스를 제작.
 *     DataManager 가 Observable 을 사용하기 위해서는 이 모듈을 사용해야함.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 18..
 */
public class MessengerObservable extends Observable{
 
    /**
     * Observer 에게 통보
     *
     * <pre>
     *     - setChanged 메소드는 해당 클래스에서 제어.
     *     - Observer 들이 DataManager 에 접근할 수 있도록 PUSH 방식 Observer 패턴을 사용.
     * </pre>
     */
    @Override
    public void notifyObservers(Object dataManager) {
        this.setChanged();
 
        // PUSH 방식 Observer 패턴 사용을 통해 DataManager 를 넘김.
        super.notifyObservers(dataManager);
    }
 
}
 
/**
 * 메신저에서 사용하는 데이터를 총괄적으로 관리하는 데이터 매니저.
 *
 * <pre>
 *     Observable 를 상속불가 우회
 *     - MessengerObservable 를 has-a 관계로 처리.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 16..
 */
public class DataManager extends SomethingSuperObject{
 
    private MessengerObservable messengerObservable = new MessengerObservable();
 
    /**
     * 추가 메소드 지원.
     *
     * <pre>
     *     - MessengerObservable 에 Observer 추가.
     * </pre>
     *
     * @param observer
     */
    public void addObserver(Observer observer) {
        messengerObservable.addObserver(observer);
    }
 
    /**
     * 삭제 메소드 지원.
     *
     * <pre>
     *     - MessengerObservable 에서 Observer 삭제.
     * </pre>
     *
     * @param observer
     */
    public void deleteObserver(Observer observer) {
        messengerObservable.deleteObserver(observer);
    }
 
    /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        // something work!!
 
        // PUSH 방식으로 우회.
        messengerObservable.notifyObservers(this);
    }
}
 
/**
 * 데모 View 갱신 컴포넌트 (V1)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V1 implements Observer {
 
    /**
     * 화면 갱신
     *
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
 
        if (arg instanceof DataManager) {
            final DataManager dataManager = (DataManager) arg;
 
            // 구성원 정보 업데이트.
            {
                // 유연한 구성원 데이터 조회
                final List<MemberVo> memberVoList = dataManager.getMemberList();
                viewUpdateByMember(memberVoList);
            }
 
            // 전자결재 정보 업데이트.
            {
                // 유연한 전자결재 데이터 조회
                final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
                viewUpdateByApprovalDoc(approvalDocList);
            }
        }
    }
}
 
cs


주목할 점은 MessengerObservable 이 PUSH 방식으로 DataManager 를 전달하기 때문에, Observer 구현체들은 두번째 파라미터인 Object arg 를 통해 DataManager 를 찾아야 합니다.


드디어, Observer 패턴과 관련된 모든 내용이 끝이 났고, 동시에 이번 스터디에서 진행예정이던 모든 패턴이 끝이 났습니다. (ㅜㅡㅜ)


이 포스팅의 결과물 프로젝트는 아래에서 참고! 



STUDY_OOP7.zip



마지막 패턴이야기라 그런지 유종의 미를 잘 거두고 싶었습니다. 

그래서 더욱 자세히 쓰고자 이 패턴에 대해 세 개의 포스팅으로 나누어 작성 했었던 것 같습니다.


주 독자와 나누는 마지막 패턴 이야기라 조금 섭섭하기도 하기도 하고, 더 알려줄 것이 없나 고민도 하게 되었던 것 같습니다. 그래서 빨리 끝낼 수 있는 걸 질질 끌었던 것 같기도.....


특히 최근에 마지막 스터디를 진행하기 전 날, [대학교 1학년]-[SW멤버십 회원]-[M사 신입사원]-[스터디 진행]까지의 있었던 일들이 주마등처럼 지나갔었습니다. 


[문과출신 공대생]이라는 핸디캡 극복을 위해 노력했었고, [4학년 동기들 중 2학년이었던 SW멤버십]에서 끝까지 살아남기 위해 발버둥을 쳤으며, [M사 신입사원] 시절 사내에서 아무도 하지 않았기 때문에 누군가의 도움도 기대할 수 없는 솔루션급 앱들을 밤새 만들고 고치며 끝내 좌절하던 경험들은 저에게 뼈아픈 교훈들만 주었던 것 같습니다.


최근에 읽었던 칼럼 중 [지금의 실력은 지식의 총합이 아닌 고통의 총합] 이라는 글은 이 때의 일을 주마등처럼 지나가게 한 촉매가 아니었나 생각이 듭니다. 


하지만, 이런 뼈아픈 교훈들은 안타깝게도 급격한 실력의 변화를 주지는 못하던 것 같습니다. 


최근에 읽었던 [소프트웨어 장인], [구글 리쿠르팅 데이의 타 회사 세션] 등의 내용을 참고하면, 앞에서 끌어주는 [메이저 주니어 이상급의 개발자]들의 활동은 무시할 수 없어보였습니다. 이를테면 페어 프로그래밍이나 코드 리뷰, 본질에 가까운 OOP&디자인 패턴 교육 등은 주니어들의 레벨을 올리는 것에 크게 기여하는 것 같아 보였습니다.


결론은 모두가 뼈아픈 교훈을 얻어갈 필요는 없는 것 같습니다. :-)

이 블로그의 글들은 대단하지 않으며 꽃길로 인도할 수도 없지만, 조금이나마 진흙을 덜 밟는데 도움이 되길 바랍니다.


감사합니다.










반응형
Posted by N'

지난 포스팅에서는 특정 주제의 변화에 따라 여러 클래스가 어떤 행위를 해야할 때, 즉 이벤트 처리를 하기 위한 대표적인 패턴인 Observer 패턴에 대해 알아보았습니다.


간단한 예제 프로그램도 한 번 작성해보았죠? :-)


해당 내용과 관련된 정보는 아래에서 참고! 



이번 포스팅에서는 지난 스터디에서 작성한 코드에 대한 약간의 리팩토링을 담을 예정입니다.


조금 더 유연한 구조를 한번 제시해 볼 생각이며, 작업 대상이 되는 프로젝트는 "실무 활용 패턴 (하) [Observer 패턴] + 추가내용" 의 하단에서 다운로드 하실 수 있습니다.


일단, 고민이 되는 이슈부터 기술을 해보도록 하죵...



1. IObserver 인터페이스의 메소드 서명은 적절한가?


구체화 된 View 컴포넌트들에게 DataManager 의 상태 변화를 알리고자, DIP 원리에 따라 추상적인 개념인 IObserver 를 만들었습니다.


아래와 같이 말이죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 옵저버 인터페이스 제작
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public interface IObserver {
 
    /**
     * 주체의 데이터가 변경될 때, 이 메소드를 실행
     *
     * <pre>
     *     - 업데이트 이벤트를 이 메소드를 통해 받을 수 있음.
     *     - 변경된 데이터가 파라미터로 넘어오는 것을 보장. 이 방식 PUSH 방식 옵저버 패턴이라고 함.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    void update(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList);
}
cs


DataManager 는 통보할 대상을 IObserver 로 관리함으로써 각 ViewComponent 들과의 직접적인 의존관계를 끊을 수 있었고, ViewComponent 들 역시 IObserver 를 구현함으로써 DataManager 에게 쉽게 구독여부를 등록할 수 있었습니다.


하지만, 이런 고민을 해볼 수 있습니다.


- 구체화된 View 컴포넌트들은 사실 모든 정보가 필요하지 않습니다. 

  

테스트로 구현해본 [DemoViewComponent_V1 은 구성원정보와 전자결재 정보]만 필요했습니다. 또한 [DemoViewComponent_V2 는 메시지 정보]만 필요했죠..


- 데이터 형식 추가에 취약해보입니다.


현재 배포할 버전에서는 구성원, 전자결재, 메시지 정보만 필요하지만, 추 후 근태정보, 평가정보 등 다른 정보가 추가되어야 한다면 IObserver::update 서명을 수정해야합니다.


인터페이스 서명의 변경은 구현된 모든 클래스들도 변경해야 함을 의미합니다.


결국, DataManager 의 통보를 하기 위한 방식에 조금 문제가 있어보이네요.

큰 구조에서 봤을 때 Subject-Observer 의 관계는 잘 나누었지만, 디테일한 부분을 조금 더 리팩토링해주면 좋을 것 같습니다.


다행히, 아직 View 컴포넌트들이 많이 생성되지 않았으니 구조를 조금 변경해볼까요? :-)



2. 통보방식의 변경


현재 IObserver:update 의 메소드 서명을 보면, 상태의 변화사실과 함께 변경된 데이터를 같이 전달하는 방식입니다. 지난 시간에 이 방식을 PUSH 방식의 Observer 패턴이라고 설명을 했었습니다.


하지만 이 방식은 결국 전달할 데이터들을 메소드 서명에 명시해야하며, 이는 전달할 데이터 형식에 유연할 수 없음을 의미합니다. 유연할 수 없는 데이터 형식 때문에 데이터 형식의 추가/삭제가 힘들고, 모든 관찰자(Observer)는 원하지도 않는 데이터들을 강제로 받아야 하죠.


그렇다면, 관찰자들이 조금은 능동적으로 본인이 원하는 정보만 취할 수 있다면 어떨까요?

주제(Subject)는 관찰자들에게 변경사실만 알리고, 관찰자들이 알아서 주제로부터 상태를 조회하는 방식이죠.


인터페이스를 다음과 같이 변경해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 옵저버 인터페이스 제작
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public interface IObserver {
 
    /**
     * 주체의 데이터가 변경될 때, 이 메소드를 실행
     *
     * <pre>
     *     - 업데이트 이벤트를 이 메소드를 통해 받을 수 있음.
     *     - 이 메소드에서는 직접적으로 변경된 데이터를 보내지 않음.
     *     - Subject 인스턴스를 전달하여, 관찰자들이 능동적으로 원하는 방식을 취하는 방식.
     *     - 이 방식 Pull 방식 옵저버 패턴이라고 함.
     * </pre>
     *
     * @param dataManager
     */
    void update(DataManager dataManager);
}
cs


변경된 IObserver::update 는 통보와 함께 변경된 데이터들을 전달하는 것이 아닌, 주제 클래스를 전달하고 있습니다. 


각 IObserver 들의 구현체들은 변경을 통보 받을 시, 전달받은 주제(DataManager) 클래스로부터 원하는 데이터를 유연하게 조회할 수 있을 것입니다. 

이와 같이, 주제는 변경에 대한 통보만 하고 데이터는 관찰자들이 주제로부터 직접적으로 데이터를 조회하는 방식 PULL 방식 옵저버 패턴이라고 합니다.


구성원 데이터와 전자결재 데이터가 필요했던, DemoViewComponent_V1 을 저는 다음과 같이 수정해 보았습니다.


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
57
58
59
60
61
62
63
64
/**
 * 데모 View 갱신 컴포넌트 (V1)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V1 implements IObserver {
 
    /**
     * 화면 갱신.
     *
     * @param dataManager
     */
    @Override
    public void update(DataManager dataManager) {
        // 구성원 정보 업데이트.
        {
            // 유연한 구성원 데이터 조회
            final List<MemberVo> memberVoList = dataManager.getMemberList();
            viewUpdateByMember(memberVoList);
        }
 
        // 전자결재 정보 업데이트.
        {
            // 유연한 전자결재 데이터 조회
            final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
            viewUpdateByApprovalDoc(approvalDocList);
        }
    }
 
    /**
     * 구성원 화면 업데이트
     *
     * @param memberVoList
     */
    public void viewUpdateByMember(List<MemberVo> memberVoList) {
        System.out.println("데모1 구성원 View 화면 갱신 중..");
 
        for (MemberVo memberVo : memberVoList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getName());
        }
 
        System.out.println("데모1 구성원 View 화면 갱신 완료!!\n");
    }
 
    /**
     * 전자결재 화면 업데이트
     *
     * @param approvalDocList
     */
    public void viewUpdateByApprovalDoc(List<ApprovalDoc> approvalDocList) {
        System.out.println("데모1 전자결재 View 화면 갱신 중..");
 
        for (ApprovalDoc memberVo : approvalDocList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getTitle());
        }
 
        System.out.println("데모1 전자결재 View 화면 갱신 완료!!\n");
    }
}
 
cs


PULL 방식 옵저버 패턴을 통해, 각 View 컴포넌트들은 원하는 데이터들만 주제로부터 유연하게 조회할 수 있겠군요. 


또한 데이터 형식이 추가된다 하더라도 IObserver 인터페이스를 수정할 필요가 없습니다. 

더이상 IObserver:update 에서 데이터 형식에 대한 서명을 명시할 필요가 없기 때문이죠.


이와 같이 상태변화를 통보하는 방식에 따라 PUSH 와 PULL 방식이 존재합니다. 

실전에서 이 패턴을 쓰기 전에, 이 두 방식에 대해 조금 더 Zoom-In 해서 볼 필요가 있을 것 같네요.



3. PUSH vs PULL


앞써, 기술한 것과 같이 상태변화 통보 방식에 따라 Observer 패턴을 [PUSH 과 PULL], 두 방식으로 크게 나눌 수 있습니다. 


두 방식에는 다음과 같은 특징이 존재합니다.


- PUSH


주제가 상태변화를 통보를 할 때, 관찰자에게 변화된 정보까지 제공하는 방식입니다.


관찰자들은 주제가 어떤 녀석인지 알 필요도 알고 싶지도 않습니다. 

단지, 무언가에 의해 통보사실과 함께 관심있는 데이터를 이용해 원하는 행위를 취하면 되죠.

소프트웨어 공학적으로 말하면, 모듈 간의 결합도(Coupling)를 낮출 수 있습니다.

좋은 프로그램은 모듈 간 낮은 결합도와 높은 응집성을 갖는 것이 이상적 입니다.


하지만, 통보받은 새 정보 중에는 알고 싶지 않은 정보도 포함되어 있습니다. 

또한 정보타입의 추가에 유연할 수가 없습니다.


- PULL


주제는 단지 상태 변화만을 통보하며, 변화된 정보는 관찰자가 주제로 부터 별도로 정보를 요구하는 방식입니다.


관찰자 입장에서는 원하는 정보를 유연하고 편리하게 요구할 수 있습니다.

또한, 정보 타입 추가에도 별도로 관찰자들을 수정을 하지 않아도 됩니다. (OCP 하군요..)


하지만, 관찰자들은 주제로 부터 정보를 별도로 요구해야 하기 때문에 주제에 대해 많은 것을 알아야 합니다. 즉 모듈 간 결합도가 증가했습니다. (tight-Coupling)


극단적인 경우, 동시성 처리를 할 시 제공받는 정보의 질이 다를 수 있습니다. 

[상태 변화 당시의 정보]와 [별도로 요구할 때의 정보]가 Thread-safe 하지 않을 수 있습니다. 


보통 권장하는 스타일은 변화에 보다 유연한 PULL 방식의 옵저버 패턴이지만, 실전에서 필요한 요구사항에 따라 적절한 방식을 선택해야 겠죠? 



오늘 포스팅에서는 추가로 제안되는 PULL 방식의 Observer 패턴을 다뤄 보았습니다.


변경된 소스는 아래 프로젝트 파일을 참고!


STUDY_OOP7.zip


Observer 패턴 자체도 많이 사용하기 때문에 중요한 패턴이지만, PUSH vs PULL 의 개념 등은 패턴 외에도 큰 모듈들 간의 관계 자체에서도 흔하게 등장하기 때문에 더욱 소개하고 싶었습니다.


또한, 지난 포스팅에서 다룬 이벤트 감지 방법인 Polling vs Interrupt 개념도 중요합니당..


흔히, 사용하는 Source-Tree 도 [프로젝트 상태 변경 사실을 표시]하고, 우리가 [PULL 을 받도록] 하죠? 프로젝트 상태 변경 사실은 [주기적으로 체크하는 Polling 방식]을 사용합니다.


이렇게 우리가 흔히 사용하는 응용 프로그램에서도 이 개념들을 많이 찾아볼 수 있습니다.


그래서 이번 주제는 단지, 디자인 패턴 뿐 아니라 소프트웨어 공학적인 여러 개념까지 다뤄 볼 수 있는 기회 였던 것 같아 뿌듯하네요.. ㅎㅎ


이 글을 보는 많은 분들에게 도움이 되었으면 좋겠습니다.:-)


반응형
Posted by N'

이번 스터디에서는 마지막으로 다룰 패턴은 상태의 변화를 관찰하는 관찰자(Observer)를 등록하여 특정 주제(Subject)의 변화에 따라 관찰자들이 어떤 행위를 할 수 있도록 관리할 수 있는 Observer 패턴에 대해 알아 봤습니다.


수많은 응용 프로그램에서는 어떤 변화, 즉 이벤트 기반의 처리를 상당히 많이 다루며, 그렇기 때문에 Observer 패턴의 중요성과 사용성에 대해 관심을 가져볼 필요가 있다고 생각하여 마지막 주제로 정하게 되었습니다.


해당 포스팅과 관련된 정보는 아래에서 참고 



Observer 패턴에 대해 알아보기 전에 소프트웨어 공학에서 말하는 이벤트와 이를 다루는 방식에 대해 알아볼 필요가 있습니다. 깊게 토끼굴로 들어가지 말고, 간단히 알아보록 하죠. :-)



1. 이벤트와 처리방식.


이벤트(Event)란 어떤 상태의 변화를 말할 수 있을 것 같습니다.


예를들어 웹 사이트의 버튼을 누르는 행위나 스크롤을 내리는 등의 사용자가 UI(User Interface)를 이용하는 것 혹은 그 외의 날짜의 변경, 어떤 임계 수치의 도달상태의 변화를 이벤트라고 할 수 있으며, 상태 변화에 따라 어떤 행위를 수행하도록 하는 것을 이벤트 처리라고 할 수 있을 것 같습니다.


이런 상태변화를 감지하여, 어떤 행위를 하는 것은 매우 중요합니다. 

하지만 그 전에 이벤트를 감지하는 것이 먼저 선행되어야 할 것 같군요. 


소프트웨어 공학에서는 크게 두 가지 방식으로 상태 변화를 감지하는 법을 말합니다.



- 폴링(Polling) 방식


어떤 요구하는 상태가 되었는지를 주기적으로 감지하는 방식입니다. 


즉 CPU 는 일정 시간마다(이를 테면, 하루정도) 주기적으로 데이터를 가진 주체의 상태를 체크하여 행위를 수행합니다. 


예를들면, [SMS 를 보내는 메시지 스케줄러]는 30초마다 보내야할 메시지가 존재하는지를 관리 Model 로부터 확인하며, 메시지가 존재하면 일괄적으로 보냅니다.


하지만, 이 방식은 CPU 가 이벤트 감지에 대해서 주기적으로 체크해야하는 비용이 존재하며 어떤 이벤트가 발생했을 때 즉시 대응할 수 없겠죠?



- 인터럽트(interrupt) 방식


폴링 방식은 관찰자가 능동적으로 특정 주기를 가지고 상태를 체크하는 반면, 인터럽트 방식은 관찰자가 수동적으로 통보만 받는 방식입니다. 


주체(Subject)는 어떤 상태가 되었을 때 관찰자들에게 상태 변화를 통보하며, 관찰자들은 받은 통보에 따라 어떤 행위를 수행합니다.


이 방식은 CPU 가 이벤트를 감시해야하는 비용이 없으며, 어떤 변화에 대해서 즉시 대응도 가능합니다. 



Observer 패턴은 각 컴포넌트(class 혹은 특정 모듈 정도라고 생각...)들의 관계들을 이러한 인터럽트 방식으로 관리할 수 있도록 제시하는 패턴입니다. 


이번 리뷰에서는 주어진 특정 요구사항에 대하여 이벤트를 처리하는 과정을 통해 Observer 패턴을 정리해볼까 합니다.



2. 메신저의 데이터 변화에 따른 UI 변경 문제.


[욕심 많은 기획자]가 이번에는 사내메신저에 대한 기획서를 들고왔습니다.


이번에 베포할 사내 메신저 버전에는 구성원정보, 메시지 히스토리, 전자결재 목록 등이 있으며, 초보 개발자인 당신은 서버에서 전달받은 이 데이터들이 변화할 때마다 화면을 갱신 하라는 이슈를 받게 되었습니다.


갱신할 화면은 정해지지 않았고, 아직 개발도 되지 않았습니다. (하하하하.....)


하지만, 그동안 [변화에 유연했던 당신]은 좋은 구조를 만들 수 있을 것이라 생각합니다.


요구사항을 받았으니 한번, 따라가며 구현을 해봅시다. :-)



3. 데이터를 관리하는 주체(Subject) 클래스의 제작.


첫 번째로 고려할 사항은 데이터의 관리 문제입니다. 


데이터를 각 화면(View)에 해당하는 컴포넌트 마다 가지고 있다면, 서버로 부터의 데이터 갱신은 쉽지 않을 것입니다. 데이터 자체는 한 곳에서 관리하여 각 화면이 이 곳을 참고한다면, 서버에서 데이터 업데이트 문제는 Simple 해질 것 같군요.


데이터를 관리하는 클래스를 저는 이렇게 만들어 봤습니다.


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
/**
 * 메신저에서 사용하는 데이터를 총괄적으로 관리하는 데이터 매니저.
 *
 * Created by Doohyun on 2017. 6. 16..
 */
public class DataManager {
 
    // 구성원 정보 목록
    private ArrayList<MemberVo> memberList = new ArrayList<>();
 
    // 전자결재 문서 목록
    private ArrayList<ApprovalDoc> approvalDocList = new ArrayList<>();
 
    // 메시지 목록
    private ArrayList<MessageVo> messageVoList = new ArrayList<>();
 
    /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
 
        // 구성원 데이터 업데이트
        {
            this.memberList.clear();
            this.memberList.addAll(memberVoList);
        }
 
        // 전자결재 데이터 업데이트
        {
            this.approvalDocList.clear();
            this.approvalDocList.addAll(approvalDocList);
        }
 
        // 메시지 업데이트
        {
            this.messageVoList.clear();
            this.messageVoList.addAll(messageVoList);
        }
    }
}
cs


DataManager 는 [구성원목록, 전자결재목록, 메시지 목록] 등 실제 데이터를 가지고 있으며, 서버로부터 데이터를 갱신하는 DataManager::loadByServer 메소드를 지원합니다.


모든 View 컴포넌트들은 이 곳에서 데이터를 조회하여 화면을 그릴 수 있겠군요. :-)



4. 데이터 변화에 대한 화면 갱신 처리.


이번 포스팅의 주제인 데이터 변화에 대한 화면 갱신을 할 때가 왔습니다. 


일단 데이터의 변화시점을 주목 해봅시다.


DataManager::loadByServer 가 실행이 되었다면, 서버로부터 새로운 데이터를 받았다고 할 수 있습니다. 즉, 이 메소드가 실행될 때 각 화면 컴포넌트들에게 어떤 액션을 취한다면 변화를 통보할 수 있고, 화면갱신을 할 수 있을 것 입니다.


하지만, 현재 단계에서는 아쉽게도 View 컴포넌트들이 모두 제작되지 않았습니다.

각 View 컴포넌트에 무슨 데이터가 화면 갱신에 필요한지도 모르며, 심지어 컴포넌트 개수 조차 알지 못합니다. 즉, DataManager 는 이벤트 발생을 알릴 대상을 관리하기에 애매한 상황입니다.


하지만, 우리는 이런 모호한 상황에서 어떻게 해야할지 알고 있습니다.


한번 지금의 문제를 생각해 볼까요?


1) DataManager 는 각 View 컴포넌트에 의존하고 있습니다. 


이벤트에 대해 알림을 해야하기 때문에, View 컴포넌트를 사용해야 합니다.


DataManager 는 데이터를 가진 주체(상위모듈)이고 이를 기반으로 View 컴포넌트들의 화면을 갱신(하위모듈)을 하는 구조라고 했을 때, 상위모듈이 하위모듈에 의존하고 있는 구조입니다.


2) 각 View 컴포넌트들은 구체적입니다.


View 컴포넌트들은 이미 구현이 완료된 구체화 클래입니다. 


우리는 지금의 상황에서 프로그램 설계원칙 중 DIP(의존성 역전 원칙)을 고려해볼 수 있습니다.


["상위모듈이 하위모듈에 의존하면 안된다. 상위모듈, 하위모듈 모두 추상에 의존해야 한다."]


기억 나시나요? 이 원칙에 따라 다음과 같은 UML 을 고려해 보겠습니다. :-)

 

 


UML 에서는 Observer(관찰자)라는 추상개념을 만들었습니다. 


그리고, 상위모듈인 DataManager 와 각 View Component 들이 모두 이 인터페이스에 의존하는 것을 표현하고 있습니다.


이 UML 에 따라 저는 아래와 같이 Observer 인터페이스를 제작하고, DataManager 에 통보대상에 대한 관리 기능을 추가했습니다.


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/**
 * 옵저버 인터페이스 제작
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public interface IObserver {
 
    /**
     * 주체의 데이터가 변경될 때, 이 메소드를 실행
     *
     * <pre>
     *     - 업데이트 이벤트를 이 메소드를 통해 받을 수 있음.
     *     - 변경된 데이터가 파라미터로 넘어오는 것을 보장. 이 방식을 PUSH 방식 옵저버 패턴이라고 함.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    void update(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList);
}
 
public class DataManager{
 
     // 통보대상 집합
    private HashSet<IObserver> iObserverSet = new HashSet<>();
 
    /**
     * 옵저버 추가
     *
     * @param observer
     */
    public void registerObserver(IObserver observer) {
        iObserverSet.add(observer);
    }
 
    /**
     * 옵저버 삭제.
     *
     * @param observer
     */
    public void deleteObserver(IObserver observer) {
        iObserverSet.remove(observer);
    }
 
    /**
     * 각 옵저버에게 현재 상태 데이터를 넘기면서, 변경사실을 알린다.
     */
    public void notifyObserver() {
        for (IObserver observer : iObserverSet) {
            observer.update(memberList, approvalDocList, messageVoList);
        }
    }
 
    /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        //// something work
        notifyObserver();
    }
}
cs


DataManager 의 입장에서는 추상개념인 IObserver 에 의존을 함으로써, 통보대상 관리를 위해 View 컴포넌트들을 알 필요성이 없어졌습니다. 


단순히 관찰자 컬렉션을 관리하면서 데이터 변경시점인 DataManager::loadByServer 가 실행될 때, DataManager::notifyObserver 도 같이 실행하여 관찰자들에게 변경사실을 통보해주면 됩니다. 이 때, 통보와 같이 업데이트할 데이터를 같이 넘겨주는 방식을 PUSH 방식이라고 합니다.


View 컴포넌트들의 입장에서는 IObserver 만 구현하고 DataManager 에게 변경사실에 대한 알람을 구독하겠다고 해주면(DataManager::registerObserver), 데이터 변화에 대한 실시간 화면갱신을 할 수 있습니다.


데모 테스트를 위해, 아래와 같이 정보를 이용해서 화면을 갱신을 하는 구체화된 View 컴포넌트 데모 클래스를 만들었습니다. 


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * 데모 View 갱신 컴포넌트 (V1)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V1 implements IObserver{
 
    /**
     * 화면 갱신
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    @Override
    public void update(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        viewUpdateByMember(memberVoList);
        viewUpdateByApprovalDoc(approvalDocList);
    }
 
    /**
     * 구성원 화면 업데이트
     *
     * @param memberVoList
     */
    public void viewUpdateByMember(List<MemberVo> memberVoList) {
        System.out.println("데모1 구성원 View 화면 갱신 중..");
 
        for (MemberVo memberVo : memberVoList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getName());
        }
 
        System.out.println("데모1 구성원 View 화면 갱신 완료!!\n");
    }
 
    /**
     * 전자결재 화면 업데이트
     *
     * @param approvalDocList
     */
    public void viewUpdateByApprovalDoc(List<ApprovalDoc> approvalDocList) {
        System.out.println("데모1 전자결재 View 화면 갱신 중..");
 
        for (ApprovalDoc memberVo : approvalDocList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getTitle());
        }
 
        System.out.println("데모1 전자결재 View 화면 갱신 완료!!\n");
    }
}
 
/**
 * 데모 View 갱신 컴포넌트 (V2)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V2 implements IObserver{
 
    /**
     * 메시지 업데이트
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    @Override
    public void update(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        viewUpdateByMessage(messageVoList);
    }
 
    /**
     * 메세지 화면 업데이트
     *
     * @param messageVoList
     */
    public void viewUpdateByMessage(List<MessageVo> messageVoList) {
        System.out.println("데모2 메시지 View 화면 갱신 중..");
 
        for (MessageVo memberVo : messageVoList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getMessage());
        }
 
        System.out.println("데모2 메시지 View 화면 갱신 완료!!\n");
    }
}
cs


필요한 기능은 모두 제작 되었으니, 제가 만든 코드가 잘 동작하나 테스트 코드를 작성해보고 퇴근하면 될 것 같군요. 


테스트 코드를 만드는 것은 매우 중요합니다. :-)


아래와 같은 테스트 코드를 간단하게 만들고자 합니다.


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
// 데이터 매니저 생성
DataManager dataManager = new DataManager();
 
// 데모 View 컴포넌트 생성.
DemoViewComponent_V1 demoViewComponent_v1 = new DemoViewComponent_V1();
DemoViewComponent_V2 demoViewComponent_v2 = new DemoViewComponent_V2();
 
// 각 컴포넌트들이 Data 변경알림에 대해 구독하겠다고 등록.
{
    dataManager.registerObserver(demoViewComponent_v1);
    dataManager.registerObserver(demoViewComponent_v2);
}
 
// something work
 
// Server update virtual Test!!!!
dataManager.loadByServer(
          Arrays.asList(new MemberVo("강현지"), new MemberVo("유덕형"), new MemberVo("유형주"))
          , Arrays.asList(new ApprovalDoc("통과 - 스터디 신청"))
          , Arrays.asList(new MessageVo("모두, 스터디 참여 잘해줘서 감사합니다."), new MessageVo("그동안 수고하셨습니다.")));
 
// CONSOLE LOG
// 데모1 구성원 View 화면 갱신 중..
// [강현지] 갱신 중
// [유덕형] 갱신 중
// [유형주] 갱신 중
// 데모1 구성원 View 화면 갱신 완료!!
// 
// 데모1 전자결재 View 화면 갱신 중..
// [통과 - 스터디 신청] 갱신 중
// 데모1 전자결재 View 화면 갱신 완료!!
//
// 데모2 메시지 View 화면 갱신 중..
// [모두, 스터디 참여 잘해줘서 감사합니다.] 갱신 중
// [그동안 수고하셨습니다.] 갱신 중
// 데모2 메시지 View 화면 갱신 완료!!
cs


다행히, 잘 작동을 하는군요. 


DataManager 의 데이터 변경에 따라(DataManager::loadByServer), 모든 View 컴포넌트 (DemoViewComponent_V1, DemoViewComponent_V2) 들은 적절한 행위를 수행하고 있습니다. 


즉, DataManager 의 데이터만 잘 관리해주면 모든 View 컴포넌트의 화면을 자동으로 잘 갱신할 수 있음을 의미합니다. 


또한, 갱신할 화면 추가를 위해 다른 코드를 수정하지 않아도 됩니다. 

만약, View 컴포넌트가 새로 추가되었고 데이터 변경에 따라 화면 갱신을 해야한다면, IObserver 를 구현하게 하고 DataManager 에 등록만 해주면 됩니다. (OCP 도 지켜진 듯 합니다.)


Observer 패턴의 핵심은 [한 객체의 상태 변화가 얼마나 많은 객체에 영향을 주어야 할지 알 수 없을 때] DIP 원리에 따라 구현을 함으로써 유연하게 대처하는 것입니다. 

(사실 관찰자들은 데이터에 따라 무슨 행위를 할지 모르기 때문에 언제나 구체적이며 다양할 수 밖에 없고, 그렇기 때문에 interface 를 이용해 추상적으로 묶어 관리를 하죠.) 


이 패턴은 또한 대표적인 Compound 패턴인 MVC 에서 Model 과 View 의 관계를 나타냅니다.

Controller 에 의해 Model 이 변경되면, Model 은 View 에게 갱신사실을 알려주죠. 

(위의 예제 역시 DataManager 를 일종의 Model로 볼 수 있으며, 데이터 갱신에 따라 View 에게 데이터를 알려줍니다.)


 


알게 모르게, Observer 패턴은 사실 많이 사용하고 있으며 매우 중요한 패턴이라고 할 수 있을 것 같습니다. 

[네트워크 비동기 처리, UI 이벤트 처리, MVC] 등 이 원리는 상당히 많이 적용하고 있으며, 꼭 숙지해야할 패턴이라는 생각이 드네요.. 


한번, 이 포스팅을 따라 꼭 실습을 해보는 것을 권장합니다. 


STUDY_OOP7.zip










반응형
Posted by N'

계속해서, Instance 생성의 캡슐화를 목적으로 하는 Factory 패턴에 대해 다루고 있습니다.


지난 스터디에서는 객체 생성 method 를 제공하지만, 어떤 Instance 를 만들지는 하위 클래스가 결정하는 Factory method 패턴에 대해서 공부하였습니다.


또한 이를 공부 하면서, Cafe-Coffee 간의 요구사항을 구현하는 실습을 가졌었습니다.


해당 포스팅과 관련된 정보는 아래에서 참고. 



특히 "[CHAPTER 5] 실무 활용 패턴 (하) [Factory method 패턴] + 추가내용2" 에는 이전에 실습한 코드가 포스팅 하단에 있습니다. 이 소스를 다운받아, 오늘 포스팅의 내용을 따라가며 직접 실습해보길 권장합니다.


이번 스터디에서는 파편화된 클래스들의 의존관계를 가진 일련의 객체를 생성하는 과정을 캡슐화한 Abstract Factory 패턴에 대해 알아보았습니다. 

즉 객체 한 개를 만들기 위해 여러 클래스들을 이용한 어떤 과정이 필요하며, 이 과정을 캡슐화를 함으로써 재활용에 이득을 보는 것을 목적으로 합니다. 


이 패턴을 공부하기 위해, 지난 시간에 만든 코드를 바탕으로 새로운 요구사항을 제시하도록 하겠습니다.



1. 브랜드 별 커피들의 재료 및 레시피 관리


브랜드 별로 제조하는 커피에 들어가는 재료와 레시피는 다를 것입니다.


커피재료에 대한 가정은 다음과 같습니다.


스타벅스에서는


에티오피아에서 공수한 커피콩을 사용합니다.

- 최고의 맛을 제공하기 위해 가장 비싼 서울우유를 사용합니다. 

- "카페모카"나 "핫초코" 등에서 적절한 달콤함을 위해 스위스미스 초콜렛 시럽을 사용하는군요.


하지만, 이 재료들은 언제 어떻게 변경이 될 지는 모릅니다. 

에티오피아 커피 콩이 너무 비싸져서, 베트남 콩을 살 수도 있죠.


레시피의 경우에는 다른 카페와 비슷한 것도 있고, 아닌 것도 있습니다.


- 아메리카노의 경우는 다른카페와 동일하게 물과 커피빈에서 에스프레소를 추출합니다.

- 카페모카 의 경우에는 다른카페와는 다르게, 초콜릿 시럽을 네 스푼 정도 넣는군요.


즉 기존에 만들었던 브랜드 별 커피(Ex. StarbuckStyleAmericano) 에 살을 붙여줘야 할 것 같아 보입니다.


기획에 따라, 귀사의 아키텍처는 아래와 같은 초기 UML 을 작성해주었습니다.

이 초기 UML 에 따라 코드를 작성해 봅시다. 하지만, 구현과정에서 달라지는 것도 있을 수 있음을 고려할 필요가 있을 듯 합니다.




2. 커피재료 제작


커피재료의 경우 작성된 UML 에 따라 커피빈(CoffeeBean), 우유(Milk), 초콜렛시럽(ChocolateSyrup) 등이 있다고 가정합시다.


요구사항에 따라 생각을 해볼 때, 


예를들어 우유는 서울우유(SeoulMilk), 연세우유(YonseiMilk) 등이 존재할 수 있습니다. 

아마 우유라는 추상화된 개념이 있고, 카페마다 특정 브랜드의 우유를 사용하는 것을 고려한 듯 합니다.


우리는 브랜드라는 개념을 표현하기 위해, 지난 포스팅에서 브랜드라는 개념을 만들었습니다.

(기억이 나나요? IBrandable )


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 브랜드에 대한 정보를 사용하기 위한 인터페이스 정의
 *
 * <pre>
 *     - 브랜드를 지닐 수 있는 모든 클래스는 이 브랜드의 메소드를 지원해야함.
 *     - Ex. 커피, 카페(스타벅스, 카페베네), 컴퓨터(애플, 삼성)
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 10..
 */
public interface IBrandAble {
 
    /**
     * 브랜드 명을 출력한다.
     *
     * @return
     */
    String getBrandName();
}
cs


이 개념을 사용해서, 각 재료들 작성에 있어서 멋진 구조를 만들 수 있을 것 같아 보입니다.

아래는 예를들어 만들어본 우유(Milk) 분류의 상속계층 클래스들입니다.


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
/**
 * 우유 클래스 정의.
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public abstract class Milk implements IBrandAble{
    @Override
    public final String toString() {
        return String.format("%s 우유", getBrandName());
    }
}
 
/**
 * 서울우유 정의.
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public final class SeoulMilk extends Milk{
 
    @Override
    public String getBrandName() {
        return "서울";
    }
}
 
/**
 * 남양우유 정의
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public final class NamyangMilk extends Milk{
 
    @Override
    public String getBrandName() {
        return "남양";
    }
 
}
cs


Milk 라는 뼈대를 만들고, Milk 의 구체화 클래스(SeoulMilk, NamyangMilk)들은 브랜드의 이름 구현에만 집중하였습니다. 지난 포스팅에서 다루었던, 브랜드 및 제품표시 형식 관리 방법을 그대로 사용했습니다. 

(코드 구현에 발전이 있군요... )


CoffeeBean 과 ChocolateSyrup 분류 역시 이와 비슷하게 작성해볼 수 있을 것 같습니다.

구현하실 수 있겠죠? :-)



3. 카페의 브랜드별 커피재료 관리


각 커피에 필요한 재료들은 만들었으니, 카페 브랜드마다 재료를 관리하는 개념을 만들어볼 차례입니다. 


즉 [카페마다 커피콩은 에티오피아커피콩을, 우유는 서울우유를 사용하는 등의 개념]을 만들어 관리를 할 수 있다면, 카페에서 재료를 변경해야할 때 이 개념만을 수정하며 해당 카페에서 사용하는 재료들을 일괄적으로 변경할 수 있을 것 같습니다.


그렇습니다. 이번에 구현할 것은 이 개념을 담은 CoffeeIngredient 분류를 만들 것입니다.


이 클래스에서 제공해주는 메소드는 카페에서 사용하는 재료(CoffeeBean, Milk, ChocolateSyrup)들을 출력해주는 메소드입니다.


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
/**
 * 커피를 만들기 위한 재료 공장 정의.
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public abstract class CoffeeIngredientFactory {
 
    /**
     * 해당 재료공장에 정의된 커피콩 생산.
     *
     * @return
     */
    public abstract CoffeeBean getCoffeeBean();
 
    /**
     * 해당 재료공장에 정의된 우유 생산.
     *
     * @return
     */
    public abstract Milk getMilk();
 
    /**
     * 해당 재료공장에 정의된 초콜렛 시럽 생산.
     *
     * @return
     */
    public abstract ChocolateSyrup getChocolateSyrup();
}
cs


CoffeeIngredientFactory 클래스는 Coffee 를 만들기 위한 재료 클래스의 Instance 를 제공하는 메소드를 정의했습니다. 하지만, 정확히 어떤 Instance 를 만들지는 정의하지 않았습니다.


어떤 Instance 를 만들지는 이 클래스의 구체개념 클래스가 정하도록 하려고 합니다.

Starbucks 의 재료관리 클래스인 StarbucksCoffeeIngredientFactory 를 다음과 같이 만들었습니다.


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
/**
 * 스타벅스 커피재료 공장 정의.
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public final class StarbucksCoffeeIngredientFactory extends CoffeeIngredientFactory{
 
    /**
     *  싱글톤의 Instance 관리를 위한 홀더패턴 정의.
     */
    private static final class ManagerHolder {
        private static final StarbucksCoffeeIngredientFactory Instance = new StarbucksCoffeeIngredientFactory();
    }
 
    /**
     * 구체화된 커피공장은 의미적으로 한개만 존재해도 될 것으로 예상.
     *
     * @return
     */
    public static final StarbucksCoffeeIngredientFactory GetInstance() {
        return ManagerHolder.Instance;
    }
 
    /**
     * 스타벅스 커피는 에티오피아 커피콩을 사용.
     *
     * @return
     */
    @Override
    public CoffeeBean getCoffeeBean() {
        return new EthiopiaCoffeeBean();
    }
 
    /**
     * 스타벅스 커피는 서울우유를 사용.
     *
     * @return
     */
    @Override
    public Milk getMilk() {
        return new SeoulMilk();
    }
 
    /**
     * 스타벅스 커피는 초콜렛 시럽을 사용.
     *
     * @return
     */
    @Override
    public ChocolateSyrup getChocolateSyrup() {
        return new SwissmissChocolateSyrup();
    }
}
 
cs


우리는 지금과 같은 클래스 구조 패턴을 공부한 적이 있습니다. 

짐작이 가시나요? 객체 생성 method 자체는 상위개념에서 제공하고, 어떤 Instance 를 만들지는 구체개념이 정하는 방식인 Factory-method 패턴입니다.


이 클래스를 각 Starbucks 의 브랜드별 Coffee 들에게 적절하게 의존하게 할 수 있다면,

Starbucks 의 모든 Coffee 에 대한 재료를 관리할 수 있을 것입니다.



4. Coffee 와 CoffeeIngredientFactory 간의 의존관계 관리


Coffee 를 만들기 위해서는 Coffee 의 재료들을 사용해야합니다. 


Coffee 를 만들기 위한 재료를 적절하게 받을 수 있게 하는 것이 목적이라고 생각할 수 있고, 이는 Coffee 가 CoffeeIngredientFactory 를 사용하는 관계(use-a) 가 되면 될 것 같습니다.


use-a 관계를 만들기 위해 기존 Coffee 클래스의 멤버변수로 CoffeeIngredientFactory 를 다음과 같이 추가하였습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static abstract class Coffee implements IBrandAble {
    
    // 커피 재료 공장 변수
    private CoffeeIngredientFactory coffeeIngredientFactory;
 
    /**
     * 커피공장 출력.
     *
     * @return
     */
    protected final CoffeeIngredientFactory getCoffeeIngredientFactory() {
        return coffeeIngredientFactory;
    }
}
cs


Coffee 클래스의 CoffeeIngredientFactory 변수의 초기화가 관건이군요. 


브랜드 Coffee 마다 적절한 커피재료공장을 주입해줘야합니다. 하지만 우리는 이와 비슷한 문제를 해결했었던 것 같군요. 네, 커피의 브랜드명 입력과 같은 문제라고 할 수 있군요.


같은 문제는 같은 방식으로 해결할 수 있겠지요. 


저는 Cafe 클래스에서 적절한 재료공장을 출력하도록 추상메소드를 제공할 것입니다. 

물론 적절한 재료공장은 구체화된 개념 클래스가 해주겠죠?


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
57
58
59
60
61
62
63
/**
 * 커피를 제공하는 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Cafe implements IBrandAble{
 
     /**
     * 커피 인스턴스 생성
     *
     * @param code
     * @return
     */
    public final Coffee getCoffee(final String code) {
        // 커피 종류에 따라 인스턴스를 생성
        // 브랜드 카페마다 동일한 부분
        final Coffee coffee = createCoffee(code);
 
        // 커피 주문을 위한 전처리 작업
        {
            // 커피 브랜드명 주입.
            coffee.brandName = getBrandName();
 
            // 커피공장 주입.
            coffee.coffeeIngredientFactory = getCoffeeIngredientFactory();
 
            coffee.order();
            coffee.prepare();
        }
 
        return coffee;
    }
 
    /**
     * 각 제조사가 제공해야할 커피재료공장 출력.
     *
     * @return
     */
    protected abstract CoffeeIngredientFactory getCoffeeIngredientFactory();
}
 
 
/**
 * 스타벅스 스타일의 커피를 생산하는 커피공장.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public final class StarbucksCafe extends Cafe {
 
    /**
     * 스타벅스의 재료공장 정의
     *
     * <pre>
     *     스타벅스의 커피재료공장 클래스를 사용.
     * </pre>
     *
     * @return
     */
    @Override
    protected final CoffeeIngredientFactory getCoffeeIngredientFactory() {
        return StarbucksCoffeeIngredientFactory.GetInstance();
    }
}
cs


이제 StarbucksCafe 에서 생산되는 Coffee 들은 StarbucksCoffeeIngredientFactory 에 정의된 재료만을 사용하는 것이 보장이 되었습니다. 


재료관리를 할 수 있게 되었으니, 각 Coffee 들의 레시피를 정리해볼 수 있을 것 같군요.



5. Coffee 들의 레시피 관리


Coffee 마다 레시피는 다를 것입니다. 

하지만, 어느 일련의 과정마다 비슷한 방법이 존재(에스프레소 커피는 커피콩을 이용해 에스프레소를 추출하는 등..)이 존재할 것 같군요.


아마 귀사의 아키텍처는 이 점을 고려하여, UML 에 복잡한 Coffee 의 상속구조를 표현한 것이 아닌가 생각이 듭니다.


[레시피에 대한 구현] 이라는 새로운 요구사항이 생겼으니, 저는 Coffee 클래스에 레시피 구현을 위한 메소드를 새로 만들어볼 생각입니다. 이 메소드 역시 고객에게 제공되지는 않고, 준비과정(Coffee::prepare) 중에 포함시키고자 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static abstract class Coffee implements IBrandAble {
 
    /**
     * 커피를 준비하는 메소드.
     *
     * <pre>
     *     - 해당 메소드는 오직 카페에서만 사용하도록 접근제한!
     * </pre>
     */
    private final void prepare() {
        System.out.println(String.format("%s를 준비중!"toString()));
 
        makeCoffee();
 
        System.out.println(String.format("%s를 준비완료"toString()));
    }
 
    /**
     * 정의된 레시피에 재료를 사용해서 커피를 제작.
     */
    protected abstract void makeCoffee();
}
cs


에스프레소를 사용하는 커피들은 모두 커피콩에서 에스프레소를 추출하는 과정을 담고 있습니다.


초기 UML 처럼 EspressoCoffee 라는 개념을 중간에 만들고, 에스프레소 추출과정을 캡슐화한 메소드를 제공해주면 모든 에스프레소를 사용하는 커피들은 이 메소드를 재활용할 수 있을 것 같아 보입니다.


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
57
58
59
/**
 * 커피콩을 이용하여, 에스프레소를 사용하는 커피클래스.
 *
 * Created by Doohyun on 2017. 6. 11..
 */
public abstract class EspressoCoffee extends Cafe.Coffee{
 
    /**
     * 커피콩을 이용하여 에스프레소를 생산.
     */
    protected final void createEspresso() {
        // 주입된 커피재료 공장 조회
        final CoffeeIngredientFactory coffeeIngredientFactory = getCoffeeIngredientFactory();
 
        // 커피콩 생산
        final CoffeeBean coffeeBean = coffeeIngredientFactory.getCoffeeBean();
 
        System.out.printf("[%s] 를 이용하여 에스프레소를 생산\n", coffeeBean);
    }
}
 
/**
 * "라떼" 클래스.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Latte extends EspressoCoffee {
 
    /**
     * "라떼" 명을 출력하도록 구현.
     *
     * @return
     */
    @Override
    protected final String getCoffeeOtherName() {
        return "라떼";
    }
 
    /**
     * 라떼 레시피에 따른 커피 생산.
     *
     * <pre>
     *     브랜드별로 커피 레시피가 다를 수 있기 때문에 final 화 하지 않음.
     * </pre>
     */
    @Override
    protected void makeCoffee() {
        createEspresso();
 
        // 커피재료 팩토리 부르기.
        CoffeeIngredientFactory coffeeIngredientFactory = getCoffeeIngredientFactory();
 
        // 사용할 우유 정의
        Milk milk = coffeeIngredientFactory.getMilk();
 
        System.out.printf("[%s] 를 데우는 중...\n", milk);
        System.out.printf("에스프레소와 데운 [%s] 을 섞는 중...\n", milk);
    }
}
cs


EspressoCoffee 개념을 만들고, 기존에 구현된 Latte 의 상속구조를 조금 변경하였습니다. 

Latte 를 만드는 방법은 어느 카페나 비슷하기 때문에 Latte 클래스에서 Coffee::makeCoffee 를 구현하였습니다.


자 이제 테스트 코드를 한번 실행해 볼까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
 // 고객은 단순히 스타벅스 카페를 통해 아메리카노를 주문한다는 것을 알 수 있음.
Cafe.Coffee coffee = StarbucksCafe.GetInstance().getCoffee(Cafe.MENU.LATTE);
// 고객에게 드러나는 메소드는 오직 drink.
coffee.drink();
 
// CONSOLE LOG
// [스타벅스 라떼]를 주문!
// [스타벅스 라떼]를 준비중!
// [에티오피아 커피콩] 를 이용하여 에스프레소를 생산
// [서울 우유] 를 데우는 중...
// 에스프레소와 데운 [서울 우유] 을 섞는 중...
// [스타벅스 라떼]를 준비완료
// 맛있는 커피 [스타벅스 라떼]
cs


StarbucksCafe 에서 주문을 했기 때문에 커피명부터 [스타벅스 라떼] 라고 잘 나오는 군요.

이번 기능 추가에서 원했던 것처럼, 각 재료들 역시 StarbucksCoffeeIngredientFactory 에 의존해서 잘 나오는 군요.


욕심많은 기획자가 원했던 의도를 잘 구현한 것 같아 마음에 듭니다! 


하지만, 갑자기 [스타벅스 라떼] 다른 Cafe 들과 다른 레시피를 사용한다고 하는군요. @.@

그래도 상관없습니다. 우리는 유연하게 개발했으니, 이 요구사항은 쉽게 수용할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
 * 스타벅스 스타일 라떼.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public final class StarbucksStyleLatte extends Latte {
 
    /**
     * 스타벅스만의 라떼 레시피 적용.
     */
    @Override
    protected final void makeCoffee() {
        createEspresso();
 
        // 커피재료 팩토리 부르기.
        CoffeeIngredientFactory coffeeIngredientFactory = getCoffeeIngredientFactory();
 
        // 사용할 우유 정의
        Milk milk = coffeeIngredientFactory.getMilk();
 
        System.out.printf("[%s] 를 딱 20초만에 80도에 맞춰 데우는 중..\n", milk);
        System.out.printf("에스프레소와 데운 [%s] 을 하트를 그리면서 섞는 중...\n", milk);
    }
}
cs


StarbucksStyleLatte 클래스가 이제야 필요한 시점이 왔군요. 


특정 브랜드의 Coffee 가 특별한 레시피를 사용하고 싶다고 한다면, 다음과 같이 레시피를 담는 Coffee::makeCoffee 를 재정의만 해주면 됩니다. 다만, 부모의 기존 행위를 무시하기 때문에 다시 잘 정의해줘야 하겠죠? :-)


이제 [스타벅스 스타일의 라떼]가 잘 작동하나 확인만 해보면 퇴근입니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
// 고객은 단순히 스타벅스 카페를 통해 아메리카노를 주문한다는 것을 알 수 있음.
Cafe.Coffee coffee = StarbucksCafe.GetInstance().getCoffee(Cafe.MENU.LATTE);
// 고객에게 드러나는 메소드는 오직 drink.
coffee.drink();
 
// CONSOLE LOG
// [스타벅스 라떼]를 주문!
// [스타벅스 라떼]를 준비중!
// [에티오피아 커피콩] 를 이용하여 에스프레소를 생산
// [서울 우유] 를 딱 20초만에 80도에 맞춰 데우는 중..
// 에스프레소와 데운 [서울 우유] 을 하트를 그리면서 섞는 중...
// [스타벅스 라떼]를 준비완료
// 맛있는 커피 [스타벅스 라떼]
cs



Abstract Factory 패턴은 이와 같이, 


특정 Instance 를 만드는 행위를 캡슐화 했다기 보다는 


일련의 집합관계(Ex. CoffeeBean, Milk 등의 집합)를 가진 class 를 Instance 를 생성하며,

집합관계 Instance 들을 통해 어떤 행위를 해야하는 것(Ex. Coffee 를 생산)


에 초점을 맞춘 패턴이라 생각해 볼 수 있을 것 같습니다.


Factory 패턴과 같이 Instance 생성을 숨기며 생성과정을 캡슐화하는 방법은 여러 비지니스 로직에서 재활용 및 유지보수성을 좋게하는 것에 기여할 수 있습니다. 이전에 배운 Adapter 패턴 역시 비슷한 경험을 했을 것이라 생각합니다.


이번 Coffee-Cafe 예제의 경우는 Instance 생성 및 의존관계에 대한 관리를 조금 복잡하게 고려해 보았습니다. 두 가지 이상되는 패턴을 한 예제로 실습하려 하니, 난이도가 조금 있었을 수도 있다는 생각이 드네요. 

(또한 UML 을 보면 알겠지만, 요구사항도 딱 정한 상태로 만든 건 아닙니다. 요구사항을 조금씩 그 때마다 추가했죠...)


그렇지만 이번 패턴 역시도 추상화와 의존관계를 조정하는 과정에 초점을 맞추어 리뷰를 올리며, 딱 이 패턴을 외워야한다는 것을 지향 하지는 않습니다.


하지만, 이 리뷰는 꼭 한번 혼자서 실습을 해보는 것을 권장합니다. 

복잡도가 조금 있으며, 여태 배웠던 패턴들을 다시 한번 실습해 볼 수 있기 때문에 실력향상에 도움이 될 수 있을 것이라 생각합니다. 이해가 안가는 부분은 언제든 질문하러 오세용. :-)


완성된 소스를 제공하며, 이번 포스팅을 마칩니다.


STUDY_OOP6.zip





반응형
Posted by N'

지난 스터디에서는 "Instance 생성에 대한 캡슐화에 초점을 맞춘 Factory 패턴"에 대한 리뷰를 하였고, 그 중 고전적인 Factory Method 패턴에 대해 다뤄봤습니다.


이를 이용하여, 가상으로 다양한 커피 종류에 대한 Instance 생성을 캡슐화하는 예제를 진행했었습니다. 고객은 단순히 원하는 Coffee 를 주문하면, 해당 Coffee Instance 를 생성하여 전달하는 예제였습니다.


해당 포스팅과 관련된 내용은 아래에서 참고!



이 포스팅에서는 기존 작성된 코드에서 작은 리팩토링에 대한 내용을 담을 생각입니다.



1. 브랜드에 따른 데코레이터 방식의 문제.


우리는 초기 목적에 따라, 랜드에 따른 커피를 만드는 것에 대하여 성공을 하였습니다. 


아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
 * 스타벅스 스타일 아메리카노
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleAmericano extends Americano {
 
    /**
     * 부모의 결과에 자신의 색깔을 치장하여 결과를 출력
     * 
     * @return
     */
    @Override
    public String toString() {
        return "스타벅스" + super.toString();
    }
}
 
cs


부모의 결과에 행위를 추가한다는 일종의 "데코레이터 패턴" 에 따라 작성이 된 코드입니다.


-> (잘못된 언급! 치장하고자 하는 대상객체의 행위추가는 맞지만, 정확히 has-a 관계로 처리하는 일반적인 데코레이터 패턴은 아닙니다.)


정확히, 데코레이터 패턴에 대해 알고 싶다면 아래 포스팅을 참고! 



하지만, 고려해볼 사항은 다른 커피종류를 만들 때마다 동일하게 치장을 해줘야 할 것 같아 보이네요.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 스타벅스 스타일 라떼.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleLatte extends Americano {
 
    /**
     * 
     * 스타벅스의 모든 커피종류는 아래와 같이 동일한 치장을 해줘야함.    
     * 우아해보이진 않음.    
     *
     * @return
     */
    @Override
    public String toString() {
        return "스타벅스" + super.toString();
    }
}
cs


물론, 커피의 이름을 만드는 목적을 수행하고있는 Coffee::toString 의 내용은 간단하기 때문에 커피종류를 만들 때마다 Copy&paste 를 한다고 하더라도 큰 문제는 생길 것 같아 보이진 않습니다.


하지만, 갑자기 "스타벅스"의 상호"슈퍼 스타벅스"로 변경해야 한다는 요구사항이 생긴다면, 이미 만들어진 모든 스타벅스 스타일의 커피를 일일이 변경해야할 것입니다.


즉 현재의 상황은 유지보수에 있어서, 안타까운 일이 생길 가능성이 있어보이는군요. ㅜㅡㅜ



2. 상호를 특정 상수로 관리.


1차적으로 생각해볼 수 있는 방법은 변경소지가 있는 중복된 상호 부분을 특정 상수로 관리하는 것을 생각해볼 수 있습니다. (모든 스타벅스 스타일 커피들이 하드코딩으로 구현한 "스타벅스" 라는 부분을 상수로 관리하자는 것을 의미합니다.)


자 이제 리팩토링을 해볼까요? 

리팩토링을 하면, 이 정도의 코드를 생각해볼 수 있을 것 같습니다.


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
/**
 * 스타벅스 스타일의 커피를 생산하는 커피공장.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public final class StarbucksCafe extends Cafe {
    public static final String PREFIX = "스타벅스";
}
 
/**
 * 스타벅스 스타일 아메리카노
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleAmericano extends Americano {
 
    /**
     * 스타벅스라는 상호를 상수로 관리하여, 상호가 변경되는 것에 대한 유연함을 가지게 됨.
     * 
     * 하지만, 매번 커피를 만들 때마다 아래와 같은 데코레이터를 만들어야 하는가?
     */
    @Override
    public String toString() {
        return StarbucksCafe.PREFIX + super.toString();
    }
}
 
/**
 * 스타벅스 스타일 라떼.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleLatte extends Americano {
 
    /**
     * 데코레이터를 하는 부분의 코드 중복
     */
    @Override
    public String toString() {
        return StarbucksCafe.PREFIX + super.toString();
    }
}
 
cs


상호를 상수로 취급하여, 상호변경에 따른 유연함을 갖추는 것에는 성공을 하였습니다.

하지만, 모든 브랜드의 커피는 Coffee::toString 메소드를 동일하게 구현해야합니다. 

특별히 커피이름에 대하여 다른 형식이 나올 일이 없다면 상관이 없지만, 커피이름 형식을 변경해야 한다면 모든 브랜드 별 커피를 수정해야하니 이 부분 역시 변화에 유연할 수 없는 구조라고 생각해 볼 수 있습니다.


예를들어, 현재의 커피명은 브랜드와 커피명의 중간에 공백을 둔 상태이지만(Ex. 스타벅스 아메리카노), 

욕심많은 기획자는 갑자기 커피명에 대괄호(Ex. [스타벅스 아메리카노])를 붙여달라고 하였습니다. 


기획쪽의 입장에서는 큰 요구사항이 아니라고 할 수 있지만, 개발자 입장에서는 모든 브랜드 커피를 고치면서 변화에 유연하지 못했던 자신의 무능함을 깨닫게 되겠죠. 야근과 함께 말이죠....


자, 이 구조 역시 좋은 구조가 아니라는 것을 깨닫게 되었습니다.

어디서부터 문제가 있는지 생각해 볼 시간입니다.



3. 브랜드 개념은 오직 "커피" 의 문제인가?


사실 브랜드라는 것은 Coffee 에만 적용되는 문제는 아닌 것 같습니다. 


Coffee 를 생산하는 공장인 Cafe 역시 특정 브랜드마다 개념을 분리할 수 있으며, 추 후 우리 회사에서는 오직 Coffee 관련된 내용이 아닌 다른 제품관련 내용을 담을 수도 있습니다. 

(아마도???  ㅡㅡ^)


다른 분류 군에 대하여 어떤 하나의 개념으로 묶어주기 위하여, interface 한 개를 만드는 것을 저는 제안해볼 생각입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * 브랜드에 대한 정보를 담기 위한 인터페이스 정의
 *
 * <pre>
 *     - 브랜드를 지닐 수 있는 모든 클래스는 이 브랜드의 메소드를 지원해야함.
 *     - Ex. 커피(스타벅스, 카페베네), 컴퓨터(애플, 삼성)
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 10..
 */
public interface IBrandAble {
 
    /**
     * 브랜드 명을 출력한다.
     *
     * @return
     */
    String getBrandName();
}
cs


Simple 한 IBrandAble interface 는 브랜드명을 출력하는 메소드 한 개만을 지원합니다.


이 interface 의 등장은 꽤 괜찮은 결과를 불러와줄 수 있지 않을까라는 생각이 듭니다.



4. Cafe 와 Coffee 클래스의 브랜드 개념 적용


브랜드 interface 를 만들었으니, 각 상위 계층의 Coffee 와 Cafe 에 이 개념을 적용해봅시다.


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
/**
 * 커피를 제공하는 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Cafe implements IBrandAble {
 
    /**
     * 커피 클래스
     *
     * Created by Doohyun on 2017. 6. 2..
     */
     public static abstract class Coffee implements IBrandAble {
     }
}
 
/**
 * 스타벅스 스타일의 커피를 생산하는 커피공장.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public final class StarbucksCafe extends Cafe {
 
    /**
     * 브랜드명 출력.
     * 
     * 기존 상수(PREFIX)는 삭제
     *
     * @return
     */
    @Override
    public String getBrandName() {
        return "스타벅스";
    }
}
cs


Cafe 와 Coffee 모두 브랜드를 구현해야 합니다. 

하지만 두 클래스 모두 추상 클래스이기 때문에 당장 구현할 필요는 없어보이는 군요.


일단 Cafe 의 경우 Cafe 클래스에서 직접 구현하지 않고, 하위 개념인 StarbucksCafe 에서 구현을 하고 있습니다. 당연히 각 구현 내용은 하위개념인 브랜드별 Cafe 마다 다르기 때문이죠.


하지만 Coffee 클래스의 경우는 이야기가 살짝 다릅니다. 

모든 브랜드별 커피(Ex. StarbucksStyleAmericano) 들에 브랜드명을 구현해주는 것은 코드 중복이며, 이는 처음 상태와 크게 다르지 않습니다.


이 때, 생각해봐야 할 것은 Coffee 의 브랜드명은 Cafe 의 브랜드명에 의존한다는 것입니다. 

Cafe 의 브랜드마다 생산되는 Coffee 의 브랜드는 동일합니다. 


즉 Coffee 의 브랜드가 Cafe 의 브랜드에 의존하도록 만들어보죠.

저는 아래와 같이 만들어 봤습니다.


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
/**
 * 커피를 제공하는 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Cafe implements IBrandAble{
 
    /**
     * 내부 클래스로 정의하여, order 와 prepare 를 Cafe 내부에서만 사용하도록 처리
     *
     * Created by Doohyun on 2017. 6. 2..
     */
    public static abstract class Coffee implements IBrandAble {
 
        // 커피의 브랜드명을 표기하는 변수.
        private String brandName;
 
        /**
         * 브랜드명을 출력하는 메소드.
         *
         * @return
         */
        @Override
        public final String getBrandName() {
            return brandName;
        }
    }
 
    /**
     * 커피 인스턴스 생성
     *
     * @param code
     * @return
     */
    public final Coffee getCoffee(final String code) {
        // 커피 종류에 따라 인스턴스를 생성
        // 브랜드 카페마다 동일한 부분
        final Coffee coffee = createCoffee(code);
        
        // 커피 주문을 위한 전처리 작업
        {
            // 커피 브랜드명 주입.
            coffee.brandName = getBrandName();
 
            coffee.order();
            coffee.prepare();
        }
 
        return coffee;
    }
}
cs


Coffee 자체에 브랜드명을 멤버변수로 가지고 있고, Cafe::getCoffee 내용 중 전처리 작업 부분에서 Coffee 의 브랜드명을 초기화하도록 처리하도록 하였습니다. 


이 곳에서 구현을 함으로써, 모든 브랜드 Cafe 클래스(Ex. StarbucksCafe)들은 오직 IBrandAble 만 구현에만 관심을 가지도록 하였습니다. 

(즉, Coffee 의 브랜드 주입에 대해 신경을 안써도 됩니다.)


이로써, 첫 번째 문제 였던 상호 관리 문제는 해결이 되었습니다. 



5. 커피 이름 형식 관리


상호가 관리가 되었으니, 커피이름 형식 역시 관리해 볼 수 있을 것 같습니다.


처음으로 돌아가서, 데코레이터 패턴을 사용했던 이유가 상호를 붙이기 위함 을 생각합시다. 

하지만 지금은 더이상 각 브랜드별 커피(Ex. StarbucksAmericano)가 상호를 관리하지 않아도 되며, 이는 즉 Coffee 클래스 자체에서 커피이름 형식을 관리해도 됨을 의미합니다.


약간의 리팩토링 과정을 저는 이렇게 하였습니다.


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
57
58
59
60
/**
  * 내부 클래스로 정의하여, order 와 prepare 를 Cafe 내부에서만 사용하도록 처리
  *
  * Created by Doohyun on 2017. 6. 2..
  */
public static abstract class Coffee implements IBrandAble {
 
   /**
    * 커피명을 toString 으로 정의.
    *
    * @return
    */
    @Override
    public final String toString(){
        return String.format("[%s %s]", getBrandName(), getCoffeeOtherName());
    }
 
  /**
    * 커피 종류에 따른 이름을 출력하는 메소드.
    *
    * @return
    */
    protected abstract String getCoffeeOtherName();
}
 
/**
 * "아메리카노" 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Americano extends Cafe.Coffee {
 
    /**
     * "아메리카노" 명을 출력하도록 구현.
     *
     * @return
     */
    @Override
    protected String getCoffeeOtherName() {
        return "아메리카노";
    }
}
 
/**
 * "라떼" 클래스.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Latte extends Cafe.Coffee {
 
    /**
     * "라떼" 명을 출력하도록 구현.
     *
     * @return
     */
    @Override
    protected String getCoffeeOtherName() {
        return "라떼";
    }
}
cs


처음의 요구사항 이었던 Coffee::toString 에 커피 이름 형식을 관리하도록 하고, 커피종류이름을 출력하는 추상메소드인 Coffee::getCoffeeOtherName 을 만들었습니다.

Coffee::getCoffeeOtherName의 구현은 상속구조의 중간단계 클래스인 Americano 와 Latte 가 구현하도록 하였습니다.


아.. 물론 욕심많은 기획자의 요구사항에 맞춰, 대괄호를 붙여주도록 하죠. [스타벅스 라떼] :-)


그럼 각 브랜드 별 커피들의 구현 상태는 어떨까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * 스타벅스 스타일 아메리카노
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleAmericano extends Americano {
}
 
/**
 * 스타벅스 스타일 라떼.
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleLatte extends Latte {
}
cs


Simple 하군요. 아무 것도 만들지 않아도 되네요. 

이는 현재의 요구사항에서는 브랜드 별 커피를 만들지 않아도 됨을 의미합니다.


하지만, 다음 실습에서 이 클래스들이 필요하니, 살려두죠...


이번 포스팅에서 진행한 "리팩토링" 에서는 각 클래스들이 가져야 할 책임에 대한 재분배를 시도해봤습니다. 

(장황해보이지만, 사실 한 것은 별로 없습니다. ~ @.@ ~)


상호와 커피이름형식을 책임져야 하는 것은 [브랜드 별 커피 클래스]가 아닌 [각 상위 개념의 클래스들]이었고, 적절한 책임의 분배 및 의존 관계를 지어주었습니다. 


이 코드를 다음 실습에서 해볼 수 있도록 아래 제공하도록 하겠습니다. (UTF-8 포멧입니다.)


STUDY_OOP6.zip


아 그리고 혹시나 있을지 모를 [스터디의 구독자] 를 위한 깜짝 퀴즈를 제시해보겠습니다. 

(보기만 하는 건 재미 없으니ㅡㅡ^, 부담없게 생각해보세요.)



욕심많은 기획자의 또 다른 요구 사항 


기껏 지금과 같이 구현을 하였지만, 욕심많은 기획자가 또 다른 기획서를 들고 왔습니다.


다른 브랜드 카페회사는 지금과 같은 커피이름포맷을 유지하기를 원합니다. [스타벅스 라떼]


하지만, 스타벅스는 모든 음료에 대해 자기들만의 특정 커피이름 포맷을 쓰고 싶어합니다. 

즉, 스타벅스만의 고유 커피이름 포멧을 적용했으면 좋겠습니다. 


이를 위한 구체 요구사항은 다음과 같습니다.


- 커피이름을 만들기 위해서는 커피명과 브랜드명을 사용할 수도 있고, 

  다른 [스타벅스 만의 고유정보]도 필요할 수 있도록 개발이 되면 좋겠습니다.


- 고객에게 Coffee::getCoffeeOtherName 과 같은 부가정보를 주고 싶진 않네요. 

  유지보수를 위해 패키지도 리팩토링하였으니, 이 것도 건들지 마세요.


물론 기존과 같이 Coffee::toString 을 통해 커피이름을 제공받고 싶어합니다.


이를 구현하기 위해서 또 모든 스타벅스의 브랜드 별 커피 마다 toString 을 재정의하는 것은 

  좋아보이진 않네요. (한 번 당했으면 됐지, 같은 실수를 반복하면 안되겠죠....)


- 개발환경은 아쉽게도 JAVA8 이 아닙니다. ㅜㅡㅜ


이 요구사항을 해결해준 분에게는 카페-커피 를 관리해준 답례로 커피를 쏘도록 하겠습니다.

꼭 해결이 아니더라도, 충분한 고민이 보인다면 뭐... ㅎㅎ 

(내 지갑은 사주다가 끝이 날지도.... @.@)








반응형
Posted by N'

객체지향 프로그램에서는 클래스라는 조형틀을 만들고, 

그 틀을 이용해 Instance 라는 조형물을 만든다는 예시를 많이 들어봤을 것이라 생각합니다.


그러나 종종 우리는 한 개의 Instance 를 제대로 만들기 위한 과정이 복잡할 수도 있고,  

또는 누가, 언제, 어떻게, 무엇을 만들 것인지에 대해 고민을 해야할 필요도 있습니다.


Factory 패턴은 이러한 복잡한 Instance 생성에 대한 캡슐화에 초점을 맞춘 패턴입니다.

웹의 벡-엔드 프로젝트에서는 클래스들의 의존 관계를 너무 많이 표현해야하는 이슈가 많이 있고, 그 패턴들이 비슷할 경우 가끔 사용하곤 합니다. (Ex. ExcelFactory)


현재 사내에서는 가끔 사용하는 패턴이지만, 이번 주제로 잡은 이유는 다른 응용프로그램 제작에서 상당히 많이 쓰이고 유명한 패턴이기 때문입니다. 

(적어도 디자인 패턴을 배웠다는 데, 팩토리 패턴을 안배웠다는 것은 앙꼬 없는 찐방같은...)


또한 실습의 흐름을 통해서도 알겠지만, 앞써 배운 패턴들을 알게모르게 사용한다는 점에 있어서 좋은 주제라고 생각을 하였습니다.


자료는 이 곳에서 참고!



지난 스터디에서는 고전적으로 소개하고 있는 팩토리 패턴의 종류 중 


"객체 생성 Method 는 제공하지만, 무엇을 만들 것인지는 하위 클래스가 결정" 하는 Factory-Method 패턴에 대한 예제를 같이 실습해보았습니다.


이번에는 사내 코드를 리뷰하기 보다는 가상의 요구사항과 예제를 만드는 실습을 진행하였고,

오늘의 포스팅 내용은 이 요구사항에 대한 코드를 소개하도록 하겠습니다.



1. 요구사항


평범한 고객들은 카페에 가서, 커피를 마십니다. 


카페의 바리스타는 고객이 원하는 커피를 주문받고 준비하며, 고객은 마시기만 하면 됩니다.


다시 정확히 요구사항을 말하면, 고객은 원하는 커피를 제공받아 마시기만 하면 됩니다.



2. 커피와 카페의 요구 명세


고객에 입장에서는 카페에 주문을 하고, 주문한 커피를 받으면 됩니다. 


즉 객체모델 대상은 크게 커피와 카페를 들 수 있을 것 같군요. 


일단 커피는 주문하는 과정준비하는 과정이 있다고 가정을 하겠습니다. 

아, 물론 고객이 소비하는 행위인 마시기도 있어야겠죠.


아래와 같은 메소드 목록을 고려해 볼 수 있겠습니다.


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
public class Coffee {
    /**
     * 주문하기
     */
    public final void order() {
        System.out.println(String.format("%s를 주문!"toString()));
    }
 
    /**
     * 커피를 준비
     */
    public final void prepare() {
        System.out.println(String.format("%s를 준비중!"toString()));
    }
 
    /**
     * 커피 마시기
     */
    public final void drink() {
        System.out.println(String.format("맛있는 커피 %s"toString()));
    }
 
    @Override
    public String toString() {
        return "커피";
    }
}
cs


카페에서 커피를 판매하는 것은 맞지만, 다양한 종류의 커피를 판매합니다.


예를 들면, 아메리카노나 라떼 등이 대표적인 메뉴(Menu)겠죠? 


즉 커피 클래스를 상속받은 아메리카노와 라떼 등을 아래와 같이 만들어 볼 수 있을 것 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 커피를 상속받은 "아메리카노 클래스"
 * 
 * Created by Doohyun on 2017. 6. 2..
 */
public class Americano extends Coffee {
    @Override
    public String toString(){
        return "아메리카노";
    }
}
 
/**
 * 커피를 상속받은 "라ㄸㅔ 클래스"
 * 
 * Created by Doohyun on 2017. 6. 2..
 */
public class Latte extends Coffee {
    @Override
    public String toString(){
        return "라떼";
    }
}
cs


이제 커피를 생산할 카페 클래스를 아래와 같이 만들어 볼 수 있을 것 같습니다. 

서비스로 메뉴보드까지 같이 만들어주죠. 


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
/**
 * 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class Cafe {
 
    /**
     * 커피 메뉴
     */
    public static class MENU{
        public static final String AMERICANO = "AMERICANO";
        public static final String LATTE = "LATTE";
    }
 
    /**
     * 커피 인스턴스 생성
     *
     * @param code
     * @return
     */
    public final Coffee getCoffee(final String code) {
        // 커피 종류에 따라 인스턴스를 생성
        final Coffee coffee;
        {
            switch (code) {
                case MENU.AMERICANO:
                    coffee = new Americano();
                    break;
 
                case MENU.LATTE:
                    coffee = new Latte();
                    break;
 
                default:
                    throw new RuntimeException("[에러] 코드가 정확하지 않음!!!");
            }
        }
 
        // 커피 주문을 위한 전처리 작업
        {
            coffee.order();
            coffee.prepare();
        }
 
        return coffee;
    }
}
 
cs


고객은 인제 Cafe 클래스에 getCoffee 메소드를 통해 커피를 받을 수 있습니다. 

물론 주문과 준비 과정이 모두 진행되었고, 고객은 마시기만 하면 됩니다.


아래와 같이 말이죠.


1
2
3
4
5
6
7
8
9
10
11
12
Cafe cafe = new Cafe();
        
// 아메리카노 주문
Coffee coffee = cafe.getCoffee(Cafe.MENU.AMERICANO);
// 아메리카노 소비
coffee.drink();
 
 
// CONSOLE LOG
// 아메리카노를 주문!
// 아메리카노를 준비중!
// 맛있는 커피 아메리카노
cs


이 코드는 잘 작동합니다. 하지만 조금의 요구사항을 더 생각해보겠습니다.


첫 번째, Coffee 클래스의 주문, 준비 과정이 고객에게 노출되어 있습니다. 

고객은 어떻게 커피를 사용할 지 모르니,  drink 만 사용할 수 있도록 하고 싶습니다.


두 번째, 이 프로그램은 잘 팔려서 여러 카페업체가 구매하고 싶다고 합니다. 

그러나 각 카페 마다 고유 스타일의 아메리카노와 라떼가 있다고 하는군요.



3. 제품 클래스 메소드 숨기기


일단 커피클래스의 메소드들 중, 생산과정에 속하는 order, prepare 메소드는 고객이 남용하지 않았으면 좋겠습니다. 

이 메소드들은 오직 카페의 바리스타가 커피를 제작하기 위해 필요한 메소드입니다.


즉 이 메소드들이 카페에만 필요하다면, 아래와 같이 리팩토링해볼 수 있을 것 같습니다.


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
/**
 * 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class Cafe {
 
    /**
     * 내부 클래스로 정의하여, order 와 prepare 를 Cafe 내부에서만 사용하도록 처리
     * 
     * Created by Doohyun on 2017. 6. 2..
     */
    public static class Coffee {
        /**
         * 주문하기
         */
        private final void order() {
            System.out.println(String.format("%s를 주문!"toString()));
        }
 
        /**
         * 커피를 준비
         */
        private final void prepare() {
            System.out.println(String.format("%s를 준비중!"toString()));
        }
 
        /**
         * 커피 마시기
         */
        public final void drink() {
            System.out.println(String.format("맛있는 커피 %s"toString()));
        }
    }
}
cs


Coffee 클래스를 Cafe의 내부 클래스로 두고, 준비과정에 속한 두 메소드의 접근 권한을 private 으로 하였습니다. 즉 order 와 prepare 는 Cafe 내부에서만 사용할 수 있습니다.



하지만, 만약 Coffee 의 준비과정 메소드들이 고객에게는 노출되지 않길 바라지만 여러 클래스에서 사용해야한다면 자바의 접근제한 중 default 형식을 고려해볼 수 있습니다.


default 형식은 아무 접근제한도 안쓰면 됩니다. 

이 메소드들은 Coffee 와 같은 패키지에 들어있는 클래스들만 사용가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Coffee {
    /**
     * 디폴트 접근제한, 오직 같은 패키지에 있는 클래스만 사용가능
     */
    final void order() {
        System.out.println(String.format("%s를 주문!"toString()));
    }
 
    /**
     * 디폴트 접근제한, 오직 같은 패키지에 있는 클래스만 사용가능
     */
    final void prepare() {
        System.out.println(String.format("%s를 준비중!"toString()));
    }
 
    /**
     * 커피 마시기
     */
    public final void drink() {
        System.out.println(String.format("맛있는 커피 %s"toString()));
    }
}
 
cs



4. 브랜드 분리를 위한 리팩토링


브랜드마다 고유 스타일의 아메리카노와 라떼가 존재한다는 사실을 인지하였습니다. 


이 사실과 더불어 당연한 것은 브랜드마다 카페가 있어야하고, 그 카페에서는 해당 브랜드의 커피가 생산되어야 합니다.


우리는 조금의 리팩토링으로 이 요구사항을 수용할 수 있을 것으로 보입니다.



- 브랜드 고유 스타일 커피 만들기.


예를들어, 제작해 볼 스타벅스 아메리카노는 굳이 Coffee 클래스를 상속받을 것 없이 이미 구현된 아메리카노를 상속받을 수 있습니다. 아메리카노의 고유 값인 toString 메시지에 약간의 행위를 추가을 해줍시다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 스타벅스 스타일 아메리카노
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksStyleAmericano extends Americano {
 
    /**
     * 부모의 결과에 자신의 색깔을 치장하여 결과를 출력
     * 
     * @return
     */
    @Override
    public String toString() {
        return "스타벅스 스타일" + super.toString();
    }
}
cs


이와 같이 부모의 메소드를 재정의는 하지만, 부모의 결과에 행위를 추가하는 것은 일종의 데코레이터 패턴이라고 볼 수 있을 것 같습니다.


-> (잘못된 언급! 치장하고자 하는 대상객체의 행위추가는 맞지만, 정확히 has-a 관계로 처리하는 일반적인 데코레이터 패턴은 아닙니다.)

정확히, 데코레이터 패턴에 대해 알고 싶다면 아래 포스팅을 참고! 





- 브랜드 고유 카페 만들기.


브랜드 고유 스타일 커피를 만들었으니, 브랜드 고유 카페를 만들어볼 차례입니다. 


주목할 부분은 Cafe 클래스의 getCoffee 입니다. 


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
/**
 * 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public class Cafe {   
    /**
     * 커피 인스턴스 생성
     *
     * @param code
     * @return
     */
    public final Coffee getCoffee(final String code) {
        
        // 커피 종류에 따라 인스턴스를 생성
        // 브랜드 카페마다 달라져야할 부분
        final Coffee coffee;
        {
            switch (code) {
                case MENU.AMERICANO:
                    coffee = new Americano();
                    break;
 
                case MENU.LATTE:
                    coffee = new Latte();
                    break;
 
                default:
                    throw new RuntimeException("[에러] 코드가 정확하지 않음!!!");
            }
        }
 
        // 커피 주문을 위한 전처리 작업
        // 브랜드 카페마다 동일한 부분
        {
            coffee.order();
            coffee.prepare();
        }
 
        return coffee;
    }
}
cs


프로그램 작성 원칙 중 가장 중요한 것 중 한 가지는 "변하는 부분과 변하지 않는 부분을 분리" 하는 것이었습니다.


기존 구현된 getCoffee 메소드 중 커피 Instance 를 생성하는 부분은 브랜드마다 달라지는 부분입니다. 반면, 커피를 준비하는 작업은 변하지 않는 부분이죠.


변한다고 생각하는 부분을 추상화시키도록 하죠. 아래와 같이 말이죠.


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
/**
 * 카페 클래스
 *
 * Created by Doohyun on 2017. 6. 2..
 */
public abstract class Cafe {
 
    /**
     * 커피 인스턴스 생성
     *
     * @param code
     * @return
     */
    public final Coffee getCoffee(final String code) {
        // 커피 종류에 따라 인스턴스를 생성
        final Coffee coffee = createCoffee(code);
        
 
        // 커피 주문을 위한 전처리 작업
        {
            coffee.order();
            coffee.prepare();
        }
 
        return coffee;
    }
 
    /**
     * 각 제조사의 커피가 등장
     *
     * @param code
     * @return
     */
    protected abstract Coffee createCoffee(String code);
}
 
cs


추상 메소드인 createCoffee 를 제작하였고, 이 메소드를 기존의 getCoffee 가 사용하도록 하였습니다. 실제 브랜드 스타일 커피를 만드는 것은 Cafe 의 구체화 클래스가 할 일인 것이죠. 


아래는 스타벅스카페라는 카페의 구체화 클래스를 명시하였습니다.


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
/**
 * 스타벅스 카페
 * 
 * Created by Doohyun on 2017. 6. 2..
 */
public class StarbucksCafe extends Cafe{
 
    public static final String PREFIX = "스타벅스 스타일";
 
    private static class ManagerHolder {
        private static StarbucksCafe instance = new StarbucksCafe();
    }
 
    /**
     * 스타벅스 카페 클래스의 인스턴스가 여러개일 필요는 없어보임
     * 
     * <pre>
     *     - 싱글톤으로 제작하여, 어떤 모델클래스에서도 전역적으로 사용하도록 하면 좋을 것으로 생각
     *     - 컴포넌트화
     * </pre>
     * 
     * @return
     */
    public static final StarbucksCafe GetInstance() {
        return ManagerHolder.instance;
    }
 
    /**
     * 스타벅스 스타일의 커피만 생산
     *
     * @param code
     * @return
     */
    @Override
    protected Coffee createCoffee(String code) {
        Coffee coffee;
        switch (code) {
            case MENU.AMERICANO:
                coffee = new StarbucksStyleAmericano();
                break;
 
            case MENU.LATTE:
                coffee = new StarbucksStyleLatte();
                break;
 
            default:
                throw new RuntimeException("[에러] 코드가 정확하지 않음!!!");
        }
        return coffee;
    }
}
 
cs


createCoffee 메소드는 스타벅스 스타일의 커피만 출력하도록 하였습니다.


추가로 살펴볼 점은 스타벅스 카페 클래스는 싱글톤입니다. 

스타벅스 카페의 인스턴스가 여러 개 있어야 할 이유는 찾아볼 수 없으며

공장 클래스의 목적답게 어느 모델 클래스(Spring 의 Service 등)에서나 전역적으로 사용하여 커피를 생산할 수 있다면 편리할 것입니다.


이제 모든 것을 다 만들었습니다. 스타벅스 카페 클래스가 잘 돌아가는지 확인해 볼까요?


1
2
3
4
5
6
7
Cafe.Coffee coffee = StarbucksCafe.GetInstance().getCoffee(Cafe.MENU.AMERICANO);
coffee.drink();
 
// CONSOLE LOG
// 스타벅스 스타일아메리카노를 주문!
// 스타벅스 스타일아메리카노를 준비중!
// 맛있는 커피 스타벅스 스타일아메리카노
cs


다행히, 잘 작동을 하는 것 같습니다. 


우리는 저번 실습에서 이 틀에 맞게 또 다른 브랜드를 쉽게 표현할 수 있었습니다. 

이미 작성된 클래스의 수정 없이 추가로만 말이죠. (OCP : 개방-폐쇄의 원칙)



다들 눈치를 조금 챘을 수도 있겠지만, 

Cafe 클래스의 getCoffee 와 createCoffee 를 보면 비슷한 알고리즘에서 특정부분만 다를 때 사용하던, Template method 패턴을 생각해볼 수 있습니다. 

(getCoffee 는 Concrete class 이고, createCoffee 라는 추상 메소드를 사용하고 있죠. ^^;)


이 패턴의 이름이 Factory-Method 인 이유를 조금 이해가 가시나요?


이와 같이 사실 패턴들은 결국 비슷한 꼴이며, 같은 목적을 가지고 있습니다. 

추상화할 부분을 잘 추상화하고, 클래스 간의 의존관계를 잘 명시하면 되는 것이죠. ㅡㅡ^


굳이 이 구조가 무슨 패턴이다를 외울 필요는 없으며, 앞 세대의 개발자들이 고민하며 고안해낸 좋은 구조를 리뷰본다는 생각으로 접하면 더 좋을 것 같습니다. 


반대로 패턴을 외워, 이 패턴을 적용해봐야지라는 마음은 살짝 어긋난 방입니다. 


OOP 스터디의 목적은 좋은 코드를 리뷰하는 것이고, 

이로 인해 좋은 코드에 대한 안목과 실력이 쌓이길 바랍니다. 




반응형
Posted by N'

지난 시간 Instance 수를 제한하는 Singleton 패턴을 배웠었고, 과제로 한 개의 Instance 만 제한하는 것이 아닌 특정 개수만큼의 Instance 를 제한하는 Multiton 을 만들어 보도록 하였습니다.


관련 내용은 아래 포스팅을 참고!



똑똑한 우리 스터디 구성원 모두 과제를 잘해왔고, 정리하는 차원에서 제가 만든 Multiton 을 Review 해보도록 하겠습니다.


일단 가정은 요구사항의 SetInstanceCount 시점에 Instance pool 을 만드는 것이 아닌(Eager-binding), 개수만 먼저 제한하고 필요시점마다 Instance 를 생성하는 게으른 할당(Lazy-binding)으로 제작해 보겠습니다.


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
57
58
59
60
61
62
63
64
public class Multiton {
    // INSTANCE POOL 관리.
    // 어떤 세팅도 하지 않았으면, Singleton 으로 관리하도록 처리.
    private static int INSTANCE_LIMIT_COUNT = 1;
 
    // Instance pool.
    private static HashMap<Integer, Multiton> POOL = new HashMap<>();
 
    private static Integer toggleCount = 0;
 
    private String toStringMessage;
 
    /**
     * 외부에서 Instance 를 만들지 못하도록 접근제한.
     */
    private Multiton(){
        // Instance 의 생성과 함께 POOL 에 넣는다.
        Integer number = POOL.size();
 
        toStringMessage = String.format("%d번 인스턴스", number);
        POOL.put(number, this);
    }
 
    /**
     * Instance 출력.
     *
     * <pre>
     *     Lock 을 메소드에 모두 걸어 Thread-safe 를 보장.
     * </pre>
     *
     * @return
     */
    public static synchronized Multiton GetInstance() {
        final Multiton result;
 
        if (POOL.size() < INSTANCE_LIMIT_COUNT) {
            // Instance 개수가 아직 제한된 POOL 만큼 생성안된 경우.
            result = new Multiton();
        } else {
            // Instance 개수가 아직 제한된 POOL 만큼 생성 된 경우.
            result = POOL.get(toggleCount);
            toggleCount = (toggleCount + 1) % INSTANCE_LIMIT_COUNT;
        }
 
        return result;
    }
 
    /**
     * 인스턴스의 개수 제한
     *
     * @param limitCount
     */
    public static final void SetInstanceCount(int limitCount) {
        if (limitCount < 0) {
            throw new RuntimeException("[에러] Instance 개수는 0보다 커야 합니다.");
        }
        INSTANCE_LIMIT_COUNT = limitCount;
    }
 
    @Override
    public String toString(){
        return toStringMessage;
    }
}
cs


코드 내부의 구체적인 주석을 명시 하였고, 주의깊게 봐야할 부분은 아래와 같습니다.


1. Thread-safe 보장을 위해 Instance 를 유일하게 외부로 내보낼 수 있는 GetInstance 메소드에 synchronized 키워드를 달았습니다.


2. 사용자가 SetInstanceCount 를 사용하지 않을 수 있기 때문에 Instance 개수 제한을 명시적으로 1로 초기화하였습니다.

(INSTANCE_LIMIT_COUNT = 1)


이렇게 제작된 코드를 Thread-safe 가 보장이 되는지 확인해보도록 하겠습니다.


아래 테스트는 Thread 를 50 개 제작하여, 실행하는 메소드입니다. 각 스레드에서는 각 Instance 의 toStringMessage 를 출력하도록 합니다.


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
// Thread Test using JAVA7
{
    System.out.println("멀티톤 테스트!!");
    Multiton.SetInstanceCount(5);
 
    // Thread 목록 생성.
    final ArrayList<Thread> multiThreadTestList = new ArrayList<>();
    {
         for (int i = 0; i < 50++i) {                    
            Thread newThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Multiton.GetInstance());
                }
            });
 
            multiThreadTestList.add(newThread);
        }
    }
 
    // Thread 실행
    {
        // Thread 생성 즉시 바로 실행하지 않고, 목록을 생성하고 실행하는 이유는 최대한 동시에 Thread 를 실행하고 싶기 때문!
        // Thread Instance 를 만드는 시간 때문에 제대로 테스트가 안될 가능성 존재.
        for (Thread thread : multiThreadTestList) {
            thread.start();
        }
    }
}
 
// Thread Test using JAVA8
{
    IntStream.rangeClosed(050).mapToObj(n -> new Thread(() -> System.out.println(Multiton.GetInstance()))).forEach(Thread::start);
}
 
// CONSOLE LOG
// 멀티톤 테스트!!
// 0번 인스턴스
// 1번 인스턴스
// 2번 인스턴스
// 3번 인스턴스
// 4번 인스턴스
// 1번 인스턴스
// 3번 인스턴스
cs


CONSOLE LOG 를 보면, Instance 가 꼭 순서대로 나오지는 않는 것을 볼 수 있습니다. 

비록 GetInstance 에서 LOCK 처리는 하였지만 Thread 의 실행 상태에 따라 순서가 바뀔 수는 있습니다. 

순서를 보장해야만 한다면 Blocking 처리 등 더 복잡한 과정이 필요하겠지만, 이번 주제는 Instance 의 개수를 제한하는 것이니 다루지 않을 예정입니다.


이 블로그가 스터디를 참여하고 있는 모두에게 좋은 정보가 되길 바랍니다. :-)

반응형
Posted by N'

이번 주에는 실무에서 아마 가장 많이 사용하는 패턴 중 하나인 Singleton 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고!



물론 웹 프로젝트에서는 직접 Singleton 을 만들기보다는 Bean 등록을 통해 Component 들을 사용하지만, 안드로이드 프로젝트 혹은 자바가 아닌 다른 환경에서는 직접 만들어야 할 일이 있을 수 있습니다.


그렇기 때문에 세 번째 실무 주제로 Singleton 을 선택하였습니다.


Singleton 은 클래스 내에서 한 개의 instance 만 생성하여, 그 인스턴스를 광역적으로 사용하기 위한 패턴입니다. 

(웹 프로젝트에서는 각종 Component 를 Controller 가 광역적으로 사용하고 있습니다.)


1. Singleton 의 기본적인 컨셉


기본적인 Singleton 의 컨셉은 아래와 같이 정의해 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스는 클래스 내부에서 공용으로 한개만 관리.
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


instance 를 한 개만 생성하고 관리하는 것이 목적이므로, 클래스 내부에서 instance 를 공용으로 관리하고 instance 를 접근할 수 있는 정적 메소드를 제공해줍니다.


하지만 이 기본적인 컨셉은 안타깝게도 동시성 처리에 취약합니다.

내부 GetInstance 를 동시에 여러 Thread 가 접근할 시, instance 의 null 체크의 Thread-safe 를 보장할 수 없습니다.



2. Thread-safe 한 Singleton


Singleton 의 Thread-safe 문제를 해결하기 위해 생각해 볼 수 있는 방법은 공용으로 관리하려는 instance 에 대한 명시적 초기화를 해주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
 * 명시적 초기화한 싱글톤 예제
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {}
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
}
cs


클래스 로드 타임에 미리 instance 가 생성되기 때문에 Thread-safe 합니다. 

이런  방식을 Eager-binding (부지런한 할당)이라 합니다. 하지만 Eager-binding  방식은 싱글톤의 instance 를 사용하지 않음에도 바로 초기화가 되어버릴 수 있습니다.


아래 예제 처럼 말이죠.


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
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = new Singleton();
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
        System.out.println("Create Singleton!!");
    }
 
    // 인스턴의 접근 제한은 아래 메소드로만 가능.
    public static Singleton GetInstance() {
        return instance;
    }
 
    /**
     * 싱글톤의 정적 메소드
     */
    public static void RunStaticTask() {
    }
}
 
Singleton.RunStaticTask();
 
// CONSOLE LOG
// Create Singleton!!
cs


하지만 만약 Singletone 을 만들기 위한 비용이 크고, 많은 Singleton 들이 Eager-binding을 시도한다면 부하가 클 수도 있습니다. 즉 Instance 를 사용할 때 생성하는 Lazy-binding(게으른 할당)이 필요해 보입니다.


Eager-binding 이 무조건 나쁜 것은 아닙니다. 서버 프로젝트의 경우 오히려 서버가 켜질 때 각 컴포넌트들이 미리 Instance 를 생성하고, Client 의 요청에 딜레이 없이 바로 응답해주는 것이 좋습니다.


3. Lazy-binding 방식의 Singleton 생성


다시 기본적인 컨셉으로 돌아와서 생각해 봤을 때, 직관적인 방법은 오직 한 개의 스레드 만 접근하도록 Lock 을 걸어주는 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (1)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 인스턴스의 명시적 초기화
    private static Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // synchronized 로 Lock 을 건다.
    // 이 메소드는 오직 한 스레드 씩 접근 가능
    public static synchronized Singleton GetInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
 
        return instance;
    }
}
cs


메소드에 Lock 을 걸었기 때문에 Thread-safe 함은 보장합니다. 

하지만 매번 사용할 때마다 한 개의 Thread 만 접근 가능하다는 것은 조금 불편한 진실인 듯 합니다. ㅡㅡ^


그렇기 때문에 정말 Thread-safe 한 부분만 Lock 이 걸리도록 변경해보려 합니다. Thread-safe 함이 보장되야할 부분은 instance 의 생성 부분입니다.


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
/**
 * 스레드의 Lock 을 이용한 싱글톤 예제 (2)
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // 스레드 간 최산 값을 읽어주기 위해 volatile 을 이용한 가시성 변수 사용
    private static volatile Singleton instance = null;
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
    
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        if (instance == null) {
            // instance 의 생성 부분만을 Lock 처리
            synchronized (Singleton.class) {
                instance = new Singleton();
            }
        }
 
        return instance;
    }
}
cs


하지만 이 방식은 가시성 변수를 사용하고 있습니다. 

instance 변수는 모든 스레드가 최신 값을 읽게 하기 위해 CPU 의 캐시를 사용하지 않으며, 그렇기 때문에 무조건 메모리로부터 데이터를 불러오게 됩니다.


- 간략한 운영체제 속 내용 소개 ㅡㅡ^


데이터는 모두 메모리(Heap, Data, Stack 영역) 위에 저장되어 있고, 보통은 CPU가 계산을 하기 위해 데이터를 읽어온 뒤 캐싱 작업을 합니다. 

캐싱작업을 하는 이유는 CPU 에서 미리 데이터를 저장함으로써, 보다 빠르게 읽고 계산하기 위해서죠.


하지만 여러 스레드가 변수를 동시접근을 하여 데이터를 조작하면, CPU 캐시의 값이 최신 값임을 보장할 수 없게 됩니다. 

즉 변경된 값을 못 읽어올 수 있죠.


그렇기 때문에 변수에 volatile 키워드를 붙여 가시성 변수임을 선언하며, 그 변수는 항상 메모리에서 읽어오게 됩니다. 

하지만 CPU 의 캐시에서 읽어오는 것보다 비용이 언제나 크겠죠? ㅜㅡㅜ



3. Lock 을 사용하지 않은 Lazy-bindig 방식의 Singleton


MARYLAND 대학의 연구원인 Bill puge 는 Lock 을 사용하는 방식은 단점을 해결하기 위해 새로운 Singleton 생성방식을 고안했습니다.


그 방식의 이름은 "Initialization on demand holder idiom" 으로 내부 클래스를 이용한 Holder 를 사용하는 기법은 클래스 로더 규칙에 의해 Lazy-binding 을 보장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Initialization on demand holder idiom 방식의 싱글톤 생성
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public class Singleton {
    // Instance 를 감싸는 홀더 생성. 클래스로더 규칙에 의해, ManagerHolder 가 로드될 때 Instance 생성
    private static final class ManagerHolder {
        private static final Singleton Instance = new Singleton();
    }
 
    // 외부에서 인스턴스를 생성 못하도록 접근제한
    private Singleton() {
    }
 
    // 개선된 Lock 처리
    public static Singleton GetInstance() {
        return ManagerHolder.Instance;
    }
}
cs


내부에 있는 ManagerHolder 는 Private 으로 접근제한 되어 있으며, 오직 GetInstance 를 통해서만 사용할 수 있습니다. 즉 GetInstance 가 호출 될 때 ManagerHolder 클래스가 로드되며 그 순간 명시적 초기화를 하고있는 내부의 Instance 가 생성됩니다.



4. Enum 을 사용하는 Singleton


Enum(열거형) 을 통해서도 Singleton 을 만들 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
/**
 * ENUM 을 이용한 싱글톤
 *
 * Created by Doohyun on 2017. 5. 14..
 */
public enum Singleton {
    INTANCE;
    
    public void doSometing(){}
}
 
// Enum 싱글톤 사용법
Singleton.INTANCE.doSometing();
cs


간결한 사용법 때문에 최근 권장하는 스타일입니다. 


이 방식은 열거형 특성 상, 컴파일 시점에 미리 Instance 가 생성되기 때문에 Thread-safe 함을 보장합니다. 하지만 무조건 Eager-binding 입니다.


Singleton 을 만드는 여러 방식을 알아보았습니다. 우리는 Spring 이 아니더라도 Singleton 을 직접만들어 사용할 수 있게 되었습니다. 


하지만 주의할 것은 스터디에서도 살펴본 것 처럼 싱글톤 병에는 안 걸리게 조심합시다. ㅡㅡ^

반응형
Posted by N'

지난 주에 이어, 계속 디자인 패턴 종류를 실습하고 있습니다.


이번 주에는 지난 스터디에서 실습한 전략패턴과 비슷한 구조의 Template Method 패턴을 다뤄보았습니다.


자료는 아래 포스팅에서 참고! 



이번 추가 내용 포스팅 역시 스터디에서 진행한 코드를 기반으로 진행하고자 합니다.


Template Method 패턴 역시 변경소지가 있는 부분을 분리하고, 위임을 통해 각 클래스가 행위를 하도록 하는 전략패턴과 비슷한 구조를 취하고 있습니다. 차이점은 전략패턴의 경우 각 클래스의 행위가 크게 다른 반면, Template Method 의 경우에는 크게 보면 비슷한 알고리즘을 사용하지만 세부 행위가 조금씩 다를 경우 사용할 수 있는 패턴입니다.


조금 더 쉬운 예제를 생각해보면, 우리가 흔히 사용하는 Collections 클래스의 sort 는 동일한 정렬 알고리즘을 사용하지만 비교하는 부분인 Comparable 은 다르기 때문에 따로 구체화하여 사용합니다.


Template Method 의 정의는 이 정도로 하고, 스터디에서 진행했던 코드를 리뷰해보도록 하겠습니다.


우리는 DB 접근의 편의성 때문에 ORM 을 사용하곤 합니다.

ORM 을 사용하면, Table 과 실제 객체간의 관계를 호스트 코드에서 쉽게 관리할 수 있기 때문에 생산성 면에서 좋다고 생각할 수 있습니다.


하지만 종종 아래와 같이 비슷한 구조의 필드를 가진 테이블이 존재할 수 있으며, ORM 클래스들은 아쉽게도 다른 테이블을 표현한 클래스들과 상속 또는 구현 관계를 취할 수 없습니다.


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
/**
 * 제너레이트 클래스 예제1
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel1 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
    private String commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    private String model1SpecialField1;
    private String model1SpecialField2;
    private String model1SpecialField3;
 
    // setter, getter 는 생략
}
 
/**
 * 제너레이트 클래스 예제2
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class GenerateModel2 {
    // 공통필드
    private Integer memberSubjectSn;
    private String name;
 
    private String commonField1;
    private String commonField2;
    private String commonField3;
    private String commonField4;
 
    // 클래스 고유 필드
    private String model2SpecialField1;
    private String model2SpecialField2;
    private String model2SpecialField3;
    private String model2SpecialField4;
 
    // setter, getter 는 생략
}
cs


ORM 의 특성 상 ORM 라이브러리에 의존해서 데이터의 CRUD 를 하기 위해서는 위의 클래스를 이용하는 방법밖에 없습니다. 그렇기 때문에 우리는 비지니스 로직 처리 중 이러한 문제를 겪을 수 있습니다.


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 * 
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        createByGenerateModel1(addTargetMemberSnList, excludeTargetMemberSnList);
 
        // GenericModel2 로직.
        createByGenerateModel2(addTargetMemberSnList, excludeTargetMemberSnList);
    }
 
    /**
     * GenerateModel1 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel1(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel1> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel1 generateModel1 = new GenerateModel1();
                generateModel1.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel1);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel1> existList = model1Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel1 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel1 model1 : groupByMemberSnMemberMap.values()) {
                model1Dao.create(model1);
            }
 
            // 제외대상 삭제.
            model1Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * GenerateModel2 에서
     *
     * <pre>
     * "addTargetMemberSnList" 의 구성원를 저장하고,
     * "excludeTargetMemberSnList" 를 제외한다.
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    private void createByGenerateModel2(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList){
 
        /**
         * 새로 저장된 대상자를 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, GenerateModel2> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel2);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<GenerateModel2> existList = model2Dao.selectList(groupByMemberSnMemberMap.keySet());
                for (GenerateModel2 model1 : existList) {
                    excludeTargetMemberSnSet.add(model1.getMemberSubjectSn());
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (GenerateModel2 model2 : groupByMemberSnMemberMap.values()) {
                model2Dao.create(model2);
            }
 
            // 제외대상 삭제.
            model2Dao.deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
}
 
cs


해당 서비스에서는 Model1, Model2 에 추가하고자하는 대상자순번 목록과 삭제하고자하는 대상자 순번 목록을 파라미터로 받고 있습니다.


내부 로직인 createByGenerateModel1, createByGenerateModel2 에서는 


1. 추가하고자 하는 대상자순번으로 추가대상 ORM 객체를 생성.

2. 이미 있는 데이터를 조회 후, 추가대상에서 제외

3. 제외대상에 있는 데이터를 추가대상에서 제외

4. 최종으로 남아있는 추가대상 데이터를 저장하고, 제외대상 데이터를 삭제


하고 있습니다.


두 메소드의 내부역할은 비슷한 알고리즘을 사용하지만,

두 ORM 클래스는 상속관계를 가질 수 없고, 더군다나 데이터 관리 Dao 도 다르기 때문에 copy&paste 로 다시 정의하게 되었습니다.


하지만 우리는 이 메소드를 작성하면서, 코드 내에서 변화하는 부분과 변하지 않는 부분을 찾아볼 수 있었습니다. 

(단순하게 바로 패턴을 적용하기 보다는 이 것을 찾아내는 능력이 중요합니다. ㅡㅡ^)


두 메소드는 동일한 알고리즘을 사용하지만 아래와 같은 차이점을 찾아낼 수 있습니다.


- ORM 객체를 만들어내는 과정.

- 이미 있는 데이터를 찾아내는 과정.

- 데이터를 추가하거나 삭제하는 과정.


즉 우리는 변하지 않는 알고리즘 베이스는 남기고, 변화하는 부분을 추상적으로 생각할 수 있을 것 같습니다. 다시말하면, 변하지 않는 부분을 정의하는 클래스변하는 부분을 구체화시킨 클래스를 작성하여 각 행위에 대한 책임을 나눠볼 수 있을 것 같습니다. 

(책임의 분리면에 있어서 SRP 를 생각해볼 수 있습니다. ㅡㅡ^) 


일단은 알고리즘 베이스에서 변화하는 부분을 추상화시킨 베이스 클래스를 아래와 같이 제작해 볼 수 있을 것 같습니다.


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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
/**
 * Model 객체의 알고리즘을 템플릿메소드화 시킨 베이스 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public abstract class BaseTargetComponent<T> {
 
    /**
     * 해당 테스크를 통해 DB 에 데이터를 저장.
     *
     * <pre>
     *     해당 메소드를 자식클래스들이 재정의하지 못하도록 final 화
     * </pre>
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public final void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
        /**
         * 새로 저장할 구성원을 순번으로 그룹핑한다
         *
         * key : 구성원순번
         * value : 모델 객체
         */
        final HashMap<Integer, T> groupByMemberSnMemberMap = new HashMap<>();
        {
            for (Integer memberSn : addTargetMemberSnList) {
                // 일단은 MemberSn 만 넣는다고 가정.
                T generateModel = toGenerateModel(memberSn);
                groupByMemberSnMemberMap.put(memberSn, generateModel);
            }
        }
 
        // 이미 존재하는 구성원이거나 제외대상자는 입력 대상에서 제외.
        {
            // 이미 존재하는 구성원순번 또는 제외 타겟 순번 집합.
            HashSet<Integer> excludeTargetMemberSnSet = new HashSet<>();
            {
                // 이미 존재하는 구성원 순번 목록 삽입.
                List<T> existList = selectList(groupByMemberSnMemberMap.keySet());
                for (T model : existList) {
                    excludeTargetMemberSnSet.add(toMemberSubjectSn(model));
                }
 
                // 제외 대상 파라미터도 추가.
                excludeTargetMemberSnSet.addAll(excludeTargetMemberSnList);
            }
 
            // 추가대상 그룹에서 제외 대상 집합을 삭제한다.
            groupByMemberSnMemberMap.keySet().removeAll(excludeTargetMemberSnSet);
        }
 
        // 데이터 트랜잭션
        {
            // 데이터 삽입.
            for (T model : groupByMemberSnMemberMap.values()) {
                insertData(model);
            }
 
            // 제외대상 삭제.
            deleteByMemberSnList(excludeTargetMemberSnList);
        }
    }
 
    /**
     * 구성원 주체순번으로 모델 생성.
     *
     * @param memberSn
     * @return
     */
    protected abstract T toGenerateModel(Integer memberSn);
 
    /**
     * 모델로부터 구성원주체순번 추출
     *
     * @param t
     * @return
     */
    protected abstract Integer toMemberSubjectSn(T t);
 
    /**
     * 구성원 순번목록으로 모델 데이터 조회
     *
     * @param memberSnList
     * @return
     */
    protected abstract List<T> selectList(Collection<Integer> memberSnList);
 
    /**
     * 데이터 추가.
     *
     * @param t
     */
    protected abstract void insertData(T t);
 
    /**
     * 구성원순번 목록으로 데이터를 삭제.
     *
     * @param memberSnList
     */
    protected abstract void deleteByMemberSnList(List<Integer> memberSnList);
}
 
cs


앞서 살펴본 변화하는 후보 요구사항들은 추상 메소드로 정의하였으며, 변하지 않는 알고리즘은 정의된 추상 메소드를 사용하도록 하였습니다.


주목할 점은 알고리즘이 정의된 메소드는 final 입니다. 

자식클래스들이 혹시나 알고리즘이 정의된 메소드를 재정의할 수 없도록 의도적으로 막았습니다.


이제 정의된 베이스 클래스를 사용하여, Model1 과 Model2 에 특화된 클래스를 만들어 볼 수 있을 것 같습니다. 아래와 같이 말이죠!


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
57
58
59
60
61
62
63
64
65
66
67
/**
 * GenerateModel1 에 특화된 구체화 클래스.
 *
 * Created by Doohyun on 2017. 5. 12..
 */
public class Model1Compoent extends BaseTargetComponent<GenerateModel1> {
 
    private Model1Dao model1Dao;
 
    /**
     * 구성원순번 을 이용하여, GenerateModel1 을 생성한다.
     *
     * @param memberSn
     * @return
     */
    @Override
    protected GenerateModel1 toGenerateModel(Integer memberSn) {
 
        GenerateModel1 generateModel1 = new GenerateModel1();
        generateModel1.setMemberSubjectSn(memberSn);
 
        return generateModel1;
    }
 
    /**
     * GenerateModel1 으로 부터 구성원순번을 추출한다.
     *
     * @param model1
     * @return
     */
    @Override
    protected Integer toMemberSubjectSn(GenerateModel1 model1) {
        return model1.getMemberSubjectSn();
    }
 
    /**
     * 구성원순번 목록을 이용하여, 모델목록을 조회한다.
     *
     * @param memberSnList
     * @return
     */
    @Override
    protected List<GenerateModel1> selectList(Collection<Integer> memberSnList) {
        return model1Dao.selectList(memberSnList);
    }
 
    /**
     * 데이터를 추가한다.
     *
     * @param model1
     */
    @Override
    protected void insertData(GenerateModel1 model1) {
        model1Dao.create(model1);
    }
 
    /**
     * 구성원순번 목록으로 데이터를 삭제한다.
     *
     * @param memberSnList
     */
    @Override
    protected void deleteByMemberSnList(List<Integer> memberSnList) {
        model1Dao.deleteByMemberSnList(memberSnList);
    }
}
 
cs


Model1Component 는 변화하는 추상적인 개념을 Model1 클래스 전용으로 구체화만 시킨 클래스입니다. 


이제 아래와 같이 ModelSampleService 의 로직을 작성된 컴포넌트를 활용하여 완성해보도록 하죠.


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
57
58
59
60
61
62
63
64
65
66
67
68
/**
 * Model1 과 Model2 를 사용하는 서비스 클래스
 *
 * Created by Doohyun on 2017. 5. 14..
 */
@Service
public class ModelSampleService {
    @Autowired
    private Model1Dao model1Dao;
 
    @Autowired
    private Model2Dao model2Dao;
 
    private Model1Compoent model1Compoent;
 
    /**
     * 데이터를 저장한다.
     *
     * @param addTargetMemberSnList
     * @param excludeTargetMemberSnList
     */
    public void createByGenerateModel(
            List<Integer> addTargetMemberSnList
            , List<Integer> excludeTargetMemberSnList) {
 
        // GenericModel1 로직.
        model1Compoent.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
 
        /**
         * GenericModel2 로직.
         *
         * <pre>
         *     구체화상태를 즉시 정의하는 방식.
         *     함수형 프로그래밍에서는 기존의 클래스에만 의존하던 동작을 즉시 정의하는 것이 핵심.
         * </pre>
         */
        new BaseTargetComponent<GenerateModel2>() {
            @Override
            protected GenerateModel2 toGenerateModel(Integer memberSn) {
 
                GenerateModel2 generateModel2 = new GenerateModel2();
                generateModel2.setMemberSubjectSn(memberSn);
 
                return generateModel2;
            }
 
            @Override
            protected Integer toMemberSubjectSn(GenerateModel2 generateModel2) {
                return generateModel2.getMemberSubjectSn();
            }
 
            @Override
            protected List<GenerateModel2> selectList(Collection<Integer> memberSnList) {
                return model2Dao.selectList(memberSnList);
            }
 
            @Override
            protected void insertData(GenerateModel2 generateModel2) {
                model2Dao.create(generateModel2);
            }
 
            @Override
            protected void deleteByMemberSnList(List<Integer> memberSnList) {
                model2Dao.deleteByMemberSnList(memberSnList);
            }
        }.createByGenerateModel(addTargetMemberSnList, excludeTargetMemberSnList);
    }
}
cs


리팩토링한 코드의 라인 수만 본다면, 기존 코드에 비해 코드가 크게 줄어들지는 않았습니다. 오히려, 호스트 코드의 파편화가 생겨 더 복잡해졌다고 생각할 수도 있습니다.


하지만, 이 방식은 변하지 않는다고 생각한 알고리즘 코드에 문제가 있다고 생각되었다면 한 번에 모든 코드를 수정할 수 있고, 비슷한 테이블이 생겼다고 했을 경우 확장에도 유연할 수 있습니다. (OCP 역시 지켜졌다고 생각할 수 있습니다.)


추가적으로 Template method 의 전문적인 용어를 조금 말하면, 각각의 정의된 메소드 역할을 아래와 같이 나눠볼 수 있습니다.


- Concrete method 

알고리즘의 뼈대가 작성된 곳입니다. 

final 화하여, 자식클래스들이 상속을 받지 못하게 합니다.


- Abstract method

변하는 부분을 추상적인 형태로 정의합니다. (추상 메소드)

자식클래스들은 모두 이 메소드를 반드시 구현해야 합니다.


- Hooker

Abstract method 와 같이 변하는 부분을 추상적으로 정의합니다.

그러나 자식클래스들은 이 메소드를 선택적으로 구현하도록 합니다.


포스팅을 마치고 나니, 샘플코드의 양이 조금 복잡함을 느꼈습니다.

모르는 부분이 있으면 언제나 질문해도 좋습니다. 피드백을 남겨주면 더욱 감사 :-)





반응형
Posted by N'