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

백-엔드에서 가장 많이 해야할 행위 중 하나는 VO 들의 호환을 위한 편집일 수 있습니다. 


백-엔드에서는 프론트-엔드로 입맛에 맞게 데이터를 출력하도록 지원할 수도 있고, 백-엔드 간에도 서로 다른 모듈 간 호환을 맞추기 위해 필요합니다.



이런 VO 의 편집 과정은 간단할 수 있지만 약간의 패턴화를 하면, 


- 더욱 흐름에 집중할 수 있는 비지니스 로직 생성


- VO 편집 과정의 재활용 및 수정의 용이성 


등의 효과를 찾아볼 수 있습니다.



이미 제공하는 클래스들과 실제로 필요한 것의 차이를 극복하기 위한 호환과정을 Adapt 라고 정의하며, 고전적으로 보여지는 Adapter 패턴의 예시를 지난 스터디에서 확인해보았습니다.


자료는 아래에서 확인 :-)




1. Adapter 의 필요성


실제 코드의 리뷰 전, 필요한 요구사항부터 정의해 보겠습니다. 


아래와 같은 클래스 A,B 를 정의해보겠습니다.


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
public class A {
 
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
 
    private String field1;
    private String field2;
    private String field3;
    private String field4;
    private String field5;
    private String field6;
 
    // SETTER, GETTER 생략  
}
 
public class B {
    private Integer memberSubjectSn;
    private String name;
    private Integer companySubjectSn;
 
    private String field1;
    private String field2;
 
    // SETTER, GETTER 생략
}
cs


그리고 보통 우리는 어떠한 필요에 의하여, 객체 A 에서 객체 B 로 변환하기 위해 아래와 같은 비지니스 로직을 작성하곤 합니다.


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
public class Service {
 
    /**
     * 데이터 읽기.
     *
     * @return
     */
    public B readData() {
        A a = otherModule.getA();
 
        // something work;
 
        // A 에서 B 로 치환.
        B b;
        {
            b = new B();
            b.setCompanySubjectSn(a.getCompanySubjectSn());
            b.setMemberSubjectSn(a.getMemberSubjectSn());
            b.setName(a.getName());
            b.setField1(a.getField1());
            b.setField2(a.getField2());
        }
 
        return b;
    };
 
    /**
     * 데이터 저장.
     *
     * @param a
     */
    public void saveData(A a) {
        // A 에서 B 로 치환.
        B b;
        {
            b = new B();
            b.setCompanySubjectSn(a.getCompanySubjectSn());
            b.setMemberSubjectSn(a.getMemberSubjectSn());
            b.setName(a.getName());
            b.setField1(a.getField1());
            b.setField2(a.getField2());
        }
 
        // something work;
    }
}
cs


A 에서 B 로 변환하는 과정은 어렵지 않지만, 필요할 때마다 코드의 중복이 일어 납니다. 코드의 중복의 존재는 A 혹은 B 에 변경이 있을 때마다 변환하고 있는 모든 곳을 수정해야함을 의미합니다.


즉 현재 상태는 우아해 보이지 않으며, 코드 중복을 피하기 위해 A에서 B 로 변환하는 메소드를 제공함으로써 아래와 같이 리팩토링을 해볼 수 있습니다.


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
public class Service {
 
    /**
     * 데이터 읽기.
     *
     * @return
     */
    public B readData() {
        A a = otherModule.getA();
 
        // something work;
 
        // A 에서 B 로 치환.
        return adapteAtoB(a);
    };
 
    /**
     * 데이터 저장.
     *
     * @param a
     */
    public void saveData(A a) {
        // A 에서 B 로 치환.
        B b = adapteAtoB(a);
 
        // something work;
    }
 
    /**
     * A 객체에서 B 로 변환
     *
     * @param a
     * @return
     */
    public B adapteAtoB(A a) {
        B b = new B();
        b.setCompanySubjectSn(a.getCompanySubjectSn());
        b.setMemberSubjectSn(a.getMemberSubjectSn());
        b.setName(a.getName());
        b.setField1(a.getField1());
        b.setField2(a.getField2());
 
        return b;
    }
}
 
cs


중복된 부분을 메소드로 캡슐화한 것은 좋은 아이디어입니다.


그러나 아쉽게도 객체 A 와 B 는 Service1, Service2, Service3 등 여러 곳에서 활용될 가능성이 존재합니다. 


즉 adapteAtoB 메소드를 특정 한 모듈에 캡슐화하는 것은 바람직해보이지 않습니다. 다시 말해서, adatpeAtoB 에 대한 책임은 Service class 가 가지고 있을 것이 아닙니다. 책임의 분배가 필요합니다. (SRP : 단일책임의 원칙)



2. 책임의 분배


A 에서 B 로 변경하는 과정을 생각해봤을 때, 이미 제공되어 있는 것(A)과 필요한 것(B)의 차이를 극복하는 것이라 생각해볼 수 있습니다. 


다시 생각해보면, 해당 행위의 이해 관계자는 A 혹은 B 이며 둘 중 하나가 책임을 가져가면 될 것으로 보입니다. 각 클래스가 행위에 대한 책임을 가져가는 방식을 아래와 같이 정의해볼 수 있습니다.


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
public class A {
    /**
     * B 로의 변환
     * 
     * <pre>
     *     A 클래스에서 B 로의 변환을 책임지는 경우
     * </pre>
     *
     * @return
     */
    public B toB(){
        B b = new B();
        b.setCompanySubjectSn(getCompanySubjectSn());
        b.setMemberSubjectSn(getMemberSubjectSn());
        b.setName(getName());
        b.setField1(getField1());
        b.setField2(getField2());
 
        return b;
    }
}
 
public class B {
    /**
     * B 클래스에서 A 를 이용하여 인스턴스를 생성
     *
     * <pre>
     *     B 클래스에서 A 를 이용해 자신로의 변환을 책임지는 경우
     * </pre>
     *
     * @return
     */
    public static B FromB(A a){
        B b = new B();
        b.setCompanySubjectSn(a.getCompanySubjectSn());
        b.setMemberSubjectSn(a.getMemberSubjectSn());
        b.setName(a.getName());
        b.setField1(a.getField1());
        b.setField2(a.getField2());
 
        return b;
    }
}
cs


