이번 포스팅은 마지막 스터디 시간에 잠시 언급을 했던 [리팩토링]이라는 주제에 대한 내용을 다뤄보고자 합니다. 

많은 개발자들이 리팩토링을 언급하는 경우가 종종있지만, 이 개념에 대해 오해하는 사람들도 종종 있으며 단순히 코드정리라고 생각하는 경우도 많이 있는 듯 합니다.


지난 시간에 진행한 스터디에서는 Martin Fowler 의 [리팩토링-코드 품질을 개선하는 객체지향 사고법] 에 서술된 내용을 기반으로 진행을 하였으며, 보다 자세한 리뷰를 이번 포스팅에서 작성하고자 합니다.


이 책이 궁금하신 분은 아래 링크를 참고해보세요. 


리팩토링 - 코드 품질을 개선하는 객체지향 사고법
국내도서
저자 : 마틴 파울러(Martin Fowler) / 김지원역
출판 : 한빛미디어 2012.11.09
상세보기


지난 스터디에서 진행했던 자료는 아래 링크에서 확인하실 수 있습니다. :-)



크게 다뤄볼 목차는 리팩토링의 개념과 필요성, 그리고 여러 방법들을 제시해 볼 생각입니다.


특히, 방법같은 경우는 새로운 개념이 아닌 우리가 보통 코드 정리를 위해 하고 있는 내용들이 주를 이룰 수도 있습니다. (즉 갑자기 새로운 개념이 등장하지는 않습니다. ㅡㅡ^)



1. 리팩토링의 개념.


리팩토링이란 이미 구현된 일련의 행위에 대한 변경 없이, 내부 구조를 변경하는 것을 말합니다.

내부 구조를 변경하는 이유는 코드의 가독성과 유지보수성을 향상시키기 위함이죠.


이 개념은 성능을 최적화하는 문제와는 다릅니다.

성능 최적화는 사실 기능을 추가하는 문제와 크게 다르지 않습니다. 


즉, 구현된 행위에 대한 스펙변경 없이 코드 품질을 위해 내부 구조를 변경하는 것을 의미합니다.


이 책의 저자인 Martin Fowler 는 


"컴퓨터가 이해하는 코드는 어느 바보나 짤 수 있다. 좋은 프로그래머는 사람이 이해하는 코드를 짠다."


라고 말했습니다.


즉, 같이 일하는 주체는 사람이며, 나중에 코드를 리뷰 해야할 주체(보통은 나... 종종 나..) 역시 사람이 때문에 이 과정은 매우 중요한 듯 합니다. 



2. 리팩토링의 필요성.


소프트웨어 개발에 있어서, 리팩토링을 하는 과정은 꽤 중요한 이슈입니다.


일단, 리팩토링 과정을 통해 중복된 코드를 함수로 만들거나 모듈로 빼내는 등의 작업들을 통해 소프트웨어 공학에서 말하는 유지보수성을 키울 수 있습니다. 즉 이러한 일련의 과정들은 전체적으로 소프트웨어 설계의 질적 향상을 불러올 수 있습니다.


이러한 소프트웨어 설계의 질적 향상은 일련의 행위에 대한 가독성을 높일 수 있을 것입니다.

같이 일하는 동료, 혹은 유지보수해야하는 본인을 위해서라도 가독성있는 코드를 작성하는 것이 중요하겠죠? 


또한, 전반적인 코드 개선의 과정을 통해 버그 수정을 수월하게 할 수 있습니다.


이런 전반적으로 좋은 코드를 바탕으로 요구사항을 수용하는 것은 과적으로 개발시간의 단축 및 강건한 프로그램을 만드는 것에 도움이 될 것입니다.



3. 리팩토링의 시점.


리팩토링 과정은 강건하고 유지보수성이 좋은 소프트웨어를 만들기 위한 과정이며, 계속해서 프로젝트를 좋은 설계 기반 위에 얹을 수 있을 것입니다.


하지만, 아무 때나 모든 코드를 깔끔하게 정리해야할까요?

그것은 조금 시간낭비일 수 있습니다. 이미 잘 작동하며, 요구사항 추가에 대한 명세가 없는 모든 모듈을 굳이 수정하는 것은 정말로 할 것이 없을 때 연습을 위해서 좋을 수도 있습니다.


[리팩토링-코드 품질을 개선하는 객체지향 사고법] 에서는 어느 정도 제안하는 리팩토링의 시점이 있으며, 이는 다음과 같습니다.


- The Rule of Three


유사한 것을 세 번하면, 리팩토링을 고려해 볼 필요가 있을 것 같습니다.

보통을 유사한 과정을 캡슐화하여, 추후 재활용을 도모합니다.


- 새로운 기능을 개발할 때.


현재 구현되어 있는 상태가 새로운 요구사항을 받아들이기 힘들 때 리팩토링을 합니다.

확장에 열려있게 수정하고(OCP), 기능을 추가하는 것이 더 수월할 것입니다.


이 때, 중요한 것은 리팩토링을 할 때는 기능을 추가하지 말고, 기능을 추가할 때는 리팩토링을 하지 않기를 권장합니다. (Two Hats)


- 버그를 수정할 때


당연한 이야기이지만, 코드에 대한 이해도가 높아지면 버그 수정이 쉬워질 것입니다.

난해한 코드 속에서 버그 수정을 하기 보다는, 리팩토링 과정을 통해 어느정도 가독성 및 이해도를 갖추고 수정하는 것이 빠른 경우가 있을 수 있습니다.


- 코드 리뷰를 할 때


팀원들과 진행하는 코드 리뷰 과정을 통해 리팩토링을 할 수 있습니다.


실력이 좋은 개발자일지라도 너무 본인이 작성한 코드를 집중해서 보면, 객관적인 가독성 및 유지보수성, 논리적 오류 등을 찾기 쉽지 않을 수 있습니다.


즉, 타인이 같이 봐주는 코드 리뷰나 페어 프로그래밍 등의 과정은 본인이 작성한 코드의 가독성 및 유지보수성을 높게 할 수 있으며, 특히 주니어 개발자들이 노하우 기반의 성장할 수 있는 기회일 수 있습니다.


하지만, 너무 많은 사람이 이 활동에 참여하면 비효율적이라고 합니다.

적당한 소수인원이 진행하는 코드리뷰가 권장됩니다.



4. 리팩토링 방법


언급한 바와 같이 리팩토링은 구현된 일련의 행위 변경 없이, 코드의 유지보수성, 확장용이성 등을 높게 하기 위해 내부 구조를 변경하는 작업입니다.


하지만, 막연하게 코드정리를 한다는 것은 아닌 듯 합니다. 

리팩토링 하는 방법과 그에 대한 이름 역시 존재합니다. 하지만, 이 방법들은 이미 여러분들이 알고 있는 방법일 수 있습니다.


- 테스트 코드 작성


리팩토링을 하기 전에 가장 먼저 작업해야할 것은 테스트 코드를 작성하는 것입니다.

기능을 유지하고 코드를 개선하는 것이 목적이며, 코드 수정 뒤에 기능이 이전과 동일하게 작동하는 지 확인을 할 필요가 있습니다.


이는 사실 리팩토링 과정에서만 필요한 것은 아닙니다.

보통 특정 기능을 만든다고 할 때, 코드 작성이 끝이 아닌 기능 단위의 검증이 수반되어야 합니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * 개인별 평가종류에 대한 기능이 잘 작동되는지에 대한 테스트.
 */
