decoration은 '장식(포장)'이란 뜻이다. 빵집에서 케이크를 만들 때 먼저 둥근 모양의 빵을 만든다. 이 위에 초콜릿을 바르면 초콜릿 케이크가 되고, 치즈를 바르면 치즈 케이크가 된다. 또 생크림을 바르고 과일을 많이 올려놓으면 과일 생크림 케이크가 된다.

 

이처럼 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 설계 패턴을 decorator 패턴이라고 한다. 이것은 기능 확장이 필요할 때 상속의 대안으로 사용한다.

그림에서 decorator 클래스가 기존에 구현되어 있는 클래스(둥근 모양의 빵)에 해당되고, concreteDecorator클래스는 그때그때 필요한 기능(초콜릿, 치즈, 생크림)을 추가(장식, 포장)해나가는 것에 해당된다.

 

카페 음료 가격을 계산하기 위해 우리는 클래스를 설계해야 한다고 하자. 보통 가장 많이 생각할 수 있는 방안이 상속을 이용한 구현이다. 아래와 같이 쉽게 구현할 수 있다.

 

기본적으로 커피를 제조할때 에스프레소를 내리고 해당 에스프레소에 다른 첨가물을 첨가하여 커피를 제조한다. 예를 들어 카페모카는 에스프레소에 우유,초코가 들어가므로 에스프레소 가격(2)+우유,초코(2)로 총 4라는 가격이 책정된다고 생각해보자. 그렇다면 상속을 이용하여 아래와 같이 구현할 수 있다.

 

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
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
 
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
 
public class CafeMocha extends Beverage{
    
    public CafeMocha() {
        super.setDescription("카페모카");
    }
    
    @Override
    public int cost() {
        return 2+2;
    }
    
    
}
cs

 

위와 같이 새로운 음료가 추가되었으므로, 새로운 클래스가 추가된다. 만약 휘핑크림이 추가된 카페모카라면? 아래와 같이 추가적인 클래스가 생성되어야 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CafeMochaWithWhip extends Beverage{
 
    public CafeMochaWithWhip() {
        super.setDescription("카페모카 휘핑크림 추가");
    }
    
    @Override
    public int cost() {
        return 2+2+1;
    }
    
    
}
cs

 

얼핏보면 괜찮은 것 같지만, 만약 음료의 종류가 엄청나게 많고 선택옵션이 엄청나게 많다면? 클래스수는 기하급수적으로 늘어날 것이다. 이것은 역시 디자인 원칙중 OCP를 위반하고 있는 것이다.(개방-폐쇄 원칙)

 

이러한 상황에서 사용하는 디자인 패턴이 데코레이터 패턴이다!

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Beverage {
    
    private String description;
    
    public String getDescription() {
        return description;
    }
    
    public void setDescription(String description) {
        this.description = description;
    }
 
    public abstract int cost();
}
cs

 

데코레이터 패턴은 구성이라는 방법을 사용한다. 즉, 추상 타입을 가지는 데코레이터들 혹은 기본구성 클래스들이어야만 데코레이터를 이용할 수 있다. 왜냐하면 뒤에서 설명하겠지만 데코레이터 역할을 클래스들이 모두 추상타입의 구현체들을 감싸게 되는 구조이기 때문이다. 즉, 위 추상클래스는 기본구성(에스프레쏘) 그리고 데코레이터(선택옵션,모카,휘핑이 들어간 모카 등)들의 타입을 마춰주기 위한 추상타입의 역할을 할 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class Espresso extends Beverage{
    
    public Espresso() {
        super.setDescription("에스프레쏘");
    }
 
    @Override
    public int cost() {
        return 2;
    }
    
}
cs

 

추상타입의 Beverage를 상속하는 기본구성을 구현한 예제이다. 데코레이터 패턴의 핵심은 기본 구성으로부터 출발하여 이것저것 데코레이터 옵션을 덧붙여나가는 것이다. 즉, 데코레이터 패턴의 시작이 되는 기본구성은 바로 추상타입의 Beverage를 상속받아 구현한다.

 

1
2
3
public abstract class BeverageDecorator extends Beverage {
    
}
cs

 

데코레이터(추가옵션)들이 상속하게 될 추상클래스이다. 데코레이터들의 모든 타입 그리고 기본구성과 데코레이터들의 타입이 동일해야하므로 Beverage를 상속한다. 만약 데코레이터들의 추가적인 행위를 구현하려면 이쪽에 추상메소드로 선언해도 무방할듯하다.

 

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
public class Mocha extends BeverageDecorator{
 