즉 우리는 필요에 따라 클래스 A, B 를 사용하는 곳 어디에서든 변환하는 메소드를 사용할 수 있게 되었습니다. 특히 두 번째 방식은 DB 와 연관이 있는 ORM 에 특화된 클래스의 변환에도 쉽게 적용할 수 있습니다. 



3. 객체 어댑터로 리팩토링


앞써 살펴본 예제들은 결국 객체 B 를 생성하 후 내부 필드를 객체 A 를 이용하여 초기화하는 과정을 캡슐화했다고 생각해 볼 수 있습니다.


이 방식은 연관이 없는 두 클래스간의 의존성을 없앨 수 있는 장점이 있지만, 결국은 깊은 복사 과정이며 어느정도 비용은 있다고 생각합니다. 

(의존성이 없다는 것은 객체 A가 사라진다고 B가 사라지는 것은 아니라는 의미입니다. B 는 그대로 존재하고, A 메소드로부터의 변환과정만 삭제하면 되죠..)


고전적인 디자인 패턴에서 제시하는 객체어댑터 방식은 제공하는 객체 A 를 use-a 관계로 유지하면서, B 의 인터페이스의 기능을 제공해주는 방식입니다.


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
public class B {
    private A a;
 
    /**
     * A 를 내부에서 참조.  
     * 
     * B 의 제공 인터페이스들은 A 인스턴스를 이용하여 구현
     * 
     * @param a
     */
    public B(A a){
        this.a  = a;
    }
 
    public Integer getMemberSubjectSn() {
        return a.getMemberSubjectSn();
    }
 
    public String getName() {
        return a.getName();
    }
 
    public Integer getCompanySubjectSn() {
        return a.getCompanySubjectSn();
    }
 
    public String getField1() {
        return a.getField1();
    }
    
    public String getField2() {
        return a.getField2();
    }
}
cs


이 방식은 얕은 복사로 어댑터를 수행할 수 있으며, B 의 목적을 충실히 달성할 수 있습니다. 그러나 A 클래스에 매우 의존적이라는 것은 염두해야합니다.



4. 상속관계를 가질 수 없는 두 클래스 간 상속관계 표현하기


ORM 은 백-엔드 개발자가 DB 를 다루는 것의 생산성을 향상 시켰습니다. 하지만 아쉽게도 ORM 클래스는 아쉽게도 개발자의 제어영역이 아닙니다. 

예를들어 IBatis에서 DB generate 를 하게되면, 해당 클래스 작업한 내역은 모두 사라집니다. ㅡㅡ^


사실 클래스의 목적 자체가 DB 의 CRUD 를 위한 목적이기 때문에 그 목적에만 충실해야하지만, 실제 비지니스 로직에서 이 클래스를 직접적으로 사용하는 것 역시 자유로워야 합니다. 


즉 이러한 사유로 아래의 두 ORM class는 특정 부모클래스나 인터페이스로 묶을 수 없습니다.


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 commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
 
    // 클래스 고유 필드
    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 commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
 
    // 클래스 고유 필드
    private String model2SpecialField1;
    private String model2SpecialField2;
    private String model2SpecialField3;
    private String model2SpecialField4;
 
    // setter, getter 는 생략
}
cs


두 클래스를 특정 인터페이스로 묶고 싶은 이유는 상위 추상 클래스를 이용하여 동일한 작업을 하고 싶기 때문입니다.


해당 요구사항을 처리하기 위한 방법으로 제시한 첫 번째 방법은 Template-method 패턴을 이용하는 것이었습니다.



하지만 모든 공용 로직을 일일이 Template-method 화 시킬 수는 없습니다. 

너무 많은 패턴화는 심각한 코드-파편화를 불러오며, 오히려 유지보수가 쉽지 않을 수 있습니다.


이 문제를 Adapter 를 이용하여 보다 쉽게 풀어볼 수 있을 것 같습니다. 아래와 같이 말이죠..


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
 
/**
 * GenerateModel1 과 GenerateModel2 두 클래스의 슈퍼클래스 정의
 * 
 * Created by Doohyun on 2017. 5. 21..
 */
public class AbstractGenerateModel {
    private Integer memberSubjectSn;
    private String name;
    private String commomField1;
    private String commomField2;
    private String commomField3;
    private String commomField4;
    
    // 남용 방지를 위한 생성자 접근제한
    private AbstractGenerateModel(){}
 
    /**
     * GenerateModel1 을 이용한 AbstractGenerateModel 생성
     * 
     * @param model1
     * @return
     */
    public static AbstractGenerateModel FromGenerateModel1(GenerateModel1 model1) {
        AbstractGenerateModel model = new AbstractGenerateModel();
 
        model.memberSubjectSn = model1.getMemberSubjectSn();
        model.name = model1.getName();
 
        model.commomField1 = model1.getCommonField1();
        model.commomField2 = model1.getCommonField2();
        model.commomField3 = model1.getCommonField3();
        model.commomField4 = model1.getCommonField4();
 
        return model;
    }
 
    /**
     * GenerateModel2 을 이용한 AbstractGenerateModel 생성
     *
     * @param model2
     * @return
     */
    public static AbstractGenerateModel FromGenerateModel1(GenerateModel2 model2) {
        AbstractGenerateModel model = new AbstractGenerateModel();
 
        model.memberSubjectSn = model2.getMemberSubjectSn();
        model.name = model2.getName();
 
        model.commomField1 = model2.getCommonField1();
        model.commomField2 = model2.getCommonField2();
        model.commomField3 = model2.getCommonField3();
        model.commomField4 = model2.getCommonField4();
 
        return model;
    }
 
    /**
     * GenerateModel1 로 치환
     * 
     * @return
     */
    public GenerateModel1 toGenerateModel1(){
        GenerateModel1 model1 = new GenerateModel1();
        model1.setMemberSubjectSn(memberSubjectSn);
        model1.setName(name);
        model1.setCommonField1(commomField1);
        model1.setCommonField2(commomField1);
        model1.setCommonField3(commomField1);
        model1.setCommonField4(commomField1);
        
        return model1;
    }
 