@Test
public void 개인별_평가종류_테스트() {
 
    // 동일 조건 테스트 케이스 제작.
    final Integer companySubjectSn = 1001;
    final Integer hrAppraisalSn = 20;
    final Integer memberSubjectSn = 1;
 
    /**
     * 테스트 결과 목록
     */
    List<Integer> targetHrAppraisalKindSnList = hrAppraisalOperateExecuteService.readTargetHrAppraisalKinsSnList(
                                                                                            companySubjectSn
                                                                                            , hrAppraisalSn
                                                                                            , memberSubjectSn);
 
    // 테스트에 대한 결과 명시..
    System.out.println(String.format("회사순번 %d, 평가순번 : %d, 구성원 순번 : %d", companySubjectSn, hrAppraisalSn, memberSubjectSn));
    System.out.println("평가종류순번 테스트 : " + targetHrAppraisalKindSnList);
}
cs



- Extract Method


너무 길게 작성된 메소드에 대하여, 일련의 특정 행위를 캡슐화하여 메소드로 분리하는 작업입니다. 

(철자 그대로 메소드 추출입니다.)

메소드가 길게 작성 되었다는 것은 해당 메소드의 책임이 너무 많음을 의미할 수 있습니다. 

그렇기 때문에 일련의 특정 행위를 메소드로 추출하는 작업은 일종의 책임의 분배를 하는 작업일 수 있습니다. (SRP)


보통은 이해가 필요한 부분에 대하여, 분리 후 주석을 명시하는 편입니다.

주석을 명시하는 것 외에도 메소드의 이름은 짧고 기능을 잘 설명할 수 있어야 한다고 합니다.


하지만, 제 생각에는 메소드의 라인이 너무 길게 작성 되었다고 무조건 메소드로 추출하는 것은 반대입니다. 재활용할 여지가 없는 로직을 굳이 분리하여 코드의 흐름이나 디버깅 등의 불편함을 주고 싶지는 않습니다.


메소드 추출 대신에, [block] 을 이용해보는 방법을 저는 제안합니다.

길게 작성된 로직에 대하여, 아래와 같이 block 으로 단계를 나누고 주석을 명시한다면 아무리 긴 로직도 가독성을 크게 해치지 않을 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void tooLongMethod() {
 
    /**
     * Map 주석 쓰는법.
     *
     * 대상자순번에 대한 이름 그룹
     *
     * key : 대상자 순번
     * value : 이름
     */
    final Map<Integer, String> recipientByNameMap;
    {
        // recipientByNameMap 을 만들기 위해 어떠한 일련의 과정을 하는중..
        // Refactoring 원칙에 따르면, 이 곳은 새 메소드로써 정의 해야함.
    }
 
    // 대상자순번에 대한 이름 그룹을 통해 이름_순번 string 을 가진 목록을 출력        
    // 자료구조를 상수화. 내부를 변경하지도 말 것.
    // 메소드 내에 많은 변수는 버그를 양산하기 쉬움.       
    final List<String> recipientBarcodeList;
    {
 
    }
}
cs


block 방식의 코드 작성은 사실 실제 메소드로 추출만 안했을 뿐이지, 캡슐화를 한 것과 별반 다르지 않습니다. 변경사항이 생겼다면, 특정 block 만 수정을 함으로써 메소드를 추출했던 것과 같은 효과를 볼 수 있습니다.


추 후, 재사용성을 위해 메소드로 추출해야 한다면, 해당 block 을 메소드로 만들고 치환하면 됩니다. 


아래는 구성원에 대한 이름 그룹을 만드는 과정을 Extract method 한 결과입니다.


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
public void tooLongMethod() {
 
    /**
     * Map 주석 쓰는법.
     *
     * 대상자순번에 대한 이름 그룹
     *
     * key : 대상자 순번
     * value : 이름
     */
    final Map<Integer, String> recipientByNameMap = getRecipientByNameMap();
 
    // 대상자순번에 대한 이름 그룹을 통해 이름_순번 string 을 가진 목록을 출력        
    // 자료구조를 상수화. 내부를 변경하지도 말 것.
    // 메소드 내에 많은 변수는 버그를 양산하기 쉬움.       
    final List<String> recipientBarcodeList;
    {
 
    }
}
 
/**
 * 대상자 순번에 대한 이름 그룹 출력.
 *
 * <pre>
 * key : 대상자 순번
 * value : 이름
 * </pre>
 *
 * @return
 */
public Map<Integer, String> getRecipientByNameMap() {
    // recipientByNameMap 을 만들기 위해 어떠한 일련의 과정을 하는중..
    // Refactoring 원칙에 따르면, 이 곳은 새 메소드로써 정의 해야함.
}
cs



- Move Method 


한 클래스의 메소드가 타 모듈에서 너무 많이 사용되거나, 해당 클래스의 상태를 전혀 사용하지 않는다면 메소드의 위치를 변경할 것을 제안합니다.


다른 모듈에서 훨씬 많이 사용하는 경우.


해당 메소드를 많이 사용하는 모듈로 이동시키고, 기존 클래스에서는 이동시킨 모듈을 이용한 위임호출로 기존 기능을 유지해 볼 수 있을 것 같습니다.



- 상태를 전혀 사용하지 않는 경우.


클래스는 상태와 행위를 결합하여 관리하고자 생긴 개념입니다.


하지만, 행위 자체가 상태를 동반하지 않는다면 이 메소드를 정적 메소드로 변경을 해볼 수 있을 것 같습니다. 

(특정 주체에 대하여, 정적 메소드가 많아진다면 Util 클래스의 제작을 고려합시다..)


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
public class MemberVo {
    private String name;
 
    public MemberVo(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    /**
     * 상태를 사용하지 않는 메소드는 정적 메소드로 만들길 권장.
     * 
     * @param name
     * @return
     */
    public static String GetMemberFormatter(String name) {
        return String.format("그 사람의 이름 : ", name);
    }
}
cs



- 불필요한 임시변수 제거


대부분의 임시 변수는 사실 메소드로 대체할 수 있습니다. 

임시 변수의 사용은 메소드의 길이를 증가시키고, 버그를 양산할 여지가 있습니다.

(임시변수를 많은 곳에서 변경하면, 디버깅 및 유지보수가 힘들 수도....)


하지만, STEP-BY-STEP 으로 작성된 코드를 리뷰함에 있어서, 상태를 잠시 어느 순간까지 가지고 있는 것은 오히려 가독성에 더 좋을 수 있습니다.

그렇지만, 변수 상태로 남겨두면 [사이드-이펙트]를 일으킬 여지가 있으니, 상수형으로 상태를 유지시키는 것도 하나의 방법이라고 할 수 있습니다. 자료구조 역시, 영속 자료구조(내부가 바뀌지 않는 구조)를 유지 해야겠죠?


아래는, 임시변수를 메소드로 대체한 것과 특정 상태를 상수로 유지하는 방법입니다.

제 입장은 가독성 및 디버깅을 위해서라도 상수형태의 상태를 남겨, STEP 별로 읽을 수 있도록 하는 것을 권장합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void tempMethodProcessing() {
 
    // 임시 변수 제거.
    // 메소드로 변수를 대체. 코드 길이를 줄일 수 있음.
    {
        for (MemberVo memberVo : memberDao.selectList(companySn)) {
            // something work!!
        }
    }
 
    // 상수로써, 데이터를 잠시 관리
    // 이 것이 오히려 디버깅이나 가독성면에서 보다 좋을 수 있음
    {
        final List<MemberVo> memberVoList = memberDao.selectList(companySn);
 
        for (MemberVo memberVo: memberVoList) {
            // something work!!
        }
    }
}
cs



- 분기 제거


일련의 분기처리 과정은 조건의 추가 및 삭제에 유연하지 못할 수 있습니다.

이는 요구사항 추가에 따라 분기한 과정들을 모두 살펴보며, 수정을 해야함을 의미합니다.


객체지향 다형성(Polymorphism)으로 이 문제를 해결할 수 있습니다.

특정 조건들 즉 상태를 객체로 관리함으로써, 기존 구조의 변경 없이 확장을 하는 것을 지향합니다.(OCP)


우리는 이에 대한 내용을 꽤 많이 다뤄보았습니다.



대표적으로 분기를 제거할 수 있는 패턴에는 방문자(Visitor), 전략(Strategy), 상태(State) 패턴 등이 있습니다. 


하지만, 모든 구조를 if 없이 구현하겠다는 의지는 좋지 않습니다. 

패턴에 의한 코드의 파편화는 결국 코드의 난이도를 올리며, 극한으로 작성된 패턴들은 오히려 가독성을 많이 저해할 수 있습니다.


요즘의 대세는 주니어가 봐도 이해하기 좋은 코드를 작성하는 것인 듯 합니다.



5. 리팩토링의 한계와 극복


리팩토링은 매우 중요한 일이지만, 모든 상황에 대하여 리팩토링할 수는 없는 것 같습니다. ㅡㅡ^

상황은 언제나 좋은 방향으로만 흘러가는 것이 아니니 말이죠. (가끔 좋지 않은 방향으로, 아니 종종...)


그렇기 때문에 이번 절에서는 리팩토링의 한계와 극복방법에 대해 다뤄보려 합니다.



- DataBase 스키마의 변경


우리가 작성하는 많은 응용 프로그램들의 로직들은 DB 스키마와 매우 밀접한 의존을 가지고 있습니다. 

아무리 잘 작성된 코드도 DB 스키마의 변경에서 자유로울 수는 없을 것 같군요....


DB 스키마의 변경에 따라 당연히 응용 프로그램 역시 변화에 대처를 해야하지만, 그 대처를 최소화하는 방법이 없는 것은 아닌 듯 합니다. 


DB 스키마 클래스와 비지니스 로직간의 중간 층(Layer)을 두고, 많은 응용프로그램에서 이 중간 층을 이용하여 개발을 한다면 생각보다 많은 부분을 변경하지 않아도 될 수도 있습니다.

스키마 변경에 따라 중간 층에만 최대한의 수정을 할 수 있다면 좋겠죠? :-)


