본문 바로가기

기타/객체지향의 탄생(2013)

객체지향의 탄생- Decorator pattern

  1. 구조 관련 패턴(Structural Patterns)
    1. 데코레이터 패턴(Decorator)

1. 사람 사는 세상의 사물들은 독립적인 사물 그 자체로 자신의 기능을 다하기도 한다. 목이 말라 지금 내가 마시는 물컵이나, 내가 참고하는 책들은 하나의 사물 그 자체로 자신의 의미와 기능을 다하는 것이다.

 

또는 사물이 하나의 사물로 존재할때 기능을 다하는 것이 아니라 부가적인 사물과 합쳐 온전하게 자신의 기능을 다 발휘하는 경우도 있다. 사람 자신도 부가적인 사물인 옷들을 입어야 다른 사람들과 어울리며 사회생활을 한다. 자동차는 오디오셋트, 네비게이션셋트, 에어벡시스템 등이 함께 있어야 제대로 작동된다. 먹거리로 피자는 불고기, 새우, 닭가슴살 토핑 중에 하나를 선택하여 피자의 맛이 완성된다. 이렇게 어떤 사물들은 자신의 정상적인 작동을 위해 부수적인 다른 사물을 필요로 하곤 한다.

 

만약 부가적인 사물이 함께 있어야 제대로 작동하는 사물을 객체지향 프로그래밍에 묘사를 해야 한다면 다양한 객체가 존재하고 연결지어져야 되기 때문에 그 설계 방법이 까다로울 것 같다.

 

단순하게 생각한다면, 핵심 역할을 하는 사물과 부가적인 역할을 하는 사물을 하나로 묶어서 하나의 사물 객체로 보는 경우가 있다. 예를들어 불고기 피자, 새우 피자, 닭가슴살 토핑 피자 이렇게 하나하나 클래스로 만들어버린다.

 

또는 핵심 역할을 하는 사물을 하나 선언하고 부가적으로 필요한 사물 객체를 선언하여 핵심 역할을 하는 사물이 이들에게 의존하는 방법이 있다. 여기서 더나아간다면 부가적인 역할을 하는 사물 객체를 묶어 인터페이스/추상클래스를 만들고 전형적인 구성의 방법으로 연결하여 더 깔끔하게 설계할수 있다. 앞에 살펴본 구성과 집합의 차이에서 구성은 핵심 역할을 하는 사물이 부가적인 역할을 하는 사물을 소유할때 사용한다고 하니 적절하게 사용되는 경우일것이다.

 

전형적인 구성-스트라테지 패턴을 이용한 방법은 이제는 객체지향 두뇌 가장 깊숙히 자리 잡을정도로 익숙하다. 뭔가 세련된 다른 방법은 없을까. 데코레이터 패턴을 알아보자.

 

2. 여자들은 가방이나 구두 옷에 대한 로망이 있다. 남자들은 멋진 기계를 다루고 싶은 로망이 있다. 자동차는 다 큰 남자들의 꿈을 이루어주는 로보트와 같은 존재이다. 나는 자동차에 대한 욕심이 없다가, 30대 넘어가서 자동차에 관심을 가졌다.

 

지금 나는 마음은 중형차나 SUV를 몰고 싶지만 지금 중고차를 운전한다. 이 차의 좋은 점은 수동 기어에 있다. 평소에는 시내 운전만 하다가 가끔 고속도로로 진입하면서 기어를 5단으로 변경하고 엑셀을 밟을때 슝~ 나갈때의 기분은 비행기가 막 이륙할때의 짜릿함이 생각난다.

 

[남자들은 차에 열광하지만 보통 현실은 중고차~]

 

자동차에 관심을 갖고 인터넷 글이나 블로그를 읽었다. 자동차 애호가 들의 튜닝 문화가 발달했다는 것을 알았다.

 

나는 돈많은 사람들이나 튜닝 한다고 생각하기도 했는데, 그정도 쓸 돈이 있으니 튜닝에 쓰는 것이고, 그만큼 자동차를 좋아한다고 볼 수 있다.

 

