주말에 랄프 존슨 특강에 참석했다. 프로젝트 초기라 바쁜 와중에 맞이하는 주말인데 굳이 유명인사 특강에 참석해야 할까 싶은 생각을 안고 자리에 나갔다. 아마도 특강 개설 과정에 참여하지 않았으면 그 자리에 있진 않았을 것이다.
랄프 존슨 특강을 듣기 시작한 지 얼마 지나지 않아 생각이 바뀌었다. 기름기가 잔뜩 꼈던 정신상태에 경종을 울렸다. 먼저 두 가지 사항을 반성했다.
영어에 자유로워지겠다고 결심했지만, 실제 행동은 그렇지 못했다.
디자인 패턴에 대해 충분히 안다고 생각했는데, 사실 아무것도 모르는 사람과 별반 다르지 않음을 느꼈다.
내 블로그가 반성을 위한 일기 역할을 하지만, 방문객을 고려해서 반성은 그만두겠다. 짧은 영어 실력 탓에 대가의 특강을 충분히 소화하지 못했다. 그럼에도 화학반응을 통해 영감을 얻어 쏟아진 생각을 메모할 겸 공유하고자 한다.
1강: Fifteen Years of Design Patterns
베스트셀러인 GoF 디자인 패턴. 실제 발표는 94년 OOPSLA인데 출간은 95년이다. 이유가 무엇일까? 출판업자는 우리나라와 다르지 않았다. 연초에 출간해야 매출에 유리하기 때문에 해를 넘겨 책을 볼 수 있었다. 후속으로 쏟아진 관련 서적이 매우 많다. 랄프는 그중에서 4권을 꼽았다. 그 중 두 권에 대한 논평이 인상깊다. 하나는 그 유명한 헤드 퍼스트 시리즈다. 헤드 퍼스트는 자신의 책보다 더 많이 팔렸다며 미소를 지었다. 그리고, 헤드 퍼스트의 독특한 전개 방식에 대해 높이 평가했다. 두 번째는 에릭 에반스의 DDD에 대한 평이다. 랄프는 크리스토퍼 알렉산더를 글 잘 쓰는 사람으로 평가했다. 그리고 컴퓨터 과학과 건축(building)은 다르다고 이야기했다. DDD는 알렉산더류에 가까우면서 매우 독창적인(very unique) 책이라는 점을 강조했다.
책 소개 이후에는 디자인 패턴 자체에 대해 설명했다. 디자인 패턴을 고급(advanced) 객체지향 프로그래밍 전형으로 소개한 점이 인상적이었다. 객체지향에선 보통 명사는 객체이고, 동사는 행위이다. 그런데 고급이란 표현은 일반적(not normal)이 아니란 의미다. 예를 들어 Strategy는 알고리즘인데, 함수가 아니라 객체로 존재한다. 뒤이어 스스로 코어라 정의한 14개 패턴을 나열했다.
Composite
Strategy
Decorator
State
Iterator
Observer
Value Object
Mediator
Facade
Proxy
Command
Template Method
Adapter
Null Object (Exceptional Object)
기울임체로 표기한 두 개 패턴은 GoF 책에는 없던 내용이다. Value Object에 대해서는 DDD에서 제대로 설명한 바 있다. 랄프가 그 이야기를 해서 반가웠다. 2판에서는 (Data) Transfer Object로 고쳤지만, 단순히 데이터 운반 수단으로만 쓰였던 Core J2EE Patterns에서 Value Object라고 작명한 내용은 틀렸다고 말했다. 그래서, 본인과 Kent Beck(Implementation Patterns 28쪽), Martin Fowler 각각 스스로 Value Object를 정의했다고 한다. 셋 모두 수긍할 수 있었던 Value Object에 대한 정의는 Eric Evans의 DDD에 나온 설명이라고 한다. :)
주로 언급한 패턴에 대해 기억에 남는 내용을 메모한다.
Composite
컴포넌트를 종국에는 종단(Leaf)과 복합체(Composite)로 나누어 이분법의 강력함을 활용할 수 있다는 점이 눈에 띄었다. 하지만, 핵심은 복합체와 구성요소(Component) 모두가 정확하게 같은 인터페이스를 갖는다는 점이다.
Strategy
알고리즘을 (일반적인 경우와 달리) 객체에 담았다. Strategy에 대해선 바로 설명하지 않고, 발표자료에서도 상당한 지면을 할애해 깊이 있게 다뤘다.
Observer
Subject의 하위 클래스는 Observer 수에 무관하게 자기 일만 충실할 수 있다. 스타크래프트에서 선수가 Observer를 신경 쓰지 않고 자기 플레이만 하듯이 말이다.
그리고 위험한 패턴으로 Mediator와 Singleton을 언급했다. 많은 사람이 잘못 사용하면서, 나쁜 패턴이라 칭하는 데 대해 일침을 가했다. Mediator는 반드시 재사용을 전제로 해야 하며, 종종 데이터와 코드를 구분하는 우를 범하기 쉬운 패턴이다. 싱글턴을 써야 하는 경우를 명확하게 정의해줬다.
encapsulate global state when it cannot be eliminated
코어에 이어서 생성 패턴을 열거했다.
Abstract factory (peripheral)
Factory method
Prototype
Builder
Singleton
Dependency Injection
스프링 사용자인터라 Dependency Injection 등장이 반가웠다. 랄프 존슨은 Dependency Injection에 대해서 의존성을 객체 외부에 보존해(Keep dependencies out)서 생기는 DI의 이점에 대해 짧은 말로 명쾌하게 설명했다. 그 외 보조(peripheral) 패턴을 7개로 분류했다.
Type Object에 대한 활용은 놀라움이었다. 패턴에 대한 나의 무지를 혁혁하게 보여주었다. 랄프는 Visitor에 대해 "Cool"이라는 극찬을 감추지 않았다. 다형성을 지켜주는 보석 같은 존재로 설명했다. Extension Object를 설명할 때는 Eric Gamma와 이클립스를 예로 들었다.
마지막 카탈로그로 복합(Compound) 패턴을 두 개 꼽았다.
Flyweight
Interpreter
Flyweight를 설명하면서 flyweight pool을 활용해서 flyweght 객체를 만들어주는 클래스를 Memorizing Factory라 명명했다. 퍼뜩 떠오르는 녀석이 Bean Factory다. 이에 대해서는 더 고민해봐야 할 듯하다. 1강의 전반부는 여기까지다. 대가에 대한 존경심과 반성을 불러온 놀라운 강의는 후반부 Patterns in Business Software에 대한 설명이다. 그 내용은 짤막한 메모로 내가 받은 놀라운 인상을 전해줄 수 있을까 싶기도 하고, 너무 긴 글에 지쳐서 다음으로 미루겠다. 긴 글이나 예제를 시도할 수도 있어 포스트로 정리하지는 않을지 모르겠다.
[COR(Chain of Responsibility) 패턴은 언제 쓸까? - 난 내일만 해, 나머지는 난 몰라.]
COR(ChainOfResponsibility)이란 몬스터로부터 마법 공격을 받으면, 마법 내성과
방호구 상황에 따라 캐릭터의 HP도 깍이고, 옷, 신발 등 여러 아이템으로 충격이 전달된다. 즉 이벤트 하나가 여러곳으로
전파되어 이벤트에 대해 각 객체들이 반응하게 된다. 이때 아이템 특성에 따라 어떤 아이템은 전혀 반응하지
않는가 하면, 어떤 아이템은 일부 손상되거나 아예 망가져버릴 수도 있다.
게임 진행을 생각해보자. 미션해결을 위해 주위 사람들에게 열심히 물어보다보면 한 곳에서 모든 답을 얻지 못할 때가 많다. 조금 이야기를 한 후 누구에게 더 물어봐라라는 말을 많이 들을 것이다. 게임
속과 마찬가지로 객체 지향 세계에서는 동일한 이벤트 또는 함수 호출에 대해 책임의 범위를 다른 놈에게 계속 전가(?)시키는
상황을 볼 수 있다. 이것을 COR이라 말한다. 판타지 소설에 많이 나오는 장면 중 하나는 주인공이 정보길드와 같은 곳에서 정보를 요구하면, 뒤에 있는 놈들과 수군거리기도 하고, 그들 권한을 넘는 경우에는
관리자들에게 통보된다.
이처럼 화면이나 서버 내의 많은 복합 구성체들에
어떤 이벤트를 던졌을 때 그 각 요소들의 역할만을 수행하고, 역할 범위를 넘은 것은 계속 책임을 전가(COR)시켜가는 것을 COR 패턴이라한다.
[Command 패턴은 언제 쓸까? - 난
여러 마법을 마구 날리는게 째미써, 머씨써...]
게임을 하다보면 멋있는 장면들이 많다. 특히 다수의 적들에게 화려한 마법을 계속 난사하는 것은 마법사만의 짜릿한 특권이기도 하다. 몬스터와의 1:1 대전이라면 객체지향에서 어떤 식으로 구현할지 눈에
선하지만, 다대다의 경우라면 쉽게 감이 안온다.
진짜 객체지향으로 생각할 때가 되었다. 만약 마법공격이라는 객체를 두고, 마법 범위에 들어와있는 여러 몬스터들을
대상으로 지정한다고 하자. 마법공격도 성격이나 범위 등이 다양하기 때문에 하기 때문에 여러 마법을 돌아가면서
난사한다면(이뮨Immune 몬스터때문이라도, 여러 마법을 난사하고 싶은 순간이 있다.) 각 마법 범주에 들어오는
몬스터들을 대상으로 하는 마법공격이라는 객체를 계속 만든다고 하자. 마법이 펼처지는 시간(속도)와 몬스터와의 거리에 따라 순차적으로 마법이 적용되기 시작해야한다. 이런 것을 포함한 복잡한 로직은 마법공격이라는 객체에게 맡기고, 중요한
것은, 마법을 난사한다는 것을 마법공격이라는 명령(Command)을
계속 생성하는 것으로 생각한다는 것이다. 이경우 몬스터도 범위 공격을 할 수 있기 때문에 캐릭터나 몬스터는
상대방에게 직접적인 가해를 가하는 전투방식을 사용할 수도 있지만, 공격을 명령에 담아 날리는 스타일의
전투방식을 사용할 수도 있다.
물론
Command를 어떻게 처리할 것인지도 쉬운 문제는 아니지만, 일단 범위 공격의 난사를
어떤식으로 해결할지를 생각해보았다.
미래의 게임은
Agent를 키우는 것이 재미의 한 몫을 단단히 한다. 잘 키운 에이전트...^^ 이때 Command를 통해
Agent의 움직임을 제어할 수 있다. 허드렛일 모드부터,
가벼운 전투 모드, 낙시질 모드. 이런 것들은 CommandForAgent라는 특별한 Command로 만들어 날리면
된다. Command에 차곡이 쌓인 명령들을 Agent는
착실히 수행해갈 것이다. 물론 위험한 것을 시키면 죽을 확률도 그만큼 클 수밖에 없기 때문에, 제자를 키우는 생각으로 일정 이상으로 크기 전에 하산시켜서는 절대 안된다.
이처럼
Command는 행위를 객체화하고 싶을 때 매우 중요하게 활용할 수 있는 패턴이다.
[Interpreter 패턴은 언제 쓸까? - 난
게임속에서 제자를 키워.]
앞에서 Agent에게
행위를 예약할 때 Command 패턴을 사용하는 것을 보았다. 하지만
구체적으로 들여다보면, 조금 더 상황이 복잡하다는 것을 알게된다.
Agent에게 행위를 예약할 때 쓰는 것은 AGML의 일부인 MLA(Mark-up Language for Agent)이다. 이
것을 활용하면 Agent가 있어야 할 시점(Time), 장소(Place), 행위(Behavior) 등을 지정할 수 있다. 특히 행위를 지정할 때 나타날 상대방 몬스터 종류와 공격력 등을 바탕으로 공격할지, 피해갈지, 아예 도망갈지 등의 로직을 부여할 수 있다. 물론 게이머는 프로그래밍을 한다는 생각 없이, 멋진 화면들을 보면서, 마치 스승이 제자에게 당부하며 일을 시키는 심정으로 마우스를 몇번 클릭하면 된다.
이때 MLA는
매우 간단한 형태이기 때문에 이에 대한 처리는 Interpreter에 의해 해석되고 실행된다. 이처럼 동적인 상황에서 발생되는 규칙(Rule)에 대한 처리는 일반적으로 Interpreter 패턴을 써서 처리할 수 있다.
[Iterator 패턴은 언제 쓸까? - 뺑뺑이는
기본 훈련.]
앞의 모든 경우에 전부 있을 수 있다.
객체지향의 특성 중 하나는 위임이다. 예전에는 관련된 데이터 구조를 주~욱 정의해놓고, 서비스를 수행하기 위한 함수들을 주~욱 정의해놓고 그 함수들은 앞에
정의한 데이터 구조들을 활용해서 프로그래밍을 하였다. 하지만 기본적인 데이터 구조의 변화 또는 데이터구조체의
추가가 발생하면, 관련된 함수들은 모조리 바뀌어야하는 불편함이 있었다.
이 경우 코드 사이즈가 매우 크면 통제하기 힘든 상황이 될 수 밖에 없다. 객체지향은 이런
상황을 근본적으로 해결하기 위해 데이터 구조체와 관련된 함수들은 모두 중복해서 구조체 안에 넣고 클래스라는 단위로 모듈화하였다. 따라서 메인 함수는 상황에 따라 여러 함수를 부르는 것이 아닌, 여러
객체들에 일을 시키는 형태로 바뀐다. 이때 순차적으로 일을 시키는 경우라면 일반적으로 Iterator를 활용한다.
예를 들어 앞에서
COR 패턴에서 필요한 역할들을 순차적으로 처리하거나, 커맨드 패턴에서 마법을 커맨드화한
것들을 순차적으로 처리하거나, 인터프리터 패턴에서 에이전트에 예약해놓았던 명령을 순차적으로 처리할 때
이터레이터 패턴을 사용할 수 있다.
이처럼 이터레이터 패턴은 다른 패턴과는 독립적으로
작용할 수도 있지만, 다른 패턴들과 함께 활용될 때도 많다.
Iterator 패턴만의 활용에 대한 예를 들자. "모든
장비들의 방어능력치를 +1 Upgrade시켜주는 Scroll을
얻어서 이를 사용했다. Iterator는 현재 내가 착용한 모든 장비들을 순차적으로 불러줄 것이고, Scroll은 각 장비들의 방어능력치를 +1 Upgrade 시켜준다."
[Mediator 패턴은 언제 쓸까? - 지역적으로는
떨어져있어도, 우리는 하나.]
팀(파티) 구성의 경우 기본적으로 쌍방향 Sync가 필요하다. 이때 팀당 1개의 Mediator가
있을 수 있다.
Mediator - "집단(마라톤, 자동차
경주, MMORPG의 팀 등)에서 나의 이야기를 다른 사람에게, 다른 사람의 이야기를 나에게 보여줄 때. 나의 모습과 함께 게임하는
상대 모습이 동시에 계속 바뀌어야하는 상황에 각 캐릭터/자동차들을
Colleage로, 경기장을 Mediator로
하면됨." 특히 팀(파티) 구성이 되어있는 경우 쌍방향 Sync가 필요함. 즉 파티당 1개의 Mediator가
각자로부터 보내지는 이벤트(공격, 피해,...)들을 다른 파티 멤버들에게 모두 보냄.
[Memento 패턴은 언제 쓸까? - 죽었던
장소로 죽어라 달려갔던 바바의 기억...]
죽으면 마을 또는 특별히 지정된 장소에서 죽기전의
상태로 부활하는 형태(패널티 유/무)도 있지만, 죽을 때 아이템 등을 찾기 위해서는, 죽었던 장소로 다시 달려가야하는 형태도 있다. 아이템 많았을 때도
죽었는데, 없는 맨몸으로 가면서 또 죽고 죽고... 아 아픈
기억.
예를 들어보자. "게임의
난이도 조절에 따라 Easy Mode에는 강력한 몬스터와 싸울 경우,
경험치는 70%만 얻지만, 죽어도 경험치를 잃지
않는 "DoNotLessonExMode"가 제공된다.
또한 죽기전 갖고 있었던 모든 아이템은 다시 소유한다. 이 모드로 세팅하고 전투하다 죽으면, 죽을 때 상태를 Memento 패턴을 활용해서 저장하고, 다시 부활할 때 미멘토를 활용해서 죽기전 상태를 복원한다." 만약
전투가 어려울 경우 현상태를 기억하고, 죽었을 때 기억을 시킨 상태부터 다시 출발할 수 있는 모드가
있다면, 이 경우에도 Memento를 통해 상태를 관리한다.
사실, 대부분의
게임들이 저장을 지원한다. 새로 게임을 시작한다면 저장해놓은 여러 게임중 원하는 상태에서 다시 시작할
수 있다. 이처럼 저장해놓은 게임을 다시 시작할 수 있는 것도 넓은 의미의 미멘토 패턴이라고 할 수
있다. 다만 이경우에는 단순한 하나의 객체 상태만을 포함한 것이 아니라, 여러 객체 상태를 저장한 미멘토가 존재할 것이다.
[Observer 패턴은 언제 쓸까? - 선택적인
통지, 관리, 제어 등을 하고 싶다면.]
정상적으로 로그인해서 게임 중에 있는 게이머에게
일방적인 통지를 할 수도 있지만, 일방적인 통지 외에 선택적인 통지 형태를 몇 가지 만들어놓고, 게이머가 선택한 통지 형태에 관련된 이벤트가 발생하면 통지를 받게한다면? 이런
상황은 주위에서 많이 볼 수 있다. 내가 원하는 조건의 부동산 매물이 나타났다면? 증권 등 금융상품이 나타났다면? SMS로 끊임없이 광고가 올 수도
있지만, 카드를 사용하거나 어떤 조건이 되었을 때만 문자 서비스를 받고 싶다면? 상황은 많다.
예를 들어, 밤새
게임을 즐기는 바람직한 게이머를 위해 아침에 학교갈 시간이 되었다는 것을 알려주는 알람 서비스가 있다면, 안심하고(?) 마음껏 게임하다가, 학교갈 시간이라는 알람을 듣고 즐거운 등교를
할 수 있도록 해준다면 어떨까. 이럴때 사용할 수 있는 패턴이
Observer 패턴이다.
[State 패턴은 언제 쓸까? - 나는
천의 얼굴?]
일반적으로 마을에 있을 때 NPC에 대한 공격은 불가능하다. 어떻게 공격을 하지 못하도록 구현할
수 있을까? 몬스터에게 아이템을 바꾸자고 말을 하거나 아이템을 팔라고 말을 하고 싶지만 그럴 수 없다. 왜일까? 매우 당연하지만 어떻게 그런 상황을 구현할까? 캐릭터는 동일한데... 이러한 상황을 구현하는 방법은 여러가지가
있을 것이다. 하지만, State 패턴을 사용해서 구현할
수도 있다. 즉 캐릭터는 여러 역할을 갖는다. 마을에 있을
때는 뜨내기 사냥꾼이고, 전투장에 있을 때는 전사/마법사의
역할을 갖게 하고, 파티 동료들에게는 지원군이라는 역할을 갖도록 한다.
캐릭터들은 전투 등 습득한 상태 값들을 갖지만 그
외에 레벨에 대한 공통된 특성치들을 갖는다. 공통 특성치들 미리 정의해놓고 레벨이 올라갈 때마다, 상위 상태로 변경시킨다면, 여기에도 State 패턴을 적용할 수 있다. 이 경우 레벨이 높아지거나 레벨이
낮아지는 상황이 발생한다면, 미멘토 패턴 대신 State 패턴을
써서 하위 또는 상위 상태로 변경시킬 수 있을 것이다.
[Strategy 패턴은 언제 쓸까? - 나는
천의 얼굴?]
Agent를 학습시킬 때 "대전 형태를 정하고 이에
대한 전략"을 Strategy 패턴을 사용해서 정의할
수 있다. 특히 초반에 치루어지는, 단순 훈련이나 레벨이
낮은 몬스터들을 상대로 하는 경험쌓기라면 매우 효과적일 것이다.
무기판매상으로부터 무기 구매방식이 다양하다. 내부적으로 Gamble을 할수도 있을 것이다. 또한 특정 Event 기간에는 구매시 10% 확률로 Unique Item을 떨구어준다던가, "생일" 전후
1주일간은 모든 전투에서 Unique Item 획득 확률을 10% 올려준다던가... 이처럼 어떤 로직을 수행하는 Algorithm을 클라이언트에 영향을 주지 않고, 바꾸어주고 싶다면 Strategy 패턴이 매우 유용하다.
만약 엄청나게 많은 수의 몬스터들이 있고, 매우 강력한 얼음화살 마법을 사용했고 그 마법이 계속 몬스터들을 통과할 수 있다면, 불화살의 궤도에 있는 몬스터들에게 특별한 효과를 주고 싶고(Stun 효과, 정지 효과, 또는 어떤) 향후라도
새로운 효과를 넣고 싶다면. 이런 효과들을 Strategy로
묶어두거나 새로이 Strategy를 정의하면, 기존 프로그램에
영향을 주지 않고 새로운 효과를 활용할 수 있을 것이다.
[Template Method 패턴은 언제 쓸까? - 나는
천의 얼굴?]
Template Method
- Agent는 매우 다양하며, 캐릭터가
스스로 정의할 수 있는 부분이 있다. 이때 공통적인 부분은
Template Method로 정의해놓지만, 캐릭터가 스스로 정의하지 않으면 Default, 정의하면 해당 기능을 Overriding시킨다.
무기들이 갖고 있는 효과 등이 다양할 수도 있고, 공통적일 수도 있다. 공통적인 부분은 makeEffect()라는 상위 함수에서 정의하고, 각 특성별/레벨별 별도 사항이 있다면 doLevelEffec(), doPlayerEffec()와 같은 사항을 각 Player들의 Level에 따라 정하면 된다.
기본적으로
Template Method의 형태는 Factory Method의 형태와 동일하다. 다만, 생성이라는 특정 목적이아닌,
일반적 형태의 함수를 유연하게 표한하고자 할 뿐이다.
[Template Method 패턴은 언제 쓸까? - 나는
천의 얼굴?]
Visitor - 게임 속의 지형/건물 등은 변화를 줄 수 없고, 거의 고정되어있다. 이런 경우에는 지형/건물 등에서 일어나는 이벤트등을 지속적으로 추가해서 게임의 신선함을 유지해야한다. 이 경우 Visitor 패턴이 짱이다.
전투장(지형, 건물,...)에는 몬스터/아이템
들이 있는데, 동일한 위치에 동일한 몬스터가 나오게 할 수도 있지만,
시간/계절 등에 따라 조금씩 다른 형태의 게임을 즐기게 할 수도 있다. 뿐만 아니라 전체적인 분위기를 바꿀 수도 있다. UI를 바꾸는 작업은
조금 별개의 작업이 되겠지만, 실제 전투 환경에 대한 내부 정보는
Visitor등을 통해 바꾸면되며, 어떤 형태로 바꿀 것인가에 대한 패턴을 하나의 Visitor로 만들면된다.
[이제는 패턴들을 엮거나, 기존 패턴의 틀을 벗어나보자.]
예를 들면, 게임
캐릭터를 생성할때 초기 세팅은 거의 비슷하다면, Basic Character라는 것을 만들고, Prototype으로 기본을 복사한 후 기존의 생성 패턴을 사용해서, Personal
Character라는 것만 세팅하도록 한다.
특히 캐릭터들은 여러 아이템, 의상, 무기 등을 장착하는데 이 경우 Meta-Composite 패턴이 활용된다.
세트 아이템을 모두 모으면 캐릭터에 변화를 주는
경우를 앞에서 생각해보았다. 뿐만 아니라, 세트 아이템을
두개 갖추게 되면, 두개의 세트 아이템이 통합되거나, 새로운
아이템이 제공되거나, 캐릭터에 전혀 다른 기능을 부여하게 할 수도 있다. 즉 Meta-Composite 패턴과 함께 Decorator 패턴들을 중첩시킬 수 있고, 이들 구조체들이 Bridge 패턴을 통해 구현되었으므로 클라이언트에 영향주지 않고, 서버쪽의
캐릭터 기능을 Upgrade시켜버릴 수도 있다. 즉 아이템들을
통해 Dynamic Typing을 통해 캐릭터의 타입을 진화시킬 수도 있는 것이다.
게임의 장소는 그대로... 랜덤한 적들 또는 신규 이벤트를 벌이는 식은 이제 그만. 장소
자체도 바꿀 수 있도록하자. 마치 도시들도 계속 변하는 것처럼... 즉
모든 것이 변화하는 실세계를 그대로 게임세계에 반영하는 것이다.
따라서 변화의 주체/원칙 및 변화가 일어나는 형태(계획/랜덤)를 규정짓고, 건물의 변화된 룰을 사용하자. 즉 개인마다 다른 화면이 뜨는 것이다. Tople의 CBT라는 것 처럼.
즉, 당신
한사람만의 모든 게임이 연출되는 것이며, "이것은 매우 비싼 이용료를 지불해야한다." 물론, 호환된다.
이제는 게임의
Personl & Ubiq...인 것이다.
또한 유전자 알고리즘에 의해 학습한다. 이것은 프로그램에 대한 패턴이 아니라, 프로그램을 만드는 패턴에
대한 이야기이다.
...아... 시간이 없어 대강 끝을 내야 할 것 같다. 배고프다...
[소프트웨어 아키텍처 패턴]
소프트웨어 아키텍처는 단순한 개념은 아니지만, 단순하게 표현한다면, 소프트웨어의 구성요소 및 그들간 관계에 대한
규칙이다.
게임 제작을 하면서 앞에서 설명한 분석 패턴과 설계
패턴을 적용했고, 많은 부분을 C++를 이용해서 구현했다면, 전체 소프트웨어는 "객체"라는
것이 중요한 구성 요소이고, 그들간 관계는 객체들간 메시지 교환을 통한 상호작용일 것이다. 이는 집을 지을 때 초가집, 벽돌집, ... 등의 표현처럼, 구성요소와 그들간 결합을 중심으로 바라본
소프트웨어 아키텍처이다.
게임이 분산환경에서 이루어진다면, 많은 클라이언트는 게임서버와는 지역적으로 떨어져있을 것이며, 기본적으로
클라이언트-서버 구조를 갖는다. 이때 클라이언트와 서버 사이에는
다양한 프로토콜이 존재할 수 있으며, 서버에는 애플리케이션 서버와 대용량 데이터를 다루기 위한 데이터베이스
서버가 있을 수 있다. 따라서 이들은 3Tier 또는 그
이상의 멀티 티어로 구성된 시스템이다. 이런 형태는 네트워크를 중심으로 분산 형태를 고려한 측면의 소프트웨어
아키텍처이다.
게임을 작성할 때,
많은 기본 라이브러리, 프레임워크, 룰, 엔진 등이 사용되었을 것이고, 이들을 기반으로한 게임 애플리케이션을
작성한다. 이런 측면에서 소프트웨어를 보면 하드웨어와 이들을 제어하는
DD(Device Driver), OS, 프레임워크, 룰,
애플리케이션 등 다양한 레이어 구성을 갖는다. 이는 소프트웨어를 일종의 층(Layer)으로 모듈화하고, 이들간 관계를 체계화한 것으로 볼 수
있다.
리파지토리
함수, 데이터, 컴포넌트, 라이브러리, 프레임워크, 서비스, 패키지, 솔루션
등 다양한 소프트웨어 구성 체계 등은 "객체"를
포함한 "서비스 또는 기능을 제공하는 크기/단위)"에 관한 소프트웨어 아키텍처 이슈이다.
일반적으로 분산환경 기반의 게임에 대한 소프트웨어
아키텍처는 단순하지 않다. 이 안에는 동기/비동기, 동시성 제어, 보안, 트랜잭션, 사용성, 성능,... 등
매우 중요한 이슈들이 포함되어있으며, 이들을 어떤 목적으로 어떻게 시스템으로 구현할 것인가는 단순한
문제가 아니다. 이는 Stakeholder와 "게임 제작 목적"과 게이머들을 위한 근본적인 문제부터
출발하는 긴 여정이 될 수도 있다...
이제 캐릭터와 같은 주요 객체들이 만들어졌으니, 본격적으로 게임을 즐길 수 있다. 패만 돌리면 된다.^^ 캐릭터를 만들거나 기존 캐릭터를 선택해, 온라인으로 접속해서
게임을 시작하면, 많은 상황들이 기다리고 있다. 쉼터, 마을, 전장(참고적으로
젠장은 없다.),...
[Adapter 패턴은 언제 쓸까? - 느그꺼
그냥 쓰고 싶은데...]
(가칭) AnyGameLand 속에서는 다양한 게임이
생겨나고, 기존 게임도 계속 변경되기 때문에 관련된 자회사나 협력사가 많다. AnyGameLand Inc.에 통합된 게임이 1000개가 넘은
시점이 2021년이니까...
예를 들어보자.
"게임은 뭐니뭐니해도 뿅뿅 스타일의 아케이드 게임이야."와 같은 사람들을
위해 다양한 뿅뿅 게임을 준비했다고 하자. 보통의 클라이언트에는 WindowsHeaven이
사용되고 있었고, AnyGameLand는 WindowsHeavenBissanServer
환경하에 운영되고 있었다. 그런데 어떤 작은 아케이트 게임 전문 회사(NJGC)에서, WindowsHeaven 커널에서 지원되는 허스키
음성 API를 활용한, 폭탄 성능에 따라 다양한 음향효과를
줄 수 있는 게임을 발표했다. 나름대로 좋은 호응을 얻었고, 이
회사와 AnyGameLand사와 적절한 합의가 이루어졌다. 다만
NJGC의 게임 인터페이스는 AnyGameLand의 표준인
AGML(AnyGame Mark-up Language)을 지원하지 못하는 상황이었다. 하지만 AnyGameLand측에서는 단순히 Adapter 패턴을 적용해줄 것을 요구했고, 비교적 쉽게 통합 작업이
이루어졌다. 양사의 인터페이스는 변화가 없지만, AnyGameLand에서
사용하고 있는 인터페이에 따라 NJGC사의 API를 불러
사용하는 Delegation을 활용한 Adapter를 제작하였다. 이에 따라 기존 클라이언트들은 새로운 음향효과를 맛볼 수 있었고,
AnyGameLand의 기존 프로그램도 별다른 영향을 받지 않았다. 사실 이런 사항들에
대한 선례가 너무 많았고, Adapter 패턴을 적용하는 것은 당연한 작업일 뿐이었다.
이처럼 다양한 객체, 라이브러리, 컴포넌트, 프레임워크들을
통합하는 과정에 있어 어댑터 패턴은 이들을 효과적으로 결합시키고, 기존 시스템에 변화를 주지 않으면서
새로운 서비스를 제공할 수 있는 바탕을 제공한다.
[Bridge 패턴은 언제 쓸까? - 야, 따로 놀자 따로 놀아.]
AnyGameLand에는 다양한 클라이언트들이 있고, 서버에는 다양한
게임들이 포함되어 있으며 실시간으로 개선(Upgrade)되고 있다. 클라이언트가
서버 측의 다양한 Upgrade에 영향을 받지 않기 위한 전략이 필요하다.
또한 무기, 아이템과 이들을 활용하는 캐릭터, 마을과 캐릭터, 전투장과 몬스터 등 각각의 단위 구조들이 복잡하기 때문에 이들간 상호관계는 복잡한 관계가 다양한 형태로 영향을
줄 수 밖에 없다. 하지만 아이템이나 마을 등이 바뀔 때마다 캐릭터를 바꿀 수는 없다. 따라서 각 단위 구조들은 그 구조들을 사용하는 모듈들에 대한 정교한 인터페이스를 정의하고, 구현을 분리시킨다. 이를 위해 대부분 모델들은 기본적으로 인터페이스를
구조화하며, Bridge 패턴을 활용해서 변화에 대한 다른 프로그램의 변경을 최소화한다. 예를 들어 무기는 그대로라 해도, 레벨이 올라갔다면 칼을 휘두를
때의 소리나 빛의 효과 등이 달라질 수 있다.(마나의 운용 능력이 올라갔으니 당연한 것이다.) 하지만 소리나 빛의 효과에 대한 기술이 Update될 때마다 기존
캐릭터들을 바꾸는 것은 불가능하다. 또한 새로운 무기 체계를 도입함으로써 지속적으로 사용자들을 현혹(?)시켜야하기 때문에 음향, 그래픽은 지속적으로 개선할 수 밖에 없다.
Bridge 패턴은 이런 경우에 기본적이면서도 중요한 기술을 제공한다. 인터페이스에
해당하는 클래스 구조만을 클라이언트에 알려주고, 그에 대한 대응 구조를 구현하는 체제로 가는 것이다. 이처럼 Bridge 패턴은 게임의 단위별 구조 설계에 기본적으로
적용할 수 있는 모델링 기법이며, 클라이언트/서버 형태의
프로그램 구조에 확장성을 주는 패턴이다.
* Adapter 패턴이 게임들간 통합을 위한 기본 전략이라면, Bridge 패턴은
게임 내의 주요 구조체들의 유연한 활용을 위한 기본 전략이고, 다음에 볼 Composite 패턴은 구조체들의 복잡한 구성 자체를 체계적으로 관리하기 위한 패턴이다. 앞의 생성 패턴을 구조 패턴이 적절히 적용된 구조체에 적용할 경우, 훨씬
효율적인 생성 패턴으로 활용할 수 있다.
[Composite 패턴은 언제 쓸까? - 봄이
왔네, 봄(BOM)이 와.]
Composite 패턴은 복잡한 구조체가 많이 필요한 곳에서는 가장 기본이면서,
많이 나타나는 패턴이 될 수 있다. 다양한 무기, 다양한
의상, 다양한 몬스터 등 게임 속에는 다양성을 가진 구조체들이 많으며,
이러한 다양성을 효과적으로 구성하고 관리하는 방법에 Composite 패턴이 딱이기 때문이다. 예를 들어 캐릭터의 자세한 안면 구조가 필요한 게임을 만든다고 가정하자. 눈, 코, 입, 귀, 피부, 머리, 수염, 여드름, 흉터 등 다양한 단위 요소들이 있고, 그들 단위 요소들이 모여 얼굴과 같은 복합체를 이루고, 얼굴, 몸통, 팔, 다리 등이
모여 캐릭터를 이루게 된다. 몬스터의 경우는 훨씬 다양한 요소와 결합 규칙이 적용된다. 눈 없는 놈, 해골만 있는 놈, 형태가
부정형인 놈... 신발과 같은 작은 아이템도 굽, 앞부분, 발 목부분, 다양한 재질 형태 부착물 등이 있으며, 그 외 의상, 무기 등도 나름대로의 구조체계를 갖고 있다. 이처럼 게임 내의 많은 구조들은 제작과정 또는 관리 상의 많은 어려움을 제공한다. 일반적으로 이와 같은 어려움을 트리 형태의 관리를 통해 복잡도를 제어하는 것이 일반적이며, Composite은 이때 활용하기 좋은 패턴이다.
디렉토리와 파일들을 생각해보면, 디렉토리는 디렉토리와 파일을 포함할 수 있는 복합체이고, 파일은
마지막 구성 요소에 해당한다. Composite 패턴은 이런 형태를 모델로 표현한 것에 불과하다. 마치 클래스 하나를 재귀관계로 표현했을 때 클래스의 유형을 두가지로(Leaf,
Node)로 분류하면서 재귀관계를 Node쪽에 살려놓은 형태이다.
BOM(Bill of
Materials)은 제조 뿐만 아니라,
다양한 도메인에서 활용할 수 있는 개념으로 어떤 구성체계를 가졌는지를 BOM을 통해 효과적으로
전달할 수 있다. Composite 패턴은 BOM을 표현하기
적당한 유연성을 갖고 있다.
구조체의 종류와 구성이 복잡한가? 일단 Composite 패턴을 적용할 것을 검토하라. 트리는 복잡도를 lognM으로 줄여줄 수 있는
효과적인 수단이다. (가지를 10개로 뻗을 수 있는 트리라면 1000개라해도 3번의 가지만 통과하면 찾아낼 수 있다. log101000 = 3^^;)
[Decorator 패턴은 언제 쓸까? - 야! 팀장 말이 전에 만든거 건들지 말고 기능을 추가하래...]
게임속의 구조체들에 기본적으로 Composite 패턴을 적용한다고 했다. 그런데 이런 구조체를 만들고
한참 쓰다보면 확장해야 할 이슈들이 등장한다. 기존 구조체는 건드리지 않고, 어떻게 확장할 수 있을까? 기본적으로 생각할 수 있는 것은 상속이다. 즉 Composite 패턴에서 복합체에 해당하는 클래스를 확장하고
싶은 타입으로 상속하면, 기존 구조들에 대해 확장된 기능을 제공할 수 있다. 즉 단위요소들을 포함하는 클래스를 확장함으로 해서, 그들 조합으로
나타나는 기능들에 변화를 부여시킬 수 있다.
예를 들어 Set
Item의 경우, 아이템들이 추가될 때마다, 기존
아이템들의 특성 및 기능이 Upgrade되며, 색의 변화
또는 반투명이나 골드코팅 효과 등을 함께 부여하고 싶다. 특히 Set
Item이 전체적으로 완성되는 경우, 캐릭터의 레벨에까지 영향을 주게 할 수도 있다. 이 경우에는 단순한 Decorator 패턴의 범주를 넘어선 것이지만, 얼마든지 응용할 수 있는 패턴으로, 기존 구조체 자체를 바꾸는 것이
어려운 경우에 활용할 수 있는 구세주와 같은 패턴이다. 이는 동적인 상황을 Decorator로 풀어서 설명한 것이며, 일반적으로, 클래스 수준에서 기존 구조체의 기능을 재정의하는 경우에 사용하면 좋다.
아이템 중에는 취득에 따라 기존 아이템까지도 새로운
기능이 부여될 수 있는 Set Items, Synergy Items, Grace Items 등을 만들고
싶을 때, Decorator가 딱이다.
[Facade 패턴은 언제 쓸까? - 난
복잡한거 싫어. 딱 한놈만 두들기면 다나와.]
많은 객체들이 상호작용하여 어떤 서비스를 제공한다고
할 때, 클라이언트 프로그램 입장에서 모든 객체를 제어하는 것은 불편할 뿐만 아니라, 레이어를 분리시킨다는 모듈 관점에서도 레이어간 상호작용/인터페이스를
간편하게 유지시키는 것이 좋은 모델링 기법이다. 네트워크를 타는 경우 리모트콜(remote call)을 줄일 수 있으며, 다른 레이어의 복잡도를
숨겨줄 수 있기 때문이다.
이때 여러 객체들간 상호작용을 클라이언트에 노출시키지
않도록 중간에서 인터페이스 역할을 해주는 클래스를 두면 좋은데 이런 것이 Facade 패턴을 활용한
예이다. 분산환경에서 리모트콜의 횟수를 줄이는 것은 기본적인 에티켓,
아니 반드시 적용해야 하는 모델링 전략이다.
이런 패턴을 응용해보자. 게임 중에 마을을 돌아다니면서 내가 필요한 것을 사기 위해 돌아다닐 수도 있지만, 여관에서 쉬면서 심부름꾼에게 부탁할 수 있다면, 신부름꾼은 캐릭터의
옷, 물품 등을 수거하고, 물약같은 필수 물품들은 사고, 그동안 사냥해온 것은 팔고, 무기는 적당히 고쳐준다. 물론 약간의 웃돈은 필요하겠지만. 소프트웨어 내의 심부름꾼을 Facade라 한다.
게임 진행상에 보면 여러 안내인 또는 가이드들이
게임속 상황에 대한 정보를 준다. 이 경우도 실제 내부 정보를 모으기 위해 동분서주할 필요없이, Facade에 해당하는 NPC들을 통해 필요한 정보를 모두 제공받을
수 있다.
[Flyweight 패턴은 언제 쓸까? - 야... 개떼처럼 많네. 이걸 다 메모리에 어떻게 올려?]
클라이언트 화면을 보면 주인공 하나에 수많은 적들이
몰려있는 상황을 많이 볼 수 있다. 이때 주인공 하나에 대한 그래픽 이미지는 별 문제 아니지만, 화면에 나타난 많은 적들 모두가 앞모습, 옆모습 등의 다양한 비트맵을
모두 갖게한다면, 적이 많아지면 클라이언트 화면은 물속의 움직임처럼 부드러워(?)질 것이다. 이 경우 각 적들은 자신의 위치와 현재 상태 같은
최소의 정보만을 갖고 비트맵 등은 공유할 필요가 있다. 이때 공유할 대상들은 작지만 공유를 통해 효율적인
시스템을 만들 수 있는데 이들을 Flyweight라 한다.
다양한 게임들이 있겧지만, 그 안에는 화면에 보여지는 많은 몬스터, 아이템 등이 있다. 기본적으로 이들은 모두 Flyweight로 구성/관리할 수 있다.
[Proxy 패턴은 언제 쓸까? - 난
운전할때는 운전수, 회사에서는 일꾼, 그거 빼고는 게이머.]
기본적으로 기능과 상관없이, 통신이 개입된 환경에서는 proxy 패턴을 쓰기 적당한 상황이다. 중간에 네트워크 채널이 있기 때문에 클라이언트가 원하는 서비스를 바로 받을 수 없고, 클라이언트가 원하는 서비스를 네트워크 넘어 다른 환경에 있는 서버에 대신 요청해줄 수 있는 객체가 필요한 것이다. 실체는 아닐지라도 클라이언트와 상호작용하는 객체를 proxy(stub,
skeleton 등 비슷한 말들이 많다.)라 한다.
또한 여러 역할을 가진 객체가 있다면, 역할별로 proxy를 두고, 각
클라이언트 프로그램들은 필요한 기능을 proxy를 통해 얻을 수 있다.
클라이언트 프로그램이 자기에게 배포된 proxy에게 어떤 요청을 하면, proxy는 메인 객체에게 그 요청을 전달해주면 될 것이다.
자... 게임
시작 장면을 생각해보자. 여러 캐릭터를 보여주고 주인공을 선택하게하는데, 이때 캐릭터 종류가 얼마 안되고, 특성 변화가 그리 없고, 확장 계획도 없다면 "패턴"에 대한 필요성은 거의 없다.
하지만, 캐릭터가
많고(종족, 타입,...),
특성(성별, 직업, 피부색, 털색, 키, 체형,...)도 많고, 캐릭터의
진화/변경도 예상되고, 새로운 캐릭터 특성 등이 계속 확장될
수 있다면(게임업계를 평정하려면 해야된다. 전세계 게임 시장을
내 손안에...^^), 어떻게 하면될까?
주인공 생성시 인간을 선택하고 남성을 선택하고, 구릿빛 얼굴과 커다란 덩치 흐트러진 머리... 를 선택한다고 가정해보자. 이렇게 여러 과정을 거처 만들어지는 주인공도 있지만, 미리 만들어
놓는 수많은 몬스터들, 마을 내의 NPC(Non-Playable
Character)들도 있고, 군데군데 떨구어 놓는 아이템들, 수많은 무기들, 건물, 산, 동굴, 기타 배경 등 다양한 것들을 게임 속에서 접하게 된다. 게임을 위해 생성할 것은 주인공만이 아닌 게임 전체 속에 있는 모든 요소들이며, 이들을 순차적으로, 때에 따라서는 한꺼번에 뭉탱이로, 또는 랜덤하게 생성하기도 한다. 몬스터가 죽으면 간혹 좋은 아이템을
남기는데(호랭이는 가죽을 남기고, 몬스터는 아이템을 남긴다...) 수천/수만의 아이템을 어떻게 유지할까? 그 많은 정보를 유지하기 쉽지도 않을 뿐더러, 계속 추가되기도 할텐데. 그때마다 관련된 프로그램이 바뀌어야한다면? 생각만해도 끔찍한 일이다. 새로운 것이 안나타나는 시간이 길어지면, 접속 사용자 수는 점차
줄어만 갈 것이고,..
결국, 무엇을
어떻게, 언제 생성할지에 대한 유연성을 부여하는 것이 사용자를 확보하는 것이고 돈버는 길이다. 자 그렇다면 "게임 제작자"라는 관점에서 어떻게 하면 사용자들을 현혹할 수 있는 복잡도를 잘 견뎌내면서도, 조금만 일해도 되는 프로그램을 만들 수 있을까...
여기서 잠깐 모델링의 원칙을 살펴보자. 똥막대기 원칙 1 : 모든 모델링의 기본은 "찾고, 관계 짓고, 상세 넣고, 정제하는
것"이다. 이 원칙에 입각해서 모델링을 시작하자.
우선 캐릭터 모델링부터 시작다면, 먼저 해야 할 일은 "관리가 필요한 Character 요소"를 찾는 것이다. 그리고 그들의 관계를 설정하는 것이며, 필요한 속성, 오퍼레이션을 정의하는 것이다. 그리고는 지속적 정제. 끝... 캐릭터라면 얼굴, 몸통, 팔, 다리, ... 등의
요소와 그들의 결합을 통해 캐릭터를 조합하는데, 종족/성별/.../장신구/의상/무기/... 에 따라 다양한 캐릭터를 만들어 낼 수 있다.
요소들을 파악했다면, 그들간 많은 관계들을 볼 수 있다. 얼굴과 눈은 Composition(무조건 있다.), 머리카락/비듬/머리핀 등은
Aggregation(있다가도 없어질 수도 있고, 중간에 만들어질 수도 있다), 종족/성별 등은 Inheritance.
하지만 대부분은 결국 Is-A, Has-A 안으로 귀결되며, 이들의 조합을 통해 매우 복잡한 모델도 만들 수 있다.
마을의 경우 NPC,
울타리, 집, 동물, 나무, 잔디, 집기, 상품/도구/..., 등의
요소와 그들의 결합을 통해 다양한 마을들을 만들어낼 수 있다. 세부적으로 보면 매우 복잡할 것이다. 예를 들어 집이라면... 방이 많은 경우, 여러 층인 경우,...복도, 광장, 기둥, 창문, 문,... 형태도 탑, 목조, 콘크리트, 폐가, 신축, 고대/중세/현대,... 등... 하지만, 이 것도 Is-A,
Has-A로 대부분 귀결된다. (흐흐흐... 사실은 UI에 보여지는 대부분을 객체화해서 만들 필요는 없다.)
전투장을 만든다면?
역시 요소들이 달라지고 그들의 결합에 따라 다양한 전투장이 만들어질 수 있을 것이다. 여기에도
다양한 타입들에 대한 Is-A, 다양한 포함관계의 Has-A가
나타날 것이다.
물론, 캐릭터와
몬스터들간의 "전투"처럼 다양한 상호작용이
있을 것이고, 이들 상호작용들은, 앞의 요소들에 나타나는
관계와는 달리, 주로 Association, Dependency에
의해 표현된다. 조금 저질스런 예를 들어보자. "코딱지를
판다." 이 경우, 코딱지, 손가락, 콧구멍(개념적인
것조차 모델링할 수 있다.^^), 코들간 Association이
표현될 것이다. 코를 후비는 것을 조금 확대해서 칼로 찌르기, 화살
날리기, 얼음창(마법) 던지기
등을 했다고 생각해보자. 관계된 객체들은 다르지만 상호작용 표현에는 유사성이 있다. 이런 형태의 상호작용은 생성 패턴에서 보다는 구조/행위 패턴에서
많이 볼 것이다.
참고로, 여기에
앞에서 살펴본 모델링 이면에는 다양한 프로그래밍이 전제가 된다. 예를 들면, Dynamic Binding과 상속을 통해 다형성(Polymorphism)을
구현하는 것이 있다. 이때 구닥다리 객체지향 언어인 C++인
경우 pure virtual function 또는 virtual
function이 필요하고, Java/C# 등을 사용한다면, 못들은 것으로 하자. 그냥 인터페이스를 쓰면된다. C는? 구조체 내에 함수 포인터를 쓰면된다. 그 외 무지하게 많은 재미있는 것들 static, this, ... 에
대해 재미있는 것들이 객체지향 프로그래밍에는 많다.
* 책 속의 특이한 특징 - 독자들의 생각/말도 함께 들어있다...
"아따... 징하다 징혀... 증말 말 많네... 증말...
경마장도 아니고... 생성패턴이라는 제목을 한참 전에서 본...적 있나? 가물가물하네...?"
여러분이 캐릭터를 만들 때 종족/특성/... 등을 선택했다면, 게임
내부에는 겉으로 보이는 화려한 캐릭터가 아닌, 메모리 내에 객체 특성값(상태)가 존재한다. 화면에
보여주는 것과는 아주 다른 이야기이다. 겉 모습에 대한 것도 있을 수는 있지만, 대부분 메모리 내의 객체 만들기에 대한 이야기들을 생성 패턴에서 다룬다.
GameStartScreen라는 놈이 있고, 이놈이 객체를 만든다고 하자. 이때 Brute Force 패턴을 적용해서 사용자가 선택한 캐릭터
구성 요소들을 생성자를 이용해서 직접 만들 수 있으며, If then else, 또는 switch를 사용해서 만들 수도 있다. 하지만 난 싫다. 차라리 빨리 만들고 남는 시간 게임을 더하는 것이 남는 일이기 때문이다. 어떻게
빨리 만들까? 궁금. 답:
남 시키면 된다??? 어떻게 남에게 시킬까? 어떤
좋은 방법(패턴)은 없을까?
[다양한 생성 패턴]
CharacterFactoryAtGameStartScreen라는 것을 만들고, 만들어야 할 요소들에 대해 생성자
호출 대신 가상함수를 호출하도록 구현한다면, Factory Method 패턴에 해당하고, 선택한 사항들을 모두 파라메터로 넘겨 캐릭터를 생성한다면 Abstract
Factory 패턴이고, 선택할 때마다 부분적 캐릭터를 만들어 통합한다면 Builder 패턴이고, 미리 대표적인 캐릭터를 몇개(얼마나 만들어놓을까?) 만들어놓고 이를 복사하고, 특성들이 다른 것들만 조금 변경하면 Prototype 패턴이다. 다만 캐릭터가 아닌 안내인 등의 NPC 등은 캐릭터 수 만큼 만들어
대응할 필요가 없다. 따라서 이 경우 동시 접속 상황을 고려해
Singleton으로(한명 또는 몇명 정도) 만들어
놓으면 된다. (뭔 말인지 이해되었으면, 이 책 절대 읽지
마라! 평생 안볼 곳에다 던져버려라. 불쏘시개도 좋다.)
[Factory Method 패턴은 언제 쓸까? - 몬스터는
죽어서 아이템을 남긴다.]
게임 중에 매우 다양한 아이템들을 얻을 수 있다. 길바닥에서 그냥 줏거나 전투를 통해 얻을 수도 있고, 남의 것을
빼앗거나, 구걸하거나(구걸신공 Skill! 이 기술만 마스터하면, 아무리 좋은 아이템을 갖고 있는
놈이라도, 구걸신공을 통해 뺏을 수 있다.ㅎㅎㅎ), 마을에서 사거나 도박을 해서 얻을 수도 있고, 일을 부탁 받고
보상으로 얻는 경우도(공짜로 일하지 마라.) 많다. 이처럼 아이템을 얻기 위해서는 누군가가 아이템을 생성하여야한다. 이때
길바닥에 버려진 경우를 제외하면, 캐릭터가 누군가에게 아이템을 요청(전투, 대화, 도박, 구매, 도움)한 것이고, 아이템을
요청 받은 대상은 아이템을 만들어 준 것이다. 따라서 이런 상황은 아이템을 생성하는 객체가 itemFactoryMethod()를 갖고 있고, 캐릭터는 이 함수를
내부적으로 호출하는 함수(combat(), beg(), talk(), buy(), help(),...)를
호출하면된다. 이때 itemFactoryMethod() 내부에는 "return new UniqueGnomSwordItem();"처럼 전달해줄 아이템을 생성하는
로직을 정하면된다. 이러한 형태를 Factory Method라한다. 다만, 추상 함수, 추상
클래스의 역할, overriding 의미 등을 명확하게 알지 못하는 사람들에게는 itemFactoryMethod()를 abstract로 만든 이유에
대해 조금 어려움을 느낄 수는 있다. 디자인 패턴이 좋은 또 다른 이유는 객체지향적 사고를 훈련하기
아주 좋다는 점이다.^^
즉 별도의
Factory 클래스를 만들 필요까지는 없이 기존 객체가 Factory 역할을 하면서 다른
객체에게 무언가를 주는(create) 상황이라면 Factory
Method 패턴을 사용하면 된다.
[Abstract Factory 패턴은 언제 쓸까? - 내
맘에 쏙 드는 종족/스타일을 만든다.]
복잡한 과정을 통해 게임 캐릭터를 만드는 경우, 화면에 보여지는 것과 실제 캐릭터가 만들어지는 것은 조금 다르다. 캐릭터
생성에 대한 과정이 모두 종료된 후, 한번의 요청으로 캐릭터를 만드는 것이 효율적이고, 화면에는 선택한 옵션에 따라 그때그때 모습만을 달리 보여주면된다. 캐릭터
생성 요청시 게이머가 선택한 사항들이 AbstractCharacterFactory로 전달된다. 이때 전달된 정보를 기반으로 다양한 캐릭터들이 만들어진다.
게임 초기에 캐릭터를 생성할 수도 있지만, 기존에 만들어놓은 캐릭터를 선택해서 게임을 할 수도 있다. 이처럼
기존 캐릭터를 선택한 경우에도 상태정보를 기준으로 Abstract Factory를 통해 캐릭터 객체를
만들 수 있다.
예를 들어, 각
종족별로 Factory를 만들고, 각 Factory는 얼굴 유형, 몸 유형, 의상 유형, 무기 유형 등 다양성을 내포하고 있는 각 클래스들을
기반으로 객체를 생성한다.
이처럼 별도의
Factory 클래스들을 정의하고, 캐릭터는 그들에 대한
Abstract Factory를 참조하도록 하면 매우 유연한 캐릭터 생성 패턴을 사용할 수 있다.
[Builder 패턴은 언제 쓸까? - 열심히
두들겨 패니까 나오네?]
객체지향적 사고라는 것은 어떤 것일까? 게임에는 부서지지 않는 벽도 있지만, 벽을 부수고 들어가 다음 전투를
수행할 수도 있다. 이때 벽을 부순다는 것은 벽에 break(this.getStrikePower())라는
메시지를 계속 던지는 것이고, 벽은 break(this.getStrikePower())라는
메시지를 받으면, 조금씩 스스로를 부서뜨리면서 새로운 입구를 조금씩 드러낸다. 다 부서지고 나면 벽은 허물어지면서 nextHidedAmaigingGate라는
객체를 캐릭터에게 넘겨준다. 캐릭터는
getIntoNextGate(this.getMissionHistory())라는 메시지를 nextHidedAmaigingGate
객체에게 전달하면, 객체는 캐릭터의 미션 수행상태를 점검하고, 필수적인 미션을 끝냈다면 통과를 허락한다. 몬스터와 상호작용하는
것은 자연스러워보일지는 몰라도, 벽하고 상호작용하고, 문하고
상호작용하는 것이 익숙하지 않을 수는 있다. 실제 벽이나 문은 스스로의 강도가 있고, 외부에서 가해지는 충격량이 견딜수 있는 강도보다 센 경우, 부서진다. 이런 상황을 유사하게 재현한 것 뿐이다. 객체지향적 사고라는 것이
이상해보여도 익숙해지면, 코딩을 모듈화하기 위한 술수(?)가
듬뿍 담겨있다는 것을 알게 될 것이다.
앞의 예를 잘 들여다보면, 비밀문을 통과하기 위해 여러번 벽을 두들겼다. 이처럼 여러번에 걸쳐
어떤 결과를 얻어내는 경우에 사용하는 패턴이 Builder이다.
시점이 연결되는 경우도 있겠지만, 시점이 떨어진 경우라도 Builder 패턴을 적용할 수 있다. 예를 들면, 미션을 완수할 때마다 무기 부품을 얻을 수 있는데, 이들 부품을 조립해서 새롭게 "LaserBOMGun 또는 LaserSword"와 같은 강력한 무기가 만들어진다면, 이
것도 Builder 패턴이라 할 수 있다. 이경우 숨겨진
미션 또는 비밀 미션 등을 통과한 후 찾아낸 특별한 부품 등을 함께 조립하면 새로운 옵션(지속적원기충전, 마법가속, 장비무게감소, 골드외장, 몬스터유혹파장, 방어파장,...)이
추가되는 부품이 만들어질 수도 있다.
이처럼 빌드 패턴은 최종 목표를 만들기까지 여러번의
과정을 통해 점증적으로 만들 때 활용하는 패턴이다. 아바타식 캐릭터를 만드는 경우도 Builder 패턴에 해당한다.
[Prototyping 패턴은 언제 쓸까? - 아저씨
칼 한자루 주세요.]
게임 속을 들여다보면 매우 많은 복제들이 일어난다. 마치 실세계에서 클로닝(cloning)이 중요한 이슈인 것처럼, 게임에서 자기 복제를 하는 것은 매우 중요한 기법이며 클로닝이라고도 한다.
캐릭터나 몬스터들의 기술 유형을 보면, 자기복제류의 기술들이 많다. 복제를 할 경우, 기본적으로 복제할 시점의 객체 상태와 동일한 상태를 갖는 프로토타이핑이 만들어진다. 무기 상점에서 물건을 구입할 경우에도 진열된 상품이 유일한 것이 아니라면, 기본적으로
복사를 해서(팔고 난 후에도 무기는 여전히 있어야하니까) 넘겨준다. 데리고 다니는 조무래기들을 생성하거나, 마법지뢰를 매설하는 경우처럼, 동일한 것을 지속적으로 만들어내지만, 각자가 상태를 갖거나 독자적인
생명이 있는 경우라면 프로토타이핑 패턴을 사용하면 된다.
스타크래프트라는 게임의 경우에 툴바를 사용해서 유닛을
생성한다. 이 경우에도 툴바에는 프로토타이핑이 존재하고, 이를
사용하는 경우 클로닝을 통해 새로운 유닛을 생성한다. 프로토타이핑의 대표적인 사례이다.
[Singleton 패턴은 언제 쓸까? - 여기는
겜마스터, 다 나와라 오버.]
Singleton은 가장 오랜 역사와 전통을 자랑하는 패턴이며 지금도 영업적으로 절대적 추앙을 받는 패턴이다. 서로 도와가며 살아가는 "우리"라는 말에 담긴 정감과는 거리가 먼, 오로지 돈이 얼마가
들어오는가에 따라 소프트웨어 사용에 대한 허가권을 내주는 서양식 발상에서 출발한 패턴이다. 모든 리소스는
돈이며 리소스 사용당 돈을 내야 한다. (원래 이런 쫀쫀함이 기술적 발전에 기여해온 것은 사실이다.) 씁쓸한 이야기는 그만 두고 게임으로 돌아가자.
가슴에 기대를 안고, 마을로 진입한다. 거기서 처음 나를 맞이해주는 것은 안내인이다. 가만보니 그 안내인은 나처럼 처음오는 초짜들을 계속 상대하고 있는 것처럼 보인다. 이처럼 많은 사용자들 수만큼 안내인이 필요하지는 않을 것이다. 그렇다면
안내인 1명 또는 제한된 인원으로 새로운 초짜들을 상대하는 것은 매우 효율적인 일이다.
"고운말 씁시다."라고 혼자서 여러 사람들을
상대하는 게임 관리자(Game Master, Manager)도 게임 내에 한명만 있으면 된다.
이처럼 사용자가 많아도, 잠깐씩 시간을 할애해서 여러 사용자를 상대하게 하거나, 방송을 통해
한꺼번에 통지할 수 있는 역할을 수행하는 객체를 사용자 수만큼 만드는 것은 엄청난 낭비일 것이다. 싱글톤
패턴은 스스로가 팩토리이면서 생성자(constructor) 접근을 제한하는 매우 특이한 취향을 가졌지만, 리소스의 효율성과 접근성의 제한이라는 강력함을 제공해주는 패턴이다.
이 글은 국내 유수의 금융권 대규모 프로젝트에서 아키텍트로 활동하고 계신 Grady님께서 작성하신 것입니다. 디자인 패턴을 어디에 써먹을 수 있을지를 MMORPG에 비유하여 설명한 글로 '도대체 패턴을 어디에 쓸까' 하는데 통찰을 제공해줍니다. 물론, 구현 수준의 설명이 아니기 때문에 이 글을 읽는다고 바로 패턴을 쓸 수 있는 것은 아니죠. 제가 미국에 있는 동안 순차적으로 연재되도록 예약 발행하겠습니다. 전체 시리즈는 총 네 편입니다.
세상에서 제일 재미있는 디자인 패턴
들어가기 전에 잠깐 우리들의 영원한 고전... Hello World를 살펴보자. (만약 당신이 코딩에 대한
경험이 없거나, Hello World가 무슨 의미인지 모른다면, 당장
읽는 것을 중지하기 바란다. 심한 경우, 당신의 우뇌부분에
상당한 손상이 가해질 수 있다. 또한 당신이 MMORPG가
뭐에 쓰는 것이 모른다면. 당장 읽는 것을 중지할 뿐만 아니라, 이
것을 읽으려 했던 사실조차 망각하기 바란다. 그 이유는... 당신은
분명히 재미없는 사람이 틀림없기 때문이다. 이 글은 재미를 추구하는 자에게만 허락된 내용이다.)
[Hello World]
"Hello
World"를 찍는 다양한 프로그램....
가장 빨리?
가장 단순(LOC)?
재사용성을 높인다면?
String을 찍는 프로그램으로 개발하고, String을 "Hello World"으로 입력?
"Hi, YourName."을 찍는 것으로 바뀌었다면?
이때 별도
File 입출력을 했다면?
편지의 앞 또는 마지막 서명 형식으로 찍는다면?
Hello World 사이에 공백을 넣어서, 한 페이지에 당신의 얼굴을
그린다면?
목적에 따라 재사용성을 미리 고민할 것인지, 복잡도를 높일 가치가 있는지가 달라진다.
수많은 Decision들. Goal을 알고, 이유Why를
알고, 최적의 솔루션을 찾고, 적임자를 찾고, 맞는 환경을 찾고,...
고객의 생각이 바뀌면 모든 것이 바뀐다!
...
흠... 뭔가
요구가 바뀌고, 환경이 바뀌면... 뭐... 체력으로 버팅기지...뭐...
이건 진리지, 진리...
음...글고, 디자인 패턴을 보기 전에 "객체"를 빠삭하게 알아야하는데... 객체 이전의 방법과 객체 지향적
방법을 간단히 살펴보자. 여기서부터 모든 것은 게임으로 통한다. 게임.ㅎㅎㅎ
[전통적 프로그래밍]
main()이 있고, initGame()이 있고, startGame()이 있고, endGame()이 있다.
initGame()에는 Player를 만들고, Monster를 만들고, BattleField를 만들고, 암튼 무지하게 만든다.
start()에는 초기 화면이 나온다. 마을일 수도 있고, 전투가 바로 시작될 수도 있다. 모든 시나리오가 이 안에 있으며, (KeyBoard, Mouse)이벤트에 따라 계속 움직이고... Player찍고
땅 찍으면, Player 상태에 따라 달릴 수도 있고 걸을 수도 있다.
Monster 찍으면 공격한다. Player의 공격과
Monster 상태를 보고, 다친 정도를 결정한다,...
죽으면 또는 다 죽이면 끝난다.
endGame()이 있다.
게임 본연의 기능 외에 CPU Clock, 메모리 최적화 등 비기능적인 문제들은 항상 따라다녔다. 뭐... 이런 것들은 항상... 기본이지.
[객체 모델링]
...Player찍고 땅 찍으면, Player는 자기 상태를 보고, 걸을 것인지 뛸 것인지 결정한다. Monster 찍으면 공격하고, Monster는 상대방 공격력과 자신의 상태를 보고 다친 정도 또는 죽을 것인지를 결정한다....
Passive user
data type에서 Active
user data type이 된 것이다. 객체들은 자기 상태에 따라 적절한 행위를 취한다. 즉, 로직이 객체안에 캡슐화되어있다.
객체지향 시대로 접어들면서 데이터와 로직이 캡슐화된
클래스의 등장과 함께, 다양한 관계 표현이 나타났다. 함수
수준의 위임이 클래스 수준의 위임(delegation)으로 확장되기 시작했으며, 상속, 포함, 의존(Dependency), 추상클래스(Abstract class), 인터페이스(interface) 등이 등장했고, 이에 따라 객체들간 다양한 형태의
상호작용 표현이 가능하였다. 실세계의 복잡도를 기능과 데이터로 해체하기 보다는 복잡도 자체를 객체 속에
담고 인터페이스를 통해 보다 유연한 상호작용을 할 수 있는 능동적(Active) 소프트웨어 개념이 언어
속에 스며든 것이다.
객체지향 프로그래밍에 대한 메모리 배치를 보면(실제 눈으로 보면... 안보인다.),
코드(Code)와 데이터 공간이 분리되어 별다른 변화가 없는 것처럼 보인다. 객체가 생성될 때마다 데이터 부분의 메모리 공간이 할당되지만 코드는 공유한다.
뭐가 달라진걸까? 객체 속에 static으로
선언되지 않은 일반 멤버함수라면, 자기 자신을 가리키는(this) 포인터가
첫번째 인자로 숨겨져있다. 이렇게 데이터와 코드를 분리했지만, 논리적으로
그들을 엮어 객체라는 개념을 메모리에 실현한 것이다. 이때부터 본격적인 표리부동과 꼼수의 역사가 시작되었고, 객체 모델링이라는 매우 강력한 파워를 얻은 대신 패러다임의 혼란이 시작되면서 1980년대 후반부터 방법론의 춘추전국시대가 열리게 된다.
자... 여기서부터, 나의 게임관이 펼쳐진다. 디자인 패턴을 재미있게 읽기 위해 반드시
거쳐가야하는 놀라운... 게임관이다.
[게임]
Any Where, Any
Time, Any Game
게임은 인생이다.
아니, 내게 게임은 또 다른 형태의 완전한 세상이다. 모든
것이 있다.
* AnyWhere
게임은 항상 내 곁에 있다. 학교에서는 Portable Game Mashine, 겜방에서는 PC, 길거리에서는
휴대폰... 난 어디에서든 게임을 즐긴다.
* AnyTime
난 상항 게임을 한다. 심지어는 자면서도 게임을 한다. 게임안에는 나의 분신(GameAgent)이 있다. GameAgent는 허드렛일을 도맡아서 한다. 내가 밥먹고, 잠자는 시간처럼 바쁠때는 스스로를 키워나간다. 내가 게임에 임할때 GameAgent는 나의 행동을 모두 배우고 스스로 규칙화하며, 내가
없을 때 나를 대신한다. 물론 그는 나의 통제하에 존재한다. 나는
24시간 게임을 즐긴다.
* AnyGame
나의 인생이 하나인 것처럼, 나에게 모든 게임은 하나다. 게임안에 시뮬레이션, 아케이드, 롤플레잉
등 모든 게임이 통합되어있다. 그들 게임은 캐릭터를 공유하며, 서로간
영향을 미친다. 철인경기에서 우승할 수록, 기본 전투력은
급성장하며, 레이싱에서 우승할 수록 나의 지적 판단능력은 높아만 간다.
나는 영웅이며 나는 모든 것을 누릴 수 있는 능력과 권한이 있다. 나는 무적이다.
음... 패턴이란 무엇인가를 설명하는 것은 재미없고, 당신이 하고 있는 것들 중 어떤 것을 패턴이라고 말하는 것인지만 알려주면...
끝.
[우리나라에서 가장 많이 쓰이는 패턴]
* 가쓰(가장 많이 쓰는) 패턴 - BFP(Brute Force Pattern) : 일명 노가다 패턴, 무뇌 패턴이라고도
한다.
* 다쓰(다음으로 많이 쓰이는) 패턴 - CPP(Cut & Paste Pattern) : 재사용이라는 심오한(!) 철학을
사용하기 시작했다. 설사 남이 만든 코드라도 쓰고 말겠다는 적극적 의지가 돋보였으며, 학교를 통해 CPP를 철저히 익힌후 실제 프로젝트에 지속적으로 적용하였다. 다만 품질 있는 코드의 재사용이 아닌 무분별한 재사용이라는 점 때문에 패턴을 계속 쓰다 보면. 정제되는 느낌 보다는, 음식 냄새가 코드에 스며드는 기분 나쁜 경험을
할 수 있다는 점이 있다. 우리 나라의 뚝배기 스타일이 아닌 스파게티와 꽈배기라는 음식냄새가 코드에서
풀풀 나기 시작한다.
객체지향 시대로 접어들면서 데이터와 로직이 캡슐화된
클래스의 등장과 함께, 다양한 관계 표현이 나타났다. 함수
수준의 위임이 클래스 수준의 위임(delegation)으로 확장되기 시작했으며, 상속, 포함, 의존(Dependency), 추상클래스(Abstract class), 인터페이스(interface) 등이 등장했고, 이에 따라 객체들간 다양한 형태의
상호작용 표현이 가능하였다. 실세계의 복잡도를 기능과 데이터로 해체하기 보다는 복잡도 자체를 객체 속에
담고 인터페이스를 통해 보다 유연한 상호작용을 할 수 있는 능동적(Active) 소프트웨어 개념이 언어
속에 스며든 것이다.
객체지향 프로그래밍에 대한 메모리 배치를 보면(실제 눈으로 보면... 안보인다.),
코드(Code)와 데이터 공간이 분리되어 별다른 변화가 없는 것처럼 보인다. 객체가 생성될 때마다 데이터 부분의 메모리 공간이 할당되지만 코드는 공유한다.
뭐가 달라진걸까? 객체 속에 static으로
선언되지 않은 일반 멤버함수라면, 자기 자신을 가리키는(this) 포인터가
첫번째 인자로 숨겨져있다. 이렇게 데이터와 코드를 분리했지만, 논리적으로
그들을 엮어 객체라는 개념을 메모리에 실현한 것이다. 이때부터 본격적인 표리부동과 꼼수의 역사가 시작되었고, 객체 모델링이라는 매우 강력한 파워를 얻은 대신 패러다임의 혼란이 시작되면서 1980년대 후반부터 방법론의 춘추전국시대가 열리게 된다.
출처: 'Grady의 골 때리는 디자인 패턴' 중에서
호응이 있으면 MMORPG와 엮어서 디자인 패턴을 어디에 써야 하는지 짧은 글로 명쾌하게 설명한 'Grady의 골 때리는 디자인 패턴' 을 제 블로그에 연재하도록 하겠습니다.
@AspectJ는 애스팩트(aspects)를 애노테이션을 부여한 일반 자바 클래스로 선언하는 프로그래밍 스타일을 의미하며, AspectJ project에 의해서 AspectJ5의 일부로 공개되었다.
Spring 2.0은 AspectJ 라이브러리를 활용하여 AspectJ5와 동일하게 애노테이션 해석 기능을 수행한다. 이렇게 하면 AspectJ 컴파일러나 위버(weaver)에 의존하지 않고 Spring AOP를 통해서 AOP 적용이 가능하다.
1. @AspectJ 지원 활성화 @AspectJ 스타일의 애스팩트를 사용하려면 Spring AOP를 @AspectJ 기반의 애스펙트와 빈에 대한 자동 프록시 적용(autoproxying)이 설정되어야 한다. 자동 프록시 적용(autoproxying)이란 Spring이 특정 빈에 대해서 애스펙트를 적용(advised)이 필요한 경우에 자동으로 프록시를 생성하여 메소드 호출을 가로채고, 애스펙트 적용이 되게 하는 것을 의미한다.
내부적으로는 상당히 복잡한 메커니즘이지만 활성화 방법은 간단하다.
<aop:aspectj-autoproxy/>
위와 같은 설정은 XML 스키마를 사용할 경우에 유효하다. 만일, DTD를 써야 한다고 해도 그다지 어렵지는 않다. 간결함이 조금 덜할 뿐이다. ^^
2. 애스팩트 선언 앞서 설명한 것처럼 @AspectJ 지원이 활성된 후라면, Spring에 빈으로 설정된 클래스에 @Aspect이 선언되어 있으면 자동으로 애스팩트로 인식된다.
3. 포인트컷(Pointcut) 선언 포인트컷은 어드바이스(advice)가 실행되는 시점을 통제하기 위해 조인 포인트(join point)를 결정한다. Spring AOP는 Spring에 설정된 빈의 메소드 호출 조인 포인트만 지원하기 때문에 포인트컷을 Spring 빈의 메소드 실행에 대응시켜 볼 수 있다.
@Pointcut("execution(* transfer(..))")// the pointcut expression private void anyOldTransfer() {}// the pointcut signature
포인트컷 시그너처(signature)는 메소드 시그너쳐와 유사하나 반드시 void가 되어야 한다. 포인트컷 표현식(expression)은 @Point 애노테이션을 사용하여 표기한다. @Point 애노테이션의 값으로는 일반적인 AspectJ 포인트컷 표현식을 사용한다.
Spring AOP는 다음의 AspectJ 포인트컷 지정자(AspectJ pointcut designators)를 지원한다.
execution: 메소드 실행에 대응하는 조인 포인트. 가장 널리 쓰인다.
within: 특정 타입(에 포함되는)의 메소드 실행으로 제한
this: 참조 빈 즉, Spring AOP 프록시가 주어진 타입인 경우로 제한
target: 프록시의 대상이 되는 객체가 주어진 타입인 경우로 제한
args: 인자가 주어진 타입인 경우로 제한
@target: 실행하는 객체의 클래스가 주어진 타입의 애노테이션을 갖는 경우로 제한
@args: 실행시점의 인자가 특정 타입의 애노테이션을 갖는 경우로 제한
@within: 주어진 애노테이션을 갖는 타입으로 제한
@annotation: 조인 포인트의 주체가 주어진 애노테이션을 갖는 경우로 제한
포인트컷 표현식에서는 또한, &&, || 및 ! 와 같은 논리 연산자를 사용할 수 있으며 다른 포인트컷의 이름을 통해 포인트컷 표현식을 재사용하는 것도 가능하다.
특정 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 전환시킨다. 인터페이스가 호환되지 않아 상호 작용할 수 없는 경우에, Adapter를 이용하여 클래스 사이의 인터페이스의 호환성을 보장할 수 있다. Adapter 패턴의 용도는 실생활에서 전기 플러그의 형태가 맞지 않은 경우에 어댑터(adapter)를 사용하는 것과 같은 이치라고 할 수 있다.
또한, 인터페이스를 맞추기 위하여 결과적으로 새로운 인터페이스로 클래스를 감싸게 되기 때문에 Wrapper 패턴이라고도 한다.
2. 적용 영역
l존재하는 클래스를 사용하고자 하는데 인터페이스가 부적절한 경우
l관계가 없거나, 예상하지 못하는 클래스와 함께 쓰일 수 있는 재사용 가능한 클래스를 생성하고자 하는 경우
lAdapter는 클래스 수준의 Class Adapter와 객체 수준의 Object Adapter로 적용할 수 있다.
lObject Adapter에서는 모든 하위 클래스의 인터페이스에 대해 Adapter를 적용하기 어렵다. 이러한 경우는 부모 클래스의 인터페이스에 Adapter를 적용한다.
3. 구조
Class Adapter의 클래스 다이어그램
Object Adapter의 클래스 다이어그램
4. 적용 결과
lClass Adapter를 사용하는 경우는 Adaptee 클래스의 하위 클래스와 상호 작용할 수 없으나, Object Adapter를 사용하는 경우는 Adaptee 타입의 모든 객체와 상호 작용할 수 있다.
lClass Adapter의 경우는 Adaptee의 하위 클래스이기 때문에 Adaptee 클래스의 행위를 재정의(overriding)할 수 있으나, Object Adapter의 경우는 별도로 Adaptee 클래스의 서브 클래스를 생성하고, Adapter에서 이러한 타입의 객체를 참조해야 하는 별도의 작업을 요하게 된다.
lClass Adapter는 하나의 객체만으로 Adapter와 Adaptee 역할을 수행하게 되는 반면, Object Adapter는 adaptee 객체에 접근하기 위한 별도의 레퍼런스가 필요하다.
5. 관련 패턴
lBridge 패턴은 Object Adapter와 유사한 구조를 갖고 있지만, 서로 의도가 다르다. Adapter는 존재하고 있는 객체에 대한 인터페이스를 바꾸기 위한 의도를 갖지만, Bridge는 인터페이스를 해당 구현체(implementation)와 분리하기 위한 목적을 띈다.
lDecorator 패턴은 인터페이스 변환 없이 다른 객체의 개선이 가능하다. 순수하게 Adapter 패턴만 적용해서는 순환 구성(recursive composition)이 불가능하고, Decorator를 이용하면 가능하다.
lProxy 패턴은 Adapter와 달리 인터페이스를 변환시키지 않고, 다른 객체에 대한 대리자 역할을 수행한다.
추상화한 것(인터페이스)과 실체(구현 객체)를 분리하여, 서로 독립적으로 다양한 형태를 띌 수 있게 하고자 하는 패턴. 하나의 추상 클래스(혹은 인터페이스)에 대해 여러 개의 구현체를 갖는 것은 상속(혹은 인터페이스 구현)을 통해서도 가능하다. 그러나, 이렇게 명시적인 바인딩이 존재하는 경우 추상 클래스와 이를 구현한 클래스가 독립적으로 수정되고, 확장되거나 재사용되는 일은 매우 어려운 일이다.
상이한 플랫폼 사이에서 동일한 기능을 수행하기 위한 클래스를 작성하는 경우 실제 구현은 해당 플랫폼에 맞게 독립적으로 구현하고, 인터페이스는 해당 기능을 사용하는데 적합하게 독립적으로 정의하는 방법을 통해 유연성을 확보할 수 있다.
위의 그림은 스위치의 인터페이스(Switch)와 구현(SwitchImp)을 구분함으로써 독립적으로 다양한 형태로 발전할 수 있는 유연성을 묘사한 것이다.
2. 적용 영역
l추상 클래스 혹은 인터페이스와 구현 클래스 사이의 항구적인 바인딩(permanent binding)을 피하고자 하는 경우
l추상 클래스 혹은 인터페이스와 구현 클래스가 별도로 상속을 통해 확장되어야 하는 경우
l구현 클래스에서의 변화가 클라이언트에 영향을 미치지 않기를 원하는 경우
l다수의 객체들이 하나의 구현 클래스를 공유하면서 이러한 사실을 모르게 하고자 하는 경우
3. 구조
4. 적용 결과
l인터페이스와 구현 사이의 결합도를 낮춘다(decoupling).
l추상 클래스 혹은 인터페이스와 구현 클래스가 독립적인 계층 구조를 지니기 때문에 확장성이 개선된다.
l클라이언트가 구현 클래스에 대한 세부 내용을 알 수 없다.
5. 관련 패턴
lAbstract Factory는 특정 Bridge를 생성하고, 설정할 수 있다.
lBridge 패턴은 Object Adapter와 유사한 구조를 갖고 있지만, 서로 의도가 다르다. Adapter는 존재하고 있는 객체에 대한 인터페이스를 바꾸기 위한 의도를 갖지만, Bridge는 인터페이스를 해당 구현체(implementation)와 분리하기 위한 목적을 띈다.
6. 참고 문헌
위키피디아: Bridge
충북대 번역글: The Bridge Pattern OOPSLA ’97 workshop of Non-Software Examples of Software Design Patterns by Michael Duell, John Goodsen, and Linda Rising
Composite 패턴은 클라이언트가 트리 구조(tree structures)를 형성하는 개별 객체들과 이들이 모여서 만들어진 구성체를을 동일하게 취급하는 것을 가능하게 한다.
그래픽 컴포넌트를 이용하여 그림을 그리는 예를 생각해보자. 선, 원, 문자 및 사각형과 같은 기본적인 그래픽 컴포넌트를 다루는 것은, 이들이 모여서 형성되는 구성체(Composite)를 다루는 방법과 동일하다. 즉, 선을 화면에 표시하는 작업이나, 선과 원이 합쳐진 객체를 화면에 표현하는 것이나 근본적으로 동일한 행위라는 것에서 고안된 패턴이 Composite 패턴이다.
J2SE SDK에 포함된 java.awt 패키지를 사용해본 개발자라면 이미 Composite 패턴에 익숙해져 있다고 할 수 있다. java.awt.Component 객체를 포함할 수 있는 Container 객체 역시 Component 클래스에 속하기 때문에 동일한 인터페이스를 갖고 있다. 다시 말해서 Container에 Component를 붙이는 방법이나, Container를 붙이는 방법이나 동일한 것과 같이 Component 클래스에 정의된 인터페이스를 이용할 수 있다는 것이다.
java.awt.Container의 계층 구조
2. 적용 영역
l객체의 부분과 전체 계층 관계(part-whole hierarchies)를 표현하고자 하는 경우
l클라이언트가 구성체(Composite)와 이들을 구성하는 개별 객체의 차이점을 인지할 필요가 없게 하고자 하는 경우
3. 구조
4. 적용 결과
lComposite 패턴은 기본 객체(primitive object)와 이들의 구성체로 이루어진 객체(composite object)로 이루어지는 클래스 계층 구조를 정의한다.
l클라이언트는 구성체로 이루어진 경우나 개별 객체의 경우 모두 동일한 형태로 취급하기 때문에, 클라이언트가 단순해진다.
l새로운 종류의 컴포넌트 추가가 용이하다. Composite나 Leaf를 상속하기만 하면 현재의 계층 구조와 클라이언트에서 그대로 사용될 수 있다.
l남용하는 경우는 너무 일반적인 설계를 초래할 수도 있다.
5. 관련 패턴
lChain of Responsibility 패턴을 위해 컴포넌트-부모 연결(component-parent link)이 사용된다.
lDecorator 패턴은 종종 Composite 패턴과 함께 사용되어, 공통의 부모 클래스를 갖는다. 이때, Decorator가 add, remove 및 getChild와 같은 Component 인터페이스를 지원해야 한다.
lFlyweight 패턴은 컴포넌트를 공유하지만, 부모 객체를 참조할 수가 없다.
lIterator 패턴은 Composite 객체를 순회(traverse)하는데 사용될 수 있다.
lVisitor 패턴은 Composite과 Leaf 클래스를 넘나들면 분포하게 되는 오퍼레이션과 행위들을 로컬화(localization)할 수 있다.
어떤 객체에 대해 동적으로 부가적인 책임(responsibilities)을 부여하고자 할 때 사용된다. Decorator 패턴은 기능을 확장하거나, 서브 클래스를 생성하는 것에 대해 유연한 대안을 제공한다.
어떤 클래스에 대해서 테두리(border)를 부여하고자 하는 경우, 상속을 통해 테두리를 갖는 하위 클래스를 만들 수 있다. 그러나, 이는 정적인 방법으로 테두리가 생성되어, 테두리가 생기는 시점이나 방법을 통제할 수가 없다. 이보다 유연한 방법은 테두리를 갖는 객체가 테두리를 갖고자 하는 객체를 포함하는 것이다. 이렇게 부가적인 기능을 원하는 객체를 포함하는 객체를 Decorator라고 부른다.
위 그림은 aTextView라고 하는 객체에 스크롤(scroll)과 테두리를 추가하고자 하는 경우, 상속을 통해 클래스를 확장하지 않고, 이를 위한 Decorator를 생성하여 해결하는 모습을 나타낸다.
2. 적용 영역
l다른 객체에 영향을 미치지 않으면서, 특정 객체에 동적으로 책임(responsibilities)을 추가하려는 경우 사용한다.
l클래스 정의를 알 수 없는 경우와 같이 상속을 통한 클래스의 확장을 할 수 없는 경우
3. 구조
4. 적용 결과
l정적인 상속을 이용하는 것보다 유연하다.
l계층구조를 따라 기능이 누적되는 현상을 막을 수 있다. Decorator 패턴을 이용하면 필요한 만큼만 기능을 구현할 수 있다. 따라서, 시스템이 작은 객체들의 상호 연결된 구조를 이루게 하여, 적절하게 수정(customize)하기는 수월하지만, 디버깅이 어려울 수 있다.
5. 관련 패턴
lDecorator 패턴은 인터페이스 변환 없이 다른 객체의 개선이 가능하다. 순수하게 Adapter 패턴만 적용해서는 순환 구성(recursive composition)이 불가능하고, Decorator를 이용하면 가능하다.
lDecorator 패턴은 종종 Composite 패턴과 함께 사용되어, 공통의 부모 클래스를 갖는다. 이때, Decorator가 add, remove 및 getChild와 같은 Component 인터페이스를 지원해야 한다.
lDecorator 패턴이 객체의 피부를 바꿀 수 있게 해준다면, Strategy 패턴은 객체의 내장을 바꿀 수 있게 해준다. 이들은 객체 변경을 위한 두 가지 선택사항이 된다.
Façade 패턴은 복잡한 서브 시스템에 통일된 인터페이스를 제공함으로써 복잡한 API를 단순화 시켜준다. 시스템을 서브 시스템 단위로 나누어 구성하는 것은 시스템의 복잡도를 낮춰주지만, 동시에 서브 시스템 사이에서의 통신 부하와 결합도가 증가하게 된다. 이러한 서브 시스템 사이의 의존도를 낮추고, 서브 시스템의 사용자 입장에서 사용하기 편리한 인터페이스를 제공하고자 하는 것이 façade 객체이다.
Façade 객체는 실생활에서의 고객 서비스 센터와 유사하다. 가령, 어떤 상품을 구매하는 과정에서 문제가 생겼다고 가정할 때, 고객이 문제의 성격에 따라 해당 부서에 직접 연락하는 것이 아니라 고객 서비스 센터를 통하는 것은 Façade 패턴에 대한 좋은 유추 사례가 될 수 있다.
2. 적용 영역
l복잡한 서브 시스템에 대해 간단한 인터페이스를 제공하기를 원하는 경우
l클라이언트와 인터페이스의 구현 클래스 사이에 의존도가 높은 경우
l서브 시스템을 레이어(layer)로 구분하고자 하는 경우
3. 구조
4. 적용 결과
l서브 시스템의 컴포넌트로부터 클라이언트를 격리하여, 클라이언트가 쉽게 서브 시스템을 이용할 수 있다.
l서브 시스템과 클라이언트 사이의 의존성을 낮춘다.
lFaçade 패턴을 사용한다고 해도, 필요한 경우 서브 시스템의 클래스에 직접 접근할 수도 있다. 즉, 일반화 정도(generality)와 개발의 편의성 사이에서의 적당한 합의점을 찾아야 한다.
5. 관련 패턴
lAbstract Factory는 Façade와 함께 사용되어 서브 시스템에 독립적으로 서브 시스템의 객체를 생성하는 인터페이스를 제공한다. 또한, 특정 플랫폼에 국한된 클래스를 숨기기 위해 Façade 대신 사용할 수도 있다.
lMediator는 기존의 클래스의 기능을 추상화한다는 의미에서 Façade와 유사하다. 그러나, Mediator의 목적은 서로 직접적인 연관을 갖는 객체(colleague object) 사이에서의 커뮤니케이션을 추상화하는 것인데 반해, Façade는 서브 시스템의 인터페이스를 추상화하는 것이다.
l하나의 Façade 객체만이 요구되는 경우에는 Façade 객체가 Singleton 형태를 취하기도 한다.
객체 지향 소프트웨어는 객체의 생성과 파괴라는 기본적인 주기에 따라 CPU 주기의 낭비, 가비지 컬렉션(garbage collection) 및 메모리 할당에 걸쳐 부하를 유발한다. 시스템이 이들 객체를 공유하게 된다면 부하를 피할 수 있다. 그러나, 특정 객체의 현재 사용자에게 국한된 정보는 공유할 수가 없다. Flyweight 패턴은 이러한 상태 정보를 객체의 재사용할 수 있는 부분과 분리시키는 접근 방법을 취한 것이다.
본질적인(intrinsic) 상태와 외래(extrinsic) 상태를 구분하는 것이 Flyweight 패턴의 핵심 개념이다. 본질적인 상태는 Flyweight 객체에 저장되어 있다. 이들 상태는 Flyweight의 컨텍스트에 독립적인 정보로 이루어지며, 공유가 가능하다. 외래 상태는 Flyweight의 컨텍스트에 따라 매우 다양하며, 공유가 불가능하다. 클라이언트 객체가 외래 정보를 요구할 때 이를 Flyweight에 전달할 의무를 지닌다.
2. 적용 영역
l애플리케이션이 다수의 객체를 이용하는 경우
l다수의 객체로 인해 저장에 고비용이 소요되는 경우
l객체의 대부분의 상태 정보가 외래 상태(extrinsic state)화 할 수 있는 경우
l외래 상태가 제거되는 경우 다수의 객체가 상대적으로 적은 숫자의 공유 객체로 대치될 수 있는 경우
l애플리케이션이 객체의 식별자에 의존하지 않은 경우
3. 구조
4. 적용 결과
l외래 상태를 전송하고, 찾아내는 과정에서 실행 시점의 비용(costs)을 초래할 수 있다. 이는 저장공간의 절약과 상충관계를 갖는다.
l저장공간 절약은 다음 몇 가지 요인과 상관관계를 갖는다.
5. 관련 패턴
lFlyweight 패턴은 종종 Composite 패턴과 결합되어 Leaf 노드를 공유하는 논리적인 그래프 구조를 구현한다.
lState와 Strategy 객체를 Flyweight로 구현하는 것은 종종 최선책이 되기도 한다.
특정 객체에 대한 접근을 통제하기 위해 대리 객체 혹은 위치를 지키는 역할(placeholder)을 제공할 수 있다. 커다란 이미지 객체를 생성하고, 초기화 하는 경우를 생각해보자. 특히, 네트워크를 통해 다운로드 받아서 객체를 생성하거나 초기화 하는 경우라고 하면, 상당한 시간이 소요될 것이다. 이러한 경우 실제 객체에 대한 대리자(proxy)를 생성하고, 객체에 대한 요청을 대신 수행할 수 있다. 단지, 실제 이미지를 화면에 표시해야 하는 경우처럼 반드시 실제 객체가 필요한 경우만 이를 생성하여 부하를 최소화 할 수 있다.
그러나, Proxy가 단지 이러한 용도에만 적용되는 것이 아니다. 아래 적용 영역 항목에서 일반적으로 Proxy가 사용되는 경우를 나열한다. 대개 단순한 포인터로서의 역할보다는 부가적인 기능을 수행하는 복잡한 레퍼런스(reference)가 필요할 때 많이 사용된다.
2. 적용 영역
lRemote Proxy로서의 역할. 원격의 객체에 대한 로컬의 대리자(local representative)를 제공하는데 쓰인다.
lVirtual Proxy로서의 역할. 많은 비용이 요구되는 객체를 생성하는 경우에 사용된다. 앞서 컨텍스트 항목에서 설명한 경우가 이러한 적용의 예다.
lProtection Proxy로서의 역할. 보호가 요구되는 객체 자체에 대한 접근을 통제하고 대리자를 통해 접근 가능한 정도만 노출시키고자 할 때 쓴다.
lSmart Reference로서의 역할. 객체에 대한 단순한 접근 이외의 부가적인 작업을 수행할 필요가 있을 때 사용한다.
3. 구조
4. 적용 결과
lRemote Proxy를 사용하는 경우 객체의 원격지 존재 여부를 숨길 수 있다.
lVirtual Proxy를 사용하는 경우 성능 최적화를 고려할 수 있다.
lProtection Proxy와 Smart Reference를 사용하는 경우는 모두 객체에 접근할 때, 수반되는 부가적인 작업이 수행되는 것을 가능하게 한다.
5. 관련 패턴
lAbstract Factory, Builder 및 Prototype을 비롯해서 많은 패턴과 병행해서 사용될 수 있다.
클래스가 단 하나의 인스턴스만을 갖도록 유일한 접근 지점을 제공한다. 중요한 자원에 대한 관리를 하는 클래스의 경우는 유일한 인스턴스의 존재를 보장해야 한다. 가령, 데이터베이스 커넥션과 같이 시스템의 소중한 자원을 관리하는 객체의 경우 다수의 객체가 커넥션을 할당하고, 해제한다면 효과적인 관리가 어려워진다. 이와 같이 시스템에 유일하게 존재하는 것을 보증할 때 사용하는 패턴이 Singleton 패턴이다.
실생활에서 한 나라의 대통령은 특정 시점에 대해서 한 사람만이 존재하기 마련이다. 중요한 의사결정을 함에 있어서 여러 명의 대통령이 각자의 의사결정을 별도로 수행한다면 곤란하지 않은가? 이러한 사례는 Singleton의 필요성을 유추하는데 개념적인 수준에서의 도움이 될 수 있다.
2. 적용 영역
l특정 클래스에 대해 클라이언트에게 잘 알려진 유일한 인스턴스가 요구될 때
l클라이언트가 특정 클래스의 유일한 인스턴스의 수정 없이 이를 상속한 클래스의 인스턴스를 사용할 때
3. 구조
4. 적용 결과
lSingleton 클래스는 유일한 인스턴스를 캡슐화한다. 이로써 인스턴스에 대한 접근 방법과 접근 시점에 대한 통제가 가능하다
lSingleton은 전역 변수(Global variable)에 대한 개선책이다. Singleton을 사용하면 전역 변수 사용시에 발생할 수 있는 네임스페이스 충돌[각주:1]을 막을 수 있다. 즉, 네임 스페이스 낭비를 막는다.
l상속을 통해 구조나 행위들을 수정할 수 있다.
l유일한 인스턴스의 사용 대신에 특정한 숫자의 인스턴스를 사용하도록 수정이 가능하다.
l언어에 따라 제약 사항을 갖게 되는 클래스 오퍼레이션(class operation)보다 유연성을 갖는다. 가령, Singleton은 하나 이상의 인스턴스 생성이 가능하다.
l자바에서 정적 메소드(static method)를 갖는 클래스와 Singleton 클래스를 비교하면, Singleton을 사용하는 경우 명확한 객체 지향 설계가 가능한 반면, 정적 메소드를 사용하는 클래스는 단순한 기능 목록이 될 우려가 있다.
l복수의 VM이 사용되는 경우 각각의 VM마다Singleton 인스턴스가 생성되기 때문에 분산 애플리케이션은 이를 유의해야 한다.
l복수의 클래스 로더가 쓰이는 경우 동일한 이름을 갖는 각각의 Singleton 인스턴스가 생성되기 때문에 네임스페이스를 구분해야 한다.
l참조되지 않는 Singleton 인스턴스는 가비지 콜렉션(garbage collection)에 의해서 파괴되고, 재생성 될 수 있다. 이때는 새롭게 초기화가 수행되는 점을 유의해야 한다.
5. 관련 패턴
lAbstract Factory, Builder 및 Prototype을 비롯해서 많은 패턴이 Singleton과 병행해서 사용될 수 있다.
case TITLED_BORDER: paintTitledBorder(g);
break;
...
}
}
위의 코드를 보자. getBorderType() 메소드에 의해 반환되는 타입에 따라, 위와 같은 복잡한 처리가 필요한 경우, Border라고 하는 공통의 인터페이스를 정의하고, 실제 알고리즘 구현은 이를 구현하는 클래스가 담당한다. 그렇게 되면 아래와 같이 클라이언트의 처리부분이 간결해진다.
객체의 내부 상태가 변화될 때, 해당 객체의 행위를 수정하는 것을 가능하게 하기 위해 사용된다.
State 패턴은 상태(state)를 클래스로 표현한 다. 현실세계에서 상태를 사물(object)로 보는 일은 거의 없기 때문에, 상태를 클래스로 표현하는 일은 어색할 수 있다. 그러나, 상태를 클래스로 표현하면 클래스의 교체를 통해서 ‘상태의 변화’를 나타낼 수 있고, 새로운 상태를 추가해야 하는 경우에는 무엇을 프로그램 하면 되는지 명확해진다.
2. 적용 영역
l객체의 행위가 객체의 상태에 의존하고, 객체의 행위가 실행 시점에서의 객체의 상태에 따라 변화되어야 할 경우
l오퍼레이션이 크고, 객체의 상태에 따라 다수의 조건문을 포함하는 경우
3. 구조
4. 적용 결과
lState 패턴은 특정 상태에 국한된 행위를 다른 상태들을 위한 행위와 구분하여 로컬화 한다.
lState 패턴은 명시적으로 상태 전이를 수행한다.
lState 객체는 공유될 수 있다.
5. 관련 패턴
lState 객체가 어떻게 공유되며, 언제 공유될 것인지는 Flyweight 패턴이 설명한다.