디자인 패턴 | 데코레이터 패턴

Jihun Kim·2022년 8월 10일
0

디자인 패턴

목록 보기
3/4
post-thumbnail

이 글은 헤드퍼스트 디자인 패턴을 읽고 정리한 것입니다.


상속만으로 해결할 수 없는 것
Subclassing을 통해 클래스를 상속하면 complie time에 해당 클래스의 행동이 static 하게 고정 된다. 즉, 자식 클래스의 행동이 고정 되어 부모 클래스와 같은 행동을 하게 된다.
그런데 만약 composition(구성)을 통해 runtime에 객체의 행동을 다양화 할 수 있다면 변경에는 닫혀 있지만 확장에는 열려 있는 구조를 만들 수 있을 것이다.
또한, 이를 통해 자식 클래스에 여러 책임을 부여할 수 있기 때문에 기존 코드를 수정함으로써 생기는 오류 발생을 줄일 수 있다.


데코레이터 패턴

상황 가정해 보기
카페에서 커피를 팔고 있다. 이 때 커피의 종류는 다음과 같다.

  • HouseBlend, DarkRoast, Dcaf, ...
    - 모두 Beverage를 상속 받는다.

이 때 고객은 여러 가지 종류의 토핑을 얹을 수 있다.

  • 코코아, 휘핑, 시나몬, ...
    - 각 토핑에 따라 가격은 달라진다.

또한 커피 사이즈는 tall/grande/venti로 3가지이며 사이즈에 따라 가격이 달라진다.
이를 만약 전략 패턴을 이용 한다면 다음과 같은 방법을 생각해 볼 수 있다.

  • CoCoaHouseBlend: Beverage -> HouseBlend -> 코코아 토핑
  • WhipHouseBlend: Beverage -> HouseBlend -> 휘핑
    ...

토핑의 종류가 증가할 때마다 variation이 수도 없이 생길 수 있을 것이다.
이렇게 되면 확장성이 매우 떨어진다.
이를 해결할 수 있는 방법 중 하나가 앞서 말했던 데코레이터 패턴이다.


예시

아래는 어떤 문제 풀이 방식을 사용하는 지에 따라서 행동이 달라지는 케이스에 대한 예시이다. 파이썬으로는 데코레이터를 사용할 수 있어 조금 더 편하게 작성한 것 같다.

  • LevelExam과 ProblemBasedLearn은 퀘스트 업데이트 + 포인트 지급
  • RetryProblem은 퀘스트 업데이트만
  • SimilarProblem은 프리미엄 여부 확인 + 퀘스트 업데이트 + 포인트 지급

Python

from abc import *

from functools import wraps

"""
데코레이터
"""


def quest_update(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{func.__name__}에서 퀘스트 업데이트 완료")
        return func(*args, **kwargs)

    return wrapper


def give_point(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{func.__name__}에서 포인트 지급 완료")
        return func(*args, **kwargs)

    return wrapper


def premium_validated(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{func.__name__}는 프리미엄만 가능합니다^^")
        return func(*args, **kwargs)

    return wrapper


"""
문제 
"""


class Problem(metaclass=ABCMeta):
    @abstractmethod
    def solved_info(self):
        raise NotImplementedError("반드시 만들어라.....")


class LevelExam(Problem):
    @give_point
    @quest_update
    def solved_info(self):
        print("진단고사 풀이 완료")


class ProblemBasedLearn(Problem):
    @quest_update
    @give_point
    def solved_info(self):
        print("문제 기반 풀이 완료")


class RetryProblem(Problem):
    @quest_update
    def solved_info(self):
        print("복습 완료")


class SimilarProblem(Problem):
    @premium_validated
    @quest_update
    @give_point
    def solved_info(self):
        print("유사 문제 풀이 완료")

java

public abstract class Problem {
	String solveProblem = "Solve your problem";
    
    public String solveProblem() {
    	return solveProblem;
    }
}

public class LevelExam extends Problem {
	public LevelExam() {
    	solveProblem = "진단고사 완료";
    }
}

public class ProblemBasedLearn extends Problem {
	public ProblemBasedLearn() {
    	solveProblem = "문제 기반 풀이 완료";
    }
}


public class RetryProblem extends Problem {
	public RetryProblem() {
    	solveProblem = "복습 완료";
    }
}

public class SimilarProblem extends Problem {
	public SimilarProblem() {
    	solveProblem = "유사 문제 풀이 완료";
    }
}

// Decorators
// Decorator class should have the same type of object as its superclass
public abstract class ProblemDecorator extends Problem {
	public abstract String solveProblem();
}

public class QuestUpdateDecorator extends ProblemDecorator {
	Problem problem;
    
    public QuestUpdateDecorator(Problem problem) {
    	this.problem = problem;
    }
    public String solveProblem() {
    	return "퀘스트 업데이트 완료" + problem.solveProblem();
    }
}

public class GivePointDecorator extends ProblemDecorator {
	Problem problem;
    
    public GivePointDecorator(Problem problem) {
    	this.problem = problem;
    }
    public String solveProblem() {
    	return "포인트 지급 완료" + problem.solveProblem();
    }
}

public class PremiumValidationDecorator extends ProblemDecorator {
	Problem problem;
    
    public PremiumValidationDecorator(Problem problem) {
    	this.problem = problem;
    }
    public String solveProblem() {
    	return "프리미엄만 사용 가능합니다" + problem.solveProblem();
    }
}

// 사용하기
public class SolveYourProblem {
	public static void main(String args[]) {
    	Problem problem = new SimilarProblem();
        problem = new GivePointDecorator(problem);
        problem = new QuestUpdateDecorator(problem);
        problem = new PremiumValidatonDecorator(problem);
      	
        System.out.println(problem.solveProblem());
    }
}

특징

  • 데코레이터는 데코레이터가 감싸는 객체에 특정 행동을 부여하기 위해 사용한다.
  • 데코레이터를 이용하면 유연성이 늘어나며 런타임에 여러 행동을 추가할 수 있다.
    - 상속만을 이용할 경우 superclass가 갖는 행동 또는 이를 오버라이딩 한 행동을 가져야 하기 때문에 다양성을 확보하기 어렵다.
  • 데코레이터를 이용할 경우 데코레이터는 데코레이팅 하는 객체와 같은 타입을 갖도록 해야 한다.

단점

  • 여러 개의 작은 데코레이터 클래스가 대상 클래스에 추가 되기 때문에 의도가 명확히 드러나지 않아 직관적이지 않다.
  • 데코레이터를 사용하면 구성 요소를 인스턴스화 하는 데 필요한 코드가 복잡해진다.

단점으로 제기된 것들은 모두 '복잡성'에 관한 것들이며 이를 감안하거나 또는 감당 가능할 정도의 수 만큼 행동을 다양화 하고 싶은 경우 데코레이터를 사용하는 것은 좋은 방법인 것 같다.

profile
쿄쿄

0개의 댓글