3. 자동차 애호가가 튜닝을 할때의 상황을 객체지향으로 묘사해 보자. 자동차 애호가가 아방때의 주인이다. 자동차를 상속받은 아방때 클래스를 만든다. 자동차 애호가가 오디오 튜닝을 한다. 아방때 오디오 튜닝된 클래스를 만든다. 자동차 애호가가 휠 튜닝을 한다. 아방때 휠 튜닝된 클래스를 만든다. 자동차 애호가가 타이어도, 엔진 튜닝도 하면 관련 튜닝된 클래스를 만든다.

 

[상속을 썼을 때]

 

전형적인 상속 구성이다. 만약 자동차 애호가가 오디오 튜닝도 하고 휠 튜닝도 같이한다면 어떻게 해야 할까. 현재 구성으로는 아방때 오디오 휠 튜닝 클래스를 따로 만들어야 한다. 코드가 중복되고 경직된 구성이라고 누구나 생각할 것이다.

 

[스트라테지 패턴]

 

스트라테지 패턴을 쓰면 좀더 낫다. 오디오, 타이어 메소드를 튜닝한 별도의 클래스가 있다면 해당 클래스로 위임하면 된다.

 

스트라테지 패턴보다 좀더 유연하고 인터페이스 메소드에 제약받지 않고 자유자재로 튜닝을 추가하는 방법이 있다.

 

[자동차 데코레이터 그룹 생성]

 

자동차 상속 구조에 튜닝을 전문 적으로 하는 데코레이터 클래스 그룹을 두고 이 데코레이터 클래스 그룹을 자동차 클래스의 인터페이스와 동일하게 맞춘다.

 

자동차 데코레이터 클래스 그룹은 '자동차 객체'를 가지고 있다. 그래서 예를들어 오디오 데코레이터에 엔진 데코레이터를 인자로 주면 오디오 데코레이터의 '기능'을 실행하면서 엔진 데코레이터의 '기능'도 실행한다.

 

자동차 관련 클래스들을 인자로 더 추가할 수 있다. 오디오, 엔진, 타이어를 연속으로 넣고 마지막으로 자동차 프리쿠스를 인자로 넣으면 오디오 튜닝 기능-> 엔진 튜닝 기능-> 타이어 튜닝 기능-> 프리쿠스 기본 기능을 실행한다. 그래서 자유롭게 튜닝을 조합하여 실행할 수 있다. 만능 변신 합체 로봇과 같다. 글로는 이해가 안될 수 있으니 코드와 같이 본다.

 

public abstract class Car {

    

    public abstract String wheel(); // 차 바퀴

    public abstract String tire(); // 타이어

    public abstract String muffler(); // 머플러

    public abstract String light(); // 라이트

    public abstract String audioSystem(); // 오디오 시스템

    public abstract String engineRoom(); // 엔진 룸

    public abstract String bodyExternal(); // 기타 자동차 외부

    public abstract String bodyInternal(); // 기타 자동차 내부

} 

[카 클래스 생성]

 

public class HyundaeAbangte extends Car {

 

    @Override

    public String wheel() {

        return "아방때의 순정 휠";

    }

 

    @Override

    public String tire() {

        return "아방때의 순정 타이어";

    }

 

    @Override

    public String muffler() {

        return "아방때의 순정 머플러";

    }

 

    @Override

    public String light() {

        return "아방때의 순정 라이트";

    }

 

    @Override

    public String audioSystem() {

        return "아방때의 순정 오디오";

    }

 

    @Override

    public String engineRoom() {

        return "아방때의 순정 엔진룸";

    }

 

    @Override

    public String bodyExternal() {

        return "아방때의 순정 외장 바디";

    }

 

    @Override

    public String bodyInternal() {

        return "아방때의 순정 내장 바디";

    }

 

} 

[아방때 자동차 클래스 생성]

 

public class ToyodaPricus extends Car {

 

    @Override

    public String wheel() {

        return "프리쿠스의 순정 휠";

    }

 

    @Override

    public String tire() {

        return "프리쿠스의 순정 휠";

    }

 

    @Override

    public String muffler() {

        return "프리쿠스의 순정 머플러";

    }

 