    private Beverage beverage;
    
    public Mocha(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 2+this.beverage.cost();
    }
    
}
 
public class Whipping extends BeverageDecorator{
    
    private Beverage beverage;
    
    public Whipping(Beverage beverage) {
        this.beverage=beverage;
    }
    
    @Override
    public int cost() {
        return 1+this.beverage.cost();
    }
    
}
cs

 

데코레이터 패턴의 핵심인듯하다. 데코레이터 객체들의 구현체이다. 잘보면 기본구성 혹은 데코레이터 객체들을 래핑하기 위해 하나의 인스턴스 변수를 가지고 있다는 것이 핵심이며 기본구성과 동일한 행위(가격계산,cost())를 오버라이드하여 구현하였으며 래핑한 객체의 동일한 행위 메소드를 호출하고 있는 것을 볼 수 있다. 이렇게 기본구성과 데코레이터 객체들의 상위타입을 마춰줌으로써 계속해서 생성자로 객체를 래핑하여 옵션을 추가 할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DecoratorMain {
 
    public static void main(String[] args) {
        
        Beverage espresso = new Espresso();
        System.out.println("description : "+espresso.getDescription() +" cost : "+espresso.cost());
        
        Beverage mocha = new Mocha(new Espresso());
        mocha.setDescription("카페모카");
        System.out.println("description : "+mocha.getDescription() +" cost : "+mocha.cost());
        
        Beverage mochaWithWhip = new Whipping(new Mocha(new Espresso()));
        mochaWithWhip.setDescription("카페모카 휘핑추가");
        System.out.println("description : "+mochaWithWhip.getDescription() +" cost : "+mochaWithWhip.cost());
        
    }
 
}
 
=>result
description : 에스프레쏘 cost : 2
description : 카페모카 cost : 4
description : 카페모카 휘핑추가 cost : 5
 
cs

 

메인클래스에서 데코레이터 패턴이 적용된 클래스를 사용하고 있는 것을 보니 많이 낯이 익다. 무엇일까? 바로 Java I/O들을 구현할 때, 데코레이터 패턴이 적용되었다! 

 

InputStream is = new BufferedInputStream(new FileInputStream("filepath"));

 

지금까지 간단히 데코레이터 패턴을 다루어보았다.

posted by 여성게
:

 

스트레티지 패턴(Strategy Pattern,전략패턴)은 알고리즘군을 정의하고 각각을 캡슐화하여 교환해서 사용할 수 있도록 만들어준다. 스트레티지 패턴을 활용하면 알고리즘을 사용하는 클라이언트와는 독립적으로 알고리즘을 변경할 수 있다.

 

바로 예제를 살펴보자.

 

Duck이라는 추상클래스가 있다고 생각해보자. 해당 추상클래스는 다양한 오리라는 서브클래스를 만들기 위해 여러가지 공통 메소드와 확장을 위한 추상 메소드를 가진 추상클래스이다. 만약 아래와 같은 요청이 있다고 생각해보자.

 

"모든 오리에게 날 수 있는 기능과 오리소리를 내는 기능을 추가해라. 하지만 구현체마다 해당 행동들은 모두 달라질 수 있다"

 

어떤 식으로 구현을 하면 좋을까? 두개의 행동이 모든 구현체마다 달라질 수 있다는데, 추상클래스에서 추상메소드로 만들어줘서 모든 서브 클래스에서 오버라이드 할 수 있게 할까? 그렇다면 어떠한 문제가 생길수 있을까 생각해보자. 문제는 이것이다. 서브 클래스에서 나는 행동과 오리소리를 내는 행동을 오버라이드 한다면 과연 이러한 코드는 재사용이 가능할까? 문제를 다시 한번보자. 

 

"구현체마다 행동들은 모두 달라질 수 있다"

 

달라질 수 있다지, 모든 구현체마다 다르다는 아니다. 이 말은 종류가 다른 오리지만 행동이 같은 교집합이 존재할 수 있다는 것이다. 그렇다면 서브클래스에서 슈퍼클래스의 나는 행동과 오리소리를 내는 행동을 오버라이드한다면 같은 행동을 갖는 다른 서브 클래스들이 동일한 코드가 중복되서 나타날 것이다. 그러면 다음 해결책을 보자.

 

"달라지는 행동들을 별도의 인터페이스로 분리하여 해당 기능이 필요한 오리 서브 클래스에 구현을 강제하자."

 

