데코레이터 패턴

차동준·2022년 7월 16일
0

CS-디자인패턴

목록 보기
5/16
post-thumbnail

👨‍💻 데코레이터(Decorator) 패턴이란?

한 객체를 다른 객체로 감싸면서 행동을 위임하거나
동적으로 행동을 추가할 수 있도록 하여
유연하게 기능을 확장할 수 있게 하는 패턴

위 정의만으로는 확실히 이해하기는 힘들기 때문에, 커피를 예로 들어서 설명을 해보겠다.
샷(Shot)이라는 추상 클래스가 있고, 각각 이 샷 클래스를 상속하는 에스프레소, 디카페인이 있을 때,

예시) Shot 추상 클래스


public abstract class Shot {
	String name;
	public abstract int cost();
}

예시) Shot 추상 클래스를 상속받는 Espresso 클래스


public class Espresso extends Shot {
	public Espresso() {
    	this.name = "에스프레소";
    }
    
    public int cost() {
    	return 3000; // 에스프레소 3000원
    }
}

예시) Shot 추상 클래스를 상속받는 Decafeine 클래스


public class Decafeine extends Shot {
	public Decafeine() {
    	this.name = "디카페인";
    }

	public int cost() {
    	return 3500; // 디카페인 3500원
    }
}



예시) 데코레이터 클래스


우리는 이 샷을 그대로 마시는 경우는 극히 드물 것이다.
그 위에 물을 추가해서 아메리카노, 디카페인 아메리카노를 만들기도 하고 우유를 추가해서 라떼를 만들기도 한다.

그러기 위해서는 Shot 타입의 에스프레소나, 디카페인을 데코레이터 클래스를 이용해서
아메리카노, 라떼 등으로 만들고(기능 확장) 에스프레소나 디카페인의 가격을 받아와(행동 위임) 일정 금액을 추가하는 등의 확장을 할 수 있다.

우선, Shot 추상 클래스를 감쌀 수 있는 데코레이터 클래스를 만들어보면 아래와 같다.

public abstract class ShotDecorator extends Shot {
	Shot shot;
    // public void cost(); => Shot의 메서드 자동 상속(같은 추상클래스이기 때문)
}

문제는 얼핏 보면 그냥 Shot 타입의 멤버 변수 하나만 추가됐을 뿐인데
이게 어떻게 데코레이터 패턴이 되는 것이냐는 것이다.
그 해답은 이 데코레이터 추상 클래스를 상속받는 클래스에 있다.



예시) 데코레이터 클래스를 상속하여 구현한 클래스


먼저 코드를 보고 설명을 하겠다.

public class Water extends ShotDecorator {
	public Water(Shot shot) {
    	this.shot = shot;
        this.name = shot.name + "에 물을 추가한 아메리카노";
    }
    
    public double cost() {
    	return shot.cost() + 1000; // 기존 샷에 1000원 추가
    }
}
public class Milk extends ShotDecorator {
	public Milk(Shot shot) {
    	this.shot = shot;
        this.name = shot.name + "에 우유를 추가한 라떼";
    }
    
    public double cost() {
    	return shot.cost() + 2000; // 기존 샷에 2000원 추가(우유는 비싸니까!)
    }
}



예시) 데코레이터 패턴을 통한 기능 확장 및 행동 위임


Water는 기존의 샷에 물을 추가해서 아메리카노로 만들고 가격을 1000원 더받고 팔게끔 만들 수 있다.
기존의 샷이 에스프레소라면 4000원(3000원+1000원), 디카페인이라면 4500원(3500원+1000원)

Milk는 기존의 샷에 우유를 추가해서 라떼로 만들고 가격을 2000원 더받고 팔게끔 만들 수 있다.
기존의 샷이 에스프레소라면 5000원(3000원+2000원), 디카페인이라면 5500원(3500원+2000원)

어떻게 사용하는지 한번 보겠다.

public static void main(String[] args) {
	Shot espresso = new Espresso(); // 에스프레소 원액 뽑기
    Shot americano = new Water(espresso); // 물을 넣어 아메리카노 만들기
    System.out.println(espresso.name + ": " + espress.cost() + "원"); // 에스프레소: 3000원
    System.out.println(americano.name + ": " + americano.cost() + "원"); // 에스프레소에 물을 추가한 아메리카노: 4000원
    
    Shot decafeine = new Decafeine();
    Shot decafLatte = new Milk(decafeine);
    System.out.println(decafeine.name + ": " + decafeine.cost() + "원"); // 디카페인: 3500원
    System.out.println(decafLatte.name + ": " + decafLatte.cost() + "원"); // 디카페인에 우유를 추가한 라떼: 5500원
}

