BoilerPlatePattern

분류:번역 분류:설계패턴


Design Patterns And 3D Gaming(1)

by Edward Kmett ([email protected])


이 패턴에 대한 내용적 토론은 여기 로..



보일러플레이트(Boilerplate) 패턴


최근 게임스파이(gamespy)에 실린 한 글에서, 팀 스위니(Tim Sweeny)는 객체 지향적 프로그래밍에서 그가 원했던 '가상 클래스'라는 '새로운' 개념을 소개했다. 여기서 설명하고자 하는 패턴은 그러한 개념을 제공하는데 쓰일 수 있으며, 또한 구현 당 하나의 클래스로 깔끔하게 분할되지 않는 다양한 개수의 구현들에 대한 하나의 퍼사드를 작성하고자 할 때 매우 유용할 것이다. 그러한 예에는 그래프, 네트워킹 계층, 렌더링 API, 사운드 서브 시스템 등이 포함된다. 그러한 것들은 노드와 변들, 소켓과 포트, 텍스쳐와 모델, 사운드 조각(sound snippets) 등등 내부적으로 작업을 수행하는 서로 복합적인 관련이 있는 클래스들을 포함한다. 이에 대한 자바의 해결책은 순수 추상 인터페이스들(블랙박스 상속)을 정의하고 구현들의 각 집합에 대한 연결을 독립적으로 구현하는 것이다. 이러한 접근은 일반적으로, 각각의 드라이버가 여러 뭉치의 보일러플레이트(boiler plate) 코드를 만들어야 하는 것처럼, 코드 복제가 일어나게 된다. 아래의 패턴은 여러분이 보통 이러한 '순수' 구현들에 대해 잘라-붙여넣기식의 성가신 코딩을 대신하는 템플릿을 만드는 방법을 소개한다.

필자는 이것을 '보일러플레이트(Boilerplate)' 패턴이라고 부르는데, 필자가 이 패턴을 필자 자신의 코드가 아닌 다른 곳에서 본것은 'C++ Gems' (Stanley Lippman 저, ISBN: 0135705819) 라는 책의 유한 상태 기계에 대한 장에서였다. 'C++ Gems'에서 이 패턴에 대한 처리는 필자의 것과 다르며 만약 아래의 완전하지 않은 코드로 어려움을 겪는다면 위 책을 참고해도 좋다. 사실 당신 스스로가 그 책을 구해보려 할 지도 모른다. 물론, Design Patterns 책을 구입하지 않았다면 먼저 구입하고 나서 말이다. 이 패턴은 눈알을 빙빙 돌게 만들며 처음 보았을 때는 언듯 간단해 보이지만, 이 패턴이 없다면 당신은 같은 코드를 수없이 작성하는 처지에 놓일 수 있다.

렌더링 API 의 시작이 될 수 있는 뼈대(skeletal) 인터페이스를 설명하고자 한다. 물론 완전한 기능을 가진 렌더링 API에서는 더 많은 메소드들을 사용하며 아마 훨씬 더 많은 클래스, 메쉬, 클리퍼, 가시/비가시 구조를 염두에 두어야 할 것이다.

    #ifndef INCLUDED_RENDERER
    #define INCLUDED_RENDERER

    class Texture {
        public:
            virtual ~Texture() {}
            virtual void makeCurrent()=0;
    };

    class Renderer {
        public:
            virtual Texture * loadTexture(const char * name);
    };

    #endif

Figure 1. renderer.h

이러한 인터페이스를 제대로 구현한다면, Renderer 객체로부터 Texture 객체를 얻을 수 있으며, Texture의 makeCurrent()를 호출해서 그 Texture를 현재 텍스쳐로 만들 수 있게 된다. 이러한 구조라면 Renderer가 추가적인 기능들(다각형의 렌더링이라던가 행렬 관리 등)을 가지도록 쉽게 확장할 수 있다.

Unfortunately implementing all of your Renderer and Texture classes for different 3d APIs could lead to the onset of repetitive strain injury and if you now have to deal with several duplicate implementations of the behind the scenes linkages between the Texture class and the Renderer. Admittedly in this contrived example we're probably only talking about 3 Implementations so its not so bad, but envision a case where you may have 50-100 implementations, perhaps your monster AI if this all seems like too much work.

