지난 포스팅에서는 특정 주제의 변화에 따라 여러 클래스가 어떤 행위를 해야할 때, 즉 이벤트 처리를 하기 위한 대표적인 패턴인 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'