에스프레소, 디카페인, 데코레이터클래스, 물, 우유 클래스 모두 전부 Shot이라는 추상클래스의 자식 클래스이기 때문에
아무런 문제 없이 잘 동작하는 것을 확인할 수 있다.

위의 과정을 좀 줄이고자 한다면, 아래처럼 바로 생성자 함수의 argument로 객체 생성문을 집어넣으면 된다.

public static void main(String[] args) {
    Shot americano = new Water(new Espresso()); // 물을 넣어 아메리카노 만들기
    System.out.println(americano.name + ": " + americano.cost() + "원"); // 에스프레소에 물을 추가한 아메리카노: 4000원
    
    Shot decafLatte = new Milk(new Decafeine());
    System.out.println(decafLatte.name + ": " + decafLatte.cost() + "원"); // 디카페인에 우유를 추가한 라떼: 5500원
}



❓java.io의 데코레이터 패턴?


어디서 많이 본 형태이다. 자바로 입출력을 다뤄보신 분이라면 한번쯤은 보셨을 것이다.

그렇다, 자바의 java.io(입출력 관리 패키지)에도 이러한 데코레이터 패턴이 적용되었다. 이 얘기는 뒤에서 다시 하도록 하겠다.

이렇듯, 어떠한 클래스 타입(Shot)을 멤버변수로 가지면서 해당 클래스 상속하는 데코레이터 클래스(ShotDecorator)를 만들고
해당 데코레이터 클래스를 상속하는 또 여러가지의 구현 클래스들을(Water, Milk) 만들게 되면,
데코레이터 클래스에 있는 어떠한 객체 타입(Shot)의 멤버 변수를 내부 변수로 가져와서 기능을 확장하거나(이름을 바꾸거나)
어떠한 행위를 위임할 수 있게 된다.(기존 가격을 불러와서 돈을 더 받음)

이게 바로 데코레이터 패턴이다.
하지만 무의미한 데코레이터 패턴은 오히려 복잡도만 증가시킬 수 있기 때문에
위의 예시처럼 어떤 요소가 계속해서 수정되야 하거나(가격처럼)
이것 저것 많이 조합해야 할 때(샷에 물을 타던지 샷에 우유를 타던지) 사용해야 좋다.

🔎 데코레이터 패턴의 장단점


장점

  1. 기존 코드의 수정 없이 기능을 확장할 수 있다.(OCP)
  2. 구성과 위임을 통해서 실행 중에 새로운 행동을 추가할 수 있다.

단점

  1. 무분별하게 객체가 많이 생성될 수 있다.
  2. 코드의 복잡도가 증가한다.



🔎 상속(extends)과 구현(implements)


데코레이터 패턴의 예시에서는 추상 클래스를 사용해서 표현하였다.
하지만, 꼭 추상클래스 뿐만 아니라 인터페이스를 사용하는 형태도 얼마든지 있을 수 있다.
하지만, 이때 상속과 구현의 차이점을 잘 알아야 문제 없이 해결할 수 있다.

먼저 사용하는 방법이다

클래스 extends 클래스
인터페이스 extends 인터페이스, 인터페이스, ...
클래스 implements 인터페이스, 인터페이스, ...

상속(extends)

  1. is-a의 관계(A는 B이다)
  2. 상위 클래스의 멤버필드, 메서드를 사용할 수 있다.
  3. 다중 상속이 불가능하다.(인터페이스 extends 인터페이스 예외)

구현(implements)

  1. can-do의 관계(A는 B할 수 있다)
  2. 다중 구현이 가능하다.
  3. 변경이 매우 어렵다.(인터페이스 내용이 바뀐다면 해당 인터페이스를 구현한 모든 클래스가 변경되야 한다.)
  4. 인터페이스에 속한 메서드를 반드시 재정의 해야 한다.(강제)



❓ java.io의 데코레이터 패턴 적용


위에서 언급했듯이 자바의 입출력을 담당하는 클래스들도 데코레이터 패턴이 적용되었다.

위 그림과 같이 InputStream이라는 추상 데코레이터 클래스를 이용하여
입출력 시스템의 기능을 확장해 가는 구조로 자바의 입출력 시스템이 구성되어 있다.
그래서 우리는 항상 자바의 입출력을 사용할 때 코드가 매우 길어지는 것을 확인할 수 있었다...

profile
백엔드를 사랑하는 초보 개발자

0개의 댓글