우리는 이 방법 역시 배운적이 있습니다.

맞습니다. 여기서 말하는 중간 층은 바로 어댑터(Adapter) 입니다.



저는 이 방법을 매우 지향합니다. 

사내에서 제 역할인 [백-엔드 개발자]로써, [프론트-엔드 개발자]에게 특정 요청에 필요한 정보만 적재적소하게 줄 수 있도록 제어를 하는 편입니다.


이 방법은 DB 스키마 변경에 따른 프론트-엔드 개발자의 영역의 변화를 최소화할 수 있으며, 최근 마감기한을 앞두고 새로운 요구사항에 의해 엎어질 뻔한 프로젝트를 구할 수 있는 [최고의 한방]이 되었습니다.



- 메소드 서명의 변경


우리는 경우에 따라 이미 개발되었고 잘 사용하고 있는 메소드를 수정할 경우가 종종 있습니다.


메소드 내부의 구조의 개선이나 최적화 등은 그나마 괜찮지만, 메소드의 서명을 변경하는 해당 메소드를 사용하는 모든 곳을 고쳐야 하기 때문에 안타까운 일이 발생할 수 있습니다.


이런 경우, 우리는 오버로드 방식을 한 번 고려해볼 수 있습니다.


즉, 기존 서명은 남기고, 새 서명을 가진 메소드를 제작하는 것입니다. 

물론 기존 서명은 새 서명을 가진 메소드를 사용하게 함으로써, 코드중복을 없앨 수 있겠죠? 


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
/**
 * 회사 순번과 구성원 순번으로 이용하여 데이터를 조회
 * 
 * <pre>
 *     하지만, 이제는 구성원 순번 목록을 이용하여 조회해야함.
 *     즉 해당 메소드는 구서명임..
 *     
 *     새로 작성된 서명을 사용하는 방식으로 내부구조를 개선..
 * </pre>
 * 
 * @param companySubjectSn
 * @param memberSubjectSn
 * @return
 */
public List<MemberVo> selectMemberList(Integer companySubjectSn, Integer memberSubjectSn) {
    return selectMemberList(companySubjectSn, Arrays.asList(memberSubjectSn));
}
 
/**
 * 새로 만든 서명.
 * 
 * 회사순번과 구성원 순번 목록으로 데이터를 조회.
 * 
 * @param companySubjectSn
 * @param memberSubjectSnList
 * @return
 */
public List<MemberVo> selectMemberList(Integer companySubjectSn, List<Integer> memberSubjectSnList) {
    // something Work!!
}
cs


만약 기존 서명을 더이상 팀의 구성원들이 지금부터라도 사용하길 지양한다면, 사장(Deprecated) 를 고려하는 것도 좋습니다.


사장을 시켰다면, 문서에 아래와 같이 사장 사유와 우회법을 명시하세요. :-)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * 회사 순번과 구성원 순번으로 이용하여 데이터를 조회
 * 
 * <pre>     
 *     Deprecated 사유 : 이 메소드는 오직 구성원 순번만 받고 있음. 
 *                      새 서명을 가진, 구성원 순번 목록을 이용하여 조회하는 새 서명을 가진 MemberDao::selectMemberList 을 사용하길 권장.
 * </pre> 
 * 
 * @param companySubjectSn
 * @param memberSubjectSn
 * @return
 */
@Deprecated
public List<MemberVo> selectMemberList(Integer companySubjectSn, Integer memberSubjectSn) {
    return selectMemberList(companySubjectSn, Arrays.asList(memberSubjectSn));
}
cs



- 리팩토링 보다는 재개발을....


작성된 기존 코드의 설계가 새 요구사항을 수용하기에 있어서, 많은 변화가 있어야 한다면 처음부터 다시 작성하는 편이 좋을 수도 있습니다.


단, 대부분의 기능이 정상적으로 작동을 할 때 이 방법을 해야할지도 모릅니다.

[버그 투성이 코드]를 참고하여 만든 새로운 코드 역시 [버그 투성이] 겠죠... ㅡㅡ^


하지만, 새로 작성하기에 시간이 부족하거나 너무 많을 경우 기존 코드를 세부 컴포넌트로 분리를 해볼 것을 제안합니다. 분리된 세부 컴포넌트들 중 특정 컴포넌트만 재개발을 하면 되는 경우도 종종 존재합니다.



- 마감시간이 다되었다면, 리팩토링은 잠시 보류...


마감시간이 다되었을 때, 리팩토링 등의 코드개선은 좋아 보이지 않습니다.

(당신의 상사에게 뒷통수를 맞을 수 있습니다... )


이미 잘 작동하며 테스트까지 끝낸 모듈에 마감시간을 앞두고 리팩토링을 하다가 버그를 심는 멍청한 짓은 좋지 못합니다.


또한, 프로그래머의 코드 철학도 중요하지만 이 제품을 쓸 고객과의 약속이 더 중요합니다.


이 내용들의 기반은 저에게 OOP 를 알려주신 교수님의 자료와 [리팩토링-코드품질을 개선하는 객체지향 사고법] 의 내용을 바탕으로 작성되었지만, 실무에서 약 2년정도 참여해보며 후배들에게 꼭 알려주고 싶은 이야기들을 담은 것 같습니다.


아직 부족한 것이 많지만, 경험을 통해 이 사례가 좋았다고 느낀 점들을 특히 부각시켜 서술하였습니다. 

(그래서 약간 꼰대 같을 수도 ㅜㅡㅜ)