    /**
     * GenerateModel2 로 치환
     *
     * @return
     */
    public GenerateModel2 toGenerateModel2(){
        GenerateModel2 model2 = new GenerateModel2();
        model2.setMemberSubjectSn(memberSubjectSn);
        model2.setName(name);
        model2.setCommonField1(commomField1);
        model2.setCommonField2(commomField1);
        model2.setCommonField3(commomField1);
        model2.setCommonField4(commomField1);
 
        return model2;
    }
 
    // setter, getter 는 생략
}
 
cs


공용 필드를 가진 클래스를 하나 작성하며 이 클래스는 어댑터 하고자 하는 두 클래스의 양방향 Adapter 역할을 수행할 수 있다면, 충분히 상위클래스의 역할을 수행해 줄 수 있습니다.


이제 비지니스 로직을 아래와 같이 작성해 볼 수 있을 것 같군요. :-)


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
public class Service {
 
    public void task() {
 
        // func1 수행
        {
            // model1 으로부터의 실행
            GenerateModel1 model1 = new GenerateModel1();
            func1(AbstractGenerateModel.FromGenerateModel1(model1));
 
            // model2 으로부터의 실행
            GenerateModel2 model2 = new GenerateModel2();
            func1(AbstractGenerateModel.FromGenerateModel1(model2));
        }
 
        // func2 수행
        {
            // model1 저장!!
            AbstractGenerateModel data1 = func2();
            GenerateModel1 model1 = data1.toGenerateModel1();
            model1Dao.create(model1);
 
            // model2 저장!!
            AbstractGenerateModel data2 = func2();
            GenerateModel2 model2 = data2.toGenerateModel2();
            model2Dao.create(model2);
        }
    }
 
    /**    
     * 공용 메소드 1
     * 
     * @param model
     */
    public void func1(AbstractGenerateModel model) {
        // SOMETHING WORK.
    }
 
    /**
     * 공용 메소드 2
     *
     * @param model
     */
    public AbstractGenerateModel func2() {
        // SOMETHING WORK.
    }
}
cs


이 방법은  Template-method 를 이용하는 방식에 비하여 제작 공수나 코드 파편화가 적습니다. 더군다나 객체 형변환을 위해 instanceof 도 사용할 필요가 없겠군요. 지원한다는 class 는 메소드명으로 확인할 수 있으니 말이죠.


하지만, 변환과정을 수행할 때마다 비용이 있다는 것은 무시할 수 없어보입니다. 게다가 깊은 복사과정을 수행하기 때문에, 어댑터 객체의 변화를 원본 객체에게 알려줄 필요가 있습니다. 마지막으로 비슷한 로직을 사용하지만 각 구체화 전략마다 특이점에 꽤 차이가 있다면, strategy, template-method 등을 다시 고려하는 것이 좋아보입니다. 



Adapter 만 잘 활용해주더라도 비지니스 로직의 큰 문제를 줄일 수 있다고 생각합니다. 보다 더욱 가독성과 유지보수에 좋은 코드를 작성할 수 있으며, 추 후 스터디함수형 메소드에 적극적으로 활용할 수 있는 여지를 만들어 줄 수 있습니다.


이 포스팅이 모두의 기술향상에 도움이 되길 바랍니다. :-)




반응형
Posted by N'

지난 주에는 객체지향의 개념을 재검토 및 SOLID 개념을 한 번 맛보는 자리였었습니다. 또한 배운 주제를 통하여 과제를 진행했었고, 예상대로 멋진고 인텔리한 우리 스터디 멤버들은 문제를 잘 해결해주었습니다. 


지난 주차에 했던 내용에 대한 포스팅은 아래 링크에서 확인! 



해당 글에서는 과제에 대한 리뷰를 진행해 보고자 합니다.


1. 기존 구현된 Task 클래스에 goMBM() 이라는 메소드를 추가.


스터디에서 진행했던, Task 에 요구사항 추가가 있었습니다. 

우리 회사에 다니는 모든 직군은 MBM 이라는 행사에 참여할 가능성이 있지만, 보통은 마케팅 팀에서 대부분 진행을 하는 것으로 압니다. 

(맞나요? ㅡㅡ^)


무튼, 아래와 같이 코드를 작성하려 하였습니다.


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. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     구체적인 것이 추상적인 상태로 올라옴.
     * </pre>
     */
    public void goMBM() {
        if (this instanceof Marketing) {
            // 마켓팅 main.
        } else if (this instanceof Devlopment) {
            // 일단 여지는 있어보임.
        }
    }
 
    /**    
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     면접관을 수행하는 메소드도 추가되길 바람.
     *     물론 이 업무 역시, 특정 직군 (마켓팅,개발) 만 할 수도 있음.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
cs


구현된 goMBM 을 보니, 좋지 않은 냄새가 납니다. 


일단 추상적인 것이 구체적인 것에 의존하면 안된다는 DIP (의존성 역전 원칙) 을 지키지 않고 있으며, 만약 해당 소스를 사용하는 어플리케이션에서 Task 의 구현체를 변경한다면, Task 자체 소스도 변경해 줘야하기 때문에 OCP (개발-폐쇄 원칙)도 지키지 않는 것으로 보입니다.


[Marketing 이 삭제가 된다면 Task 내부 메소드를 수정해야 하며, 새로운 작업을 MBM 으로 추가하고 싶다면 또 다른 구현체에 의존해야 합니다.]


이 소스의 리팩토링 여지는 있으며, 어떻게 생각해보면 간단하게 해결될 수 도 있습니다. 위 소스의 가장 큰 문제는 추상적인 것이 구체적인 것에 의존한고 있다는 것이며, 그 고리만 끊어주면 파생되는 문제는 해결될 것으로 보입니다.


일단 아래와 같이 Task 클래스 내부의 goMBM() 은 아무 일도 하지 않게 하려 합니다. Task 라는 추상개념으로 goMBM() 을 실행하는 것이 목적이기 때문에 invoker 는 살려두려 합니다. 


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
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다.
     * </pre>
     */
    public void goMBM() {
    }
 
    /**    
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     추상적인 곳에서 추상클래스가 아닌 아무 일도 하지 않는 메소드를 hooker 라고 합니다.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
cs


필수적인 runTask() 만이 추상의 구현을 하위 개념에게 강제하였고, 나머지 메소드 (goMBM(), goInterview()) 는 hooker 로써, 구현의 여부를 선택하게 하였습니다. 


goMBM() 메소드는 현재 요구사항으로 보았을 때, 일단 Marketing 에 구현될 필요가 있어 보입니다. goMBM() 에서 Marketing 이 해야할 일을 구체개념으로 빼도록 하겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 마케팅 업무
 * Created by Doohyun on 2017. 4. 17..
 */
