지난 스터디에서는 "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'