어찌하든, 이 것으로 [CHAPTER X] 도 끝입니다. 


끝까지 읽어주신 여러분 정말로 감사합니다. :-)


반응형
Posted by N'

6. 리팩토링.pdf



마지막 스터디에서 잠시 진행했던, Extra 챕터입니다. 


모두들 수고하셨습니다. @.@ [꾸벅][꾸벅]



참고자료 

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

- Refactoring


추가내용!


Refactoring.zip


이 곳의 내용을 보충할 실습 코드를 첨부합니다.




반응형
Posted by N'

Observer 패턴에 다루는 마지막 포스팅입니다. 


정말, 이 패턴에 대해 할 말이 많군요...

그만큼 많이 사용하고 유명한 패턴이기 때문에 그런 듯 합니다.

(그렇다고, Observer 패턴은 사랑은 아닙니다...     ㅡㅡ^)


Observer 패턴과 관련된 정보는 아래에서 참고!



앞써, 언급한 바와 같이 Observer 패턴은 매우 많이 사용하는 패턴입니다.

그렇기 때문에 JAVA 를 만든 개발자들은 이 패턴을 보다 적극적으로 활용할 수 있도록, JDK1.0 부터 모듈을 배포하였습니다.


오늘 포스팅에서는 지난 포스팅에서 만든 예제를 자바의 Observer 패턴 관련 클래스를 이용하여 리팩토링해보려 합니다. 대상 프로젝트는 "[CHAPTER 5] 실무 활용 패턴 (하) [Observer 패턴] + 추가내용2" 하단에서 다운로드 할 수 있습니다.


일단, 자바에서 제공하는 내장 모듈인 Observable 과 Observer 부터 소개를 해보겠습니다.



1. 자바 내장 모듈 [Observable 과 Observer]


앞써, 알아봤던 Observer 패턴에서는 크게 두 개의 컴포넌트가 있음을 확인했습니다.

바로 주제(Subject)와 관찰자(Observer) 입니다. 기억이 나시나요?


자바 내장 모듈은 이 [두 개의 컴포넌트]들을 쉽게 제작할 수 있도록 지원하고 있습니다.


- Observable [java.util.Observable]


주제(Subject)에 해당하는 컴포넌트를 만들 수 있도록 지원하는 클래스입니다. 

변화를 통보할 대상인 [Observer 인터페이스]목록을 Thread-safe 하게 관리하며, 통보할 수 있는 메소드를 지원하고 있습니다.


통보하는 메소드는 물론 PUSH, PULL 방식 모두 지원하도록 설계되어 있습니다.


1
2
3
4
5
// PULL 방식 notify. (아무것도 보내지 않는 방식)
observable.notifyObservers();
        
// PUSH 방식 notify. (아쉽게도, 통보할 때 보내는 데이터 타입은 한 개로 제한..)
observable.notifyObservers(memberList);
cs


편리해보이지만, 주의해볼만한 사항은 Observable 이 class 라는 점입니다.

Observer 인터페이스들을 관리해야하는 로직을 담아야하기 때문에 interface 로 제작할 수는 없었던 듯 합니다.


- Observer [java.util.Observer]


관찰자(Observer)에 해당하는 컴포넌트를 만들 수 있도록 지원하는 인터페이스입니다.

이 전 예제의 IObserver 를 대체할 수 있겠군요.


Observer 의 내부를 보면, 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Observer {
    /**
     * This method is called whenever the observed object is changed. An
     * application calls an <tt>Observable</tt> object's
     * <code>notifyObservers</code> method to have all the object's
     * observers notified of the change.
     *
     * @param   o     the observable object.
     * @param   arg   an argument passed to the <code>notifyObservers</code>
     *                 method.
     */
    void update(Observable o, Object arg);
}
cs


파라미터로는 [주제에 해당하는 Observable o] 과 [Push 방식을 통해 전달되는 데이터인 Object args] 이 존재 합니다. 


[Object args]의 주석을 보면, Observable::notifyObservers 를 통해 넘겨지는 데이터가 온다고 써있습니다. 즉, PUSH 방식으로 통보를 하면 통보 시 전달한 데이터가 넘어오고, PULL 방식을 사용하면 NULL 임을 짐작할 수 있을 것 같습니다.



쉽게 사용할 수 있도록 제작된 모듈이니, 이 정도면 한 번 모듈을 사용해서 Observer 패턴을 구현해 볼 수 있을 것 같군요. 


한번, 지난 포스팅에서 작성한 예제를 바탕으로 리팩토링을 한번 해볼까요? 



2. 내장 모듈을 사용한 리팩토링


이미 우리는 Observer 패턴을 구현하였기 때문에 JDK 의 내장모듈을 쉽게 부착할 수 있을 지도 모릅니다. 


먼저 우리가 작성했던 주제(Subject) 컴포넌트인 DataManager 부터 손을 보도록 하죠.


DataManager 클래스는 Observable 클래스를 상속받을 예정이며, 이를 통해 Observer 를 관리하던 메소드 및 멤버변수도 삭제할 것입니다. 


저는 아래와 같이 만들어 봤습니다. (정확히 말하면, 기능 삭제만 했습니다. @.@)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
 * 메신저에서 사용하는 데이터를 총괄적으로 관리하는 데이터 매니저.
 *
 * <pre>
 *     Observable 를 상속!
 *     그러나, 추 후 DataManager 가 상속받아야 하는 클래스가 있으면 문제 발생.
 *     (JAVA 는 다중상속을 할 수 없음..)
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 16..
 */
public class DataManager extends Observable{
 
    // 통보대상 집합 (삭제. Observable 클래스가 지원)
    // private HashSet<IObserver> iObserverSet = new HashSet<>();
 
    /**
     * 옵저버 추가
     *
     * <pre>
     *     Deprecated 사유 : Observable::addObserver 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     * @param observer
     */
    @Deprecated
    public void registerObserver(IObserver observer) {
      //  iObserverSet.add(observer);
    }
 
    /**
     * 옵저버 삭제
     *
     * <pre>
     *     Deprecated 사유 : Observable::deleteObserver 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     * @param observer
     */
    @Deprecated
    public void deleteObserver(IObserver observer) {
     //   iObserverSet.remove(observer);
    }
 
    /**
     * 각 옵저버에게 현재 상태 데이터를 넘기면서, 변경사실을 알린다.
     *
     * <pre>
     *     Deprecated 사유 : Observable::notifyObservers 를 통해 이 기능을 대체 가능.
     *
     *     삭제할 것.
     * </pre>
     *
     */
    @Deprecated
    public void notifyObserver() {
     /*   for (IObserver observer : iObserverSet) {
           observer.update(this);
        }*/
    }
 
   /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        // something work
 
        // 변화가 일어났음을 알림. (이 메소드 호출을 통해 자주 통보하는 것을 막을 수 있음)
        setChanged();
 
        // Observable::notifyObservers 기능 사용. PULL 방식
        notifyObservers();
    }
}
cs


Observer 를 관리하는 서명들은 위에서 모두 주석 혹은 사장(Deprecated) 처리가 되었지만, 최종적으로 삭제할 것입니다. 


Observable 을 상속받음으로써, DataManager 는 서버로부터 전달된 데이터만 업데이트 하고,  Observable::setChanged와 Observable::notifyObservers 만 사용해주면 되는군요. 


여기서, 주목할 것은 Observable::setChanged로 상태 변화가 일어났음을 나타내는 메소드 입니다. 

이 메소드를 통해 너무 자주 통보되는 것을 막을 수 있으며, 이를 호출하지 않고 Observable::notifyObservers 만 호출하면 아무 일도 일어나지 않습니다. 

적절한 비지니스 로직으로 Observable::setChanged 를 제어할 수 있을 것 입니다.