public class Marketing extends Task{
 
    @Override
    public void runTask() {
        System.out.println("마케팅 일을 하는 중...");
    }
 
    @Override
    public void goMBM() {
        System.out.println("마케팅은 MBM 을 떠난다.");
    }
}
cs


주목할 점은 goInterview() 를 오버라이딩 하지 않고 있으며, 즉 선택적으로 행위를 추가할 수 있음을 알 수 있습니다.



2. 가위바위보의 승패를 출력하는 메소드 제작


두 상태에 대한 승패여부를 if 나 자료구조 없, 나타내는 방법을 생각해보라는 주제였습니다. 아래 소스는 초기에 주어진 요구사항으로, 각 케이스에 대한 모든 분기가 되어있음을 알 수 있습니다.


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
/**
  * 심플한 가위,바위,보 결과
  * 
  * @param me
  * @param enemy
  * @return
  */
public static String 가위바위보_결과 (final String me, final String enemy) {
    if (me.equals("가위")) {
        switch (enemy) {
        case "가위":
            return "무승부";
        case "바위":
            return "패";
        case "보":
            return "승";
        }
    } else if (me.equals("바위")) {
        switch (enemy) {
        case "가위":
             return "승";
        case "바위":
            return "무승부";
        case "보":
            return "패";
        }
    } else if (me.equals("보")) {
        switch (enemy) {
        case "가위":
            return "패";
        case "바위":
            return "승";
        case "보":
            return "무승부";
        }
    }
 
    throw new RuntimeException("입력 오류");
}
 
cs


참여자 전원 모두 과제를 잘 해왔으며, 제출자의 코드를 기반으로 포스팅을 하고자합니다.


리팩토링을 생각해볼 부분은 분기처리이며, 각 분기처리에 대해서 책임소지가 있는 클래스로 기능을 이관(SRP)하는 작업이 필요할 것 같습니다.


책임 분배를 위해 아래와 같은 구체화 클래스를 만들려고 합니다. 

서로의 매치를 if 없이 관계를 주기 위하여, 오버로딩 개념을 사용하였습니다.


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
/**
 * 클래스 간의 서로의 사용개념을 처리하기 위한 오버로딩 인터페이스
 */
public interface StrategyVisitor {
    String visit(가위 가위);
    String visit(바위 가위);
    String visit(보 가위);
}
 
// 구체화된 가위 클래스
public class 가위 implements StrategyVisitor{
 
    @Override
    public String visit(가위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(보 enemy) {
        return "승";
    }
 
    @Override
    public String toString() {
        return "가위";
    }
}
 
// 구체화된 바위 클래스
public class 바위 implements StrategyVisitor {
 
    @Override
    public String visit(가위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(보 enemy) {
        return "패";
    }
 
    @Override
    public String toString() {
        return "바위";
    }
}
 
// 구체화된 보 클래스
public class 보 implements StrategyVisitor {
 
    @Override
    public String visit(가위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(보 enemy) {
        return "무승부";
    }
 
    @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
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
 
// 가위에 대한 응용처리
{
    가위 a = new 가위();
 
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + a.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(a) + "\n");
    }
}
 
// 바위에 대한 응용처리
{
    바위 b = new 바위();
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + b.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(b) + "\n");
    }
}
 
// 보에 대한 응용처리
{
    보 c = new 보();
            
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    for (StrategyVisitor game1 : me) {
        System.out.println("me:" + game1.getClass().getSimpleName() + "  enemy:" + c.getClass().getSimpleName());
        System.out.println("result:" + game1.visit(c) + "\n");
    }
}
cs


요구사항처럼 if 없이 관계는 처리가 되었습니다. 

하지만 제출자의 입장에서는 위와 같이 세 가지 케이스에 대하여 모든 경우를 비지니스 로직으로 만들고 싶지는 않았고, 자료구조를 두 개 선언하여 2중 for 문으로 해결하고 싶었다고 합니다.


하지만 아래와 같은 사유로 실패했습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
List<StrategyVisitor> enemy = Arrays.asList(new 가위(), new 바위(), new 보());
        
for (StrategyVisitor a : me) {
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
    
    for (StrategyVisitor b : enemy) {
        System.out.println("me:" + a.getClass().getSimpleName() + "  enemy:" + b.getClass().getSimpleName());
        
        // 오버로딩 전략으로 가려고 했지만, enemy 리스는 추상적인 StrategyVisitor!!!!
        System.out.println("result:" + a.visit(b) + "\n");
    }
}
cs


두 자료구조의 for-loop 을 방문하면서, 로직을 처리하기 위하여 고전적인 방문자 패턴을 응용해보려 합니다.


3. 방문자패턴을 이용한 3차 리팩토링


오버로드를 수행하는 StrategyVisitor 를 일종의 방문자라 생각한다면, 방문할 Element 만 구현해주면 그만입니다.


방문할 Element 객체를 아래와 같이 정의합니다.


1
2
3
4
5
6
7
8
/**
 * 방문 대상 Element
 *
 * Created by Doohyun on 2017. 4. 27..
 */
public interface Strategy {
    String accept(StrategyVisitor strategyVisitor);
}
cs


방문대상인 Strategy 는 기존 StrategyVisitor 를 매개변수로 받는 메소드를 정의했습니다. 즉 방문자가 해당 Element 를 방문했을 때, 해야할 일을 구현체에 작성해주면 됩니다. 


이를위해 저는 기존 만들어진 가위,바위,보를 재활용하고자 합니다. 아래와 같이 말이죠.


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
/**
 * Element 와 Visitor 를 한 곳에 구현.
 * 구체화된 가위 클래스
 */
public class 가위 implements StrategyVisitor, Strategy{
 
    @Override
    public String visit(가위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(보 enemy) {
        return "승";
    }
 
    // 방문자가 본인을 다녀갔을 때의 처리를 수행합니다.
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
       return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "가위";
    }
}
 
