계속해서, 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'

5. 실무 활용 패턴 (하).pdf



디자인 패턴과 관련된 마지막 STUDY 입니다.


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


참고자료 

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

- [Head First] Design Pattern

반응형
Posted by N'