즉 구현에 있어서 Observer 관련 내용을 신경쓰지 않아도 됩니다.



이제 관찰자(Observer) 컴포넌트를 수정할 차례입니다. 


우리는 이전에 IObserver 를 구현하는 데모 View 컴포넌트를 만든 적이 있습니다. 

이제는 Observer 를 구현하도록 수정하고, IObserver 와 작별을 할 것입니다. :-)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class DemoViewComponent_V1 implements Observer {
 
    /**
     * 화면 갱신
     *
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
        if (instanceof DataManager) {
            final DataManager dataManager = (DataManager) o;
 
            // 구성원 정보 업데이트.
            {
                // 유연한 구성원 데이터 조회
                final List<MemberVo> memberVoList = dataManager.getMemberList();
                viewUpdateByMember(memberVoList);
            }
 
            // 전자결재 정보 업데이트.
            {
                // 유연한 전자결재 데이터 조회
                final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
                viewUpdateByApprovalDoc(approvalDocList);
            }
        }
    }
}
cs


일단, DataManager 의 서명이 변경되었으니 테스트 코드 쪽에서도 불이 났을 것입니다. 

테스트 코드 역시 조금 수정을 할 필요가 있겠군요. 


새 메소드 서명에 따라 다음과 같이 변경할 수 있어 보입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 데이터 매니저 생성
DataManager dataManager = new DataManager();
 
// 데모 View 컴포넌트 생성.
DemoViewComponent_V1 demoViewComponent_v1 = new DemoViewComponent_V1();
DemoViewComponent_V2 demoViewComponent_v2 = new DemoViewComponent_V2();
 
// 각 컴포넌트들이 Data 변경알림에 대해 구독하겠다고 등록.
dataManager.addObserver(demoViewComponent_v1);
dataManager.addObserver(demoViewComponent_v2);
 
// something work
dataManager.loadByServer(
        Arrays.asList(new MemberVo("강현지"), new MemberVo("유덕형"), new MemberVo("유형주"))
        , Arrays.asList(new ApprovalDoc("통과 - 스터디 신청"))
        , Arrays.asList(new MessageVo("모두, 스터디 참여 잘해줘서 감사합니다."), new MessageVo("그동안 수고하셨습니다.")));
 
 
// CONSOLE LOG
// 데모2 메시지 View 화면 갱신 중..
// [모두, 스터디 참여 잘해줘서 감사합니다.] 갱신 중
// [그동안 수고하셨습니다.] 갱신 중
// 데모2 메시지 View 화면 갱신 완료!!
// 
// 데모1 구성원 View 화면 갱신 중..
// [강현지] 갱신 중
// [유덕형] 갱신 중
// [유형주] 갱신 중
// 데모1 구성원 View 화면 갱신 완료!!
// 
// 데모1 전자결재 View 화면 갱신 중..
// [통과 - 스터디 신청] 갱신 중
// 데모1 전자결재 View 화면 갱신 완료!!
cs


다행히 DataManager 의 변화에 따라 View 컴포넌트들이 적절한 행위를 잘 해주는 군요. :-)



3. 자바 내장 모듈의 한계와 극복


Observable 과 Observer 를 사용하면 편리하게 Observer 패턴을 사용할 수 있음을 확인하였습니다. 

하지만, 이 내장 모듈에는 한계가 존재합니다. Observable 이 클래스라는 것이죠. 


Observable 을 개발한 개발자의 의도는 Observable 을 상속받길 원했겠지만, 아쉽게도 DataManager 에 상속을 해야할 일이 있다고 가정해봅시다.


Java 에서는 다중상속을 지원하지 않으니, Observable 을 상속하는 것을 포기해야 합니다.

또한 상태의 변화가 있다고 체크하는 Observable::setChanged 는 protected 입니다. 상속이 힘들다고, DataManager 에 Observable 인스턴스를 만들어 관리한다 하더라도 Observer 들에게 통보를 할 수 없습니다. 


이는, 설계원칙 Favor has-a over is-a (상속해야하는 것을 has-a 로 바꿀 수 있어야 한다.)에 위배됩니다. 

안타깝군요. ㅜㅡㅜ


하지만 조금 돌아가면 방법이 없지는 않습니다.

Observable 을 상속한 다른 클래스(이를테면, MessengerObservable) 을 만들고, DataManager 가 MessengerObservable 을 has-a 관계로 취할 수 있을 것 같습니다.


저는 이렇게 작성을 해보았습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
/**
 * 메신저에서 사용할 Observable 클래스
 *
 * <pre>
 *     DataManager 에 직접 상속을 할 수 없기 때문에, 해당 클래스를 제작.
 *     DataManager 가 Observable 을 사용하기 위해서는 이 모듈을 사용해야함.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 18..
 */
public class MessengerObservable extends Observable{
 
    /**
     * Observer 에게 통보
     *
     * <pre>
     *     - setChanged 메소드는 해당 클래스에서 제어.
     *     - Observer 들이 DataManager 에 접근할 수 있도록 PUSH 방식 Observer 패턴을 사용.
     * </pre>
     */
    @Override
    public void notifyObservers(Object dataManager) {
        this.setChanged();
 
        // PUSH 방식 Observer 패턴 사용을 통해 DataManager 를 넘김.
        super.notifyObservers(dataManager);
    }
 
}
 
/**
 * 메신저에서 사용하는 데이터를 총괄적으로 관리하는 데이터 매니저.
 *
 * <pre>
 *     Observable 를 상속불가 우회
 *     - MessengerObservable 를 has-a 관계로 처리.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 16..
 */
public class DataManager extends SomethingSuperObject{
 
    private MessengerObservable messengerObservable = new MessengerObservable();
 
    /**
     * 추가 메소드 지원.
     *
     * <pre>
     *     - MessengerObservable 에 Observer 추가.
     * </pre>
     *
     * @param observer
     */
    public void addObserver(Observer observer) {
        messengerObservable.addObserver(observer);
    }
 
    /**
     * 삭제 메소드 지원.
     *
     * <pre>
     *     - MessengerObservable 에서 Observer 삭제.
     * </pre>
     *
     * @param observer
     */
    public void deleteObserver(Observer observer) {
        messengerObservable.deleteObserver(observer);
    }
 
    /**
     * 서버로부터 데이터 로드
     *
     * <pre>
     *     각 정보 목록들은 한 트랜잭션에서 동시에 갱신된다고 가정.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    public void loadByServer(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList) {
        // something work!!
 
        // PUSH 방식으로 우회.
        messengerObservable.notifyObservers(this);
    }
}
 
/**
 * 데모 View 갱신 컴포넌트 (V1)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V1 implements Observer {
 
    /**
     * 화면 갱신
     *
     * @param o
     * @param arg
     */
    @Override
    public void update(Observable o, Object arg) {
 
        if (arg instanceof DataManager) {
            final DataManager dataManager = (DataManager) arg;
 
            // 구성원 정보 업데이트.
            {
                // 유연한 구성원 데이터 조회
                final List<MemberVo> memberVoList = dataManager.getMemberList();
                viewUpdateByMember(memberVoList);
            }
 
            // 전자결재 정보 업데이트.
            {
                // 유연한 전자결재 데이터 조회
                final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
                viewUpdateByApprovalDoc(approvalDocList);
            }
        }
    }
}
 
cs


주목할 점은 MessengerObservable 이 PUSH 방식으로 DataManager 를 전달하기 때문에, Observer 구현체들은 두번째 파라미터인 Object arg 를 통해 DataManager 를 찾아야 합니다.


드디어, Observer 패턴과 관련된 모든 내용이 끝이 났고, 동시에 이번 스터디에서 진행예정이던 모든 패턴이 끝이 났습니다. (ㅜㅡㅜ)


이 포스팅의 결과물 프로젝트는 아래에서 참고! 



STUDY_OOP7.zip



마지막 패턴이야기라 그런지 유종의 미를 잘 거두고 싶었습니다. 

그래서 더욱 자세히 쓰고자 이 패턴에 대해 세 개의 포스팅으로 나누어 작성 했었던 것 같습니다.


주 독자와 나누는 마지막 패턴 이야기라 조금 섭섭하기도 하기도 하고, 더 알려줄 것이 없나 고민도 하게 되었던 것 같습니다. 그래서 빨리 끝낼 수 있는 걸 질질 끌었던 것 같기도.....


특히 최근에 마지막 스터디를 진행하기 전 날, [대학교 1학년]-[SW멤버십 회원]-[M사 신입사원]-[스터디 진행]까지의 있었던 일들이 주마등처럼 지나갔었습니다. 


[문과출신 공대생]이라는 핸디캡 극복을 위해 노력했었고, [4학년 동기들 중 2학년이었던 SW멤버십]에서 끝까지 살아남기 위해 발버둥을 쳤으며, [M사 신입사원] 시절 사내에서 아무도 하지 않았기 때문에 누군가의 도움도 기대할 수 없는 솔루션급 앱들을 밤새 만들고 고치며 끝내 좌절하던 경험들은 저에게 뼈아픈 교훈들만 주었던 것 같습니다.


최근에 읽었던 칼럼 중 [지금의 실력은 지식의 총합이 아닌 고통의 총합] 이라는 글은 이 때의 일을 주마등처럼 지나가게 한 촉매가 아니었나 생각이 듭니다. 


하지만, 이런 뼈아픈 교훈들은 안타깝게도 급격한 실력의 변화를 주지는 못하던 것 같습니다. 


최근에 읽었던 [소프트웨어 장인], [구글 리쿠르팅 데이의 타 회사 세션] 등의 내용을 참고하면, 앞에서 끌어주는 [메이저 주니어 이상급의 개발자]들의 활동은 무시할 수 없어보였습니다. 이를테면 페어 프로그래밍이나 코드 리뷰, 본질에 가까운 OOP&디자인 패턴 교육 등은 주니어들의 레벨을 올리는 것에 크게 기여하는 것 같아 보였습니다.


결론은 모두가 뼈아픈 교훈을 얻어갈 필요는 없는 것 같습니다. :-)

