계속해서, 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 를 다음과 같이 추가하였습니다.
| 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 를 구현하였습니다.
자 이제 테스트 코드를 한번 실행해 볼까요?
| // 고객은 단순히 스타벅스 카페를 통해 아메리카노를 주문한다는 것을 알 수 있음. 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 를 재정의만 해주면 됩니다. 다만, 부모의 기존 행위를 무시하기 때문에 다시 잘 정의해줘야 하겠죠? :-)
이제 [스타벅스 스타일의 라떼]가 잘 작동하나 확인만 해보면 퇴근입니다.
| // 고객은 단순히 스타벅스 카페를 통해 아메리카노를 주문한다는 것을 알 수 있음. 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