본문 바로가기
Design Pattern/구조 패턴(Structural Patterns)

데코레이터(Decorator) 패턴 - 구조 패턴 (Structural Patterns)

by 김 민 준 2024. 5. 23.

 

데코레이터 패턴은 개체에 동적으로 새로운 기능을 추가할 수 있게 해주는 디자인 패턴이다. 개체를 다양한 방법으로 확장할 수 있어 코드의 유연성과 재사용성을 높일 수 있으며, 기존 개체를 변경하지 않고 기능을 확장할 수 있다. 

 

예를 들어 커피머신을 생각해 보자 

 

커피머신의 기본은 당연히 커피이다. 하지만 우유나 설탕을 추가함으로써 여러 조합을 만들 수 있다. 

 

 

Component 인터페이스는 커피의 공통 인터페이스 역할을 한다. 

// Coffee 인터페이스: 커피의 공통 기능 정의
interface Coffee {
    String getDescription();
    double getCost();
}

 

 

ConcreateComponent 클래스

 

// BasicCoffee 클래스: 기본 커피
class BasicCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Basic Coffee";
    }

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

 

데코레이터 클래스에는 커피에 추가할 다른 재료들의 공통 클래스로 선언한다. 

 

 

 

// CoffeeDecorator 클래스: 커피 데코레이터
abstract class CoffeeDecorator implements Coffee {
    protected Coffee coffee;

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

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public double getCost() {
        return coffee.getCost();
    }
}

 

ConcreateDecorator 클래스에는 구체적인 데코레이터, 즉 우유와 설탕등을 선언한다.

 

// MilkDecorator 클래스: 우유를 추가하는 데코레이터
class MilkDecorator extends CoffeeDecorator {
    public MilkDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.5;
    }
}

// SugarDecorator 클래스: 설탕을 추가하는 데코레이터
class SugarDecorator extends CoffeeDecorator {
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Sugar";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 0.2;
    }
}

 

이를 사용하는 소스코드를 확인해 보자

 

// 사용 예시
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Coffee basicCoffee = new BasicCoffee();
        System.out.println(basicCoffee.getDescription() + " $" + basicCoffee.getCost());

        Coffee milkCoffee = new MilkDecorator(basicCoffee);
        System.out.println(milkCoffee.getDescription() + " $" + milkCoffee.getCost());

        Coffee milkAndSugarCoffee = new SugarDecorator(milkCoffee);
        System.out.println(milkAndSugarCoffee.getDescription() + " $" + milkAndSugarCoffee.getCost());
    }
}

//출력
//Basic Coffee $2.0
//Basic Coffee, Milk $2.5
//Basic Coffee, Milk, Sugar $2.7

 

실무에서는 어떻게 자주 쓰일까?

 

웹 애플리케이션에서 Request를 처리하기 전에 여러 가지 기능을 추가하는 미들웨어를 구현해야 하는 경우를 생각해 보자.

 

기본적인 요청 처리기를 선언한다.

// RequestHandler 인터페이스: 요청 처리의 공통 인터페이스
interface RequestHandler {
    void handleRequest(String request);
}

// 기본 요청 처리기
class BasicRequestHandler implements RequestHandler {
    @Override
    public void handleRequest(String request) {
        System.out.println("Handling request: " + request);
    }
}

 

데코레이터 클래스를 선언한다.

 

// RequestHandler 데코레이터: 요청 처리기에 기능을 추가하는 공통 클래스
abstract class RequestHandlerDecorator implements RequestHandler {
    protected RequestHandler decoratedHandler;

    public RequestHandlerDecorator(RequestHandler decoratedHandler) {
        this.decoratedHandler = decoratedHandler;
    }

    @Override
    public void handleRequest(String request) {
        decoratedHandler.handleRequest(request);
    }
}

 

로깅과 인증을 처리하는 데코레이터 클래스를 선언한다.

 

// LoggingDecorator 클래스: 요청을 처리하기 전에 로깅을 추가
class LoggingDecorator extends RequestHandlerDecorator {
    public LoggingDecorator(RequestHandler decoratedHandler) {
        super(decoratedHandler);
    }

    @Override
    public void handleRequest(String request) {
        System.out.println("Logging request: " + request);
        decoratedHandler.handleRequest(request);
    }
}

// AuthenticationDecorator 클래스: 요청을 처리하기 전에 인증을 추가
class AuthenticationDecorator extends RequestHandlerDecorator {
    public AuthenticationDecorator(RequestHandler decoratedHandler) {
        super(decoratedHandler);
    }

    @Override
    public void handleRequest(String request) {
        System.out.println("Authenticating request: " + request);
        // 인증 로직 추가
        decoratedHandler.handleRequest(request);
    }
}

// RedisCachingDecorator 클래스: 요청을 처리하기 전에 Redis 캐싱을 추가
class RedisCachingDecorator extends RequestHandlerDecorator {
    public RedisCachingDecorator(RequestHandler decoratedHandler) {
        super(decoratedHandler);
    }

    @Override
    public void handleRequest(String request) {
        if (isCached(request)) {
            System.out.println("Fetching from cache: " + request);
        } else {
            System.out.println("Caching request: " + request);
            decoratedHandler.handleRequest(request);
            cacheRequest(request);
        }
    }

    private boolean isCached(String request) {
        // Redis 캐시에서 요청을 조회하는 로직 (가상의 예제)
        return false; // 실제 구현에서는 Redis에서 요청을 조회
    }

    private void cacheRequest(String request) {
        // Redis 캐시에 요청을 저장하는 로직 (가상의 예제)
    }
}

 

이렇게 사용

// 사용 예시
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        RequestHandler handler = new BasicRequestHandler();
        handler = new LoggingDecorator(handler);
        handler = new AuthenticationDecorator(handler);
        handler = new RedisCachingDecorator(handler);

        handler.handleRequest("GET /home");
    }
}

//출력
//Logging request: GET /home
//Authenticating request: GET /home
//Caching request: GET /home
//Handling request: GET /home

 

예시에서는 실제 인증로직이나 기타 다른 부분에 대해서는 구현하지 않았지만 예제 소스코드를 확인해 보면 요청 처리 로직을 변경하지 않고 필요한 기능을 유연하게 확장하여 처리할 수 있는 것을 확인할 수 있다. 

 

이처럼 데코레이터 패턴을 이용하면 동적으로 새로운 기능들을 추가할 수 있다.

 

예를 들어 GUI를 구현할 때 버튼이나 텍스트 필드 등의 컴포넌트에 스크롤, 테두리 등 다양한 기능을 동적으로 추가하는 경우가 있을 것이다. 혹은 데이터 스트림 처리를 할 때 버퍼링이나 필터링 등 다양한 기능을 추가하는 경우가 발생하는데 이때 데코레이터 패턴을 이용하여 구현하기도 한다.