C++ 디자인 패턴 : 전략 패턴 (strategy pattern)

함수 포인터로 구현한 전략 패턴




가상 함수를 대신할 방법을 찾아보자는 이슈에서

시작하여 전략패턴을 구현하는 방법까지 알아보게 되었다.

이미 NVI 관용구라는 훌륭한 방법이 있지만 좀 더 알아보겠다.




지금 우리는 게임의 캐릭터 클래스를 디자인 하고 있고

체력치를 계산하는 작업을 가상 함수를 통해 하고 있다.

체력을 계산하는 알고리즘은 캐릭터마다 다를 것이기 때문에

지극히 정상적인 설계이다. 하지만 여기서 가상 함수 대신

함수 포인터를 사용하여 좀 더 유연한 설계를 해보도록 하자.




함수 포인터를 이용한 전략 패턴은 체력치 계산을 어떤

캐릭터의 일부로 두지 않는다는 것부터 시작한다.

캐릭터의 생성자에 체력치 계산용 함수 포인터를

넘기게 만들고, 이 함수를 호출해서 실제 계산을 수행한다.




함수포인터를 이용한 전략(Strategy) 패턴의

단순한 예를 보여주는 클래스이다.






같은 캐릭터 타입으로부터 만들어진 객체들도

체력치 계산 함수를 다르게 가질 수 있는 융통성이 생겼다.

또한 게임이 실행되는 도중에 특정 캐릭터에 대한

체력치 계산함수를 바꿀수도 있다.




하지만 이 전략 패턴은 체력치가 계산되는

대상 객체의 비공개 데이터는 이 함수로 접근할 수 없다.

public 인터페이스로 얻은 정보만을 사용할 수 있기 때문이다.

( 멤버 함수가 아니기 때문에)




따라서 프렌드 함수로 선언을 하여 비공개 데이터에

접근을 할 수 있게 하여 캡슐화를 떨어뜨릴 것이냐.

즉 , 캡슐화냐 캡슐화를 떨어뜨려 얻을 수 있는 전략 패턴의

이점이 중요한가는 실제로 맡은 설계를 보면서 판단해야 한다.




C++ 디자인 패턴 중 Builder(빌더) 패턴에 대해 알아보자.


빌더 패턴은 추상 팩토리 패턴과 상당히 유사한데,

추상 팩토리 패턴은 부품을 만들고 바로 리턴하는 반면

빌더 패턴은 부품을 만들고 완성된 제품을 리턴한다.

즉, 추상 팩토리 패턴에서 만드는 것은 꼭 모여야만

의미가 있는 것이 아니라 하나만으로 의미가 있는

제품을 생성할 때 쓰는 것이 맞다 할 수 있겠다.




솔직히 말하면 어떠한 패턴인지 그려지기는 하지만

완벽하게 이해하는 수준이 아니라 설명이 매우 허접하다.




빌더 패턴의 이해를 돕기 위해

전에 알아봤던 추상 팩토리 패턴과

비교를 해가면서 알아보도록 하겠다.




추상 팩토리 패턴에서 팩토리 클래스는

추상 클래스로 선언이 되었다. 하지만 빌더 패턴의

Builder에 있는 메서드는 가상함수로 선언하되

구현을 하지 않는 것이 일반적이다.

왜냐하면 서브클래스에서 필요한 메서드만

가져다가 재정의하기 위함이다.




01. 맵을 생성하기 위한 예제

추상 팩토리 패턴에 쓴 예제이다.




02. 추상 팩토리 패턴과는 다르게 빌더 클래스는

메소드 내에서 맵을 어떻게 구성할지를 결정한다.

추상 팩토리에서는 그저 생성하고 리턴만 할 뿐이다.

(부품을 그냥 리턴 : 추상 팩토리)

(부품을 만들고 조립까지 해서 리턴 : 빌더)




따라서 빌터 패턴은 바로 반환하는 것이 아니라

자신의 메모리에 조립이 완성될 때까지 가지고 있다가

완성되면 리턴한다. ( 아래에 계속)



03. 빌더의 서브클래스에서 구체적인 구현이 이루어 진다.


