[DesignPattern] Decorator Pattern

suhan0304·2024년 9월 27일

Design Pattern

목록 보기
10/16
post-thumbnail

Decorator Pattern

데코레이터 패턴은 대상 객체에 대한 기능 확장이나 변경이 필요할 때 객체의 결합을 통해 서브 클래싱 대신 쓸 수 있는 유연한 대안 구조 패턴이다. 마치 기본 제품에 포장지나 외부 디자인을 살짝 변경해주면서 새로운 기능을 부여하는 것과 같이, 객체 지향 프로그래밍에서 원본 객체에 대해서 무언가를 장식하여 더 멋진 기능을 가지게 만드는 것이 데코레이터 패턴이다.

데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에 동적으로 생성할 수 있다. 데코레이터할 대상 객체를 새로운 행동들을 포함함 특수 데코레이터 객체에 넣어서 행동들을 해당 데코레이터 객체마다 연결시켜, 서브 클래스로 구성할 때 보다 훨씬 유연하게 기능할 수 있고, 기능을 구현하는 클래스들을 분리함으로써 수정이 용이해진다.


Stucture

  • Component(Interface) : 원본 객체와 장식된 객체 모두를 묶는 역할
  • ConcreteComponent : 원본 객체 (데코레이팅 할 객체)
  • Decorator : 추상화된 장식자 클래스
    - 원본 객체를 합성(composition)한 wrappee 필드와 인터페이스의 구현 메소드를 가지고 있다
  • ConcreteDecorator : 구체적인 장식자 클래스
    - 부모 클래스가 감싸고 있는 하나의 Component를 호출하면서 호출 전/후로 부가적인 로직을 추가할 수 있다.

How

// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface IComponent {
    void operation();
}

// 장식될 원본 객체
class ConcreteComponent implements IComponent {
    public void operation() {
    }
}

// 장식자 추상 클래스
abstract class Decorator implements IComponent {
    IComponent wrappee; // 원본 객체를 composition

    Decorator(IComponent component) {
        this.wrappee = component;
    }

    public void operation() {
        wrappee.operation(); // 위임
    }
}

// 장식자 클래스
class ComponentDecorator1 extends Decorator {

    ComponentDecorator1(IComponent component) {
        super(component);
    }

    public void operation() {
        super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
        extraOperation(); // 장식 클래스만의 메소드를 실행한다.
    }

    void extraOperation() {
    }
}

class ComponentDecorator2 extends Decorator {

    ComponentDecorator2(IComponent component) {
        super(component);
    }

    public void operation() {
        super.operation(); // 원본 객체를 상위 클래스의 위임을 통해 실행하고
        extraOperation(); // 장식 클래스만의 메소드를 실행한다.
    }

    void extraOperation() {
    }
}
public class Client {
    public static void main(String[] args) {
        // 1. 원본 객체 생성
        IComponent obj = new ConcreteComponent();

        // 2. 장식 1 하기
        IComponent deco1 = new ComponentDecorator1(obj);
        deco1.operation(); // 장식된 객체의 장식된 기능 실행

        // 3. 장식 2 하기
        IComponent deco2 = new ComponentDecorator2(obj);
        deco2.operation(); // 장식된 객체의 장식된 기능 실행

        // 4. 장식 1 + 2 하기
        IComponent deco3 = new ComponentDecorator1(new ComponentDecorator2(obj));
    }
}

데코레이터 된 객체는 메서드를 호출할 때 장식한 메서드를 호출하여 반환 로직에 추가적으로 더 덧붙여서 결과값을 반환할 수 있다. 장식 중첩 부분만 시퀀스 다이아 그램으로 나타내면 아래와 같이 표현된다.

IComponent deco = new ComponentDecorator1(new ComponentDecorator2(new ConcreteComponent()));


When

  • 객체 책임과 행동이 동적으로 상황에 따라 다양한 기능이 빈번하게 추가/삭제되는 경우
  • 객체의 결합을 통해 기능이 생성될 수 있는 경우
  • 객체를 사용하는 코드를 손상시키지 않고 런타임에 객체에 추가 동작을 할당할 수 있어야 하는 경우
  • 상속을 통해 서브클래싱으로 객체의 동작을 확장하는 것이 어색하거나 불가능 할 때

