이번 스터디에서는 마지막으로 다룰 패턴은 상태의 변화를 관찰하는 관찰자(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'