Design Pattern

Design Pattern | 데코레이터 패턴(Decorator Pattern)

이진유진 2024. 3. 22. 14:30
반응형
오늘은 디자인 패턴 스터디 3번째 항목인 데코레이터 패턴에 대하여 알아보겠습니다! 

 

https://sun-22.tistory.com/5

데코레이터 패턴(Decorator Pattern)

객체 지향 디자인 패턴 중 하나로, 

객체에 동적으로 새로운 기능을 추가할 수 있도록 하는 패턴입니다. 

이 패턴은 기본 객체를 변경하지 않고, 기능을 추가하거나 수정할 수 있도록 합니다. 

데코레이터 패턴은 개방-폐쇄 원칙(Open-Closed Principle)에 따르며, 코드 변경 없이 기능 확장할 수 있도록 도와줍니다.

 

데코레이터 패턴 예제 코드와 주요 구성 요소

Component 

// Component interface
interface Coffee {
    String getDescription();
    double getCost();
}

 

데코레이터 패턴에서 기본이 되는 인터페이스나 추상 클래스입니다. 

기본 객체와 데코레이터 객체들이 구현해야 하는 공통 인터페이스를 정의합니다. 

 

위의 예시 코드에서, Coffee Interface는 커피 객체의 기본적인 동작을 정의합니다. 

getDescription() 메서드는 커피의 설명을 반환하고,

getCost() 메서드는 커피의 가격을 반환합니다. 

 

ConcreteComponent 

// ConcreteComponent
class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple coffee";
    }

    @Override
    public double getCost() {
        return 1.0;
    }
}

 

Component 인터페이스를 구현한 실제 객체입니다. 

이 객체에 새로운 기능을 추가할 수 있습니다.

 

위의 예시 코드에서, SimpleCoffee 클래스는 Coffee 인터페이스를 구현한 구체적인 커피 객체입니다. 

이 클래스는 단순한 커피를 나타내며, 

getDescription() 메서드는 "Simple coffee" 를 반환하고, 

getCost() 메서드는 1.0을 반환합니다. 

Decorator

// Decorator
abstract class CoffeeDecorator implements Coffee {
    protected Coffee decoratedCoffee;

    public CoffeeDecorator(Coffee decoratedCoffee) {
        this.decoratedCoffee = decoratedCoffee;
    }

    public String getDescription() {
        return decoratedCoffee.getDescription();
    }

    public double getCost() {
        return decoratedCoffee.getCost();
    }
}

 

Component와 같은 인터페이스를 구현하며, 구체적인 데코레이터 객체들의 기반이 되는 추상 클래스입니다. 

이 클래스는 기본 객체를 감싸는 역할을 합니다. 

 

위의 예시 코드에서 , CoffeeDecorator 클래스는 Coffee 인터페이스를 구현한 구체적인 데코레이터 클래스들의 기반이 되는 추상 클래스입니다. 

이 클래스는 커피 객체를 감싸는 역할을 하며, 

getDescription() 메서드와 getCost() 메서드를 기본 구현으로 가지고 있습니다. 

 

ConcreteDecorator 

// ConcreteDecorator
class WhippedCreamDecorator extends CoffeeDecorator {
    public WhippedCreamDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public String getDescription() {
        return super.getDescription() + ", with whipped cream";
    }

    public double getCost() {
        return super.getCost() + 0.5; // 휘핑 크림 가격 추가
    }
}

// Another ConcreteDecorator
class SyrupDecorator extends CoffeeDecorator {
    public SyrupDecorator(Coffee decoratedCoffee) {
        super(decoratedCoffee);
    }

    public String getDescription() {
        return super.getDescription() + ", with syrup";
    }

    public double getCost() {
        return super.getCost() + 0.3; // 시럽 가격 추가
    }
}

 

Decorator 클래스를 상속받아 실제로 기능을 추가하는 구체적인 데코레이터 클래스입니다. 

이 클래스에서 새로운 기능을 추가하거나 기존 기능을 수정할 수 있습니다. 

 