이러한 데코레이터 패턴을 사용하게 되면..

  • 데코레이터를 사용하면 서브클래스를 만들때보다 훨씬 더 유연하게 기능을 확장할 수 있다.
  • 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합할 수 있다.
  • 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경할 수 있다.
  • 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙을 준수
  • 클라이언트 코드 수정없이 기능 확장이 필요하면 장식자 클래스를 추가하면 되니 개방 폐쇄 원칙을 준수
  • 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙 준수

But

  • 만일 장식자 일부를 제거하고 싶다면, Wrapper 스택에서 특정 wrapper를 제거하는 것은 어렵다.
  • 데코레이터를 조합하는 초기 생성코드가 보기 안좋을 수 있다. new A(new B(new C(new D())))
  • 어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정지게 되는데, 만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어렵다.

Example

게임 개발 중에 총에 파츠를 붙이는 시스템을 만들어보려고 한다. 기본 기능만 있는 총에 여러 악세서리를 총에 장착(Decorator) 시킴으로써 유탄 발사, 스코프 줌 기능도 되는 총 객체를 구성하고자 한다.

만약 데코레이터 패턴을 생각지도 모사고 다양한 엑세서리 기능을 장착한 라이플을 구현하려면 어떻게 구현할까? 유탄 발사가 되는 총, 스코프가 달린 총, 개머리판 + 유탄 발사가 되는 총 따로, 클래스를 정의해 인스턴스화 해서 사용했을 것이다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

class Weapon
{
    void aim_and_fire()
    {
    }
}

class BaseWeapon : Weapon
{
    public void aim_and_fire()
    {
        Debug.Log("총알 발사");
    }
}

class GeneradeBaseWeapon : Weapon
{
    public void aim_and_fire()
    {
        Debug.Log("총알 발사");
    }
    public void generade_fire()
    {
        Debug.Log("유탄 발사");
    }
}

class ScopedBaseWeapon : Weapon
{
    public void aim_and_fire()
    {
        aiming();
        Debug.Log("조준하여 총알 발사");
    }
    public void aiming()
    {
        Debug.Log("조준 중...");
    }
}

class ButtstockScopedGeneradeBaseWeapon : Weapon
{
    public void aim_and_fire()
    {
        holding();
        aiming();
        Debug.Log("조준하여 총알 발사");
    }
    public void aiming()
    {
        Debug.Log("조준 중...");
    }
    public void holding()
    {
        Debug.Log("견착 완료...");
    }
}

당연하게도 이런식으로 구성하면 라이플 악세서리가 많아지면 많아질 수록 클래스도 추가되고 코드도 길어진다. 즉, 유지보수가 어려워진다는 뜻이다.

Decorator Pattern

따라서 각 악세서리를 장착한 상태의 라이플을 일일이 구현하는게 아니라, 각 무기 악세서리 클래스들을 미리 정의해두고 new BaseWeapon(new Generade(new Scoped())) 이런식으로 생성자를 감싸듯이 구성하여, 마치 동적으로 무기 악세서리를 자유롭게 붙이도록 구성해주면 된다.

  1. 먼저 대상 클래스와 데코레이터 클래스를 모두 묶어 다형성 처리를 위한 Weapon 인터페이스를 선언한다.
  2. 데코레이터를 추상화한 WeaponAccessory를 선언한다. 굳이 추상 클래스로 선언한 이유는 동기화 처리 외에 또다른 처리 기능이 추가되었을때 유연하게 확장하기 위해서 이고 각 데코레이터 클래스의 중복되는 코드를 묶기 위해서 이다.
  3. WeaponAccessory 추상 클래스를 상속하는 각 무기 악세서리 Generade, Scoped, Buttstock 서브 데코레이터 클래스 구현체를 선언한다.
  4. 만일 새로운 악세서리를 추가할 게 있다면 WeaponAccessory를 상속한 장식자 클래스르 추가하면 된다.
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface Weapon
{
    void aim_and_fire();
}

// 장식될 원본 객체
class BaseWeapon : Weapon
{
    public void aim_and_fire()
    {
        Debug.Log("총알 발사");
    }
}
// 장식자 추상 클래스
public abstract class WeaponAccessory : Weapon
{
    private Weapon rifle;
    
    protected WeaponAccessory(Weapon rifle) { this.rifle = rifle; }