그러나 서로 다른 3D API들을 지원하게 하려면 그 종류만큼의 여러 Renderer 와 Texture 클래스들을 구현해야 하며, 또한 그 클래스들이 제대로 작동하기 위해서는 여러 Texture 클래스와 Renderer 클래스를 결합시키는 또 다른 구조들이 필요할 것이다. Texture와 Renderer 클래스들이 많을 수록 조합의 수도 많아지므로 상당한 반복 작업이 필요한데, 이는 매우 고통스러운 일이 될 것이다. 이 글에 나오는 예제에서는 3개 정도의 구현들에 대해서만 이야기할 것이기 때문에 그건 그다지 나쁘지 않지만, 50-100 개의 구현들을 가지는 경우를 상상해 보라.... 너무 많다고 생각되는가? 아마 당신의 몬스터 인공지능의 경우가 그러할 것이다.

The real evil in cut & paste code comes when you discover a bug. If the code that you cut&pasted in 30 places to manage a linked list leaks memory whenever you delete a node, you may wind up discovering and fixing each of these mistakes as you find them rather than fixing them all at once, simply because you're apt to forget all the locations you pasted the code to, so OOP came around and people started generalizing algorithms and structures and the C++ Standard Library was born, which had all sorts of handy-dandy containers that worked of void*'s to juggle and sort and mangle your objects, these ran afoul of the fact that void*'s could be cast to anything and miscasting is a source of a good deal of bugs. The Standard Template Library implements these algorithms and structures using templates, trading a bit of compiler time for type checking your code.

잘라-붙여넣기식의 코드의 정말 나쁜 점은 버그를 찾을 때 나타난다. 만약 당신이 링크드 리스트를 관리하기 위해 30 군데에 잘라-붙여넣기식으로 작성한 코드가 노드를 삭제할 때마다 메모리를 누수시킨다면, 당신은 모두를 한번에 고치기 보다는 그 실수를 찾을 때마다 하나씩 발견해 고쳐야 하는 처지에 놓일 것이다. 이는 당신이 코드를 어디에 붙여넣기를 했는지 그 모든 위치를 기억하기 어렵기 때문이며, 그러한 이유로 객체지향프로그래밍(OOP) 가 만들어졌고 사람들이 알고리즘과 구조체들을 일반화시키기 시작해서 C++ 표준 라이브러리가 탄생하게 된 것이다. 이 라이브러리는 당신의 객체들을 저글(juggle)하고 정렬하고 맹글(mangle) 하기 위해 void* 들로 작업된 모든 종류의 수작업 컨테이너들을 가지고 있다. 이는 void* 들이 어떤 것으로라도 캐스팅될 수 있으며 잘못된 캐스팅은 수많은 버그의 원인이 된다는 사실과 상반된다. 표준 템플릿 라이브러리(The Standard Template Library)는 이러한 알고리즘과 구조들을 템플릿을 사용해 구현하는데, 이때 코드를 타입 체킹하기 위한 약간의 컴파일러 시간을 감수하게 된다.

Continuing with the example, I'm going to use templates that change who they inherit from based on a passed in parameter. This is a sneaky way to avoid the overhead of writing repetitive proxy code for mixin classes and other such busywork.

넘겨받은 인자에 따라 어떤 것을 상속받을 지를 결정하는 템플릿을 사용하여 예제를 계속하겠다. 이것은 여러 클래스들을 위한 반복적인 코드를 작성해야 하는 오버헤드 뿐 아니라 다른 그러한 힘든일들을 피할 수 있는 다소 치사한 방법이다.

    #ifndef INCLUDED_RENDERER_TEMPLATES
    #define INCLUDED_RENDERER_TEMPLATES

    #include 'renderer.h'

    template <class Baseclass, class Renderer> class TextureTemplate : public Baseclass {
        private:
            Renderer * m_renderer;
        public:
            TextureTemplate(Renderer &amp; rend, const char * name) : m_renderer(&amp;rend), Baseclass(name) { }
            ~TextureTemplate() {
                m_renderer->unregisterTexture(this);
            }
            void makeCurrent() {
                m_renderer->makeCurrent(this);
            }
    };

    template <class Baseclass, class TextureBase> class RendererTemplate : public Baseclass {
        public:
            typedef TextureTemplate<TextureBase> MyTexture;

            Texture * loadTexture(const char * name) {
                return new MyTexture(this,name)
            }
    };

    #endif

Figure 2. renderer_templates.h

Now we've got 2 classes and 2 templates that don't seem to do anything. The templates lie in a separate header because we don't want to trouble anyone who is just using the interface with templates that are only useful for implementing the interface.