위의 예시 코드에서, WhippedCreamDecorator(휘핑 크림 데코레이터) 클래스는 

CoffeeDecorator 클래스를 상속하여 구현한 휘핑 크림을 추가하는 구체적인 데코레이터 클래스입니다. 

이 클래스는 getDescription() 메서드를 오버라이드하여 커피 설명에 "with whipped cream"을 추가하고, 

getCost() 메서드를 오버라이드하여 휘핑 크림의 추가 비용을 더합니다. 

 

SyrupDecorator(시럽 데코레이터) 클래스도 마찬가지입니다. 

 

public class Main {
    public static void main(String[] args) {
        // 간단한 커피 주문
        Coffee coffee = new SimpleCoffee();
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());

        // 휘핑 크림 추가
        Coffee whippedCoffee = new WhippedCreamDecorator(coffee);
        System.out.println("Cost: " + whippedCoffee.getCost() + ", Description: " + whippedCoffee.getDescription());

        // 시럽 추가
        Coffee syrupCoffee = new SyrupDecorator(coffee);
        System.out.println("Cost: " + syrupCoffee.getCost() + ", Description: " + syrupCoffee.getDescription());

        // 휘핑 크림과 시럽 추가
        Coffee whippedSyrupCoffee = new SyrupDecorator(new WhippedCreamDecorator(coffee));
        System.out.println("Cost: " + whippedSyrupCoffee.getCost() + ", Description: " + whippedSyrupCoffee.getDescription());
    }
}

 

위와 같이 메인 코드를 짤 수 있는데, 

이렇게 구현한 데코레이터 패턴을 사용하여, 

객체 지향 디자인에서의 객체의 기능을 확장하고 수정할 수 있습니다. 

 

데코레이터 패턴의 실제 예시들 

데코레이터 패턴은 다양한 소프트웨어 시스템에서 사용될 수 있습니다. 

 

Java I/O 라이브러리에서의 InputStream 및 OutputStream 클래스, 

스트림에 대한 암호화나 압축을 처리하는 클래스 등이 있습니다. 

 

import java.io.*;

