[CHAPTER 5] 실무 활용 패턴 (하) [Factory method 패턴] + 추가내용
객체지향 프로그램에서는 클래스라는 조형틀을 만들고,
그 틀을 이용해 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 스터디의 목적은 좋은 코드를 리뷰하는 것이고,
이로 인해 좋은 코드에 대한 안목과 실력이 쌓이길 바랍니다.
'스터디 > [STUDY] OOP' 카테고리의 다른 글
[CHAPTER 5] 실무 활용 패턴 (하) [Abstract Factory 패턴] + 추가내용 (2) | 2017.06.10 |
---|---|
[CHAPTER 5] 실무 활용 패턴 (하) [Factory method 패턴] + 추가내용2 (1) | 2017.06.10 |
[CHAPTER @] Break Time (잠깐 쉬는 후기) (0) | 2017.06.04 |
[CHAPTER 4] 실무 활용 패턴 (중) [Adapter 패턴] + 추가내용 (0) | 2017.05.21 |
[CHAPTER 4] 실무 활용 패턴 (중) + 과제리뷰 (0) | 2017.05.20 |