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