    public void aim_and_fire()
    {
        rifle.aim_and_fire();
    }
}

// 장식자 클래스 (유탄 발사기)
class Generade : WeaponAccessory
{
    Generade(Weapon rifle) : base(rifle)
    {
    }

    public void aim_and_fire()
    {
        base.aim_and_fire();
        generade_fire();
    }

    public void generade_fire()
    {
        Debug.Log("유탄 발사");
    }
}

// 장식자 클래스 (조준경)
class Scoped : WeaponAccessory
{
    Scoped(Weapon rifle) : base(rifle)
    {
    }

    public void aim_and_fire()
    {
        aiming();
        base.aim_and_fire();
    }

    public void aiming()
    {
        Debug.Log("조준 중..");
    }
}

// 장식자 클래스 (개머리판)
class Buttstock : WeaponAccessory
{
    Buttstock(Weapon rifle) : base(rifle)
    {
    }

    public void aim_and_fire()
    {
        holding();
        base.aim_and_fire();
    }

    public void holding()
    {
        Debug.Log(".견착 완료...");
    }
}
using UnityEngine;

public class Player : MonoBehaviour
{
    void Start() {
        // 1. 유탄 발사기가 달린 총
        Weapon generade_rifle = new Generade(new BaseWeapon());
        generade_rifle.aim_and_fire();
        
        Debug.Log("-------------");
        
        // 2. 개머리판을 장착하고 스코프를 달은 총
        Weapon buttstock_scoped_rifle = new Buttstock(new Generade(new BaseWeapon()));
        buttstock_scoped_rifle.aim_and_fire();
        
        Debug.Log("-------------");
        
        // 3. 유탄 발사기 + 개머리판 + 스코프가 달린 총
        Weapon buttstock_scoped_generade_rifle = new Buttstock(new Scoped(new Generade(new BaseWeapon())));
        buttstock_scoped_generade_rifle.aim_and_fire();

    }
}

데코레이터 순서는 원본 대상 객체 생성자를 장식자 생성자가 래핑(wrapping)하는 순서라고 보면 된다.

데코레이터의 특징은 아래 코드와 같이 객체의 생성자의 생성자의 생성자 형식이다.

Weapon buttstock_scoped_generade_rifle = new Buttstock(new Scoped(new Generade(new BaseWeapon())));

이렇게 구성하면 각 장식자 클래스의 부모 메서드 호출 부분 base.aim_and_fire() 메서드가 각 상위 장식자의 메서드로 교체되어 위어 결과와 같이 결과값이 변하게 되는 것이다. 실제로 위의 buttstock_scoped_generade_rifle 객체를 클래스로 표현하자면 마치 여러 개의 클래스가 합쳐져 다음과 같이 구성되는 것과 비슷하다고 볼 수 있다.

class 장식된최종클래스 : WeaponAccessory {

    public void aim_and_fire() {
        holding(); // Buttstock 클래스로부터 장식됨
        
        aiming(); // Scoped 클래스로부터 장식됨
        
        Debug.Log("총알 발사"); // BaseWeapon 원본 클래스 동작
        
        generade_fire(); // Generade 클래스로부터 장식됨
    }

    public void holding() {
        Debug.Log("견착 완료");
    }
    
    public void aiming() {
        Debug.Log("조준 중..");
    }
    
    public void generade_fire() {
        Debug.Log("유탄 발사");
    }
}

데코레이터는 간단한 래핑 원리 패턴인 것 같지만, 어느 장식자를 먼저 감싸느냐에 따라 그에 대한 행동 패턴이 완전히 달라지게 된다. 예를 들어 라이플 예제에서 개머리판을 먼저 부탁하고 스코프를 부착하느냐, 아니면 스코프를 부착하고 개머리판을 부착하느냐의 순서에 따라 총을 발사하는 사람의 행동이 다음과 같이 달라진다.

public class Player : MonoBehaviour
{
    void Start() {
        // 1. 개머리판을 장착하고 스코프를 달은 총
        Weapon buttstock_scoped_rifle = new Buttstock(new Scoped(new BaseWeapon()));
        buttstock_scoped_rifle.aim_and_fire();
        
        Debug.Log("-------------");

        // 2. 개머리판을 장착하고 스코프를 달은 총
        Weapon scoped_buttstock_rifle = new Scoped(new Buttstock(new BaseWeapon()));
        scoped_buttstock_rifle.aim_and_fire();
    }
}

