[λ””μžμΈνŒ¨ν„΄] λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄ (Decorator Pattern)

λ°˜μ‘ν˜•

 

1. μ •μ˜

πŸ’‘ λ°μ½”λ ˆμ΄ν„° νŒ¨ν„΄ (decorator pattern)
객체에 좔가적인 μš”κ±΄μ„ λ™μ μœΌλ‘œ μ²¨κ°€ν•œλ‹€.
λ°μ½”λ ˆμ΄ν„°λŠ” μ„œλΈŒν΄λž˜μŠ€λ₯Ό λ§Œλ“œλŠ” 것을 ν†΅ν•΄μ„œ κΈ°λŠ₯을 μœ μ—°ν•˜κ²Œ ν™•μž₯ν•  수 μžˆλŠ” 방법을 μ œκ³΅ν•œλ‹€.

 

2. 예) 컀피 μ£Όλ¬Έ μ‹œμŠ€ν…œ

ν˜„μž¬μ˜ 컀피 μ‹œμŠ€ν…œμ€ λ‹€μŒκ³Ό 같이 λ˜μ–΄ μžˆλ‹€.

Beverage λΌλŠ” μŠˆνΌν΄λž˜μŠ€κ°€ μ‘΄μž¬ν•˜κ³ , 이 클래슀λ₯Ό μƒμ†ν•˜μ—¬ Espresso, Decaf λ“± 각 음료 ν΄λž˜μŠ€κ°€ μžˆλ‹€.

// Beverage 슈퍼 클래슀
public abstract class Beverage {

    String description = "무슨 음료";
    // getter
    public String getDescription() {
        return description;
    }

    /** 가격은 λ‹€λ₯΄λ―€λ‘œ 각 ν΄λž˜μŠ€μ—μ„œ κ²°μ •*/
    public abstract Integer cost();
}
// Beverage λ₯Ό 상속받아 λ§Œλ“  Espresso 클래슀
public class Espresso extends Beverage {
    public Espresso() {
        description = "μ—μŠ€ν”„λ ˆμ†Œ";
    }
    
    @Override
    public Integer cost() {
        return 3000;
    }
}
// κ·Έμ™Έ 음료 클래슀..​

그런데, μƒˆλ‘œμš΄ 주문방법을 λ„μž…ν•œλ‹€κ³  ν•œλ‹€. 고객이 μ›ν•˜λŠ” 각쒅 μ»€μŠ€ν…€μ„ μ§€μ›ν•œλ‹€.

컀피λ₯Ό μ£Όλ¬Έν• λ•Œ μ›ν•˜λŠ” μ‹œλŸ½μ„ μΆ”κ°€ν•˜κ±°λ‚˜ λΊ„μˆ˜λ„, 우유λ₯Ό λ‘μœ λ‘œ λ°”κΎΈλŠ” λ“±μ˜ μ»€μŠ€ν…€μ„ μ§€μ›ν•˜λ©° 가격은 λͺ¨λ‘ λ‹€λ₯΄λ‹€.

  • μ—μŠ€ν”„λ ˆμ†Œμ— λ‘μœ  μΆ”κ°€ν•΄μ£Όμ‹œκ³  νœ˜ν•‘ν¬λ¦Ό μ–Ήμ–΄μ£Όμ„Έμš”. (EspressoWithSoyAndWhip 클래슀 생성)
  • λ””μΉ΄νŽ˜μΈμ— 우유 μΆ”κ°€ν•΄μ£Όμ„Έμš”. (DecafWithWholeMilk 클래슀 생성)

μ΄λ ‡κ²Œ λͺ¨λ“  μš”κ΅¬μ‚¬ν•­μ„ μ‘°ν•©ν•˜μ—¬ 클래슀λ₯Ό λ§Œλ“€λ©΄ κ·Έ μˆ˜κ°€ λ„ˆλ¬΄ λ§Žμ•„μ§„λ‹€. λ‹€λ₯Έ 방법을 κ³ μ•ˆν•΄μ•Ό ν•œλ‹€.

슈퍼 ν΄λž˜μŠ€μ— milk, soy, whip λ“±μ˜ boolean λ³€μˆ˜λ₯Ό μΆ”κ°€ν•œν›„ ν•˜μœ„ ν΄λž˜μŠ€μ—μ„œ cost() μ—μ„œ λ”ν•΄μ£ΌλŠ” 방식은 μ–΄λ–¨κΉŒ?

// μŠˆνΌν΄λž˜μŠ€μ— extra λ₯Ό boolean λ³€μˆ˜λ‘œ μ •μ˜ν•˜κ³  getter, setter μΆ”κ°€
public abstract class BeverageNo {

    String description = "무슨 음료";
    boolean milk;	// 우유 μΆ”κ°€μ—¬λΆ€ λ³€μˆ˜
    boolean soy;	// λ‘μœ  μΆ”κ°€μ—¬λΆ€ λ³€μˆ˜
    boolean whip;	// νœ˜ν•‘ μΆ”κ°€μ—¬λΆ€ λ³€μˆ˜
    boolean mocha;	// λͺ¨μΉ΄ μΆ”κ°€μ—¬λΆ€ λ³€μˆ˜

    // getter
    public String getDescription() {
        return description;
    }

    /** 좔상 λ©”μ†Œλ“œ : 가격은 각 음료 ν΄λž˜μŠ€μ—μ„œ κ²°μ • */
    public abstract Integer cost();

    // boolean λ³€μˆ˜λ“€μ˜ getter, setter
    public boolean isMilk() {
        return milk;
    }
    public void setMilk(boolean milk) {
        this.milk = milk;
    }
    // soy, whip, mocha 의 getter, setter λŠ” μƒλž΅..
}
// Beverage λ₯Ό 상속받은 각 음료 ν΄λž˜μŠ€μ—μ„œ boolean λ³€μˆ˜λ‘œ cost() κ³„μ‚°ν•œλ‹€.
public class Espresso extends BeverageNo {
    public Espresso() { description = "μ—μŠ€ν”„λ ˆμ†Œ"; }
    
    @Override
    public Integer cost() {
        Integer result = 3000;
        // 각 extra λ³€μˆ˜λ₯Ό ν™•μΈν•˜κ³  true 이면 가격을 더해쀀닀
        if (milk) { result += 2000; }
        if (soy) { result += 3000; }
        if (mocha) { result += 1500; }
        if (whip)  { result += 1500; }
        return result;
    }
}
// λ‹€λ₯Έ μŒλ£Œλ“€ μƒλž΅..

μ΄λ ‡κ²Œ 상속을 톡해 각 κ΅¬ν˜„ ν΄λž˜μŠ€μ—μ„œ μ˜€λ²„λΌμ΄λ“œλ‘œ κ΅¬ν˜„ν•˜λŠ” 방식이 쒋은 λ°©μ‹μΌκΉŒ?

각 ν΄λž˜μŠ€λ§ˆλ‹€ 변경이 있으며 μ½”λ“œ 쀑볡도 λ°œμƒν•˜λ‹ˆ 쒋은 방법이라 λ³Ό 수 μ—†λ‹€.

μƒμ†λ§Œ ν•œλ‹€κ³ ν•΄μ„œ μœ μ—°ν•˜κ³  관리가 μ‰¬μš΄ 것이 μ•„λ‹ˆλ‹€. ꡬ성과 μœ„μž„μ„ 톡해 μ‹€ν–‰ 쀑에 행동을 상속 μ‹œν‚€λŠ” 방법이 μžˆλ‹€.

μœ„μ™€κ°™μ΄ μ„œλΈŒν΄λž˜μŠ€λ₯Ό λ§Œλ“œλŠ” λ°©μ‹μœΌλ‘œ 행동을 μƒμ†λ°›μœΌλ©΄ κ·Έ 행동은 μ»΄νŒŒμΌμ„ ν• λ•Œ μ™„μ „νžˆ κ²°μ •λœλ‹€. κ²Œλ‹€κ°€ λͺ¨λ“  μ„œλΈŒν΄λž˜μŠ€μ—μ„œ 같은 행동을 μƒμ†λ°›μ•„μ•Όν•œλ‹€. κ·ΈλŸ¬λ‚˜ ꡬ성을 톡해 객체의 행동을 ν™•μž₯ν•˜λ©΄ 싀행쀑에 λ™μ μœΌλ‘œ κ²°μ •ν•  수 μžˆλ‹€.

