본문 바로가기
디자인 패턴

Gof 구조 디자인 패턴 : 데코레이터

by oncerun 2022. 11. 21.
반응형

 

DTO 리팩터링이 너무 어렵다....

 

DTO 자체가 프레젠테이션 계층에 너무 종속되어 있기도 하고 여러 생성 메서드들이 많아서 어떻게 해야는지 참.. 고민이 많다. 

 

하여튼 오늘 마무리는 데코레이터 패턴이다. 

 

데코레이터의 핵심은 래퍼 객체이다.

 

개발을 하다보면 클라이언트에서 필요한 책임을 가진 객체를 런타임에 변경하여 해당 기능을 구현할 수 있다. 

그런데 이것도 해야 되고 저것도 해야 하는데? 하나만 하면 안 되는 상황이 올 때 어떻게 해야 할까?

 

두 기능을 합친 클래스를 만들어 제공한다? 

 

오 이 접근 방식은 실제 합성 클래스 코드뿐만 아니라 클라이언트의 코드도 복잡하게 될 것입니다. 

왜냐? 하나의 클래스가 여러 개의 책임을 가질뿐더러 이에 필요한 객체들을 주입하는 과정도 두배가 될 것이고 기능 요청이 오면 합성 클래스의 개수는 더욱더 많아질 것이고 각 클래스의 변경의 영향이 어디까지 갈지 상상이 가시죠?

 

해결책을 찾아야 합니다. 

 

우리는 먼저 객체의 동작을 변경하고 싶어 합니다. 그럼 일단 우리는 확장을 염두에 두고 생각할 수밖에 없습니다. 

 

상속으로 처리할까? 

 - 네 상속은 정적이라서 이제 런타임에 행동 변경은 불가능합니다. 그리고 자식 클래스는 더 이상 상속을 활용하지 못하게 됩니다.  뿐만 아니라 클라이언트가 의존하는 타입이 부모 클래스로 한정되어 버립니다. 가장 큰 이유로 볼 수 있습니다. 

 

대신 합성, 집합 관계를 가질 수 있습니다. 여러 패턴에도 합성은 많이 이용하는 것을 볼 수 있습니다. 

이러한 대안책은 한 객체가 다른 객체에 대한 참조를 갖는 필드를 가지고 일부 작업을 위임합니다. 

 

이를 통해 참조 필드를 쉽게 대체할 수 있게 구조를 작성하여 런타임에 행동을 변경할 수 있고, 객체는 여러 클래스의 행동들을 사용할 수 있고, 다양한 종류의 작업을 위임할 수 있습니다. 

 

뭔가 래퍼 클래스 안에 다양한 무언가가 있다고 느껴집니다.

 

실제 래퍼는 일부 대상 객체와 연결할 수 있는 객체로, 래퍼에는 실제 요청을 위임할 객체와 같은 메서드 집합이 존재하는 것과 마찬가지고, 이는 요청을 위임하는 역할을 합니다. 프락시 패턴도 엇비슷할 수 있습니다. 

 

다들 위임하기 전 무언가를 수행할 수 있는 권한이 있습니다. 

 

래퍼는 래핑 된 객체와 같은 인터페이스를 구현해야 합니다. 그러므로 클라이언트에서는 전부 동일한 타입으로 볼 수 있습니다. 

 

아까 한 가지가 아닌 여러 가지의 행동을 해야 한다고 했을 때, 클라이언트에서 인터페이스를 기준으로 요구사항들과 일치하는 행동들을 가진 데코레이터 집합으로 래핑해야 합니다. 

 

1. Component는 래퍼들과 래핑 된 객체들에 대한 공통된 인터페이스입니다. 

 

2. 그에 대한 구현체인 Concrete Component는 래핑 되는 객체들의 클래스로 기본 행동을 정의하고 이러한 기본 행동들을 데코레이터들이 변경할 수 있도록 합니다 

 

3. 기초 데코레이터들은 Component를 필드로 가지고 있으며 이는 작업을 전부 데코레이터에게 위임합니다. 

 

 

따라가면서 구현해보죠.

 

데코레이터들이 변경할 수 있는 작업을 가진 인터페이스 하나를 구현합니다.

public interface DecoratorService {

    void deco();
}

 

실제 인터페이스의 구현체는 작업들에 대해 기본 구현을 제공하면 됩니다.

public class DefaultDeco implements DecoratorService{
    @Override
    public void deco() {
        System.out.println("default jobs");
    }
}

 

이제 래핑 할 데코레이터 클래스를 만드는데 이때 다른 컴포넌트들과 같은 인터페이스를 따른다. 

이 클래스의 주목적은 모든 구현체 데코레이터에 대한 래핑 인터페이스를 정의하는 것이다. 

public class DecoratorWrapper implements DecoratorService{

    private DecoratorService decoratorService;