이 해결책 또한 재활용하기 힘들다. 서브 클래스에서 인터페이스의 구현을 강제하여 나는 행동과 오리소리 행동을 구현한다면 이 역시 재활용할 수 없는 코드가 될 것이다. 즉, 구현 클래스에서만 구현코드가 나타나기 때문에 행동이 같은 다른 서브 클래스에서 해당 코드를 재활용할 수 없다.

 

여기서 사용할 수 있는 디자인 패턴이 스트래티지 패턴(Strategy Pattern)이다. 이 패턴의 핵심은 달라지는 행동부분을 분리하고 캡슐화하여 클라이언트와는 독립적으로 행동을 주입할 수 있게 하는 것이다. 바로 구현 코드를 살펴보자.

 

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 Duck {
    
    private FlyBehavior fly;
    private QuackBehavior quack;
    
    public void swim() {
        System.out.println("헤엄친다.");
    }
    
    public void fly() {
        fly.fly();
    }
    
    public void quack() {
        quack.quack();
    }
 
    public void setFly(FlyBehavior fly) {
        this.fly = fly;
    }
 
    public void setQuack(QuackBehavior quack) {
        this.quack = quack;
    }
    
}
cs

 

위의 추상 클래스를 살펴보면 이 추상클래스는 다양한 오리의 구현체의 공통 행동들을 담은 추상클래스이다. 그리고 오리 구현체들마다 달라질 수 있는 행동을 인터페이스로 분리하여 캡슐화하고 있다. 이 말은 상속이나 구현을 강제하는 것이 아니고 인스턴스 변수로 달라지는 행동을 구성(composition)하였다. 그리고 달라지는 행동들은 인터페이스를 구현하여 특정 클래스의 군(알고리즘군)으로 만들어지기 때문에 같은 행동을 하는 다른 오리들이 같은 클래스를 사용하므로 재사용성이 높아진다.

 

1
2
3
4
5
6
7
public interface FlyBehavior {
    public void fly();
}
 
public interface QuackBehavior {
    public void quack();
}
cs

 

달라지는 행동들은 인터페이스로 분리하였다.

 

1
2
3
4
5
6
7
public class ADuck extends Duck{
 
}
 
public class BDuck extends Duck{
 
}
cs

 

추상클래스를 상속받은 오리서브 클래스들은 만들었다. 특징은 ADuck은 헤엄은 칠 수 있지만 날지 못하고 오리소리를 내지 못한다. BDuck은 수영도 가능하고 날수도 있고 울수도 있다. 과연 이러한 달라진 행동들을 실제로 어떻게 사용할까?

 

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
public class NotFlyBehavior implements FlyBehavior {
 
    @Override
    public void fly() {
        System.out.println("못 날아요");
    }
 
}
 
public class DoFlyBehavior implements FlyBehavior {
 
    @Override
    public void fly() {
        System.out.println("난다.");
    }
 
}
 
public class NotQuackBehavior implements QuackBehavior {
 
    @Override
    public void quack() {
        System.out.println("못 운다.");
    }
 
}
 
public class DoQuackBehavior implements QuackBehavior {
 
    @Override
    public void quack() {
        System.out.println("운다.");
    }
 
}
 
 
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
/*
 * ADuck은 날지못하고 울지도 못한다.
 * BDuck은 날수 있고 울 수도 있다.
 */
public class DuckMain {
 
    public static void main(String[] args) {
        
        Duck a = new ADuck();
        a.setFly(new NotFlyBehavior());
        a.setQuack(new NotQuackBehavior());
        
        
        Duck b = new BDuck();
        b.setFly(new DoFlyBehavior());
        b.setQuack(new DoQuackBehavior());
        
        System.out.println("===========ADuck===========");
        
        a.swim();
        a.fly();
        a.quack();
        a.setFly(new DoFlyBehavior());
        a.fly();
        
        System.out.println("===========BDuck===========");
        
        b.swim();
        b.fly();
        b.quack();
    }
 
}
 
=>결과
===========ADuck===========
헤엄친다.
못 날아요
못 운다.
난다.
===========BDuck===========
헤엄친다.
난다.
운다.
 
cs

 

이렇게 사용 시점에 알고리즘을 set 메소드로 주입하여 사용한다. 만약 추후에 ADuck이 나는 기능이 추가된다면 위 코드와 같이 날수 있는 구현 클래스를 주입해주기만 하면 ADuck은 날 수 있게 된다. 즉, ADuck은 행동을 인터페이스로 의존만 하고 있고(구성,composition) 구체적인 행동을 몰라도 되는 것이다.

posted by 여성게
: