단순한 데이터 구조의 장점

데이터 구조가 단순하면 이를 처리하는 함수가 단순해지고 그러면 테스트가 쉬워진다고 생각하게 된 경험을 정리했다.

자바스크립트를 사용하여 개발하면서 많은 기술들을 접하게 된다. 모두 검토하여 좋은 것만 추려 배우고 일에 적용하고 싶지만 현실은 녹녹지 않다. 그래서 올해 배울 기술 두 개를 정했는데 함수형 프로그래밍 방법과 테스트다. 그중 함수형 프로그래밍을 익히기 위해 선택한 방법은 작은 게임을 여럿 만들어 보는 것이다. 함수형 프로그래밍을 위한 라이브러리인 lodash를 사용하기로 했고 아래와 같은 전제를 두었다.

데이터 구조는 작고 간단하게 만들고 여러 함수를 통하여 데이터를 변경한다

억지로 if, for 문을 사용하기 않으려고 노력해 보기도 했다. 이 차원 배열 여러 개를 사용하여 작은 게임 세 개를 만들고 보니 데이터 구조는 단순하고 함수는 점점 더 작아지는 것을 발견했다. 이 게임에서 사용한 데이터 구조를 정리했다.

tetris game screenshotsnake game screenshotblock game screenshot

제목이 기능을 설명할 만큼 간단한 게임들이다. 각 게임은 게임 모듈을 따로 가지고 있으며 게임 모듈은 테스트 용으로 터미널에서 동작하는 기능을 가지고 있다. 게임들은 정보를 배열에 저장하는데 이 배열은 두 가지 이벤트에 의하여 변경된다.

  • 일정한 간격의 틱
  • 사용자의 키 입력

일정한 간격의 틱
테트리스 게임에서 틱은 바닥에 쌓을 블록을 밑으로 내려오게 한다. 뱀 게임에서는 틱이 뱀을 줄이어 나가게 하고 블록 게임에서는 운석 불록을 밑으로 내려가게 하는 동시에 우주선이 발사한 미사일을 위로 옮기는 일을 한다.

사용자의 키 입력
테트리스 게임에서는 밑으로 내려오는 블록의 방향을 정하고 회전한다. 뱀 게임에서는 뱀 머리의 방향을 정하고 블록 게임에서는 바닥에 붙어 있는 우주선을 좌우로 움직이며 미사일을 발사한다. 자 이게 다다. 더 줄여서 말하면 데이터 구조는 배열이고 이 배열들을 변경하는 함수들은 여럿이다. 사용자의 키 입력이나 주기적인 틱이 발생하면 배열 데이터를 변경한다. 그렇다면 함수형 프로그래밍 방법으로 만들어서 좋은 점은 무엇일까? 세 가지다.

  • 단순한 데이터 구조
  • 단순한 함수
  • 쉬운 테스트

단순한 데이터 구조
아래 세 게임의 데이터 구조는 아래처럼 단순하다.

Tetris
+-----------+   +-----------+   +-----------+
|           |   | +-------+ |   | +-------+ |
|           |   | | +-----+ |   | | +-----+ |
|     +-+   | + | +-+       | = | +-+ + +   |
+--+  | +-+ |   |           |   |--+  | +-+ |
|  +--+   +-+   |           |   |  +--+   +-|
+-----------+   +-----------+   +-----------+
  bgPanel         toolPanel        screen

Snake
+-----------+   +-----------+   +-----------+
| +-+       |   |       +-+ |   | +-+   +-+ |
| +-+       |   |       | | |   | +-+   | | |
|           | + |       | | | = |       | | |
|           |   | +-----+ | |   | +-----+ | |
|           |   | +-------+ |   | +-------+ |
+-----------+   +-----------+   +-----------+
  applePanel      snakePanel       screen

Block
+-----------+   +-----------+   +-----------+
|           |   |           |   |  +----+   |
|           |   |           |   |  |  +-+   |
|           | + |    +-+    | + |  +--+     |
|    +-+    |   |    +-+    |   |           |
|  +-+ +-+  |   |           |   |           |
+--+-----+--+   +-----------+   +-----------+
 shuttlePanel    missilePanel  meteoritePanel

각 게임은 배열을 변경하기 위해 4개의 함수를 사용한다.

  • init
  • tick
  • key
  • join

각 함수의 입력과 출력을 나타내면 아래와 같다. 이차원 배열을 패널이라고 하겠다.

init => 패널들
패널들 => tick => 패널들
패널들, 사용자 키 => key => 패널들
패널들 => join => 패널

init은 처음 패널들을 만들고 tick과 key는 패널들을 받아서 패널들을 돌려준다. join는 패널들을 받아서 하나의 패널을 돌려준다. 게임 중간중간의 경과를 보여주기 위해서다. 함수간 서로 간섭이 없음으로 init 함수를 제외하고는 순서에 상관없이 호출될 수 있는 장점이 있다. 패널들이 단순함으로 이를 표시하는 대상이 브라우저나 터미널처럼 서로 달라도 구현하기 쉽다. 하나의 데이터 구조를 계속 변경하는 과정이 게임이다. join 함수에서 돌려주는 패널들을 저장했다가 다시 사용하면 저장한 시점에서 바로 이어 게임을 할 수 있다. 이 코드는 아래처럼 단순하다. 게임의 현재 상태는 패널들에 저장되어 있으며 이 값들은 어느 함수에도 저장되지 않는다. 현재는 블록 게임 모듈에만 적용되어 있다.

// save
global.savedState = _.cloneDeep(global.panels);

// load
global.panels = global.savedState;

아래 그림처럼 패널 정보를 계속 업데이트하는 방식이다.

단순한 함수
단순한 데이터 구조는 단순한 함수를 가능하게 한다. 대부분의 함수가 패널들을 입력 받고 패널들을 출력하는 인자가 한 개인 순수 함수다. 이런 함수는 읽기도 좋고 변경하기도 테스트하기도 쉽다. 이런 작은 함수들을 조합해서 게임을 만들 수 있다. 하나의 함수의 결과가 다른 함수의 입력으로 이어지는 과정을 통하여 함수들을 파이프라인 처럼 붙여서 사용할 수 있다. 추가 기능을 넣을 때는 수정할 함수를 찾거나 없다면 새롭게 함수를 하나 더 만들어서 파이프라인에 끼워 넣으면 된다. 예를 들어 tick 함수가 호출되는 파이프라인을 그리면 아래와 같다.

tick => isPaused => end
            \
             `---=> p.up
                    p.down
                    checkCollisionWithMissileAndMeteorite => diff => checkMeteoritePanel => p.isEmpty => end
                                                             p.difference                        \
                                                                                                  `---=> createMeteoritePanel => p.createPanel => paintMeteorite => p.paint => p.adjustRadomCenter

p. 이 붙은 함수가 panel.js에서 제공되는 함수다. 패널들이 tick 함수로 입력되면 내부에서 여러 함수의 입력으로 사용되다가 결국 변경된 패널들을 돌려준다. 호출 경로가 길어서 그렇지 동작은 단순하다.

쉬운 테스트
예를 들어 블록 게임 모듈에서 사용하는 파일을 자바스크립트 파일은 세 개다.

+-----------+
|           |
|           |
| Terminal  |
|           |
|           |
+--------+--+
         ^
         |
+--------|--+     +-----------+     +-------------+
|        |  |     |           |     |createPanel  |
|        |  |     |init       |     |paint        |
|timer---|------->|tick       | use |getZeroPoints|
|user key|------->|key        |     |up           |
|        +--------|join       |     |down         |
+-----------+     +-----------+     +-------------+
 example.js         index.js          panel.js

example.js는 사용자의 키 입력과 틱을 전달하며 그 결과를 터미널에 표시한다. index.js는 게임 알고리즘을 가지고 있으며 panel.js에서 제공되는 2차원 배열 처리 함수를 사용한다. 여기서 panel.js는 단순한 함수의 집합이다. 이 파일에서 제공하는 함수는 21개인데 이중 19개의 함수가 패널들을 인자로 가지는 함수다.

  • createPanel
  • paint
  • getZeroPoints
  • up
  • down
  • left
  • right
  • rotate
  • assign
  • difference
  • getMaxRow
  • getMaxColumn
  • adjustCenter
  • adjustBottom
  • adjustRandomCenter
  • isEmpty
  • isOverlap
  • isOnTheLeftEdge
  • isOnTheRightEdge
  • isBottom
  • isBlankItem

게임의 알고리즘을 담당하는 index.js 파일은 위의 21개 함수들을 사용해서 동작한다. 테스트를 한다고 가정할 때 클래스보다는 이런 단순한 함수들을 테스트하는 것이 쉽다고 생각한다. 비슷하고 적은 수의 인자를 가지는 함수들은 입력 값에만 의존해서 동작하며 외부 값에 영향을 주지 않는 순수 함수다. 당연히 순수 함수는 테스트가 쉽다. 연결되어 사용되는 함수들의 테스트는 부분 함수 테스트의 합이므로 부분 테스트가 곧 전체 테스트다.

마무리
작성하는 코드의 일부분을 이러한 작은 데이터 구조와 순수 함수를 이용해서 만든다면 버그가 줄어들고 테스트가 편한 소스가 될 것이라고 생각한다. 앞으로 좀 더 복잡한 게임을 만들 예정이니 궁금하신 분들은 1년쯤 뒤에 한번 들러주길 바란다. 참고로 5월 현재 패널들만 처리하는 함수들을 모아 모듈로 만들었다. 아래의 구성을 가지고 있다.

    
    +---------+     +---------+
    |  Tetris |     |  Tetris |
    |  Game   | use |  Game   | use --------+
    |         |     |  Module |             |
    +---------+     +---------+             v
    +---------+     +---------+        +---------+
    |  Snake  |     |  Snake  |        |         |
    |  Game   | use |  Game   | use -->|  Panel  |
    |         |     |  Module |        |  Module |
    +---------+     +---------+        +---------+
    +---------+     +---------+             ^
    |  Block  |     |  Block  |             |
    |  Game   | use |  Game   | use --------+
    |         |     |  Module |
    +---------+     +---------+

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중

This site uses Akismet to reduce spam. Learn how your comment data is processed.