    public DecoratorWrapper(DecoratorService decoratorService) {
        this.decoratorService = decoratorService;
    }

    @Override
    public void deco() {
        decoratorService.deco();
    }
}

또한 모든 작업을 래핑 된 컴포넌트에 위임한다. 데코레이터들은  추가 행동이 추가될 수 있다.

이제 실제 데코레이터를 만든다.

 

public class Deco1 extends DecoratorWrapper{

    public Deco1(DecoratorService decoratorService) {
        super(decoratorService);
    }

    @Override
    public void deco() {
        System.out.println("1");
        super.deco();
    }
}
public class Deco2 extends DecoratorWrapper{

    public Deco2(Deco1 deco11) {
        super(deco11);
    }

    @Override
    public void deco() {
        System.out.println("deco2");
        super.deco();
    }
}

클라이언트는 외부에서 주입받아 사용할 수 있다.

public class Client {

    private DecoratorService decoratorService;

    public Client(DecoratorService decoratorService) {
        this.decoratorService = decoratorService;
    }

    public void write() {
        decoratorService.deco();
    }
}

 

앱에서 조건에 따라 여러 데코를 조합한다.

public class App {

    public static void main(String[] args) {

        DecoratorService defaultDeco = new DefaultDeco();
        //if 1이 필요하면
        Deco1 deco11 = new Deco1(defaultDeco);

        //if 2도 필요해
        Deco2 deco2 = new Deco2(deco11);


        Client client = new Client(deco2);
        client.write();
    }
}

 

순서는 2, 1, default 순으로 호출된다. 

 

 

데코레이터들은 결국 부모의 메서드를 호출하기 이전 이후에 무언가를 할 수 있는 권한을 부여받을 것과 다름이 없다. 

이게 가능한 이유는 결국 래퍼 클래스가 Component 인터페이스를 필드로 가지고 있고, 하는 책임이 단순히 위임을 하는 행동밖에 없기 때문이다.

 

 

구현 방법을 정리해보자

 

1. 기본 컴포넌트에 필요한 공통적인 메서드들이 무엇인지 파악해야 한다. 적용하려는 대상인 기본 컴포넌트와 추가하려는 기능들을 기본 컴포넌트 타입으로 묶을 수 있어야 한다.

 

2. 이제 구상 컴포넌트를 만든 이후 기존에 하던 작업을 밀어 넣는다.

 

3. 구상 클래스, 컴포넌트 인터페이스가 만들어졌다면 다음은 데코레이터 래퍼를 만들어야 한다. 

이 래퍼 클래스에는 래핑 된 인터페이스 타입의 참조 필드가 존재해야 한다. 

이 필드가 데코레이터들 및 구상 컴포넌트들과의 연결을 하기 위해 동일 인터페이스로 만든다. 

그리고 이 래퍼는 해당 참조 필드를 통해 모든 작업을 위임해야 한다.

 

4. 모든 클래스 ( 래퍼, 기본 구상 컨포넌트) 들이 컴포넌트 인터페이스를 구현하게 만든다. 

 

5. 이후 래퍼 클래스를 상속받아 실제 구상 데코레이터를 생성한다. 

구상 데코레이터는 항상 부모 메서드 super.method()를 호출 전 또는 호출 후에 행동을 실행해야 한다. 

 

6. 이제 필요한 곳에 적용하여 사용한다. 이러한 구성은 클라이언트가 책임을 진다.

 

 

장점

 

객체의 행동을 자식 클래스를 만드는 대신 확장할 수 있다. 

런타임에 여러 책임을 부여하거나 제거 가능

여러 행동을 합성하여 사용할 수 있다. 

 

다만 순서에 의존하지 않는 방식으로 구현하기 어렵고, 계층 초기 코드를 통해 코드가 복잡해질 수 있다.

 

 

재밌는 점은  어댑터 패턴은 다른 인터페이스를, 프락시는 같은 인터페이스를 데코레이터는 향상된 인터페이스를 래핑된 객체에 제공한다.

 

실제 프록시 패턴과 구조가 비슷하지만 의도는 매우 다르다. 

두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임해야 하는 합성 원칙을 기반으로 한다.

다만 프록시는 일반적으로 자체적인 자신의 서비스 객체의 수명 주기를 관리한다는 반면 데코레이터 패턴은 항상 클라이언트에 의해 제어된다는 점이다.

 

 

나는.. AOP 쓸래..

반응형

'디자인 패턴' 카테고리의 다른 글

Gof 구조 디자인 패턴 : proxy  (0) 2022.11.20
Gof 행동 디자인 패턴 : State  (0) 2022.11.20
Gof 생성 디자인 패턴 : Builder  (0) 2022.11.19
GoF 행동 패턴 소개 (1)  (0) 2022.11.15
GoF 구조적인 패턴 소개  (0) 2022.11.06

댓글