이해를 돕기 위해 롤토체스를 예시로 들어보자.
롤토체스에는 150개 이상의 챔피언이 존재한다.
챔피언들은 기본적으로 원딜, 서폿, 미드, 탑, 정글의 포지션중에 하나를 가지고 있다.
챔피언의 포지션만을 고려하여 챔피언의 판매 가격을 결정한다고 가정해보자.
챔피언 판매로 데코레이터 패턴 이해해보기
모든 챔피언들은 하나의 포지션을 갖는다.
이를 클래스로 단순하게 나타내 보자.
abstract class Champion
{
abstract public int GetCost();
}
모든 챔피언들의 부모 클래스인 Champion 클래스는 abstract 클래스로 GetCost 추상 메소드를 가진다.
Champion 클래스를 상속받는 챔피언들은 자신이 가지고 있는 포지션을 고려하여
GetCost 추상 메소드를 implement 해야한다.
게임 기획자에 의해, 포지션별 판매 가격은 다음과 같이 이미 정해져 있었다.
원딜 : +2원
서폿 : +1원
미드 : +3원
탑 : +4원
정글 : +5원
따라서 데이터 테이블대로 GetCost 메소드를 구현한다.
class Adc : Champion
{
public int GetCost()
return 2;
}
class Sup : Champion
{
public int GetCost()
return 1;
}
class Mid : Champion
{
public int GetCost()
return 3;
}
class Top : Champion
{
public int GetCost()
return 4;
}
class Jug : Champion
{
public int GetCost()
return 5;
}
여기까지만 놓고 보았을 때, 어떤 챔피언이든 Champion 클래스를 상속 받으므로
GetCost()만 호출하게 되면 포지션이 어디든 이미 계산된 가격을 받을 수 있다.
기획이 수정되었다
포지션만 고려하는것은 너무 단순해
라는 지적이 나와 기획자가 새로운 기획을 던져주었다.
이제는 포지션 말고도 챔피언마다 가지고 있는 특성까지 고려해야된다.
기획에 따르면 챔피언 특성은 다음과 같다고 한다.
1. 총을 무기로 사용함
2. 두 발로 걸어 다님
3. 날개가 달림
모든 챔피언은 위에 써놓은 각 특성을 가질 수도, 안 가질 수도 있다.
구조를 바꾸기 귀찮은 나는, 포지션과 특성을 조합해서 가지고 있는 클래스들을 만들어 냈다.
class Adc : Champion
{
public int GetCost()
return 2;
}
class AdcWithGun : Champion
{
public int GetCost()
return 2 + 1; // 총 가격은 1원임
}
class AdcWithTwoFeet : Champion
{
public int GetCost()
return 2 + 2; // 이족보행 가격은 2원임
}
class AdcWithGunAndTwoFeet : Champion
{
public int GetCost()
return 2 + 1 + 2; // 총 가격은 1원임, 이족보행 가격은 2원임
}
class AdcWithGunAndTwoFeetAndWing : Champion
{
public int GetCost()
return 2 + 1 + 2 + 3; // 총 가격은 1원임, 이족보행 가격은 2원임, 날개 가격은 3원임
}
//... 너무 많아 생략
무지성으로 클래스 복사 붙여넣기가 끝난후에 코드를 다시 보니
클래스가 말도안되게 많아졌다.
돌아가면 그만이다라는 생각에 빠질 때 조금 더 나은 클래스 설계를 생각해냈다.
아예 부모 클래스에 특성을 추가하자
모든 챔피언 클래스는 특성을 가질수 있으며 해당 특성을 고려하여 GetCost 메소드를 implement 하니,
아예 부모 클래스에 특성을 두어 상속받게 하는 구조이다.
따라서 abstract Champion 클래스는 다음과 같이 바뀐다.
class Champion
{
bool hasGun;
bool hasTwoFeet;
bool hasWing;
public bool HasGun()
=> hasGun;
public bool HasTwoFeet()
=> hasTwoFeet;
public bool HasWing()
=> hasWing;
public int virtual GetCost()
{
int totalCost = 0;
if(HasGun()) // 총을 가지고 있으면 1원
totalCost += 1;
if(HasTwoFeet()) // 이족보행이면 2원
totalCost += 2;
if(HasWing()) // 날개를 가지고 있으면 3원
totalCost += 3;
return totalCost;
}
}
특성을 저장할 수 있는 변수를 새로 선언하고, GetCost 함수에서 챔피언이 가지고 있는 특성을 고려하여
cost를 계산할 수 있게 해놨다. 따라서 GetCost 메소드는 더 이상 abstract가 아니며 virtual 키워드로 새롭게 수식하였다.
포지션별 자식 클래스는 GetCost 메소드를 override하여 포지션까지 고려된 판매 가격을 결정할 것이다.
우리에게 남은건?
골치아픈 특성 계산을 위와 같이 해결하고 포지션만 고려해주면 된다.
따라서 포지션별 챔피언들의 클래스는 다음과 같아진다.
class Adc : Champion // 특성들을 Champion 클래스로 부터 이미 상속 받았다.
{
public override int GetCost()
return 2 + base.GetCost();
}
class Sup : Champion
{
public override int GetCost()
return 1 + base.GetCost();
}
class Mid : Champion
{
public override int GetCost()
return 3 + base.GetCost();
}
class Top : Champion
{
public override int GetCost()
return 4 + base.GetCost();
}
class Jug : Champion
{
public override int GetCost()
return 5 + base.GetCost();
}
문제점은 무엇일까
처음 방법보다는 그나마 괜찮아 보인다.
몇 가지 마음에 걸리는게 있지만 말이다.
뭐가 문제일까?
1. 해당 특성을 가지지 않는 챔피언도 특성에 대한 변수(인스턴스)를 가지고 있어야 한다.
탑은 남자의 라인이므로 총을 들고 오는 챔피언은 없다고 하자.
따라서 탑 챔피언에게 총이 있냐고 물어볼 일이 전혀 없다.
하지만 최상위 부모 클래스인 Champion에는 hasGun 변수와 HasGun() 메소드를 가지고 있어
자연스레 Top Class도 이들을 가지게 된다. 이는 불필요한 멤버들을 가지고 있는 꼴이다.
2. 특성이 3개보다 더 많아지면?
아무래도 3개는 너무 적어서 100개로 특성을 늘렸다.
자연스레 Champion 클래스는 100개의 특성을 나타내는 변수와 메소드를 가지게 된다.
너무 거대해진다.
3. 특성들의 가격이 달라진다면
특성을 고려하여 가격을 책정하는 로직은 Champion 클래스 안에 들어있으므로
기존의 코드를 수정해야한다.
4. 동적으로 특성을 부여할 수 없다.
지금과 같은 방식으로는 동적으로 특성을 부여할 수 없으므로
확장에 불리하다.
데코레이터 패턴으로 다시 구현해보기
데코레이터 패턴은 이름에서 알 수 있듯이
객체를 데코하는 패턴이다.
데코레이터 객체들은 자신이 데코를 하는 객체를 감싸는 형태로 존재하며
이를 도식화 하면 다음과 같다.
특성3은 특성2를, 특성2는 특성1을, 특성1은 챔피언을 레퍼런스로 가지고 있는 형태이다.
이게 가능한 이유는 밑에서 설명하겠다.
우리가 꾸미려는 객체는 포지션별 기본 뼈대가 되는 클래스들이고
데코레이터 패턴으로 꾸미게 된다면 다음과 같아진다.
Champion 클래스 재 작성
abstract class Champion
{
abstract public int GetCost();
}
class Adc : Champion
{
public int GetCost()
return 2;
}
class Sup : Champion
{
public int GetCost()
return 1;
}
class Mid : Champion
{
public int GetCost()
return 3;
}
class Top : Champion
{
public int GetCost()
return 4;
}
class Jug : Champion
{
public int GetCost()
return 5;
}
다시 Champion 클래스는 abstract가 되었고 이를 상속받는 클래스들은 GetCost 메소드를 implement한다.
특성의 데코레이터 객체화
abstract class Decorator : Champion // Decorator 객체들의 wrapper abstract class
{
abstract int GetCost();
}
class Gun : Decorator
{
Champion championObj; // 자신이 꾸미고 있는 객체에 대한 레퍼런스를 가지고 있어야 한다
public Gun(Champion champion) // 자신이 꾸밀 객체를 받는다
championObj = champion;
public int GetCost()
{
return 1 + championObj.GetCost();
}
}
class TwoFeet : Decorator
{
Champion championObj; // 자신이 꾸미고 있는 객체에 대한 레퍼런스를 가지고 있어야 한다
public TwoFeet(Champion champion) // 자신이 꾸밀 객체를 받는다
championObj = champion;
public int GetCost()
{
return 2 + championObj.GetCost();
}
}
class Wing : Decorator
{
Champion championObj; // 자신이 꾸미고 있는 객체에 대한 레퍼런스를 가지고 있어야 한다
public Wing(Champion champion) // 자신이 꾸밀 객체를 받는다
championObj = champion;
public int GetCost()
{
return 3 + championObj.GetCost();
}
}
도식화한 그림에서 볼 수 있듯이 일종의 chaining을 걸어 GetCost 함수를 유기적으로 연결할 수 있다.
실제 객체 생성은 어떤식으로 할까?
Champion champion = new Adc();
print(champion.GetCost()); // 아무 특성도 없는 원딜의 가격은 2원
Champion champion2 = new Adc();
champion2 = new Gun(champion2); // 총으로 원딜을 꾸며준다
print(champion2.GetCost()); // 총을 들고 있는 원딜의 가격은 2 + 1원
Champion champion2 = new Adc();
champion2 = new Gun(champion2); // 총으로 원딜을 꾸며준다
champion2 = new TwoFeet(champion2); // 이족보행으로 원딜을 꾸며준다
champion2 = new Wing(champion2); // 날개로 원딜을 꾸며준다
print(champion2.GetCost()); // 총을 들고 있고 이족보행이며 날개가 있는 원딜의 가격은 2 + 1 + 2 + 3원
문제점 되돌아 보기
1. 해당 특성을 가지지 않는 챔피언도 특성에 대한 변수(인스턴스)를 가지고 있어야 한다.
이제 특성에 대한 정보가 없어도 특성까지 고려한 GetCost 함수를 완벽히 implement 할 수 있다.
2. 특성이 3개보다 더 많아지면?
chaining을 좀 더 걸어주면 될 뿐 챔피언 클래스에게 부담이 되는건 하나도 없다.
3. 특성들의 가격이 달라진다면
더 이상 특성들의 가격을 Champion 클래스에서 결정하는것이 아닌
각 특성 데코레이터 객체 안에서 계산이 된다.
다시 말해 의존성이 줄어들었다.
4. 동적으로 특성을 부여할 수 없다.
new로 chaining을 걸어주면 끝이다!
데코레이터 패턴 돌아보기
Decorator를 상속받는 클래스들은 결국에는 Champion 클래스를 상속받는것과 같다.
상속을 이용해 예시를 구현하긴 했지만, 데코레이터 패턴에서 중요한 요소는 상속이 아닌 추상요소 구현이다.
1. 약속된 요소를 가지고 있다는 전제 하에 원활히 동작 가능한 기능을 구현할 때 데코레이터 패턴은 힘을 발휘하며
약속된 요소(Interface)가 아닌 특정 구상 구성요소에 의존하는 기능은 데코레이터 패턴에 적합하지 않다.
ex) Gun과 Wing의 시너지로 골드 계산
2. 또한 데코레이터 패턴은 혼자 쓰이기 보다는 팩토리나 빌더 패턴같은 다른 패턴과 함께 쓰일 때 더 강력해진다.
데코레이터 패턴 한 문장 정리
데코레이터 패턴은 객체에 추가적인 요소를 동적으로 첨가한다.
데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있다.
디자인 원칙
클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.(Open-Closed Principle, OCP)
'Programming > 디자인 패턴' 카테고리의 다른 글
[디자인 패턴] 5. 싱글턴 패턴 (0) | 2022.03.22 |
---|---|
[디자인 패턴] 4. 팩토리 패턴 (0) | 2022.03.17 |
[디자인 패턴] 2. 옵저버 패턴 (0) | 2022.03.10 |
[디자인 패턴] 1. 스트래티지 패턴 (0) | 2022.03.10 |
[디자인 패턴] 디자인 패턴이란? (0) | 2022.03.10 |