// 구체화된 바위 클래스
public class 바위 implements StrategyVisitor, Strategy {
 
    @Override
    public String visit(가위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "무승부";
    }
 
    @Override
    public String visit(보 enemy) {
        return "패";
    }
 
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
        return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "바위";
    }
}
 
// 구체화된 보 클래스
public class 보 implements StrategyVisitor, Strategy {
 
    @Override
    public String visit(가위 enemy) {
        return "패";
    }
 
    @Override
    public String visit(바위 enemy) {
        return "승";
    }
 
    @Override
    public String visit(보 enemy) {
        return "무승부";
    }
 
    @Override
    public String accept(StrategyVisitor strategyVisitor) {
        return strategyVisitor.visit(this);
    }
 
    @Override
    public String toString() {
        return "보";
   }
}
cs


accept 메소드를 모두 구현했으니 아래와 같이 클라이언트 코드를 작성해볼 수 있을 것 같습니다. 앞써, 제작된 코드와는 달리 다형화의 선택조건을 구현체 내부에서 구현하고 있기 때문에 아래와 같은 구현이 가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
List<StrategyVisitor> me = Arrays.asList(new 가위(), new 바위(), new 보());
List<Strategy> enemy = Arrays.asList(new 가위(), new 바위(), new 보());
 
for (StrategyVisitor visitor : me) {
    System.out.println("============================");
    System.out.println("게입 시작합니다! \n");
 
    for(Strategy strategy : enemy){
        System.out.println(String.format("me : %s,  enemy: %s", visitor, strategy));
        System.out.println(String.format("result : %s\n", strategy.accept(visitor)));
    }
}
cs


나름 신경을 써 볼 만한 문제 였던 것 같습니다. 다들 한 번이라도 신경을 써서 문제를 해결해줘서 고맙고, 다시 이 글을 봐주셔서 감사합니다. [꾸벅]


다음 주, Study 도 파이팅!!! 

반응형
Posted by N'

지난 스터디에서 과제에 대한 내용을 포스팅하지 않았기 때문에 어떤 것을 해야하는 지에 대한 내용이 명확하지 않았던 것 같습니다. (경험이 짧은 그룹장이라 죄송합니다. ㅜㅡㅜ)


그렇기 때문에 해야할 요구사항에 대한 정리내용을 작성하고자 합니다.


1. 기존 구현된 Task 클래스에 goMBM() 이라는 메소드를 추가.


앞서, 스터디에서 다뤘던 내용 중 Task 에 요구사항을 추가하고 싶은 경우가 생겼습니다. 

Task 클래스 내부에 goMBM 이란 항목을 추가하고 싶으며, 해당 항목은 Marketing 만 가고 싶습니다. 물론 추 후에는 Development 도 MBM 에 가야할 수도 있습니다.


일단은 아래와 같이 임시로 코드를 만들었지만, 우리 MIDAS 의 개발자들은 똑똑하니 알아서 잘 리팩토링 해줄 수 있을 것이라 생각합니다.


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
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
 
    /**
     * MBM 을 수행하는 메소드
     * 
     * <pre>
     *     구체적인 것이 추상적인 상태로 올라옴.
     * </pre>
     */
    public void goMBM() {
        if (this instanceof Marketing) {
            // 마켓팅 main.
        } else if (this instanceof Devlopment) {
            // 일단 여지는 있어보임.
        }
    }
 
    /**
     * 면접관을 담당하는 메소드
     * 
     * <pre>
     *     면접관을 수행하는 메소드도 추가되길 바람.
     *     물론 이 업무 역시, 특정 직군 (마켓팅,개발) 만 할 수도 있음.
     * </pre>
     * 
     */
    public void goInterview() {
    }
}
 
cs


2. 가위바위보의 승패를 출력하는 메소드 제작


스터디 내부에서 했을 때는 요구사항을 의도했던 것과 달리, 문제 제시를 제대로 못했던 것 같습니다. input & output 과는 별도로 아래의 요구사항을 만족하면 됩니다.


가위바위보의 두 상태에 따른 승&패 여부를 확인할 수 있는 코드를 작성하고 싶습니다. 그렇기 때문에 첫 코드를 아래와 같이 작성하고자 하였습니다.  


[아마 첫 요구사항은 입력에 대한 이기는 결과를 출력해달라고 했을 것 같은데, 의도 했던 것은 사실 아래와 같았습니다. (미안, 기획을 잘해야돼 ㅜㅡㅜ)]


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
/**
  * 심플한 가위,바위,보 결과
  * 
  * @param me
  * @param enemy
  * @return
  */
public static String 가위바위보_결과 (final String me, final String enemy) {
    if (me.equals("가위")) {
        switch (enemy) {
        case "가위":
            return "무승부";
        case "바위":
            return "패";
        case "보":
            return "승";
        }
    } else if (me.equals("바위")) {
        switch (enemy) {
        case "가위":
             return "승";
        case "바위":
            return "무승부";
        case "보":
            return "패";
        }
    } else if (me.equals("보")) {
        switch (enemy) {
        case "가위":
            return "패";
        case "바위":
            return "승";
        case "보":
            return "무승부";
        }
    }
 
    throw new RuntimeException("입력 오류");
}
cs


다음부터 과제를 준비한다면, 이번 포스팅처럼 구체적으로 작성해보는 것으로 하겠습니다. 기획서 없이 개발하라고 시킨 것 같아 미안하군요. ㅜㅡㅜ 


그럼 오늘도 좋은 일만 가득하길 바래요. :-)





반응형
Posted by N'

지난번과 마찬가지로 오늘 스터디 했던 내용에 대한 추가설명 및 코드를 남깁니다.


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




1. 캡슐화의 역할 중 하나는 행위의 숨김


데이터와 행위를 한 단위로 관리하는 캡슐화로 인하여, 크게 두 가지의 이점을 얻을 수 있습니다.


- 접근제어 (private, public) 등으로 의도치 않은 데이터 변경을 막을 수 있습니다.

- 행위가 숨겨져 있습니다. 즉 형태만 유지된다면 다른 알고리즘을 사용할 수 있습니다. (다형성)


예를들어 아래와 같이 Duck 클래스의 행위의 형태(메소드)만 지켜진다면 내부 알고리즘은 다르게 사용할 수 있습니다. 

