본문 바로가기
IT/Java

[Java] Java Decorator Pattern

by kyu-nahc 2024. 9. 9.
반응형

 

Decorator Pattern

데코레이터 패턴(Decorator Pattern)은 대상 객체에 대한 

기능 확장이나 변경이 필요할 때 객체의 결합을 통해 쓸 수 있는 유연한 대안 구조 패턴이다.
Decorator을 해석하자면 '장식자'라는 의미를 가지고 있는데,  

마치 기본 제품에 포장지나 외부 디자인을 살짝 변경해 줌으로써 

새로운 기능을 부여하는 것과 같이, 객체 지향 프로그래밍에서 원본 객체에 대해서 

무언가를 장식하여 더 추가적인 기능을 가지게 만드는 것이기 때문에 이 명칭이 붙었다고 보면 된다.


데코레이터 패턴을 이용하면 필요한 추가 기능의 조합을 런타임에서 동적으로 생성할 수 있다. 

데코레이터 할 대상 객체를 새로운 행동들을 포함한 특수 장식자 객체에 넣어서 

행동들을 해당 장식자 객체마다 연결시켜, 

서브클래스로 구성할 때 보다 훨씬 유연하게  기능을 확장할 수 있다. 

즉 추가적인 상속을 통해 클래스를 확장하는 대신,

객체를 감싸는 방식을 사용하여 기능을 추가하거나 변경한다.

따라서 기존 코드를 수정하지 않고도 새로운 기능을 추가하거나 수정할 수 있게 된다.

 

 

Decorator Pattern 구조

 

Component ( 추상 클래스 or 인터페이스 )

원본 객체와 장식된 객체 모두를 묶는 역할


ConcreteComponent

원본 객체 (데코레이팅 당하는 객체)


Decorator

추상화된 장식자 클래스
원본 객체를 합성(composition)한 component 필드와

인터페이스 또는 추상 클래스의 구현 메소드를 가지고 있다

ConcreteDecorator ( DecoratorA,B... )

구체적인 장식자 클래스
부모 클래스가 감싸고 있는 하나의 Component를 호출하면서

호출 전 / 후로 부가적인 로직을 추가할 수 있다.

코드를 통해 살펴보면 다음과 같다. 

// 원본 객체와 장식 객체를 모두 묶는 추상 클래스
public abstract class Component {
    public abstract void process();
}
// 원본 객체. Decorator 할 객체
public class ConcreteComponent extends Component{

    @Override
    public void process() {
        System.out.println("Concrete Component Process");
    }
}
// 추상화된 Decorator 클래스
public abstract class Decorator extends Component{

    Component component;

    public Decorator(Component component){
        this.component = component;
    }

    @Override
    public void process(){
        component.process();
    }
}
// 추상화 Decorator 클래스를 구현하는 구현체
public class DecoratorA extends Decorator{

    public DecoratorA(Component component) {
        super(component);
    }

    public void process(){
        super.process();
        decoratorProcess();
    }
    private void decoratorProcess(){
        System.out.println("Decorator A Process");
    }
}
// 추상화 Decorator 클래스를 구현하는 구현체
public class DecoratorB extends Decorator{

    public DecoratorB(Component component) {
        super(component);
    }

    public void process(){
        super.process();
        decoratorProcess();
    }
    private void decoratorProcess(){
        System.out.println("Decorator B Process");
    }
}
public class Main {

    public static void main(String[] args) {

        Component component = new ConcreteComponent();

        Component decoratorA = new DecoratorA(component);
        decoratorA.process();

        Component decoratorB = new DecoratorB(component);
        decoratorB.process();

        Component twoDeco = new DecoratorB(new DecoratorA(component));
        twoDeco.process();
    }
}

 

Decorator Pattern 특징

데코레이터 된 객체는 메서드를 호출할 때,

장식한 메서드를 호출하여 반환 로직에 추가적으로 더 덧붙여서 결과값을 반환할 수 있다. 

 

패턴 적용 시기

- 객체 책임과 행동이 동적으로 상황에 따라 변경되는 경우

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

 

패턴 장점

- 데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 더 유연하게 기능을 확장
- 객체를 여러 데코레이터로 래핑하여 여러 동작을 결합
- 컴파일 타임이 아닌 런타임에 동적으로 기능을 변경 가능
- 각 장식자 클래스마다 고유의 책임을 가져 단일 책임 원칙(SRP)을 준수
- 클라이언트 코드 수정 없이 기능 확장이 필요하면

  장식자 클래스를 추가하면 되니 개방 폐쇄 원칙(OCP)을 준수
- 구현체가 아닌 인터페이스를 바라봄으로써 의존 역전 원칙(DIP)을 준수

 

패턴 단점

만일 장식자 일부를 제거하고 싶다면,

Wrapper 스택에서 특정 wrapper를 제거하는 것은 어렵다.
데코레이터를 조합하는 초기 생성코드가 보기 안 좋을 수 있다. 
어느 장식자를 먼저 데코레이팅 하느냐에 따라 데코레이터 스택 순서가 결정지게 되는데, 

만일 순서에 의존하지 않는 방식으로 데코레이터를 구현하기는 어렵다.

 

 

Decorator Pattern 적용 예시

Decorator의 적용 예시를 살펴보자.

런타임에 동적으로 기능을 변경할 수 있는 유연성이 Decorator 패턴의 핵심이다.

또한 기능 확장에 대해 Decorator 클래스를 하나 추가한다면 다른 코드의 변경 없이

new DecoratorA(new DecoratorB( ConcreteObject ))) 부분만 변경하면 된다.