    @Override

    public String light() {

        return "프리쿠스의 순정 라이트";

    }

 

    @Override

    public String audioSystem() {

        return "프리쿠스의 순정 오디오";

    }

 

    @Override

    public String engineRoom() {

        return "프리쿠스의 순정 엔진";

    }

 

    @Override

    public String bodyExternal() {

        return "프리쿠스의 순정 외장 바디";

    }

 

    @Override

    public String bodyInternal() {

        return "프리쿠스의 순정 내장 바디";

    }

 

}

[프리쿠스 자동차 클래스 생성]

 

public class CarDecorator extends Car {

    protected Car car;

    

    public CarDecorator(Car car) {

        this.car = car;

    }

 

    @Override

    public String wheel() {

        return car.wheel();

    }

 

    @Override

    public String tire() {

        return car.tire();

    }

 

    @Override

    public String muffler() {

        return car.muffler();

    }

 

    @Override

    public String light() {

        return car.light();

    }

 

    @Override

    public String audioSystem() {

        return car.audioSystem();

    }

 

    @Override

    public String engineRoom() {

        return car.engineRoom();

    }

 

    @Override

    public String bodyExternal() {

        return car.bodyExternal();

    }

 

    @Override

    public String bodyInternal() {

        return car.bodyInternal();

    }

 

}

[자동차 데코레이터 클래스 생성]

 

public class AudioDecorator extends CarDecorator {

 

    public AudioDecorator(Car car) {

        super(car);

    }

 

    @Override

    public String audioSystem() {

        return car.audioSystem()+", 오디오 튜닝을 더함.";

    }

} 

[오디오 데코레이터 클래스 생성, 오디오 관련 메소드만 오버라이드 하여 인자로 넘겨받은 자동차 클래스 더하고 오디오 튜닝과 관련된 로직을 자유롭게 추가한다.]

 

public class EngineDecorator extends CarDecorator {

 

    public EngineDecorator(Car car) {

        super(car);

    }

    

    @Override

    public String engineRoom() {

        return car.engineRoom()+", 엔진 튜닝 더함";

    }

 

} 

[엔진 데코레이터 클래스 생성, 엔진 관련 메소드만 오버라이드 하여 인자로 넘겨받은 자동차 클래스 더하고 엔진 튜닝과 관련된 로직을 자유롭게 추가한다.]]

 

public class TireDecorator extends CarDecorator {

    public TireDecorator(Car car) {

        super(car);

    }

 

    @Override

    public String tire() {

        return car.tire() + ", 타이어 튜닝을 더함.";

    }

}

[타이어 데코레이터 클래스 생성, 타이어 관련 메소드만 오버라이드 하여 인자로 넘겨받은 자동차 클래스 더하고 오디오 튜닝과 관련된 로직을 자유롭게 추가한다.]]

 

    public static void main(String[] args) {

        Car abangte = new HyundaeAbangte(); // 아망때 차 클래그 생성

        

        // 1.아방때

        // 아방때 순정 상태 확인

        System.out.println("아방때 순정 상태 확인");

        System.out.println("휠 상태 : " + abangte.wheel());

        System.out.println("타이어 상태 : " + abangte.tire());

        System.out.println("머플러 상태 : " + abangte.muffler());

        System.out.println("라이트 상태 : " + abangte.light());

        System.out.println("오디오 상태 : " + abangte.audioSystem());

        System.out.println("엔진룸 상태 : " + abangte.engineRoom());

        System.out.println("외장 바디 상태 : " + abangte.bodyExternal());

        System.out.println("내장 바디 상태 : " + abangte.bodyInternal());

 

        System.out.println("");

        

        // 아방때에 오디오 튜닝과 타이어 튜닝을 더하다.

        Car abangteTunning = new TireDecorator(new AudioDecorator(new HyundaeAbangte()));

        System.out.println("아방때에 오디오 튜닝과 타이어 튜닝을 더하다.");

        System.out.println("휠 상태 : " + abangteTunning.wheel());

        System.out.println("타이어 상태 : " + abangteTunning.tire());

        System.out.println("머플러 상태 : " + abangteTunning.muffler());

        System.out.println("라이트 상태 : " + abangteTunning.light());

        System.out.println("오디오 상태 : " + abangteTunning.audioSystem());

        System.out.println("엔진룸 상태 : " + abangteTunning.engineRoom());

        System.out.println("외장 바디 상태 : " + abangteTunning.bodyExternal());

        System.out.println("내장 바디 상태 : " + abangteTunning.bodyInternal());

    } 