public class Main {
    public static void main(String[] args) {
        try {
            // 파일에서 데이터를 읽어오는 InputStream 생성
            InputStream inputStream = new FileInputStream("input.txt");
            
            // InputStream을 이용해 데이터를 버퍼링하는 BufferedInputStream 생성
            BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
            
            // 버퍼링된 데이터를 문자로 변환하는 InputStreamReader 생성
            InputStreamReader inputStreamReader = new InputStreamReader(bufferedInputStream);
            
            // 문자 데이터를 읽어오는 BufferedReader 생성
            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            
            // 한 줄씩 읽어와 출력
            String line;
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
            }
            
            // 스트림 닫기
            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

위의 예시 코드에서, InputStream, BufferedInputStream, InputStreamReader, BufferedReader 등은 

모두 데코레이터 패턴을 사용하여 구현되었습니다. 

각각의 데코레이터는 입력 스트림의 기능을 확장하거나 수정합니다. 

BufferedInputStream은 입력 스트림의 데이터를 버퍼링하여 성능을 향상시키고,

InputStreamReader은 바이트 스트림을 문자 스트림으로 변환합니다.

 

데코레이터 패턴의 장단점

장점

  1. 유연성과 확장성
    1. 데코레이터 패턴을 사용하면, 기존 객체의 동작을 변경하지 않고도 새로운 기능을 동적으로 추가하거나 확장할 수 있어 객체를 유연하게 조합해 다양한 기능을 구현할 수 있도록 합니다.
  2. 단일 책임 원칙 준수
    1. 각 데코레이터 클래스는 자신의 기능에 대해서만 책임을 집니다. 이는 단일 책임 원칙을 준수하고 코드의 가독성과 유지 보수성을 높여줍니다.
  3. 기존 코드 수정 최소화
    1. 데코레이터 패턴을 사용하면 기존의 코드를 변경하지 않고도 새로운 기능을 추가할 수 있으므로, 기존 코드를 수정하는 일이 최소화됩니다. 이는 시스템의 안정성과 신뢰성을 유지하는 데 도움이 됩니다. 

단점

  1. 복잡성 증가
    1. 데코레이터 패턴을 사용하면 객체 간의 연쇄적인 구성이 증가할 수 있습니다. 이는 클래스의 수가 늘어나고 객체를 생성하고 조합하는 과정이 복잡해질 수 있음을 의미합니다.
  2. 클래스 수 증가
  3. 순서의 중요성
    1. 데코레이터들 간의 순서를 잘못 정의하면 예상치 못한 동작이 발생할 수 있습니다. 
// 데코레이터 패턴에서 순서가 중요한 예시 코드 (위의 코드와 이어집니다.) 


public class Main {
    public static void main(String[] args) {
        // 기본 커피 주문
        Coffee coffee = new SimpleCoffee();
        
        // 휘핑 크림을 추가한 후
        coffee = new WhippedCreamDecorator(coffee);
        
        // 시럽을 추가
        coffee = new SyrupDecorator(coffee);
        
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
    }
}

 

위의 코드에서는 먼저 휘핑 크림 데코레이터를 추가한 후에 시럽 데코레이터를 추가하였습니다.

이 경우에는 "with whipped cream, with syrup"라는 설명이 출력되고, 가격은 휘핑 크림 가격과 시럽 가격이 더해진 값이 출력됩니다.

 

public class Main {
    public static void main(String[] args) {
        // 기본 커피 주문
        Coffee coffee = new SimpleCoffee();
        
        // 시럽을 추가한 후
        coffee = new SyrupDecorator(coffee);
        
        // 휘핑 크림을 추가
        coffee = new WhippedCreamDecorator(coffee);
        
        System.out.println("Cost: " + coffee.getCost() + ", Description: " + coffee.getDescription());
    }
}

 

이 경우에는 "with syrup, with whipped cream"라는 설명이 출력되고, 가격도 시럽 가격과 휘핑 크림 가격이 더해진 값이 출력됩니다.

 

따라서 데코레이터들의 순서에 따라 커피에 적용되는 기능이 달라지므로, 순서를 잘 결정해야 올바른 동작을 얻을 수 있습니다.

 

데코레이터 패턴을 적용하기 좋은 상황은 언제인가?

기능 확장이 필요한 경우

기존의 클래스를 변경하지 않고도 새로운 기능을 추가하고자 할 때 데코레이터 패턴을 사용할 수 있습니다. 

이는 기존의 클래스를 수정하지 않고도 확장된 기능을 구현할 수 있어, 코드를 수정하거나 재컴파일할 필요가 없게됩니다. 

단일 책임 원칙을 준수하고 싶은 경우

데코레이터 패턴은 단일 책임 원칙을 준수하여 각 클래스가 하나의 기능에만 집중하도록 해줍니다. 

이를 통해 클래스의 변경이나 확장이 필요할 때 해당 클래스만 수정하면 되므로 코드의 유지보수성이 향상됩니다. 

런타임에 동적으로 기능을 추가하고 제거해야 하는 경우 

데코레이터 패턴을 사용하면 런타임에 객체의 기능을 동적으로 추가하거나 제거할 수 있습니다.

이는 객체의 동작을 유연하게 변경할 수 있게 해줍니다. 

객체 간의 결합도를 줄이고자 하는 경우 

데코레이터 패턴을 사용하면 기능을 각각의 데코레이터 클래스로 분리하여 객체 간의 결합도를 줄일 수 있습니다. 

이는 코드의 재사용성을 높이고 객체 간의 의존성을 최소화하는 데 도움이 됩니다.

 

오늘은 데코레이터 패턴에 대하여 알아보았습니다! 

디자인 패턴은 알면 알수록 더 재밌는 것 같아요 :> 
일주일에 한번씩 디자인 패턴을 스터디하려고 노력중인데, 
얼른 모든 디자인 패턴을 머리에 넣고, 항상 생활화 하고싶네요 :)

다들 개발공부 화이팅입니다 :) !
반응형