이 블로그의 글들은 대단하지 않으며 꽃길로 인도할 수도 없지만, 조금이나마 진흙을 덜 밟는데 도움이 되길 바랍니다.


감사합니다.










반응형
Posted by N'

지난 포스팅에서는 특정 주제의 변화에 따라 여러 클래스가 어떤 행위를 해야할 때, 즉 이벤트 처리를 하기 위한 대표적인 패턴인 Observer 패턴에 대해 알아보았습니다.


간단한 예제 프로그램도 한 번 작성해보았죠? :-)


해당 내용과 관련된 정보는 아래에서 참고! 



이번 포스팅에서는 지난 스터디에서 작성한 코드에 대한 약간의 리팩토링을 담을 예정입니다.


조금 더 유연한 구조를 한번 제시해 볼 생각이며, 작업 대상이 되는 프로젝트는 "실무 활용 패턴 (하) [Observer 패턴] + 추가내용" 의 하단에서 다운로드 하실 수 있습니다.


일단, 고민이 되는 이슈부터 기술을 해보도록 하죵...



1. IObserver 인터페이스의 메소드 서명은 적절한가?


구체화 된 View 컴포넌트들에게 DataManager 의 상태 변화를 알리고자, DIP 원리에 따라 추상적인 개념인 IObserver 를 만들었습니다.


아래와 같이 말이죠..


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 옵저버 인터페이스 제작
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public interface IObserver {
 
    /**
     * 주체의 데이터가 변경될 때, 이 메소드를 실행
     *
     * <pre>
     *     - 업데이트 이벤트를 이 메소드를 통해 받을 수 있음.
     *     - 변경된 데이터가 파라미터로 넘어오는 것을 보장. 이 방식 PUSH 방식 옵저버 패턴이라고 함.
     * </pre>
     *
     * @param memberVoList
     * @param approvalDocList
     * @param messageVoList
     */
    void update(List<MemberVo> memberVoList, List<ApprovalDoc> approvalDocList, List<MessageVo> messageVoList);
}
cs


DataManager 는 통보할 대상을 IObserver 로 관리함으로써 각 ViewComponent 들과의 직접적인 의존관계를 끊을 수 있었고, ViewComponent 들 역시 IObserver 를 구현함으로써 DataManager 에게 쉽게 구독여부를 등록할 수 있었습니다.


하지만, 이런 고민을 해볼 수 있습니다.


- 구체화된 View 컴포넌트들은 사실 모든 정보가 필요하지 않습니다. 

  

테스트로 구현해본 [DemoViewComponent_V1 은 구성원정보와 전자결재 정보]만 필요했습니다. 또한 [DemoViewComponent_V2 는 메시지 정보]만 필요했죠..


- 데이터 형식 추가에 취약해보입니다.


현재 배포할 버전에서는 구성원, 전자결재, 메시지 정보만 필요하지만, 추 후 근태정보, 평가정보 등 다른 정보가 추가되어야 한다면 IObserver::update 서명을 수정해야합니다.


인터페이스 서명의 변경은 구현된 모든 클래스들도 변경해야 함을 의미합니다.


결국, DataManager 의 통보를 하기 위한 방식에 조금 문제가 있어보이네요.

큰 구조에서 봤을 때 Subject-Observer 의 관계는 잘 나누었지만, 디테일한 부분을 조금 더 리팩토링해주면 좋을 것 같습니다.


다행히, 아직 View 컴포넌트들이 많이 생성되지 않았으니 구조를 조금 변경해볼까요? :-)



2. 통보방식의 변경


현재 IObserver:update 의 메소드 서명을 보면, 상태의 변화사실과 함께 변경된 데이터를 같이 전달하는 방식입니다. 지난 시간에 이 방식을 PUSH 방식의 Observer 패턴이라고 설명을 했었습니다.


하지만 이 방식은 결국 전달할 데이터들을 메소드 서명에 명시해야하며, 이는 전달할 데이터 형식에 유연할 수 없음을 의미합니다. 유연할 수 없는 데이터 형식 때문에 데이터 형식의 추가/삭제가 힘들고, 모든 관찰자(Observer)는 원하지도 않는 데이터들을 강제로 받아야 하죠.


그렇다면, 관찰자들이 조금은 능동적으로 본인이 원하는 정보만 취할 수 있다면 어떨까요?

주제(Subject)는 관찰자들에게 변경사실만 알리고, 관찰자들이 알아서 주제로부터 상태를 조회하는 방식이죠.


인터페이스를 다음과 같이 변경해보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 옵저버 인터페이스 제작
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public interface IObserver {
 
    /**
     * 주체의 데이터가 변경될 때, 이 메소드를 실행
     *
     * <pre>
     *     - 업데이트 이벤트를 이 메소드를 통해 받을 수 있음.
     *     - 이 메소드에서는 직접적으로 변경된 데이터를 보내지 않음.
     *     - Subject 인스턴스를 전달하여, 관찰자들이 능동적으로 원하는 방식을 취하는 방식.
     *     - 이 방식 Pull 방식 옵저버 패턴이라고 함.
     * </pre>
     *
     * @param dataManager
     */
    void update(DataManager dataManager);
}
cs


변경된 IObserver::update 는 통보와 함께 변경된 데이터들을 전달하는 것이 아닌, 주제 클래스를 전달하고 있습니다. 