이 부분이 데코레이터 패턴에서 헷갈리는 부분일 수 있는데 이에 대한 다음 데코레이터 구현 예제를 살펴보면 이해에 도움이 된다.

이번엔 온라인 환경에서 Data 클래스를 멀티 쓰레드 환경에서도 사용할 수 있도록 동기화 처리 해주고 싶다고 가정해보자.

class MyData
{
    private int data;

    public virtual void setData(int data)
    {
        this.data = data;
    }

    public virtual int getData()
    {
        return this.data;
    }
}

class SynchronizedData : MyData
{
    private readonly object syncLock = new object();

    public override void setData(int data)
    {
        lock (syncLock)
        {
            base.setData(data);
        }
    }

    public override int getData()
    {
        lock (syncLock)
        {
            return base.getData();
        }
    }
}
public class DataManager : MonoBehaviour
{
    public void Start()
    {
        SynchronizedData data = new SynchronizedData();
        data.setData(1234);
        Debug.Log(data.getData());
    }
}

코드 동작 자체는 문제가 없다. 그러나 위해서도 경험했듯이, 만일 동기화 처리 외에 다른 부가 처리 기능이 추가된다면 서브 클래스 폭발이 일어나게 된다. 더구다나 원본 MyData의 data 필드 변수가 private로 설계되어있기 때문에 상속으로 재사용이 불가능하여 서브 클래스에 또 변수를 선언하여 사용하였다. 이를 데코레이터 패턴을 적용하면 깔끔하게 정리할 수 있다.

데코레이터 패턴은 기능 확장이 필요할 때 서브 클래싱 대신 사용할 수 있는 유연한 대안이다.

  1. 먼저 동기화 처리가 안된 data 클래스와 동기화 처리가 된 data 클래스를 모두 묶어두는 IData 인터페이스를 선언한다.
  2. 데코레이터를 추상화한 MyDataDecorator를 선언한다. 굳이 추상 클래스를 선언하는 이유는 동기화 처리 외에 또다른 처리 기능이 추가되었을 때 유연하게 확장하기 위해서 이고 각 장식자 클래스의 중복되는 코드를 묶기 위해서이다.
  3. MyDataDecorator 추상 클래스를 상속하는 서브 장식 클래스 구현체 SynchronizedDecorator를 선언한다.
  4. 또다른 부가 기능을 장식할게 있다면 간단하게 MyDataDecorator를 상속한 장식자 클래스를 추가해주기만 하면 된다.
// 원본 객체와 장식된 객체 모두를 묶는 인터페이스
interface IData
{
    void setData(int data);
    int getData();
}

// 장식될 원본 객체
class MyData : IData
{
    private int data;

    public void setData(int data)
    {
        this.data = data;
    }

    public int getData()
    {
        return data;
    }
}
// 장식자 추상 클래스
abstract class MyDataDecorator : IData
{
    private IData mydataObj; // 최상위 인터페이스 타입으로 장식할 원본 객체 선언

    protected MyDataDecorator(IData mydataObj) 
    {
        this.mydataObj = mydataObj;
    }

    public virtual void setData(int data)
    {
        this.mydataObj.setData(data); 
    }

    public virtual int getData()
    {
        return mydataObj.getData(); 
    }
}

// 장식자 클래스
class SynchronizedDecorator : MyDataDecorator
{
    private readonly object syncLock = new object(); // lock에 사용할 객체

    public SynchronizedDecorator(IData mydataObj) : base(mydataObj)
    {
    }

    public override void setData(int data)
    {
        lock (syncLock)
        {
            Debug.Log("동기화된 data 처리를 시작합니다.");
            base.setData(data); // 부모 클래스의 setData 호출
            Debug.Log("동기화된 data 처리를 완료했습니다.");
        }
    }

    public override int getData()
    {
        lock (syncLock)
        {
            Debug.Log("동기화된 data 처리를 시작합니다.");
            int result = base.getData(); // 부모 클래스의 getData 호출
            Debug.Log("동기화된 data 처리를 완료했습니다.");
            return result;
        }
    }
}

// 나중에 기능 추가가 되도 수정없이 유연하게 클래스만 정의하면 추가 가능
class AnotherSkillDecorator : MyDataDecorator
{
    private IData mydataObj;

