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'