각 IObserver 들의 구현체들은 변경을 통보 받을 시, 전달받은 주제(DataManager) 클래스로부터 원하는 데이터를 유연하게 조회할 수 있을 것입니다. 

이와 같이, 주제는 변경에 대한 통보만 하고 데이터는 관찰자들이 주제로부터 직접적으로 데이터를 조회하는 방식 PULL 방식 옵저버 패턴이라고 합니다.


구성원 데이터와 전자결재 데이터가 필요했던, DemoViewComponent_V1 을 저는 다음과 같이 수정해 보았습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
 * 데모 View 갱신 컴포넌트 (V1)
 *
 * <pre>
 *     - 구성원과 전자결재 화면을 업데이트.
 * </pre>
 *
 * Created by Doohyun on 2017. 6. 17..
 */
public class DemoViewComponent_V1 implements IObserver {
 
    /**
     * 화면 갱신.
     *
     * @param dataManager
     */
    @Override
    public void update(DataManager dataManager) {
        // 구성원 정보 업데이트.
        {
            // 유연한 구성원 데이터 조회
            final List<MemberVo> memberVoList = dataManager.getMemberList();
            viewUpdateByMember(memberVoList);
        }
 
        // 전자결재 정보 업데이트.
        {
            // 유연한 전자결재 데이터 조회
            final List<ApprovalDoc> approvalDocList = dataManager.getApprovalDocList();
            viewUpdateByApprovalDoc(approvalDocList);
        }
    }
 
    /**
     * 구성원 화면 업데이트
     *
     * @param memberVoList
     */
    public void viewUpdateByMember(List<MemberVo> memberVoList) {
        System.out.println("데모1 구성원 View 화면 갱신 중..");
 
        for (MemberVo memberVo : memberVoList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getName());
        }
 
        System.out.println("데모1 구성원 View 화면 갱신 완료!!\n");
    }
 
    /**
     * 전자결재 화면 업데이트
     *
     * @param approvalDocList
     */
    public void viewUpdateByApprovalDoc(List<ApprovalDoc> approvalDocList) {
        System.out.println("데모1 전자결재 View 화면 갱신 중..");
 
        for (ApprovalDoc memberVo : approvalDocList) {
            System.out.printf("[%s] 갱신 중\n", memberVo.getTitle());
        }
 
        System.out.println("데모1 전자결재 View 화면 갱신 완료!!\n");
    }
}
 
cs


PULL 방식 옵저버 패턴을 통해, 각 View 컴포넌트들은 원하는 데이터들만 주제로부터 유연하게 조회할 수 있겠군요. 


또한 데이터 형식이 추가된다 하더라도 IObserver 인터페이스를 수정할 필요가 없습니다. 

더이상 IObserver:update 에서 데이터 형식에 대한 서명을 명시할 필요가 없기 때문이죠.


이와 같이 상태변화를 통보하는 방식에 따라 PUSH 와 PULL 방식이 존재합니다. 

실전에서 이 패턴을 쓰기 전에, 이 두 방식에 대해 조금 더 Zoom-In 해서 볼 필요가 있을 것 같네요.



3. PUSH vs PULL


앞써, 기술한 것과 같이 상태변화 통보 방식에 따라 Observer 패턴을 [PUSH 과 PULL], 두 방식으로 크게 나눌 수 있습니다. 


두 방식에는 다음과 같은 특징이 존재합니다.


- PUSH


주제가 상태변화를 통보를 할 때, 관찰자에게 변화된 정보까지 제공하는 방식입니다.


관찰자들은 주제가 어떤 녀석인지 알 필요도 알고 싶지도 않습니다. 

단지, 무언가에 의해 통보사실과 함께 관심있는 데이터를 이용해 원하는 행위를 취하면 되죠.

소프트웨어 공학적으로 말하면, 모듈 간의 결합도(Coupling)를 낮출 수 있습니다.

좋은 프로그램은 모듈 간 낮은 결합도와 높은 응집성을 갖는 것이 이상적 입니다.


하지만, 통보받은 새 정보 중에는 알고 싶지 않은 정보도 포함되어 있습니다. 

또한 정보타입의 추가에 유연할 수가 없습니다.


- PULL


주제는 단지 상태 변화만을 통보하며, 변화된 정보는 관찰자가 주제로 부터 별도로 정보를 요구하는 방식입니다.


관찰자 입장에서는 원하는 정보를 유연하고 편리하게 요구할 수 있습니다.

또한, 정보 타입 추가에도 별도로 관찰자들을 수정을 하지 않아도 됩니다. (OCP 하군요..)


하지만, 관찰자들은 주제로 부터 정보를 별도로 요구해야 하기 때문에 주제에 대해 많은 것을 알아야 합니다. 즉 모듈 간 결합도가 증가했습니다. (tight-Coupling)


극단적인 경우, 동시성 처리를 할 시 제공받는 정보의 질이 다를 수 있습니다. 

[상태 변화 당시의 정보]와 [별도로 요구할 때의 정보]가 Thread-safe 하지 않을 수 있습니다. 


보통 권장하는 스타일은 변화에 보다 유연한 PULL 방식의 옵저버 패턴이지만, 실전에서 필요한 요구사항에 따라 적절한 방식을 선택해야 겠죠? 



오늘 포스팅에서는 추가로 제안되는 PULL 방식의 Observer 패턴을 다뤄 보았습니다.


변경된 소스는 아래 프로젝트 파일을 참고!


STUDY_OOP7.zip


Observer 패턴 자체도 많이 사용하기 때문에 중요한 패턴이지만, PUSH vs PULL 의 개념 등은 패턴 외에도 큰 모듈들 간의 관계 자체에서도 흔하게 등장하기 때문에 더욱 소개하고 싶었습니다.


또한, 지난 포스팅에서 다룬 이벤트 감지 방법인 Polling vs Interrupt 개념도 중요합니당..


흔히, 사용하는 Source-Tree 도 [프로젝트 상태 변경 사실을 표시]하고, 우리가 [PULL 을 받도록] 하죠? 프로젝트 상태 변경 사실은 [주기적으로 체크하는 Polling 방식]을 사용합니다.


이렇게 우리가 흔히 사용하는 응용 프로그램에서도 이 개념들을 많이 찾아볼 수 있습니다.


그래서 이번 주제는 단지, 디자인 패턴 뿐 아니라 소프트웨어 공학적인 여러 개념까지 다뤄 볼 수 있는 기회 였던 것 같아 뿌듯하네요.. ㅎㅎ


이 글을 보는 많은 분들에게 도움이 되었으면 좋겠습니다.:-)


반응형
Posted by N'

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

오늘의 포스팅은 살짝 특별합니다. 


왜냐하면, 기술블로그인 "초보프로그래머의 개발 이야기" 에서 첫 번째로 기술내용이 없는 포스팅이기 때문이죠. 

그렇습니다. 오늘 포스팅 주제는 잠시 쉬어가는 후기입니다. 


그럼 정식으로 다시 인사를...



 

뜬금없이 갑자기 웬 후기인가 생각해보면, 


조회수가 8000명이 돌파! 


는 아니고, 


현재 진행하는 이 블로그의 두 번째 장인 [OOP & FP 연구] 수업자료 제작이 끝이 났고, 연구 주제 중 OOP 의 마지막 챕터가 끝나가고 있기 때문이죠. 즉 1부 끝? ㅡㅡ^


마치 만화책 단행본에 있는 작가 후기같은 글을 한 번 써보고 싶었습니다.

일단 지금까지는 계획대로 스터디(한 번쯤은 꼭 제대로 해보고 싶었던 ㅜㅡㅜ)가 잘 진행되고 있고,  다음 주제도 잘해보자는 자축 뻘글입니다.