빌더 패턴의 가장 큰 특징은 맵이 어떻게 구성되는지가

빌더 클래스 내에서 캡슐화가 되었다는 것이다.

추상 팩토리 패턴에서는 CreateMap() 내에서

맵을 구성했지만, 그 부분이 이제는 빌더를

상속받는 서브클래스에서 구현하고 있다는 점!





추상 팩토리 패턴과 매우 흡사하지만

흡사해서 더 헷갈리는 느낌이 든다..

뭐가 다른지, 이렇게 해서 서로의 장단점이 무엇인지

언제 이 둘을 구분해서 써야하는지 등등

차라리 완전 다른 패턴이었다면 더 이해하기 쉬웠을 듯


지금 까지 허접한 Builder (빌더) 패턴 포스팅이었다.



C++ 디자인 패턴 중에 하나인 Abstract Factory

추상 팩토리 패턴에 대해 알아보도록 하겠다.


먼저 추상 팩토리 패턴이란

생성 방법을 알고 있는 객체를 매개변수로

넘겨받음으로써 생성할 객체의 유형을 달리하는 것이다.

(무슨 말인지 모르겠다. 포스팅의 마지막엔 이해가 될 것이다.)




팩토리 패턴의 특징에 대해서는 길게 설명하지 않는다.

추상 팩토리 패턴이 도대체 무엇인지, 어떻게 구현되는지만

코드로서 이해해보는 시간을 가지겠다.

(제대로된 코드는 아니지만 충분히 이해가 될 것이다)


먼저 맵을 만드는 클래스가 있다.

이 클래스는 필드와 장애물을 추가할 수 있다.







또한 이들을 묶어서 맵을 구성하는 MapMgr 클래스가 있다.




위의 코드에는 문제점이 있는데 살펴보면

그렇다. 위의 코드는 유연성과 거리가 먼 하드코딩 되어 있다는 점.


CreateMap()에서 객체를 이름으로 생성했기 때문에

저 함수는 이제 함수 내에서 생성된 객체에 의해

구성된 맵만 반환할 것이다. (재사용성 제로)







자 이제는 CreateMap() 함수 내에서 이름으로

생성하지 않고 팩토리 객체로 생성하고 있다.

따라서 어떤 객체가 들어오느냐에 따라 생성방식이 달라진다는 점.


자 그렇다면 맵의 구성요소들과 팩토리 객체를

추상 클래스로 만들고 파생시켜보자.

(수정) CreateMap()은 맵을 리턴한다.


이제는 추상 팩토리 클래스의 파생클래스 객체를 매개변수로 전달하면

그에 맞는 맵이 생성되게 된다. 위의 관계를 그림으로 나타내면





마지막으로 추상 팩토리 패턴의 단점 하나를 짚고 가겠다.

만약 맵을 만들기 위한 용도가 아닌 곳에서 사용하려 한다면

( 만약 맵이 아니라 몬스터를 찍어내려 한다면)

팩토리의 구현을 변경해야 한다. 이는 추상 팩토리와 모든

서브클래스의 변경을 가져온다. 즉 팩토리는 자신이

생성 할 수 있는 집합에만 고정되어 있다.




지금까지 C++ 디자인 패턴 인 Abstract Factory

추상 팩토리 패턴에 대해 이해하는 시간을 가져보았다.

다음에는 팩토리 메서드 패턴으로 돌아오겠다.





C++ 디자인 패턴 : Strategy 패턴 (전략 패턴)


게임 캐릭터의 체력 구현 알고리즘을 만들고 있다고 가정하자.

기본 플레이어는 기본 체력 구현 알고리즘을 가지고 있을 것이다.

이 플레이어가 전직을 하여 전사가 되면 방어력이 더욱 강해질 것이고,

마법사로 전직을 하게 되면 방어력이 더욱 약해질 것이다.







위의 방법은 너무나 일반적인 설계이다. 하지만 여기서 캐릭터의 

알고리즘의 변경과 알고리즘 자체를 개조할 수 있는 방법이 없을까.


예를 들어 캐릭터가 너프 마법이나 버프 마법을 받게된다면

체력 구현 알고리즘이 달라져야 할 것이다. 전략 패턴을 써보자.