이제 아무것도 할 것 같지 않은 2개의 클래스와 2개의 템플릿을 만들었다. 템플릿은 별도의 헤더파일에 있는데, 이는 인터페이스를 구현할 때에만 필요한 템플릿들의 인터페이스를 사용하기만 하는 사람들이 혼란스럽지 않게 하기 위함이다.

    #ifndef INCLUDED_GLRENDERER
    #define INCLUDED_GLRENDERER

    #include 'renderer_templates.h'

    class GLTextureImpl : public Texture {
        public:
            GLTextureImpl(const char * name, blah blah blah) {
                load up texture using opengl...
            }
            virtual ~GLTextureImpl() {}
    };

    class GLRendererImpl : public Renderer {
        public:
            void makeCurrent(GLTextureImpl &amp; texture) {
                ... do gl texture stuff here ...
            }
        void unregisterTexture(GLTextureImpl &amp; texture) {
            ... do gl texture stuff here ...
            }

    };

    typedef TextureTemplate<GLTextureImpl,GLRendererImpl> GLTexture;
    typedef RendererTemplate<GLRendererImpl,GLTextureImpl> GLRenderer;

    #endif

Figure 3. glrenderer.h

The templates are initially a fair chunk of work, but the work pays off as you add implementations by reducing mindless repetitive tasks and letting you focus on real work. The templates do not affect your overall compile time appreciably as they are included only in the individual source files that have to do with implementing your Renderer. You gain implementation while maintaining type safety by sandwiching your code between the templated typed code and the abstract implementation. Inheritance allows you to let calls trickle down the hierarchy and the template allows calls to be made 'up' the hierarchy without the virtual function call overhead.

템플릿들은 처음에는 어지간히 할 작업이 많지만, 구현들(implementations)을 점점 추가할수록 이익이 된다(단순 반복 작업이 줄며, 그만큼의 시간에 진정한 작업에 몰두할 수 있으므로). 템플릿들 때문에 전반적인 컴파일 시간이 현저히 떨어지지는 않는다. 템플릿들은 Renderer 클래스의 구현과 관계있는 개별 소스 파일들에만 포함되기 때문이다. 당신은 템플릿으로 타입이 정의된 코드와 추상 구현들 사이에 당신의 코드를 끼워넣음으로써 타입 안전성(type safety)을 유지하면서 구현할 수 있게 된다. 상속은 호출들(calls)을 계통적 구조 안에서 아래로 타고내려갈 수 있게 만들며, 또한 템플릿은 가상 함수 호출의 추가 부담없이도 호출들을 계통 구조의 '위'로 거슬러 올라갈 수 있게 만든다.

    Interface
        Implementation : public Interface
            TemplateWrapper<Implementation> : public Implementation

Figure 4. Hierarchy

Combined with other patterns like the Singleton and Factory you can reduce the repetitiveness of your code. While this may have perverse effects when a customer or old fashioned manager rates you by lines of code written, it can make for much more maintainable code and gain you the respect and fear of your peers. =)

단일체(Singleton)와 팩토리(Factory) 같은 다른 패턴들과 조합하면 코드의 반복을 줄일 수 있다. 물론 고객이나 구식의 관리자가 코드의 양으로 보수를 책정한다면 수입이 줄어들지도 모르겠지만, 코드의 반복을 줄이면 훨씬 더 관리하기 쉬운 코드를 만들 수 있고 당신의 동료에게서 존경과 경외를 받을 수도 있다. =)

Harmless January 26, 2000

2000년 1월 26일

You can contact the author at the following address:

다음의 주소를 통해 저자와 연락할 수 있다: mailto:[email protected]

You can also check out his web site at: http://www.bloodshed.com/

또한 그의 웹사이트를 방문할 수도 있다: http://www.bloodshed.com/

This document is Copyright ?1999 Edward Kmett and may not be reproduced in any way without explicit permission from the author (Edward Kmett). All Rights Reserved. Printed on flipCode with permission. Any and all trademarks used belong to their respective owners. Best viewed at a high resolution. The views expressed in this document are the views of the author and NOT neccesarily of anyone else associated with flipCode.

이 문서는 1999 Edward Kmett 에게 저작권이 있으며 저자(Edward Kmett)의 명백한 허가없이는 어떠한 형태로도 재생산될 수 없다. 저작권 소유. flipCode 에는 허락하에 게시되었다. 사용된 모든 상표권들은 각 소유자들에게 속한것이다. 고해상도에서 가장 잘 보인다. 이 문서에서 설명된 내용은 저자의 견해이며 flipCode 에 관계된 어떤 다름 사람의 견해도 아니다.

by Edward Kmett ([email protected])


References

(1) See