[CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용3
Observer 패턴에 다루는 마지막 포스팅입니다.
정말, 이 패턴에 대해 할 말이 많군요...
그만큼 많이 사용하고 유명한 패턴이기 때문에 그런 듯 합니다.
(그렇다고, Observer 패턴은 사랑은 아닙니다... ㅡㅡ^)
Observer 패턴과 관련된 정보는 아래에서 참고!
2017/05/18 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 5] 실무 활용 패턴 (하)
2017/06/17 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용
2017/06/18 - [일상/[STUDY] OOP & FP 연구] - [CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용2
앞써, 언급한 바와 같이 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 (o 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 패턴과 관련된 모든 내용이 끝이 났고, 동시에 이번 스터디에서 진행예정이던 모든 패턴이 끝이 났습니다. (ㅜㅡㅜ)
이 포스팅의 결과물 프로젝트는 아래에서 참고!
마지막 패턴이야기라 그런지 유종의 미를 잘 거두고 싶었습니다.
그래서 더욱 자세히 쓰고자 이 패턴에 대해 세 개의 포스팅으로 나누어 작성 했었던 것 같습니다.
주 독자와 나누는 마지막 패턴 이야기라 조금 섭섭하기도 하기도 하고, 더 알려줄 것이 없나 고민도 하게 되었던 것 같습니다. 그래서 빨리 끝낼 수 있는 걸 질질 끌었던 것 같기도.....
특히 최근에 마지막 스터디를 진행하기 전 날, [대학교 1학년]-[SW멤버십 회원]-[M사 신입사원]-[스터디 진행]까지의 있었던 일들이 주마등처럼 지나갔었습니다.
[문과출신 공대생]이라는 핸디캡 극복을 위해 노력했었고, [4학년 동기들 중 2학년이었던 SW멤버십]에서 끝까지 살아남기 위해 발버둥을 쳤으며, [M사 신입사원] 시절 사내에서 아무도 하지 않았기 때문에 누군가의 도움도 기대할 수 없는 솔루션급 앱들을 밤새 만들고 고치며 끝내 좌절하던 경험들은 저에게 뼈아픈 교훈들만 주었던 것 같습니다.
최근에 읽었던 칼럼 중 [지금의 실력은 지식의 총합이 아닌 고통의 총합] 이라는 글은 이 때의 일을 주마등처럼 지나가게 한 촉매가 아니었나 생각이 듭니다.
하지만, 이런 뼈아픈 교훈들은 안타깝게도 급격한 실력의 변화를 주지는 못하던 것 같습니다.
최근에 읽었던 [소프트웨어 장인], [구글 리쿠르팅 데이의 타 회사 세션] 등의 내용을 참고하면, 앞에서 끌어주는 [메이저 주니어 이상급의 개발자]들의 활동은 무시할 수 없어보였습니다. 이를테면 페어 프로그래밍이나 코드 리뷰, 본질에 가까운 OOP&디자인 패턴 교육 등은 주니어들의 레벨을 올리는 것에 크게 기여하는 것 같아 보였습니다.
결론은 모두가 뼈아픈 교훈을 얻어갈 필요는 없는 것 같습니다. :-)
이 블로그의 글들은 대단하지 않으며 꽃길로 인도할 수도 없지만, 조금이나마 진흙을 덜 밟는데 도움이 되길 바랍니다.
감사합니다.
'스터디 > [STUDY] OOP' 카테고리의 다른 글
[CHAPTER X] 리팩토링 + 추가내용 (0) | 2017.06.19 |
---|---|
[CHAPTER X] 리팩토링 & ETC 패턴. (0) | 2017.06.18 |
[CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용2 (0) | 2017.06.18 |
[CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용 (2) | 2017.06.17 |
[CHAPTER 5] 실무 활용 패턴 (하) [Abstract Factory 패턴] + 추가내용 (2) | 2017.06.10 |