디자인 패턴 - 바이트코드 패턴(Bytecode pattern)
게임에서의 마법 하나하나를 만들거나 고칠 때마다 코드를 수정하고 컴파일과 빌드를 거치기에는 반복이 너무 비효율적이다. 이럴 때, 코드를 수정하는 대신 코드는 특정 데이터를 읽게 하고, 데이터를 만들어서 반복을 수행하면 훨씬 효율적일 것이다.
행동을 데이터 파일에 따로 정의해놓고 게임 코드에서 읽어서 실행할 수 있다면 많은 장점이 있다.
데이터를 정의하기 위해 어떻게 할 것인가?
인터프리터 패턴을 통해 데이터를 만들 수도 있겠다. 그러나, 인터프리터 패턴은 너무나도 느리고 메모리를 많이 소비한다. 더 빠른 수단을 찾아보자. 극단적으로 생각해보면, 기계어가 가장 빠를 것이다.
기계어의 장점.
밀도가 높으며, 선형적이고, 저수준이며, 그러므로 빠르다.
그러나 데이터를 기계어로 짜게하는 것은 어불성설이다.
대신 가상 기계어를 정의하면 어떨까? 그리고 그 가상 기계어를 실행하는 간단한 에뮬레이터도 만든다면?
이 가상 기계어는 게임에서 완전히 제어할 수 있으며, 밀도가 높고 선형적이고 상대적으로 저수준이다.
이 에뮬레이터를 가상 머신이라 하고, 가상 기계어를 바이트 코드라 하자.
바이트코드 패턴을 사용하면 좋은 경우
행동을 데이터 파일에 따로 정의해놓고 게임 코드에서 읽어서 실행할 수 있다면 많은 장점이 있다.
데이터를 정의하기 위해 어떻게 할 것인가?
인터프리터 패턴을 통해 데이터를 만들 수도 있겠다. 그러나, 인터프리터 패턴은 너무나도 느리고 메모리를 많이 소비한다. 더 빠른 수단을 찾아보자. 극단적으로 생각해보면, 기계어가 가장 빠를 것이다.
기계어의 장점.
밀도가 높으며, 선형적이고, 저수준이며, 그러므로 빠르다.
그러나 데이터를 기계어로 짜게하는 것은 어불성설이다.
대신 가상 기계어를 정의하면 어떨까? 그리고 그 가상 기계어를 실행하는 간단한 에뮬레이터도 만든다면?
이 가상 기계어는 게임에서 완전히 제어할 수 있으며, 밀도가 높고 선형적이고 상대적으로 저수준이다.
이 에뮬레이터를 가상 머신이라 하고, 가상 기계어를 바이트 코드라 하자.
바이트코드 패턴을 사용하면 좋은 경우
- 언어가 너무 저수준이라 만드는 데 손이 많이 가거나 오류가 생기기 쉽거나
- 컴파일 시간이나 다른 빌드 환경 때문에 반복 개발하기가 너무 오래 걸리거나
- 정의하려는 행동을 나머지 코드로부터 격리하고자 할 때
네이티브 코드보다는 느리므로 성능이 민감한 곳에 사용하지는 말자.
만드려는게 일종의 API라고 생각하면 좀 더 접근하기 쉬울 것이다.
마법 데이터를 만드는 예제를 통해 알아보겠다.
마법은 명령어 집합으로 생각한다.
그리고 그 명령어는 열거형 값을 배열에 저장해 데이터로 인코딩한다.
한 바이트로 전체 열거형 값을 다 표현할 수 있다. 데이터를 구성하는 코드가 실제로는 이런 바이트들의 목록이다보니 바이트코드라 부른다.
명령 하나를 실행하려면 어떤 명령인지 보고 이에 맞는 API메서드를 호출하면 된다.
가상 머신이 바이트 배열을 순회하면서 스위치문을 통해 해당 명령을 실행하면 가장 간단한 가상 머신 구현이 끝난다. 물론 이 첫번째 가상머신은 유연하지 않고, 표현력도 낮다.
실제 언어와 같은 표현력을 갖추기 위해 매개변수를 받을 수 있어야 한다. 이를 위해 스택 머신을 만들 수 있다.
리터럴을 나타내는 명령어를 만들고, 리터럴 명령어가 다음 바이트 값을 읽어서 스택에 저장해놓게 한다. 그리고 다른 명령이 들어올 때 매개변수 숫자만큼 스택에서 꺼내온다.
그러나 아직 행동을 표현하기에는 부족하다. 행동을 표현하려면 조합을 할 수 있어야 한다.
스택에 지정된 값을 넣을 수 있는 명령어를 만들어보자.
특정 값을 읽어서 스택에 넣을 수 있게 되었다. 그러나 아직 부족하다.
이제 가상 머신에 계산능력을 부여하자. 스택에서 값을 꺼내서 더하는 명령어를 만들어본다. 그리고 다른 계산능력도 부여할 수 있을 것이다.
이런 식으로 계속 명령어를 추가하면서 가상머신을 만들 수 있다.
스택, 반복문, 다중선택문 만으로 간단한 가상 머신을 만들 수 있다.
위와 같은 바이트코드 데이터를 통해 행동을 안전하게 코드에서 격리할 수 있다. 바이트코드로 정의된 행동 데이터는 코드 상에 미리 정의해놓은 명령 몇 개를 통해서만 다른 코드에 접근할 수 있다. 그러므로 악의적인 코드를 실행하거나 잘못된 위치에 접근할 방법이 없다.
이렇게 만들어낸 바이트코드를 텍스트로 편집하는 것은 C++보다도 저수준으로 작업하는 셈이 될 것이다. 그러므로 클릭해서 상자를 드래그 앤 드롭하거나 메뉴를 선택하는 식으로 행동을 조립할 수 있는 툴을 만들어 GUI를 제공하도록 하자.
이런 GUI에서는 잘못된 코드를 아예 만들 수 없게 할 수 있다. 오류를 토해내는 대신, 아예 버튼을 못 누르게 하거나 기본값을 넣으면 오류 없는 상태를 유지할 수 있다.
GUI를 통해 바이트코드를 제어하면 문법이나 파서를 만들 필요가 없다.
바이트패턴의 목적은 사용자가 행동을 고수준 형식으로 편하게 표현할 수 있도록 하는 데 있다. 먼저 사용성을 좋게 하고, 이를 저수준으로 변환해야 한다.
바이트코드 가상 머신을 만들 때에, 스택 기반이 아니라 레지스터 기반으로도 만들 수 있다.
스택 기반 가상 머신의 특징
- 명령어가 짧다.
- 코드 생성이 간단하다.
- 행동을 위한 명령어 개수가 많아진다.
레지스터 기반 가상 머신의 특징
- 명령어가 길다.
- 대신 행동을 위한 명령어 개수가 줄어든다.
그러나, 가능하면 스택 기반 가상 머신을 쓰도록 한다. 왜냐하면 구현하기 쉽고 코드 생성도 훨씬 간단하기 때문이다.
필요한 명령어들의 종류
- 외부 원시명령
- 가상 머신 외부 게임 코드에 접근해서 유저가 볼 수 있는 일들을 처리한다.
- 내부 원시명령
- 리터럴, 연산, 비교, 스택에 값을 주고받는 명령어들로 VM 내부 값을 다룬다.
- 흐름 제어
- 점프를 통해 흐름 제어를 할 수 있다.
- 추상화
- 별도의 반환 스택을 통해 함수 호출과 반환을 제어한다.
값을 어떻게 표현할 것인가?
- 단일 자료형. 간단하지만, 다른 자료형을 다룰 수가 없다.
- 태그 붙은 변수. 흔한 방식. 두 부분으로 나눠서 어떤 자료인지와 값을 나타냄.
- ...
바이트 코드를 어떻게 만들 것인가?
- 텍스트 기반 언어
- 문법 정의
- 파서 구현
- 문법 오류 처리
- 비-프로그래머에게 불친절
- UI 저작 도구
- UI 구현 필요. 버튼, 클릭, 드래그 등.
- 적은 오류
- 낮은 이식성. UI를 위한 프레임워크가 특정 OS에 종속적일 수도.
바이트코드 패턴은 인터프리터 패턴의 형제이다. 인터프리터 패턴은 행동 관련 코드를 바로 실행하고, 바이트코드 패턴은 나중에 실행할 수 있도록 바이트코드 명령어로 출력한다는 점이다.
루아는 게임 쪽에서 가장 널리 사용하는 스크립트 언어로서, 레지스터 기반 바이트코드 가상머신으로 간결하게 구현되어 있다.
댓글
댓글 쓰기