(노란오리와 형오리는 다르지만, 사용자인 runTest 입장에서는 뭐든지 중요하지 않습니다. Duck 인 것이 중요하죠. ㅡㅡ^)


이를 다형성이라고 하며, OOP 에서 객체를 다루는 것의 핵심 개념이라 할 수 있습니다.


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
/**
 * 추상적인 오리 클래스
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public abstract class Duck {
 
    public abstract String getName();
 
    /**
     * 비행하는 행위 수행.
     */
    public abstract void fly();
 
    /**
     * 꽥꽥소리를 내는 행위 수행
     */
    public abstract void quack();
}
 
/**
 * 브로덕 클래스 (오리의 형태를 지켜주고 있음)
 * 
 * Created by Doohyun on 2017. 4. 16..
 */
public class BroDuck extends Duck {
    @Override
    public String getName() {
        return "브로덕 한국말로 형오리!";
    }
 
    @Override
    public void fly() {
        System.out.println("나는 날 수 없다!");
    }
 
    @Override
    public void quack() {
        System.out.println("오리! 꽥꽥!");
    }
}
 
/**
 * 노란 오리 정의
 * 
 * Created by Doohyun on 2017. 4. 16..
 */
public class YellowDuck extends Duck{
 
    @Override
    public String getName() {
        return "노란 오리";
    }
 
    @Override
    public void fly() {
        System.out.println("오리 날다!!! 50m");
    }
 
    @Override
    public void quack() {
        System.out.println("꽥");
    }
}
 
 
/**
 * 오리를 사용하는 함수
 * @param duck
 */
public void runTest(Duck duck) {
    System.out.println("오리 이름 : " + duck.getName());
    duck.fly();
    duck.quack();
}
 
 
//////////////////////////////////////////
 
 
// 노란 오리 테스트
runTest(new YellowDuck());
 
// 형오리 테스트
runTest(new BroDuck());
 
// PRINT RESULT 
//        
// 오리 이름 : 노란 오리
// 오리 날다!!! 50m
// 꽥
//
// 오리 이름 : 브로덕 한국말로 형오리!
// 나는 날 수 없다!
// 오리! 꽥꽥!
cs



2. SRP, OCP, DIP 리팩토링 예제


SOLID 원칙 중 세 가지가 적용된 리팩토링 예제를 보았었습니다. 


준비된 첫 코드로 리팩토링을 했었는데, 그 과정을 블로그에 리뷰 해보도록 하겠습니다.


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
/**
 * 강현지 객체는 현재 기획,디자인,개발,마케팅 업무를 담당하고 있다.
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class Hyunji {
    private String name = "강현지";
 
    public String getName() {
        return name;
    }
 
    // Run Work.
    public void runTask() {
        workByPlanning();
        workByMarketing();
        workByDesign();
        workByDevelopment();
    }
 
    public void workByPlanning() {
        System.out.println("기획 일을 하는 중...");
    }
 
    public void workByDesign() {
        System.out.println("디자인 일을 하는 중...");
    }
 
    public void workByDevelopment() {
        System.out.println("개발 일을 하는 중...");
    }
 
    public void workByMarketing() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
    
    System.out.println("업무 수행자 : " + hyunji.getName());
    hyunji.runTask();
}
cs


첫번 째 기준에서 작성된 Hyunji 클래스는 기획, 디자인, 개발, 마케팅 일을 하고 있습니다. 


Client code 는 이 클래스를 이용하여 잘 사용하고 있지만, 언제나 그랬듯이 사용하고 있는 클래스는 변화가 생길 것입니다. 우리가 할 일은 최대한 변화에 유연하게 설계를 하는 것입니다.


이 클래스는 적어도 4가지 이유로 변경될 여지가 있습니다. 기획, 디자인, 개발, 마케팅 일 중 하나가 변경될 때마다 이 클래스를 고쳐야 합니다. 또는 인사업무, 총무 등 다른 업무가 추가될 수도 있죠. 


다른 시기, 다른 이유로 클래스를 변경해야한다는 것은 그리 반가운 일은 아닌 것 같습니다. 일단  먼저 살펴볼 것은 변화하는 부분과 변하지 않는 부분을 찾아보는 것입니다. 


현재 주어진 스펙을 보면, Hyunji 클래스는 종사자로써 그 자체가 변한다기보다는 업무자체가 많이 변할 소지가 있어보입니다. (종사자의 업무가 바뀌는 것인지 종사자 자체가 변화하는 것은 아닐 것 같습니다. ㅡㅡ^ ) 


"클래스 하나 당 단일 책임을 부여하자는 SRP 원칙"에 따라 업무관련 메소드를 아래와 같이 Task 클래스로 이관하고자 합니다.


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. 4. 16..
 */
public class Hyunji {
    private String name = "강현지";
    private Task task = new Task();
 
    public String getName() {
        return name;
    }
 
    // Run Work.
    public void runTask() {
        System.out.println("업무 수행자 : " + hyunji.getName());
        hyunji.workByPlanning();
        hyunji.workByMarketing();
        hyunji.workByDesign();
        hyunji.workByDevelopment();
    }
}
 
/**
 * 업무 처리
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public class Task {
    /**
     * 기획 일을 처리.
     */
    public void workByPlanning() {
        System.out.println("inHR 기획 일을 하는 중...");
    }
 
    /**
     * 디자인 일을 처리.
     */
    public void workByDesign() {
        System.out.println("inHR 디자인 일을 하는 중...");
    }
 
    /**
     * 개발 일을 처리.
     */
    public void workByDevelopment() {
        System.out.println("개발 일을 하는 중...");
    }
 
    /**
     * 마케팅 일을 처리.
     */
    public void workByMarketing() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
    hyunji.runTask();
}
cs


Task 로 업무 관련 메소드를 모두 옮겼기 때문에, 업무가 변경된다고 Hyunji 가 변경될 필요는 없습니다. 현재 Hyunji 는 Task에게 의존적인 상태가 되었습니다.


그러나 아쉽게도 업무가 삭제 되거나 추가되는 것에 대하여, Hyunji 클래스는 대응을 할 수 없습니다. 업무가 삭제된다면 Hyunji 클래스의 runTask() 메소드를 수정해줘야 하고, 업무가 추가된다면 runTask() 에 등록을 해줘야합니다. 