[실행]

 

4. 이번 데코레이터 패턴으로의 진화과정을 보면서 상속의 전형적인 문제점을 다시 절감하게 되었다. 모든 서브클래스에서 똑같은 행동을 상속받는데 때로 어떤 서브 클래스는 이 행동은 상속받으면 안되는 경우가 생긴다. 서브 클래스간에도 중복된 기능이 보여서 클래스 관계도와 코드가 지저분 해지는 경우가 생긴다. 무엇보다 행동이 컴파일시에 다시 지우지 못하는 볼펜처럼 결정 되어버려 어플리케이션이 경직되는 경우가 생긴다.

 

먼저 스트라테지 패턴으로 위의 문제를 해결하였다. 변하는 부분을 변하지 않는 부분으로부터 분리시켜 캡슐화시켰고, 구현이 아닌 인터페이스에 맞춰 의존하여 클라이언트 객체는 수많은 서브 요소들을 신경쓰지 않아도 되었다.

 

하지만 클라이언트 객체와 조합해야 하는 기능이 무수히 많을 경우는 더 깔끔하게 해결하는 방법을 찾아야 했다. 데코레이터 패턴의 데코레이터는 같은 클래스군에 속하면서, 데코레이터가 구성으로 클라이언트 객체에 의존하고 있다. 일단 모든 객체가 하나의 패밀리에 속하기 때문에 어떠한 객체라도 심지어는 데코레이터들도 데코레이터의 구성을 통해 인자로 받아들여 재귀적으로 실행할수 있다.

 

같은 클래스 그룹에 속한 것은 재귀적인 결합이 가능하게 해주었고, 데코레이터와 추상 클래스의 구상연결은 실행시에 다른 데코레이터나 다른 서브 클래스를 인자로 받아들여 얼마든지 기능을 추가로 확장하게 해주었다.

 

그 결과 무수히 많은 요소의 결합이 요구되도 유연하게 요구사항을 받아들일 수 있다. 이때 쓰인 디자인 원칙중 하나가 '클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다' 는 OCP 원칙이다.

 

만약 상속만을 사용했다면, 아방때 오디오 튜닝, 아방때 엔진 튜닝 처럼 기능을 확장할수는 있지만 코드의 중복이 생겨서 더 큰 문제가 발생한다.

 

그러나 스트라테지 패턴과, 데코레이터 패턴을 활용하면 새로운 기능들을 얼마든지 확장하여 클라이언트 객체에 어플리케이션 작동중에도 기능을 변경할수 있다. 더 나아가 데코레이터 패턴을 활용하면 새로운 기능을 얼마든지, '몇개라도' 조합하여 확장할 수 있다.

 

그림처럼 데코레이터에 속한 클래스들은 자기가 감싸고 있는 객체들의 메소드를 재귀적으로 호출한 결과에 자신의 새로운 기능을 더하여 행동을 유연하고 자연스럽게 확장하게 된다.

 

[데코레이터 패턴 원본]

 

덧 ) 이 객체지향의 탄생 원고는 제가 책으로 내려다가 일단 잘 안되었는데요. 이유는 비문이 많다. 단락내 주제가 중복된다. 어떤 상황 설명을 과장한다.등 입니다. 그래도 원고를 일단 블로그에 몽땅 풀어보고 언젠가 제대로 교정해서 다시 도전할 생각입니다. 이점을 감안해서 읽고 객체지향을 이해하는데 도움이 되셨으면 좋겠습니다. 의견도 주셨으면 좋겠습니다. 원고 조금만 교정하면 괜찮을것 같은 출판사 관계자분의 피드백도 환영합니다. 매주 월요일 발행 예정입니다.