    public AnotherSkillDecorator(IData mydataObj) : base(mydataObj)
    {
    }
    
    // 추가 기능 --- 
}
public class DataManager : MonoBehaviour
{
    public void Start()
    {
        // 동시성이 필요없을 때
        IData data = new MyData();
        
        // 동시성이 필요할 때
        IData dataSync = new SynchronizedDecorator(data);
        dataSync.setData(13579);
        Debug.Log(dataSync.getData());
    }
}

한번 실제 추가 장식 기능을 구현해보자. 추가할 장식 기능은 원본 객체의 메서드 로직 실행 시간을 측정하는 기능을 구현해보자.

class timerMeasureDecorator : MyDataDecorator
{
    public timerMeasureDecorator (IData mydataObj) : base(mydataObj)
    {
    }

    public override void setData(int data)
    {
        Stopwatch stopwatch = Stopwatch.StartNew(); // 시간 측정 시작
        base.setData(data);
        stopwatch.Stop(); // 시간 측정 종료
        Debug.Log(stopwatch.ElapsedTicks + " ticks"); 
    }

    public override int getData()
    {
        Stopwatch stopwatch = Stopwatch.StartNew(); // 시간 측정 시작
        int result = base.getData();
        stopwatch.Stop(); // 시간 측정 종료
        Debug.Log(stopwatch.ElapsedTicks + " ticks"); 
        return result;
    }
}

C#에서는 System.Diagnostics 네임 스페이스의 Stopwatch를 이용해서 실행 시간을 측정할 수 있다.

public class DataManager : MonoBehaviour
{
    public void Start()
    {
        IData data = new MyData();
        
        // 시간 측정 하고 싶을 때
        IData data1 = new timerMeasureDecorator(data);
        data1.setData(13579);
        
        Debug.Log("------------");
        
        // 동서시성이 적용된 로직 안의 코드의 시간 측정을 하고 싶을 때
        IData data2 = new SynchronizedDecorator(new timerMeasureDecorator(data1));
        data2.setData(24680);
        Debug.Log(data2.getData());
        
        Debug.Log("------------");
        
        // 동시성이 적용된 코드를 시간 측정을 하고 싶을 때
        IData data3 = new timerMeasureDecorator(new SynchronizedDecorator(data1));
        data3.setData(99999);
        Debug.Log(data3.getData());
    }
}

다시 한번 데코레이터 순서를 정확하게 파악해보자. 어느걸 먼저 장식하느냐에 따라 결과값이 달라지는건 위의 사진을 통해서 확인할 수 있다. 동서시성이 적용된 로직 안의 코드의 시간 측정동시성이 적용된 코드를 시간 측정이 말로만 보면 이게 뭔 소리지? 하겠지만 중첩으로 장식된 코드 로직 부분을 플어내면, 어떤 순서대로 데코레이팅 하느냐에 따라 완전히 달라진다. 따라서 장식자를 감쌀때 적절한 로직인지 끊임없이 검토를 수행해야 한다.

=IData data2 = new SynchronizedDecorator(new timerMeasureDecorator(data));
public void setDate(int data)
{
    lock (syncLock)
    {
        Debug.Log("동기화된 data 처리를 시작합니다.");
        
        Stopwatch stopwatch = Stopwatch.StartNew(); // 시간 측정 시작
        base.setData(data);
        stopwatch.Stop(); // 시간 측정 종료
        Debug.Log(stopwatch.ElapsedTicks + " ticks"); 
        
        Debug.Log("동기화된 data 처리를 완료했습니다.");
    }
}

IData data3 = new timerMeasureDecorator(new SynchronizedDecorator(data));
public void setDate(int data)
{
    Stopwatch stopwatch = Stopwatch.StartNew(); // 시간 측정 시작
    
    lock (syncLock)
    {
        Debug.Log("동기화된 data 처리를 시작합니다.");
        base.setData(data); // 부모 클래스의 setData 호출
        Debug.Log("동기화된 data 처리를 완료했습니다.");
    }
    
    stopwatch.Stop(); // 시간 측정 종료
    Debug.Log(stopwatch.ElapsedTicks + " ticks"); 
}


profile
Be Honest, Be Harder, Be Stronger

0개의 댓글