즉 초기 Decorator 할 객체 생성 부분만 수정하면 되므로 굉장히 유연하다.

실제 개발 환경에서는 이 패턴을 다음과 같이 사용할 수 있다.:

  1. 로깅 : 주요 기능에 로그를 추가할 때 Decorator로 쉽게 확장 가능.
  2. 캐싱 : 데이터베이스에서 조회한 값에 캐싱 기능을 추가하는 데 유용.
  3. 권한 검사 : 특정 기능에 권한 체크 로직을 동적으로 추가할 때 활용.

위의 개발 환경의 예시 중 캐싱의 예시를 살펴보면 다음과 같다.

해당 예제는 간단한 예제로, Decorator 패턴을 활용하여

데이터베이스에서 조회한 값에 캐싱 기능을 동적으로 추가하는 것이다.

public interface DataService {
    String fetchData(String key);
}

 

데이터를 조회하는 기본 인터페이스를 정의한다.

이것이 원본 객체와 장식된 객체 모두를 묶는 역할을 하는 매개체이다.

위의 설명에서는 추상 클래스로 구현하였지만,

만약 멤버변수가 필요 없을 경우 인터페이스를 통해 추상화를 진행해도 상관없다.

멤버변수가 존재한다면 추상 클래스메소드만 있다면 인터페이스로 묶으면 된다.

public class DatabaseDataService implements DataService {

    @Override
    public String fetchData(String key) {
        // 실제로는 데이터베이스 접근 로직이 들어감.
        System.out.println("데이터베이스에서 데이터 가져오기: " + key);
        return "Data for " + key;
    }
}

 

데이터베이스에서 실제로 데이터를 가져오는 클래스를 구현한다.

실제로는 fetchData()에 데이터베이스 접근 및 조회 로직이 들어간다.

public abstract class DataServiceDecorator implements DataService {
    protected DataService decoratedDataService;

    public DataServiceDecorator(DataService dataService) {
        this.decoratedDataService = dataService;
    }

    @Override
    public String fetchData(String key) {
        return this.decoratedDataService.fetchData(key);
    }
}

 

캐싱 기능을 추가하기 위한 Decorator 추상 클래스를 만든다.

위의 설명과 동일하게 묶는 역할을 하는 추상클래스를 상속받거나, 인터페이스를 구현한다.

이는 구조 유지를 위한 추상 클래스로,

이를 상속받아서 Decorator 역할을 하는 캐시 접근 로직을 구현하는 구현체를 구현하면 된다.

구조 유지를 하는 이유는 현재는 하나의 캐시 접근을 통해 로직을 구현하지만.

위의 패턴 구조 코드에서 여러 개의 Decorator가 생겨날 경우 이를 추상화하여 묶을 객체가 필요하다.

여러 개의 Decorator를 추상화하여 묶기 위해 추상 클래스를 생성하는 것이다.

만약 1차 캐시 이외에 추가적인 2차 캐시를 적용해야 한다면 2차 캐시 로직을 구현하는

클래스를 생성한 후 생성자 코드만 변경하여 적용할 수 있는 것이다.

public class CachingDataServiceDecorator extends DataServiceDecorator {
    private final Map<String, String> cache = new HashMap<>();

    public CachingDataServiceDecorator(DataService dataService) {
        super(dataService);
    }

    @Override
    public String fetchData(String key) {
        if (cache.containsKey(key)) {
            System.out.println("캐시에서 데이터 가져오기: " + key);
            return cache.get(key);
        } else {
            String data = super.decoratedDataService.fetchData(key);
            cache.put(key, data);
            return data;
        }
    }
}
public static void main(String[] args) {
    // 캐싱이 없는 데이터 서비스
    DataService dataService = new DatabaseDataService();
    dataService.fetchData("user1");
    dataService.fetchData("user1");

    // 캐싱이 추가된 데이터 서비스
    DataService cachedDataService = new CachingDataServiceDecorator(new DatabaseDataService());
    cachedDataService.fetchData("user2");
    cachedDataService.fetchData("user2");
}

 

첫 번째 클래스는 캐싱 로직을 담은 구체적인 Decorator 클래스이다.

마찬가지로 추상 메소드를 오버라이드 한 후 캐시에 접근하여 데이터를 조회하는 로직을 추가한다.

해당 예제는 Map 자료구조를 이용하는 간단한 예제이므로,

구체적인 캐시 컨트롤 로직은 존재하지 않고 Decorator 패턴을 활용하는 예를 보여주기 위한 로직이다.

캐시에 접근하여 데이터가 존재한다면 해당 구문을 출력하고, 아니라면 캐시에 데이터를 넣어준다.

메인함수에서는 캐싱이 없는 서비스와 캐싱이 추가된 서비스를 비교하도록 코드를 구현한다.

 

다음과 같이 데코레이터 패턴을 적용하여 캐싱을 구현할 수 있다.

만약 캐시를 추가해야 한다면,

DataServiceDecorator를 상속받는 2차 캐시 클래스를 생성하고 로직을 구현한 후, 

생성한 캐시 구현체로 ConcreteComponent를 한 번 더 감싸주면 된다.

이처럼 Decorator Pattern은 훨씬 더 유연하게 기능을 확장할 수 있으며,

OCP원리에 부합하도록 코드를 구현할 수 있다.

 

 

참고자료

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0Decorator-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java Builder Pattern  (2) 2024.09.12
[Java] Java Factory Pattern  (1) 2024.09.11
[Java] Java Observer Pattern  (7) 2024.09.08
[Java] Java Strategy Pattern  (0) 2024.08.29
[Java] Java 함수형 프로그래밍  (0) 2024.08.27

loading