객체λ₯Ό λ™μ μœΌλ‘œ κ΅¬μ„±ν•˜λ©΄, κΈ°μ‘΄ μ½”λ“œλ₯Ό κ³ μΉ˜λŠ” λŒ€μ‹  μƒˆ μ½”λ“œλ₯Ό λ§Œλ“€μ–΄ μƒˆ κΈ°λŠ₯을 μΆ”κ°€ν•  수 μžˆλ‹€. κΈ°μ‘΄ μ½”λ“œλ₯Ό 바꾸지 μ•ŠμœΌλ―€λ‘œ μ˜λ„μΉ˜ μ•Šμ€ 버그λ₯Ό 방지할 수 μžˆλ‹€.

 

β–  λ””μžμΈ 원칙 OCP (Open - Closed Principle)

πŸ“Œ λ””μžμΈ 원칙 OCP (Open - Closed Principle)
ν΄λž˜μŠ€λŠ” ν™•μž₯에 λŒ€ν•΄μ„œλŠ” μ—΄λ € μžˆμ–΄μ•Όν•˜μ§€λ§Œ μ½”λ“œ 변경에 λŒ€ν•΄μ„œλŠ” λ‹«ν˜€ μžˆμ–΄μ•Ό ν•œλ‹€.

상속을 μ¨μ„œ μŒλ£Œκ°€κ²©κ³Ό Extra 가격을 ν•©ν•΄ κ³„μ‚°ν•˜λŠ” 방법은 ν΄λž˜μŠ€κ°€ μ–΄λ§ˆμ–΄λ§ˆν•˜κ²Œ λ§Žμ•„μ§€κ±°λ‚˜, μ„œλΈŒν΄λž˜μŠ€μ— μ ν•©ν•˜μ§€μ•Šμ€ κΈ°λŠ₯을 μΆ”κ°€ν•˜κ²Œ 될 μˆ˜λ„ μžˆλ‹€. 즉, μŒλ£Œμ—μ„œ μ‹œμž‘ν•΄μ„œ μ—‘μŠ€νŠΈλΌλ‘œ μž₯식(decorate) ν•˜μž!

  1. Espresso 객체λ₯Ό κ°€μ Έμ˜¨λ‹€.
  2. Soy 객체둜 μž₯μ‹ν•œλ‹€.
  3. Whip 객체둜 μž₯μ‹ν•œλ‹€.
  4. cost() λ©”μ†Œλ“œλ₯Ό ν˜ΈμΆœν•œλ‹€. μ΄λ•Œ μ²¨κ°€λ¬Όμ˜ 가격을 κ³„μ‚°ν•˜λŠ” 일은 ν•΄λ‹Ή κ°μ²΄λ“€μ—κ²Œ μœ„μž„λœλ‹€.

 

1. Beverage 슈퍼클래슀

public abstract class Beverage {

    String description = "무슨 음료";

    // getter
    public String getDescription() {
        return description;
    }

    /** 좔상 λ©”μ†Œλ“œ : μ„œλΈŒν΄λž˜μŠ€μ—μ„œ 가격결정 */
    public abstract Integer cost();
}

2. ExtraDecorator 클래슀 (Beverage λ₯Ό κΎΈλ©°μ£ΌλŠ” 클래슀)

public abstract class ExtraDecorator extends Beverage {
    Beverage beverage;
    public abstract String getDescription();
}

3. Espresso 클래슀

public class Espresso extends Beverage {
    public Espresso() {
        description = "μ—μŠ€ν”„λ ˆμ†Œ";
    }

    @Override
    public Integer cost() {
        return 3000; // μ—μŠ€ν”„λ ˆμ†Œ κΈ°λ³Έ 가격
    }
}

4. ExtraDecorator λ₯Ό 상속받은 Soy 클래슀, Mocha 클래슀