알고리즘을 클래스로 만들어 가상함수를 선언했다.

따라서 이를 상속하는 모든 클래스들은 이 함수를

재정의 할 수 있다. (알고리즘의 조정/개조가 가능해짐)







또한 이 알고리즘의 포인터를 가진 Player클래스는

언제든 체력 알고리즘을 바꿀 수 있는 융통성이 생기게 된다.




이 패턴의 핵심은 한쪽 클래스 계통에 속해 있는

가상 함수를 다른 쪽 계통에 속해 있는 가상 함수로 대체한다는 것이다.


NVI idiom

(non-virtual-interface idiom)


템플릿 메서드 패턴을 C++ 식으로 구현한 것이다.

간단한 패턴이니 예제를 통해 바로 보도록 하겠다.



가상 함수는 반드시 private 멤버로 두어야 한다는

가상함수 은폐론이 기초가 된 기법이다.


가상합수를 private으로 두면 이를 상속하는 클래스는

재정의는 할 수 있지만 실제 호출은 할 수없다.

호출을 하려면 hpValue() 라는 함수를 거쳐야 하는데

이것이 바로 NVI 관용구의 핵심이다.

여기서 hpValue()는 가상함수의 랩퍼라고 부른다.




hpValue()를 호출하게 되면 가상함수를 호출하기 전에

사전 동작이 가능하며 가상함수 호출 후 사후 동작도 가능하다.

또한 재정의가 가능하기 때문에 구현의 자유가 있지만

이 함수를 호출하는 시점은 기본 클래스의 고유권한으로 묶인다.




꼭 가상함수가 private여야 한다는 것은 아니지만

protected나 public멤버라면 NVI를 적용하는 의미가 없다.

pimpl idiom


컴파일 의존성을 줄이기 위한 하나의 설계 기법이다.

많이 쓰여지는 기법 중 하니이기 때문에 거의 패턴으로 굳어져 있다.


include가 필요한 사용자 정의 타입의 멤버 변수들을

해당 멤버 변수들을 포함한 클래스(구조체)의 포인터로 대체하는 방법이다.






이런 형식의 문제점은 헤더파일 내에서 다른 헤더파일을

include하고 있다는 것이다. include를 하게 되면 전처리기는

해당 헤더의 내용을 그대로 복사해서 집어넣는다. 그렇다면

AAA.h를 include하는 모든 cpp 파일에 AAA.h가 include하는

모든 헤더가 포함되게 되므로 코드량이 매우 커진다.

따라서 컴파일 시간이 늘어난다.




또한 myString같은 유저 정의 클래스의 경우

내용이 바뀔 가능성이 매우 높다. myString의

내용이 바뀐다면 AAA.h는 물론 AAA.h를 포함하는

모든 파일도 다시 재컴파일 될 수 밖에 없다.


pimpl idiom을 통해 해결해보자.






include가 필요한 멤버 변수들을 모아 Aimpl 구조체에 저장하자.

그리고 그 구조체에 대한 포인터를 멤버 변수로 들고 있는다.

Aimpl 구조체 정의는 cpp 파일로 미룬다.

여기서 중요한 점은 cpp파일의 include는

오직 해당 cpp 파일에만 영향을 미친다는 것이다.

따라서 위의 코드는 헤더파일의 include를

구현파일로 모두 보내버렸다는 것이 포인트다.



한 걸음 더 나아가 tr1의 shared_ptr을 사용해서

구현을 한다면 더욱 좋은 설계가 될 것이다.

물론 AAA의 소멸자에서 delete연산을 해주지 않아도 된다.




구조체가 아닌 클래스로 pimpl을 구현해보자.

필요한 클래스를 전방 선언 후 Person 클래스 선언 내에서

참조자 혹은 포인터의 정의 그리고 값의 전달 및 반환


그야말로 인터페이스와 구현이 뼈와 살이 분리되듯 떨어진다.

이러한 방법은 pimpl idiom 외에도 인터페이스 클래스를

설계하는 방법도 생각해볼 수 있으나, 패턴으로 볼 수 없기 때문에

후에 C++ 카테고리에서 포스팅하도록 하겠다.



+ Recent posts