[CHAPTER 6] Decorator Pattern - Review [과제 리뷰]
Effective OOP 스터디에서 첫 번째 주제는
"주어진 상황 및 용도에 따라 객체에 책임을 추가할 수 있는 Decorator(장식) Pattern"
에 대해 알아보았습니다.
관련 내용은 아래 포스팅에서 참고. :-)
과제로 진행할 수 있는 파일까지 아래 포스팅에 제공되고 있습니다.
이 패턴은 기존 클래스의 기능 추가를 위해 is-a 관계(상속)를 하는 것 대신, has-a 관계(의존)를 취함으로써 다양하게 책임을 추가할 수 있고 또한 이 추가 작업을 실행시간에 할 수 있다는 것에 대해 알아보았습니다.
(Favor has-a over is-a, 모든 is-a 는 has-a 로 변경할 수 있습니다.)
또한 Decorator 에 대해 조금 더 알아보기 위해 해당 주제에 대한 과제가 있었고, 이번 포스팅에서는 과제에 대한 리뷰를 작성해보려 합니다.
총 세가지 정도의 과제가 있었고, 단계별로 글을 진행하고자 합니다.
1. 장식 벗기기
첫 번째 실습은 현재 제작된 장식과 장식대상 클래스 사이의 구조에서 장식된 객체의 장식을 한 개씩 제거하는 메소드(removeCondiment)를 만들어 보는 것이었습니다.
그러나 과제에서는 메소드를 제작하기 전, 사전조건이 두 가지가 있었습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // TRY 1. Coffee coffee = new HouseBlendingEspresso(); coffee = new MilkDecorator(coffee); coffee = new LatteDecorator(coffee); // 장식과 관련된 기능을 장식 대상 클래스에 제공하는 것은 올바르지 않아 보입니다. // CondimentDecorator 가 없어도 Coffee 는 독립적으로 존재해야 합니다. coffee = coffee.removeCondiment(); // TRY 2. Coffee coffee = new HouseBlendingEspresso(); coffee = new MilkDecorator(coffee); coffee = new LatteDecorator(coffee); // TRY 1 의 조건에 따라, removeCondiment 를 CondimentDecorator 로 옮겼습니다. // 그러나, 장식을 벗길 때마다 아래처럼 캐스팅을 해야하는 중복 코드가 발생하게 됩니다. if (coffee instanceof CondimentDecorator) { coffee = ((CondimentDecorator)coffee).removeCondiment(); } | cs |
위의 두 문제를 해결하기 위해서는 결국 Coffee 의 타입 체크 및 캐스팅과 관련된 작업이 적절한 곳에 캡슐화되어 사용할 수 있어야하는 것 처럼 보입니다.
하지만, removeCondiment 의 위치 적용에 있어서, Coffee 와 CondimentDecorator 두 클래스 모두 애매한 위치임을 앞의 사전 조건에서 볼 수 있었습니다.
보통 이런 기능들은 저같은 경우에는 유틸(util) 기능으로 분리하는 편입니다. 장식과 관련된 기능이니 CondimentDecorator 에 해당 기능을 제작해보려합니다.
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 | public abstract class CondimentDecorator extends Coffee { private Coffee coffee; /** * 정적메소드로 구현된, RemoveCondiment 구현. * * <pre> * 해당 기능에서는 어떤 객체 자신의 상태를 사용하지 않으며, 유틸성 기능을 제공하고자 합니다. * 제 습관 중 하나는 정적 메소드(static)의 경우 앞에 대문자를 붙이곤 합니다. * </pre> * * @param coffee * @return */ public static Coffee RemoveCondiment(Coffee coffee) { if (coffee instanceof CondimentDecorator) { // 장식 객체일 경우 장식 대상 객체를 내보냅니다. return ((CondimentDecorator) coffee).coffee; } else { // 아닐 경우 본인을 내보냅니다. return coffee; } } } // 테스트 코드 Coffee coffee = new HouseBlendingEspresso(); coffee = new MilkDecorator(coffee); coffee = new LatteDecorator(coffee); // 정적메소드로 제작된, RemoveCondiment 테스트 coffee = CondimentDecorator.RemoveCondiment(coffee); | cs |
유틸 기능으로 제공함으로써, 안전하게 장식을 제거할 수 있군요.
기능 역시 잘 작동하는 것처럼 보이네요.
그러나 여담으로 한 가지를 언급해보자면,
보통 장식 패턴에서는 이미 장식된 객체에서 한번 장식된 기능을 제거하기 보다는 동일하게 다시 만드는 경우가 더 많다고 합니다. ㅡㅡ^
2. 전략패턴으로 장식 구현
제게 OOP 를 가르쳐 주셨던 교수님께서는
프로그래머는 같은 문제를 다른 방법으로도 풀 수 있어야 하며,
그 중에 가장 좋은 방법을 선택해야 한다
고 하셨습니다.
그런 의미에서, 두 번째 과제는 이미 구현된 장식 패턴의 구조를 전략 패턴으로 제작해보는 것이었습니다.
전략패턴에 대한 설명은 아래 글을 참고. @.@
2017/04/25 - [스터디/[STUDY] OOP] - [CHAPTER 3] 실무 활용 패턴 (상)
2017/04/27 - [스터디/[STUDY] OOP] - [CHAPTER 3] 실무 활용 패턴 (상)[Strategy 패턴] + 추가내용
이를 위해, CondimentDecorator 가 Coffee 에 의존했던 구조를 Coffee 가 장식 전략에 의존하도록 변경할 계획입니다.
Coffee 에는 여러 장식을 저장할 수 있도록 목록(List) 형태로 장식들을 가지고 있을 생각이며, [장식 전략 인터페이스 ICondiment] 역시 제공해보려 합니다.
일단, Coffee 내부에는 장식 전략을 의존할 수 있도록 Coffee::addCondiment 를 제공해도록 하겠습니다.
아래 코드를 참고해주세요. ^^
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 | /** * 커피 클래스 * @author khj1219 * */ public abstract class Coffee { // 장식을 유지할 수 있는 멤버 변수 private List<ICondiment> condimentList = new ArrayList<>(); /** * 장식을 추가하는 메소드 * * @param condiment */ public void addCondiment(ICondiment condiment){ this.condimentList.add(condiment); } /** * 커피 가격 출력 * * * @return */ public Integer getCost(){ // Coffee 의 특정 기능이 수행될 때, 장식 목록을 이용할 예정.. for(ICondiment condiment : condimentList){ // something work.... // 장식의 속성은 어떻게 사용을 하나요?? @.@ } return cost; } } | cs |
추가된 장식의 순서를 유지하고자 컨테이너로 List 를 선택하였습니다.
또한 장식들은 추가될 때마다 Coffee 클래스의 속성을 바로 변경하는 것이 아닌, 특정 기능을 수행될 때 목록을 순회하며 장식의 속성을 반영할 생각입니다.
그러나 장식 패턴의 장식들은 장식 대상 객체를 has-a 관계로 가지고 있기 때문에 이전 속성들과 현재 속성을 반영하여 결과를 출력할 수 있던 반면, 전략 장식들은 더이상 장식 대상 객체들을 가지고 있지 않습니다.
즉 이전 속성들을 알 수 없기 때문에, ICondiment 에서는 특정 기능을 수행할 때 파라미터로 Coffee 의 현재 속성을 넘기는 방식을 이용할 생각입니다.
그에 따른 인터페이스 명세는 다음과 같습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public interface ICondiment{ /** * 커피 이름 출력 * * @param name * @return */ String getName(String name); /** * 코스트에 대한 메소드 서명 정의 * * @param cost * @return */ Integer getCost(Integer cost); } | cs |
이를 이용하여, Coffee::getCost 를 구현을 완성해보려 합니다.
아 물론 처음에 제공을 했던 Coffee::getCost 는 추상 메소드였고, 현재는 ICondiment 목록을 이용하기 위해 정의를 할 생각입니다.
기존 Coffee::getCost 의 추상적인 역할은 Coffee::getTemplateCost 를 따로 정의함으로써, 기존 구조를 유지하고자 합니다.
그에 따라 작성된 Coffee 클래스는 다음과 같습니다.
(Coffee::getName 과 관련된 내용은 작성하지 않았습니다. 이 글을 확인하고, 한번 직접 작성해보세요.)
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 | /** * 커피 클래스 * * @author ndh1002 */ public abstract class Coffee { // 장식을 유지할 수 있는 멤버 변수 private List<ICondiment> condimentList = new ArrayList<>(); /** * 커피 가격 출력 * * @return */ public Integer getCost(){ Integer cost = coffee.getTemplateCost(); for(ICondiment condiment : this.condimentList){ cost = condiment.getCost(cost); } return cost; } /** * 가격에 대한 메소드 서명 정의 * * <pre> * 기존 Coffee::getCost 의 추상적인 역할을 수행하기 위한 추상 메소드 서명 정의 * </pre> * @return */ public abstract Integer getTemplateCost(); } | cs |
전략 패턴에 따라 구조를 변경 하였고, 그에 따른 테스트 코드는 다음과 같습니다.
모든 장식들을 전략으로 제공하지는 않고, 샘플로 ICondiment 를 구현하는 MilkDecorator 를 제공하고자 합니다.
(이 글을 보고 있는 스터디 그룹원들은 모두 잘하니, 나머지는 잘 구현할 수 있겠죠? ^^)
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 | /** * 우유 데코레이터 * * <pre> * 샘플로 작성된 우유 데코레이터.. * </pre> * * Created by Doohyun on 2017. 6. 25.. */ public class MilkDecorator implements ICondiment { /** * 장식대상 객체이름을 그대로 사용. * * @return */ @Override public String getName(String name) { return name; } /** * 우유 가격인 900원 추가. * * @return */ @Override public Integer getCost(Integer cost) { return cost + 900; } } // TEST CODE Coffee coffee = new HouseBlendingEspresso(); // 우유 -> 라떼 -> 샷 -> 샷 coffee.addCondiment(new MilkDecorator()); coffee.addCondiment(new LatteDecorator()); coffee.addCondiment(new ShotDecorator()); coffee.addCondiment(new ShotDecorator()); | cs |
작성된 코드는 기존 장식 패턴 구조와 비교하여 잘 작동하는 듯 합니다.
이제 한 번 고민해 볼만한 내용이 또 있습니다.
Coffee 의 각 기능에 대한 책임을 덧붙이기 위한 문제는 아래와 같이 세 가지 방법 정도를 구현해 보았습니다.
- 상속구조를 이용하는 방법
- 장식 패턴을 이용하는 방법
- 전략 패턴을 이용하는 방법
한 문제를 다양한 방법으로 생각해보았고, 어떤 방법이 가장 적절하지 한번 다시 리뷰를 해보면 좋을 것 같습니다. ^^;
3. 장식 비교
마지막 과제는 장식된 객체들 간의 비교를 구현하는 내용이었습니다.
비교에 있어서, 사전 조건은 아래와 같았습니다.
- 장식의 순서에 상관없이 동일하고, 같은 커피(예를들어 에스프레소)를 사용할 때 동일
- 자바의 객체 비교 메소드 equals 를 재정의할 것.
비교 연산를 함에 있어서, CondimentDecorator 나 Coffee 든 동일하게 비교는 가능해야 할 것 같습니다.
일단 해당 문제를 해결하기 위해서, Coffee 와 CondimentDecorator 에 각각 다른 equals 를 제작하려 합니다.
각자 구현 함으로써 Coffee 는 장식이 안된 순수 클래스, CondimentDecorator 는 장식이 된 클래스로 분류를 시킬 수가 있습니다.
이는 Coffee::equals 를 구현 시 장식에 대해 신경쓰지 않아도 되며, 오직 CondimentDecorator 에서만 장식 관련 역할을 수행하도록 구현할 수 있음을 의미합니다.
(SRP : 단일 책임 원칙)
한번 이 논리에 따라 기능을 구현해보도록 하겠습니다.
첫 번째는 Coffee::equals 구현입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public abstract class Coffee { /** * Coffee 객체의 동일 여부 판단 * * @param coffee * @return */ @Override public Boolean equals(Object coffee) { // 장식을 하지 않은 단일 객체, 오직 본인과 동일 클래스 여부를 확인합니다. return this.getClass().getName().equals(coffee.getClass().getSimpleName()); } } | cs |
Coffee::equals 의 경우 장식이 안된 순수 클래스임이 위의 논리에 따라 정해졌습니다.
오직 파라미터로 넘어온 비교 대상 객체가 자신과 동일한 클래스인지만 확인하면 될 것 같습니다.
두 번째는 CondimentDecorator::equals 의 구현입니다.
장식 클래스에서 재정의를 하기 때문에 Coffee::equals 를 따라가지 않으며, 장식의 비교만 이 곳에서 구현하면 될 것 같습니다.
CondimentDecorator 의 동일여부 판단은 사전조건에서 나타난 것처럼, 여태까지 장식된 종류 및 베이스 커피가 무엇인지 확인하는 것이 먼저일 것 같습니다.
장식된 종류 목록을 구하기 위해서는 CondimentDecorator 가 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 | public abstract class CondimentDecorator extends Coffee { /** * Coffee 를 받아, 장식된 목록을 출력하는 메소드 정의. * * <pre> * - 클래스명에 따라 정렬이 됨을 보장. * - 객체의 상태를 따로 사용하지 않음으로, 정적메소드 형태로 구현. * </pre> * * @param coffee * @return */ private static List<String> GetSortedDecoratorList(Coffee coffee) { ArrayList<String> decoratorList = new ArrayList<>(); // 커피 목록 세팅. { Coffee targetCoffee = coffee; do { // 커피 이름 추가. decoratorList.add(targetCoffee.getClass().getSimpleName()); if (targetCoffee instanceof CondimentDecorator) { // 장식 커피라면, 현재 장식 대상이 된 커피를 출력. targetCoffee = ((CondimentDecorator) targetCoffee).coffee; } else { targetCoffee = null; } } while (targetCoffee != null); } // 이름에 따라 정렬을 수행. { decoratorList.sort(String::compareTo); } return decoratorList; } } | cs |
장식 목록에 정렬을 수행한 이유는 장식의 순서에 상관없이 비교를 해야하기 때문입니다.
클래스명을 오름차순으로 정렬하면, 동일한 장식이 사용된 경우 쉽게 비교를 할 수 있겠죠?
이 메소드를 이용한 CondimentDecorator::equals 의 구현은 아래와 같습니다.
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 abstract class CondimentDecorator extends Coffee { /** * Coffee 객체의 동일 여부 판단 * * @param targetCoffee * @return */ @Override public boolean equals(Object targetCoffee) { if (targetCoffee instanceof Coffee) { List<String> myDecoratorCoffeeList = GetSortedDecoratorList(this); List<String> targetDecoratorCoffeeList = GetSortedDecoratorList((Coffee) targetCoffee); // 목록이 서로 같은지 비교. // List 의 equals 는 내부적으로 데이터가 동일한지 비교합니다. // khj1219 감사합니다. return myDecoratorCoffeeList.equals(targetDecoratorCoffeeList); } else { // 비교 대상 객체가 커피가 아니라면, 동일하지 않다고 볼 수 있음. return false; } } } | cs |
커피의 장식 목록을 구할 수 있으니, 본인과 비교대상의 장식들을 조회하여 비교하는 로직입니다.
재미있는 점은 List::equals 는 내부적으로 목록간 데이터가 동일한지 이미 구현이 되어있습니다.
과제를 해준 한 스터디원이 알려줬습니다. 감사합니다. @.@
스터디 내에서는 이에 대해 불안해서 못쓰겠다고 하였지만, 잘 숙지하고 좋은 툴을 쓰는 것이 올바른 자세인 듯 합니다. 아직 옹졸한듯... ^^;
모두 구현을 하였고, 이에 대한 테스트 코드는 다음과 같습니다.
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 | Coffee coffee1 = new HouseBlendingEspresso(); // 우유 데코레이터로 치장. coffee1 = new MilkDecorator(coffee1); // 라떼 데코레이터로 치장. coffee1 = new LatteDecorator(coffee1); // 더블샷 데코레이터 치장. coffee1 = new ShotDecorator(coffee1); coffee1 = new ShotDecorator(coffee1); Coffee coffee2 = new HouseBlendingEspresso(); // 더블샷 데코레이터 치장. coffee2 = new ShotDecorator(coffee2); // 우유 데코레이터로 치장. coffee2 = new MilkDecorator(coffee2); coffee2 = new ShotDecorator(coffee2); // 라떼 데코레이터로 치장. coffee2 = new LatteDecorator(coffee2); System.out.println("coffee1과 coffee2는 같나요? " + coffee1.equals(coffee2)); // CONSOLE LOG // coffee1과 coffee2는 같나요? true | cs |
잘 작동하는 듯 합니다.... ㅎㅎ
장식 패턴과 관련된 과제였지만,
조금 더 중요하게 생각했던 것은 리팩토링에 대한 내용을 다룰 때 다뤘던 것들을 해볼 수 있었던 것 같습니다.
이를테면, 아래 내용에 대해 한 번 생각해주세요.
- 정적(static) 메소드를 왜 사용했는가?
- Coffee 에 장식과 관련된 내용을 담지 않고, CondimentDecorator 로 분리한 이유.
(CondimentDecorator::RemoveCondiment, Coffee 와 CondimentDecorator 의 다른 equals 재정의)
또한, 해당 문제 해결을 위해 주말동안 수고 많으셨습니다.
이 노력들이 꼭 도움이 되길 바랍니다.
감사합니다. :-)
'스터디 > [STUDY] Effective OOP' 카테고리의 다른 글
[CHAPTER 20] Compound Pattern - Review (2) | 2017.11.19 |
---|---|
[CHAPTER 19] Visitor Pattern - Review (8) | 2017.09.10 |
[CHAPTER 18] Composite Pattern - Review (0) | 2017.09.03 |
[CHAPTER 16] Iterator Pattern - Review (0) | 2017.08.02 |
[CHAPTER 13] Builder Pattern - Review (0) | 2017.07.20 |