어쨌든 이 블로그와 스터디 의 머리글을 이제서야 한 번 써볼까 합니다.



1. JAVA8 카테고리


블로그를 처음 운영하게 된 것은, 할 일 없는 잉여 개발자가 갑자기 JAVA8 에 꽂혀서 보게된 JAVA8 in Action 이었습니다. 


우리 스터디의 두번 째 주제인 FP 진행을 할 수 있는 바탕이죠. (스포일러)

그런데 어쩌다 선정한 도서가 사내 표준이 되는 건 위험한데..... 뭐 그건 중요한게 아니니..



자바 8 인 액션
국내도서
저자 : 라울-게이브리얼 우르마(RAOUL-GABRIEL URMA),마리오 푸스코(MARIO FUSCO),앨런 마이크로프트(ALAN MYCROFT) / 우정은역
출판 : 한빛미디어 2015.04.01
상세보기



작년 이 맘 때, 일하다가 쉬는 시간에 페이스북의 어느 타임라인을 훑어보다가 우연히 JAVA8 을 익혀야 하는 이유에 대한 어느 삽화를 보게 되었습니다. 


그 삽화의 내용은 "나무를 도끼로 잘 베던 나무꾼이 전기톱이 나왔음에도 불구하고 도끼만 사용하는 이야기" 였습니다. 


이 삽화 내용은 단순했지만, 아는 것만 가지고 그 선에서 제품을 만들고 있는 저에게 꽤 신선한 충격을 주었습니다. 후배들에게 Comfort zone 을 벗어나야 한다고 말했지만, 벗어나지 못하는 것은 저였죠. ㅜㅡㅜ


그래서 무작정 책을 구입 했는데 내용이 꽤 어렵다는 생각도 들었고,

당시 사내에서는 JAVA8 을 쓰지 못했기 때문에 망각의 늪에 빠질 수 밖에 없다는 생각이 들었습니다.


그래서 생각한 것이 바로 "블로그" 였습니다.

혼자 공부를 하게 되면 모르는 것은 그냥 넘어갈 것이 뻔하고, 남에게 공개하는 내용이니 JAVA8 말고도 관련 내용을 깊게 알아야 할 수 밖에 없겠다는 생각을 했죠. 


무엇보다 이 곳은 공부한 후 핵심을 정리하는 곳이 되니, 여러모로 이득일 것이라 생각했습니다.


그래서 아래와 같이 첫 글을 쓰고,



열공모드에 들어갔습니다. 


하지만 작년 하반기에 일정이 빡세서(쿨럭) 망할 뻔했으나 ㅡㅡ^

다행히도 미친잠재력을 가진 의지 덕분에 약 9개월간의 포스팅은 무사히 잘 끝냈습니다.





2. OOP & FP 스터디 개설


스터디는 회사에 입사하기 전 부터도 많이 해봤었지만, 제대로 해 본 경험은 없었습니다. 


망하는 이유는 여러가지가 있는데 경험 상 간단히 추려보면,


- 다수 사람이 모이면, 약속시간을 잡기 애매해지는 문제 (각자의 사정이 너무 많은...)

- 스터디 이탈 인원들이 누적되는 문제 (한 주씩 빼먹는 사람들이 많으면.. 에효..)


그리고 가장 큰 문제는 처음의 열정이 지속되지 않는 문제 정도라고 할 수 있습니다.


의무감 없는 모임이기 때문에 쉽게 생각하는 경향이 많으며, 이러한 분위기가 스터디 내에 커지면 곧 망한다는 것을 많이 확인 했었습니다.


평소부터 스터디 그룹을 운영해보고 싶다는 생각은 계속 했었지만 시간을 굳이 내서 같이 할 사람들은 안타깝게도 없었고, 아마 이러한 사유로 블로그에 포스팅 정도 했었던 것 같네요..


그러나 회사에서 갑자기 웬 걸, 업무 시간에 시간내서 스터디를 하라고 하는군요.


처음에는 혼자 맥북들고 로비로 내려가서 공부하려 했으나, 

혼자만의 생각에 갇혀버리는 것을 견제하고 같이 연구하는 문화를 만들고 싶었습니다.

(개발자는 은퇴할 때까지 평생 연구를 해야 하니까요. ㅡㅡ^)


일단 가장 만만한 부사수와 막 들어온 신입사원을 포획했습니다.

(갑자기 공부하자고 하니 싫었을 것 같은데, 못난 선배를 만난 안타까운 현실...)

 


이제 사람은 모았고, 스터디를 시작하게 되었습니다. 


주제는 JAVA8 을 생각하고 있었습니다. 

작년부터 포스팅한 내용들이 있으므로 스터디 주체자 입장에서도 부담이 가지 않았고, 곧 JAVA8 역시 팀 내에서 사용할 수 있을 것 같았기 때문이었죠.


하지만 후배들에게 JAVA8 보다는 일단 학부시절 때부터 배운 OOP 의 기초부터 필요하지 않을까에 대한 고민을 했습니다. 


제 입장에서는 학부 때부터 계속 OOP 를 보왔고 최근까지 OOP & FP 열코딩, 열공을 했지만, 후배들 입장에서 갑자기 쌩뚱맞은 FP 가 포함된 JAVA8 을 잘 흡수할 수 있을까에 대한 고민을 했습니다. 

(공감이 안되면, 의미없는 시간이 아닌지...)


이러 저런 고민 후 공표는 했는데, 한 가지 간과한 사실이 있었습니다.


"아... 난 OOP 자료가 없지..   "


급 후회가 몰려 왔지만, 내가 모집한 스터디의 후배들이 자신의 시간을 써주겠다는 데 형편 없는 강의안을 만들 수는 없었습니다. 


매일같이 집 앞의 STARBUCKS 를 일요일마다 출근하며, 구글링+책을 통해 나름 복습의 시간을 거치면서 어떻게든 강의안과 샘플예제, 추가 후기들을 뽑아냈던 것 같습니다.



바쁜 일정에 몸은 조금 지쳤지만, 남에게 공유하기용으로 자료를 준비하면서 얻는 것이 많았고 다시 한번 가지게 된 OOP 복습의 시간도 의미있는 시간들이었습니다.


하지만 무엇보다도 스터디하는 시간이 좋았으며(생애 처음으로 잘 된 정규 모임),

실제 후배 소스에서 알려준 것이 등장 하거나, 도움이 된다는 말들(실제는 모르지만...)은 계속 자료를 만들 수 있는 원동력이 되었던 것 같습니다.



어쨌든 혼자가 아닌 같이 잘 따라와주는 후배들의 도움 덕분에, 스터디의 첫번째 목표였던 OOP 관련 내용도 슬슬 끝을 향해 달려가고 있습니다.


많은 디자인 패턴 책에 등장하는 공통적인 모든 패턴을 다루면 좋겠다고 생각하지만,

(스터디장의 부질없는 욕심 )


현재 사내에서 진행하는 웹 프로젝트에서는 사용할만한 패턴은 정해져 있다는 판단이 들었고, 당장 쓰기 힘든 패턴을 많이 공유하는 것이 의미가 있을지에 대한 생각이 들었습니다.

(나머지패턴들은 More OOP & FP 스터디에서 다루는 것도 방법일 듯 ^^;)


남은 [FP 관련 주제들 + 내가 알고 있는 지식공유] 역시 잘 끝나길 바라며, 이 스터디가 끝날 때 쯤 다시 쓰는 후기에는 평판이 올라간 후배들 자랑을 쓰고 싶네요...



이상 후기 끝입니다.



참고 : 삽화들은 직접 그린 것임..



반응형
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'