// Soy
public class Soy extends ExtraDecorator {

    public Soy(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public Integer cost() {
        return beverage.cost() + 3000; // 기본이 λ˜λŠ” μŒλ£Œμ— Soy 의 가격을 λ”ν•΄μ€Œ
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", λ‘μœ "; // μ„€λͺ…에도 μΆ”κ°€
    }
}
// Mocha
public class Mocha extends ExtraDecorator {

    public Mocha(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public Integer cost() {
        return beverage.cost() + 1500; // μŒλ£Œμ— Mocha 의 가격을 λ”ν•΄μ€Œ
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", λͺ¨μΉ΄"; // μ„€λͺ…에도 μΆ”κ°€
    }
}

5. 기타 음료 및 Extra 클래슀 μƒλž΅..

6. μ‹€ν–‰

@Slf4j
@Test
void espressoWithSoyAndMocha() {
    Beverage esp = new Espresso();
    Beverage withSoy = new Soy(esp);
    Beverage withSoyMocha = new Mocha(withSoy);
    log.info("espressoWithSoyAndMocha.getDescription() = {}", withSoyMocha.getDescription());
    log.info("espressoWithSoyAndMocha.cost() = {}", withSoyMocha.cost());
    Assertions.assertThat(withSoyMocha.cost()).isEqualTo(7500); // OK
}

@Test
void espressoWithSoy() {
    Beverage esp = new Espresso();
    esp = new Soy(esp);
    log.info("espressoWithSoy.getDescription() = {}", esp.getDescription());
    log.info("espressoWithSoy.cost() = {}", esp.cost());
    Assertions.assertThat(esp.cost()).isEqualTo(6000); // OK
}

좜λ ₯κ²°κ³Ό

1. espressoWithSoyAndMocha() κ²°κ³Ό
espressoWithSoyAndMocha.getDescription() = μ—μŠ€ν”„λ ˆμ†Œ, λ‘μœ , λͺ¨μΉ΄
espressoWithSoyAndMocha.cost() = 7500

2. espressoWithSoy() κ²°κ³Ό
espressoWithSoy.getDescription() = μ—μŠ€ν”„λ ˆμ†Œ, λ‘μœ 
espressoWithSoy.cost() = 6000

 

β–  UML Class Diagram

  • Component : ConcreteComponent 와 Decorator κ°€ κ΅¬ν˜„ν•  μΈν„°νŽ˜μ΄μŠ€
  • ConcreteComponent : Componentλ₯Ό κ΅¬ν˜„ν•œ λ°μ½”λ ˆμ΄ν„°λ‘œ 꾸며쀄 λŒ€μƒ 객체
  • Decorator : Component λ₯Ό 가지고 μžˆλŠ” 좔상 클래슀
    • Decorator λŠ” 꾸며쀄 λŒ€μƒμ΄ μžˆμ–΄μ•Όν•œλ‹€. κ·ΈλŸ¬λ―€λ‘œ Component λ₯Ό 가지고 μžˆμ–΄μ•Όν•˜λŠ”λ° 각 λ°μ½”λ ˆμ΄ν„°κ°€ μ»΄ν¬λ„ŒνŠΈλ₯Ό 가지면 μ½”λ“œμ€‘λ³΅μ΄ λ°œμƒν•œλ‹€. κ·Έλž˜μ„œ Decorator둜 좔상 클래슀λ₯Ό λ§Œλ“€μ–΄ ν•΄κ²°ν•  수 μžˆλ‹€.
  • ConcreteDecorator : Decorator λ₯Ό κ΅¬ν˜„ν•œ λ°μ½”λ ˆμ΄ν„° 객체

 


μ°Έκ³ 
ν—€λ“œ 퍼슀트 λ””μžμΈ νŒ¨ν„΄ (Head First Design Patterns) - 에릭 프리먼,μ—˜λ¦¬μžλ² μŠ€ 둭슨,μΌ€μ΄μ‹œ μ‹œμ—λΌ,λ²„νŠΈ 베이츠

λ°˜μ‘ν˜•