즉 수정사항이 생길 때마다 상위 모듈 내부를 고쳐야 합니다. 


우리는 수정에는 닫혀있고, 확장에는 열려있으라는 OCP 원칙을 배웠습니다.


- 수정에 닫혀있다는 것은 내부 코드를 수정하지 않아도, 확장하거나 변화할 수 있음을 말합니다.

- 확장에 열려있다는 것은 모듈을 추가하거나 행위를 변경할 수 있다는 것입니다.


즉 현재 Task 클래스의 내부 메소드(기획,디자인,마케팅,개발)들은 확장의 대상으로 보이며, 앞으로 다른 업무(인사, 총무 등)들을 추가할 여지도 있습니다.


또한 한 가지 배운 또 다른 법칙은 상위모듈(Hyunji)이 하위모듈(Task)에 의존하지 말고, 두 모듈 모두 추상에 의존하라는 DIP(의존성 역전 법칙)이 있었습니다. 


이 원칙들에 따라, Task 자체를 조금 더 추상화하고, 구체적인 업무개념(디자인, 개발, 마케팅, 기획 등)을 서술해보고자 합니다. Hyunji 는 여전히 Task(추상)에 의존하고 구체적인 업무(하위모듈) 역시 Task(추상)에 의존할 계획입니다.


아래는 작성된 코드입니다.


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
public class Hyunji {
    private String name = "강현지";
 
    private List<Task> taskList = new LinkedList<>();
 
    public String getName() {
        return name;
    }
 
    /**
     * 일을 추가한다
     *
     * @param task
     */
    public void addTask(Task task) {
        taskList.add(task);
    }
 
    /**
     * 업무를 수행.
     *
     */
    public void runTask() {
       taskList.stream().forEach(Task::runTask);
    }
}
 
public abstract class Task {
 
    /**
     * 일을 한다는 약속
     *
     * <pre>
     *     이 메소드를 실행시키면 일을 하는 것.
     * </pre>
     */
    public abstract void runTask();
}
 
/**
 * 마케팅 업무
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public class Marketing extends Task{
 
    @Override
    public void runTask() {
        System.out.println("마케팅 일을 하는 중...");
    }
}
 
/**
 * 개발업무
 * 
 * Created by Doohyun on 2017. 4. 17..
 */
public class Devlopment extends Task {
 
    @Override
    public void runTask() {
        System.out.println("개발 일을 하는 중...");
    }
}
 
 
// Client Code.
{
    Hyunji hyunji = new Hyunji();
 
    // Task(추상)을 만족하는 업무를 유동적으로 변경할 수 있다. 변경에 있어서 다른 곳의 영향은 없음.
    hyunji.addTask(new Marketing());
    hyungi.addTask(new Devlopment());
 
    hyunji.runTask();
}
cs


변화하는 부분(업무의 개수 및 종류 등..)의 추가나 삭제가 있어도 다른 영역(Hyunji, Task 등)의 수정은 없어보입니다. [OCP를 일단 만족, 다른 업무를 추가하고 싶다면 Task 를 상속받은 객체를 Hyunji 의 addTask 메소드를 통해 추가시켜주면 됩니다. ㅡㅡ^ ]



3. 리스코프 치환 법칙 예제


LIP 는 하위 개념이 기반 개념의 행위를 불법으로 만들거나 무시하면 안된다는 법칙입니다. y 가 x 의 하위개념이라 했을 때 상위 개념 x 가 f(x) 를 만족한다면, y 도 f(y) 를 만족해야함을 말합니다. 


스터디에서는 이 개념을 설명하기 위해 가장 많이 제시되는 사각형 예제를 사용하였습니다.


사각형인 Rectangle 클래스를 만들었고, 이 클래스는 width 와 height 값을 가지고 있습니다. Rectangle 클래스의 기능은 width 와 height 을 변경하는 것이며, 아래와 같이 사용가능한 유닛테스트를 작성하였습니다.


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
/**
 * 심플한 사각형을 나타내는 객체
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class Rectangle {
    private Integer width = 0;
    private Integer height = 0;
 
    public Integer getWidth() {
        return width;
    }
 
    public void setWidth(Integer width) {
        this.width = width;
    }
 
    public Integer getHeight() {
        return height;
    }
 
    public void setHeight(Integer height) {
        this.height = height;
    }
 
    /**
     * 넓이 계산
     *
     * @return
     */
    public Integer getArea() {
        return width * height;
    }
 
    @Override
    public String toString() {
        return String.format("사각형 정보 [가로 : %d, 세로 : %d, 넓이 : %d]", width, height, getArea());
    }
}
 
/**
 * 사각형 서비스
 *
 * <pre>
 *     사각형 객체를 테스트 목적으로 사용하는 서비스.
 *     인스턴스는 오직 한 개로 존재해도 무방.
 * </pre>
 *
 * Created by Doohyun on 2017. 4. 16..
 */
public class RectangleTestUnitService {
 
    private RectangleTestUnitService(){}
 
    private static class ManagerHolder {
        private static RectangleTestUnitService unique = new RectangleTestUnitService();
    }
 
    public static RectangleTestUnitService GetInstance() {
        return ManagerHolder.unique;
    }
 
    private void printResult(Rectangle rectangle) {
        System.out.println("세팅 완료 -> " + rectangle.toString());
    }
 
    /**
     * 사각형 객체와 가로 세로 높이를 세팅한 후, 넓이를 체크한다.
     *
     * @param rectangle
     * @param w
     * @param h
     */
    public void checkArea(Rectangle rectangle, Integer w, Integer h) {
        rectangle.setWidth(w);
        rectangle.setHeight(h);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (w * h)) {
            throw new RuntimeException("[에러] 사각형의 넓이가 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
 
    /**
     * 가로 값 세팅에 대하여 유효성 체크를 한다.
     *
     * @param rectangle
     * @param w
     */
    public void checkAreaOnlyWidth(Rectangle rectangle, Integer w) {
        rectangle.setWidth(w);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (w * rectangle.getHeight())) {
            throw new RuntimeException("[에러] 사각형의 가로 계산이 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
 
    /**
     * 가로 값 세팅에 대하여 유효성 체크를 한다.
     *
     * @param rectangle
     * @param h
     */
    public void checkAreaOnlyHeight(Rectangle rectangle, Integer h) {
        rectangle.setHeight(h);
 
        if ((rectangle.getHeight() * rectangle.getWidth()) != (h * rectangle.getWidth())) {
            throw new RuntimeException("[에러] 사각형의 세로 계산이 올바르게 계산되지 않음");
        }
 
        printResult(rectangle);
    }
}
 
// Client Code
{
    Rectangle rectangle = new Rectangle();
 
    RectangleTestUnitService rectangleTestUnitService = RectangleTestUnitService.GetInstance();
 
    rectangleTestUnitService.checkArea(rectangle, 54);
    rectangleTestUnitService.checkAreaOnlyWidth(rectangle, 2);
    rectangleTestUnitService.checkAreaOnlyHeight(rectangle, 9);
 
}
 
// PRINT RESULT
//
// 세팅 완료 -> 사각형 정보 [가로 : 5, 세로 : 4, 넓이 : 20]
// 세팅 완료 -> 사각형 정보 [가로 : 2, 세로 : 4, 넓이 : 8]
// 세팅 완료 -> 사각형 정보 [가로 : 2, 세로 : 9, 넓이 : 18]
 
cs


결과는 잘 나오는 것으로 보입니다. 우리는 잘 나오는 Rectangle 클래스를 이용하여, 그의 상속 개념인 Square(정사각형)을 구현하고자 합니다. 


논리적으로 "정사각형은 사각형"이니 문제 없을 것으로 보입니다. 하지만 주의할 것은 width 나 height 이 수정된다면, 언제나 가로/세로 길이를 아래와 같이 맞춰줘야한다는 것입니다.


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
/**
 * 정사각형 객체
 *
 * Created by Doohyun on 2017. 4. 17..
 */
public class Square extends Rectangle{
 
    /**
     * 정사각형의 개념을 살리고자, 가로/세로 길이를 동기화
     * 
     * @param width
     */
    @Override
    public void setWidth(Integer width) {
        super.setWidth(width);
        super.setHeight(width);
    }
 
    /**
     * 정사각형의 개념을 살리고자, 가로/세로 길이를 동기화
     *
     * @param width
     */
    @Override
    public void setHeight(Integer width) {
        super.setWidth(width);
        super.setHeight(width);
    }
}
 
// Client Code
{
    Square square = new Square();
 
    RectangleTestUnitService rectangleTestUnitService = RectangleTestUnitService.GetInstance();
 
    rectangleTestUnitService.checkArea(square, 54);
    rectangleTestUnitService.checkAreaOnlyWidth(square, 2);
    rectangleTestUnitService.checkAreaOnlyHeight(square, 9);
 
}
 
// ERROR LOG
Exception in thread "main" java.lang.RuntimeException: [에러] 사각형의 넓이가 올바르게 계산되지 않음
 
cs


디버깅을 해보니, 가로/세로 길이의 동기화가 사각형유닛테스트 모듈의 에러 원인이었습니다.


Square 는 기존 Rectangle 의 width 혹은 height 값을 변경하기 위해 setter 메소드를 오버라이딩하였고, 그 메소드들은 Rectangle의 의도와 달리 모든 변의 값을 변경시키고 있습니다. 즉 ISP 에 위반하였고, Square 와 Rectangle 이 다름을 인지하였습니다.


그러나 Square 객체로 그동안 만든 Rectangle 의 기능들을 사용하고 싶은 리즈가 있습니다. 왜냐하면, 인간 개념에서 논리적으로 Square 는 Rectangle 이기 때문이죠. 이를 위해 저번 스터디에서 배웠던 어댑터 패턴을 이용하고자 합니다.


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
public class Square {
 
    // 정사각형은 사실 변의 길이만 알고 있으면 됨.
    private Integer length;
 
    /**
     * 정사각형을 사각형으로 어댑팅 해주는 메소드 추가.
     *
     * @return
     */
    public Rectangle toRectangle() {
        Rectangle rectangle = new Rectangle();
        rectangle.setHeight(length);
        rectangle.setWidth(length);
 
        return rectangle;
    }
 
    public void setLength(Integer length) {
        this.length = length;
    }
}
 
// Clinet Code
{
    Square square = new Square();
 
    Rectangle rectangle = square.toRectangle();
 
    RectangleTestUnitService squareTestUnitService = RectangleTestUnitService.GetInstance();
 
    squareTestUnitService.checkArea(rectangle, 54);
    squareTestUnitService.checkAreaOnlyWidth(rectangle, 2);
    squareTestUnitService.checkAreaOnlyHeight(rectangle, 9);
}
 
cs


비록 RectangleTestUnitService 의 내부 메소드들은 Rectangle 에 맞춰있기 때문에 Square 의 고유 특성을 살려 작업할 수 없습니다. (맞춘다는 것 자체가 넌센스 ㅡㅡ^) 


하지만 우리는 두 클래스의 관계를 과정을 거쳐 지금같이 명시를 함으로써, Square 인스턴스가 RectangleTestUnitService 에서 일으킬 부작용을 막았으며 혹여나 사용하고 싶다면 위와 같이 우회할 수 있습니다. 


이번 스터디 내용은 뭔가 어려운 것 같지만, 두 가지 정도로 요약해 볼 수 있을 것 같습니다.


- 변하는 부분과 변하지 않는 부분을 분리할 것.


- 추상화할 수 있는 부분을 최대한 추상화하고, 복잡한 개념은 하위 개념으로 미루자. 


이 개념들을 잘 살려서, 이번 주에 제시한 문제들을 해결할 수 있기를 바랍니다. 

(오므라이스는 먹을 수 있나요? 인증샷을 안찍었으니 마지막은 오므라이스 사진 ㅋㅋㅋ )





반응형
Posted by N'

2. OOP 개념의 재검토 (with SOLID).pdf


OOP 개념 기초부터 설계원칙 SOLID 를 스터디하고자 합니다.

먼저 읽어보면 최고, 프린트 해오면 최고 (용지 아끼자 ㅡㅡ^ [쿨럭])


참고자료 

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

- [Head First] Object-Oriented Analysis & Design

반응형
Posted by N'