18장 더티 플래그 (Dirty Flag)
18.1 의도
불필요한 작업을 피하려고 실제로 필요할 때까지 그 일을 미룹니다.
18.2 동기
많은 게임은 월드의 모든 객체를 장면 그래프(scene graph)라는 큰 자료구조에 저장합니다. 렌더링 코드는 이를 보고 무엇을 그릴지 결정합니다.
가장 단순하게 만들면 객체 리스트 하나면 됩니다. 각 객체에는 모델이나 그래픽 기본 단위 데이터와 함께 변환(transform) 값이 있습니다. 변환은 위치, 회전, 크기 조절(scale)을 나타내므로, 이 값만 바꿔 쉽게 객체를 옮기거나 회전할 수 있습니다.
원주 1 변환을 저장하고 바꾸는 구체적인 방법은 이 책에서 다루지 않습니다. 반쯤 농담하자면 ‘4×4 행렬’로 요약할 수 있습니다. 두 변환을 합쳐 하나로 만들 수 있습니다. 이동한 뒤 회전하려면 두 행렬을 곱해 하나의 변환을 얻습니다. 왜, 어떻게 되는지는 직접 연습해보세요.
렌더러는 객체의 모델을 받아 변환을 적용한 다음 화면에 그립니다. 장면 그래프가 아니라 단순한 장면 집합(scene bag)이라면 이것으로 끝났을 것입니다.
하지만 장면 그래프는 대부분 계층형입니다. 객체는 자신에게 붙어 있는 객체의 상위 객체가 될 수 있습니다. 이때 하위 객체의 변환은 절대 위치가 아니라 상위 객체에 상대적인 값입니다.
바다에 떠 있는 해적선을 생각해봅시다. 돛대에는 망대가 있고, 망대 위에는 해적이 구부정하게 서 있으며, 해적 어깨에는 앵무새가 있습니다. 배의 변환은 바다에서의 위치, 망대의 변환은 배에서의 위치를 나타내는 식입니다.
그림 18-1 · 프로그래머가 그린 예술 작품!
상위 객체가 이동하면 하위 객체도 함께 움직입니다. 배의 지역 변환을 바꾸면 망대, 해적, 앵무새도 움직입니다. 배가 이동할 때마다 하위 객체가 미끄러지지 않도록 각각의 변환을 직접 바꿔야 한다면 골치 아플 것입니다.
원주 2 사실 배 위에서는 파도가 출렁일 때마다 미끄러지지 않도록 계속 균형을 잡아야 합니다. 더 단순한 예를 들었어야 했을지도 모르겠습니다.
그러나 앵무새를 화면에 그리려면 절대 위치를 알아야 합니다. 즉 지역 변환(local transform)이 아니라 월드 변환(world transform)이 필요합니다.
지역 변환과 월드 변환
월드 변환 계산은 간단합니다. 최상위 노드에서 내려오며 변환을 계속 합치면 됩니다. 앵무새의 월드 변환은 다음과 같습니다.
원주 3 상위 객체가 없으면 지역 변환과 월드 변환이 같습니다.
그림 18-2 · 상위 객체의 지역 변환에서 앵무새의 월드 변환을 계산합니다.
원주 4 배에는 상위 노드가 없으므로 배의 지역 변환이 월드 변환과 같습니다. — 옮긴이
모델마다 행렬을 몇 번 곱하는 정도라도 모든 객체에 대해 매 프레임 계산하면 성능에 큰 영향을 줍니다. 상위 객체의 이동은 자신뿐 아니라 모든 하위 객체에 재귀적으로 영향을 주므로, 행렬을 최신 상태로 유지하기 쉽지 않습니다.
가장 간단한 방법은 렌더링할 때마다 계산하는 것입니다. 어차피 최상위 노드부터 순회하니 그때마다 월드 변환을 계산해 그리면 됩니다.
그러나 이것은 귀중한 CPU 자원을 낭비합니다. 매 프레임 움직이는 객체는 많지 않습니다. 레벨을 이루는 정적 지형처럼 전혀 움직이지 않는 것의 월드 변환을 매번 재계산할 이유가 없습니다.
월드 변환 값 캐싱
확실한 해결책은 변환 값을 캐시하는 것입니다. 객체마다 지역 변환과 거기서 얻은 월드 변환을 저장합니다. 렌더링에는 미리 계산한 월드 변환을 씁니다. 움직이지 않았다면 캐시를 그대로 쓰면 됩니다.
객체가 움직일 때 월드 변환도 갱신하면 됩니다. 단, 계층을 잊지 마세요. 상위 객체가 움직이면 그 객체뿐 아니라 모든 하위 객체의 월드 변환도 재귀적으로 다시 계산해야 합니다.
복잡한 장면을 상상해봅시다. 한 프레임 안에 배는 바다 위에서 출렁이고 망대는 바람에 흔들립니다. 해적은 망대 위에 있고 앵무새는 해적 머리 위를 왔다 갔다 합니다. 네 개의 지역 변환을 바꿔야 합니다. 그때마다 관련 월드 변환을 모두 즉시 재계산하면 어떻게 될까요?
그림 18-3 · 중복 계산이 많습니다.
원주 5 별표를 보면 앵무새의 월드 변환을 네 번 계산한다는 것을 알 수 있습니다. 실제로는 마지막 값만 필요합니다.
객체 네 개가 움직였을 뿐인데 월드 변환 계산을 열 번 했습니다. 여섯 번의 결과는 렌더링에 쓰이기도 전에 버려졌습니다. 앵무새를 한 번 그리려고 네 번 계산한 것입니다.
월드 변환이 여러 지역 변환에 의존하는 것이 문제입니다. 상위 객체의 지역 변환 중 하나만 바뀌어도 즉시 재계산하니, 한 프레임에 같은 변환을 여러 번 계산합니다.
재계산 미루기
지역 변환 변경과 월드 변환 갱신을 분리하겠습니다. 필요한 지역 변환을 모두 바꾼 뒤, 렌더링 직전에 필요한 월드 변환만 한 번씩 재계산하면 됩니다.
원주 6 많은 소프트웨어 아키텍처가 의도적으로 어떤 일을 조금 늦춘다는 사실이 흥미롭습니다.
장면 그래프 객체에 플래그(flag)를 추가합니다. 프로그래밍에서 ‘플래그’와 ‘비트’는 둘 중 하나의 상태만 갖는 작은 데이터를 뜻하는 동의어입니다. 상태는 참·거짓, 켜짐·꺼짐 등으로 부르며, 이 책에서도 혼용합니다.
지역 변환이 바뀌면 플래그를 켭니다. 월드 변환이 필요할 때 검사하고, 켜져 있다면 계산한 뒤 끕니다. 플래그는 ‘월드 변환이 더 이상 맞지 않는다’를 뜻합니다. 유래는 분명하지 않지만 이렇게 유효하지 않음을 표시하는 플래그를 전통적으로 ‘더러운’이라는 뜻의 더티(dirty)라고 부릅니다. 패턴 이름도 여기서 왔습니다.
패턴을 적용하고 객체를 이동하면 다음과 같습니다.
그림 18-4 · 더 이상 중복 계산을 하지 않습니다.
움직인 객체마다 월드 변환을 한 번씩만 계산하므로 이보다 나은 결과는 없습니다. 단 1비트로 다음 효과를 얻습니다.
•
상위 노드를 따라가며 지역 변환을 여러 번 곱하던 일을 객체당 한 번의 재계산으로 합칩니다.
•
움직이지 않는 객체는 변환을 계산하지 않습니다.
•
렌더링 전에 제거된 객체라면 월드 변환을 계산하지 않아도 됩니다.
‘변경됨’과 ‘지금 계산함’을 분리합니다 — 배를 세 번 움직여도 화면에 필요한 것은 최종 위치입니다. 변경할 때는 dirty = true만 기록하고, 최종 결과를 읽는 순간 한 번 계산합니다. 중간 결과를 아무도 사용하지 않는다는 조건이 중요합니다.
18.3 패턴
계속 바뀌는 기본 값(primary data)이 있고, 여기에 비싼 작업을 해야 얻는 파생 값(derived data)이 있습니다. 더티 플래그는 파생 값이 참조한 기본 값이 바뀌었는지 추적합니다.
기본 값이 바뀌면 플래그를 켭니다. 파생 값을 쓸 때 플래그가 켜져 있으면 재계산한 뒤 끕니다. 꺼져 있으면 이전에 캐시한 파생 값을 그대로 씁니다.
18.4 언제 쓸 것인가?
더티 플래그는 이 책의 다른 패턴보다 용도가 구체적입니다. 대부분의 최적화처럼, 복잡해지는 코드를 감수할 만큼 성능 문제가 심할 때 사용해야 합니다.
주로 계산과 동기화에 사용합니다. 둘 다 기본 값에서 파생 값을 얻는 일이 오래 걸리거나 다른 이유로 비쌉니다.
장면 그래프는 계산량이 많아 느렸습니다. 반면 동기화는 파생 값이 디스크나 네트워크의 다른 기기 같은 원격에 있어 접근 비용이 큽니다.
다른 요구사항도 있습니다.
•
파생 값을 쓰는 횟수보다 기본 값이 더 자주 바뀌어야 합니다. 중간에 기본 값이 또 바뀌어, 계산한 결과가 사용되기도 전에 무효화되는 일을 피하는 패턴입니다. 기본 값이 바뀔 때마다 파생 값이 필요하다면 아무 쓸모가 없습니다.
•
점진적 갱신이 어려워야 합니다. 해적선 선창의 노획물 총무게를 구한다고 합시다. 더티 플래그를 쓰면 노획물을 넣고 뺄 때 플래그를 켜고, 총무게가 필요할 때 모든 노획물을 합산한 뒤 끕니다.
하지만 총무게를 유지하며 노획물을 추가·제거할 때 그 무게만 더하거나 빼는 것이 더 간단합니다. ‘물건을 사지 않고 대여료를 내듯’ 변경할 때마다 조금씩 갱신할 수 있다면, 필요할 때 전부 합산하는 것보다 나은 경우가 많습니다.
조건만 보면 적용할 곳이 거의 없어 보이지만, 앞으로 유용한 사례를 계속 보게 될 것입니다. 현재 게임 코드에서도 dirty를 검색하면 쉽게 찾을 수 있을 것입니다.
원주 7 저자가 검색했을 때에는 ‘지저분한(dirty)’ 꼼수에 대한 변명 주석도 많이 나왔습니다.
18.5 주의사항
문제에 딱 맞는 패턴이라 해도 몇 가지 주의점이 있습니다.
너무 오래 미루면 비용이 듭니다
비싼 작업을 실제 필요할 때까지 미루지만, 그때가 되면 결과가 당장 필요한 경우가 많습니다. 그런데 이 패턴을 쓰는 이유부터가 계산이 느리기 때문입니다!
월드 좌표는 한 프레임 안에 금방 계산하므로 큰 문제가 아닙니다. 하지만 처리에 상당한 시간이 걸리는 작업을 사용자가 결과를 보려는 순간 시작하면 짜증 나는 화면 멈춤이 생길 수 있습니다.
원주 8 자동 메모리 관리의 가비지 컬렉션에서도 비슷한 절충을 봅니다. 참조 횟수가 0이 되자마자 메모리를 해제할 수 있지만, 참조가 바뀔 때마다 횟수를 갱신하느라 CPU를 쓰게 됩니다.
단순한 GC는 새 메모리를 할당할 수 없을 때까지 회수를 미룹니다. 그러면 정작 메모리가 필요할 때 전체 힙을 정리하느라 게임이 멈추는 ‘GC 멈춤(GC pause)’이 생깁니다. Full GC에서 발생하는 이 멈춤을 stop-the-world(STW)라고도 합니다. — 옮긴이
지연 참조 카운팅(deferred ref-counting)이나 증분(incremental) GC는 두 방식을 절충합니다. 순수 참조 카운팅보다는 드물게, 전체 GC보다는 자주 회수합니다.
또 미뤄둔 작업은 문제가 생겼을 때 전부 날아갈 수 있습니다. 특히 영속적인 상태 저장에 사용할 때 그렇습니다.
텍스트 편집기는 변경 사항이 아직 저장되지 않았는지 압니다. 제목 표시줄의 파일명 옆 *는 더티 플래그를 눈에 보이게 표현한 것입니다. 기본 값은 메모리에 열린 문서, 파생 값은 디스크의 파일입니다.
그림 18-5 · GUI로 보는 더티 플래그
많은 편집기는 문서를 닫거나 프로그램을 끝내기 전까지 디스크에 저장하지 않습니다. 보통 괜찮지만, 전원 케이블이라도 뽑히면 애써 한 작업이 사라집니다.
백그라운드 자동 백업은 이를 보완합니다. 충돌했을 때 잃는 작업량을 줄이려면 자동 저장을 자주 하고, 계속 저장해서 파일 시스템이 지저분해지는 것을 줄이려면 빈도를 낮추면 됩니다.
상태가 바뀔 때마다 플래그를 켜야 합니다
파생 값은 기본 값에서 계산했으므로 본질적으로 캐시입니다. 캐시에서 어려운 것은 캐시 무효화(cache invalidation)입니다. 원본이 바뀌어 캐시가 더 이상 맞지 않음을 제때 알려줘야 합니다. 여기서는 기본 값이 바뀔 때 플래그를 켜는 일입니다.
원주 9 필 칼턴(Phil Karlton)은 “컴퓨터 과학에서 어려운 것은 캐시 무효화와 이름 정하기, 이렇게 딱 두 개밖에 없다”라고 했습니다.
한 곳이라도 빠뜨리면 오래된 파생 값을 사용합니다. 게임이 이상하게 동작하거나 찾기 어려운 버그가 생길 수 있습니다. 기본 값을 바꾸는 모든 코드에서 플래그도 설정해야 합니다.
기본 값을 변경하는 코드를 인터페이스로 캡슐화하면 도움이 됩니다. 하나의 API로만 상태를 바꾸게 하면 플래그 설정을 빠뜨리지 않을 수 있습니다.
이전 파생 값을 메모리에 저장해야 합니다
파생 값이 필요할 때 플래그가 꺼져 있다면 이전 결과를 사용합니다. 당연하지만, 그러려면 그 결과를 메모리에 보관해야 합니다.
원주 10 동기화에 쓸 때에는 파생 값이 메모리 밖에 있으므로 큰 문제가 되지 않습니다.
이 패턴을 쓰지 않으면 필요할 때 계산하고, 사용 뒤 버리면 됩니다. 계산 비용은 들지만 캐시 메모리 부담은 없습니다.
다른 최적화처럼 이 패턴도 속도를 위해 메모리를 희생합니다. 이전 계산 결과를 저장해 값이 그대로일 때 재계산을 피합니다. 계산이 느리고 메모리가 넉넉하면 좋습니다. 메모리보다 시간이 남는다면 매번 계산하는 편이 낫습니다.
원주 11 반대로 압축 알고리즘은 압축을 푸는 계산 시간을 대가로 공간을 절약합니다.
18.6 예제 코드
앞서 본 요구사항을 모두 만족한다고 가정하고 구현해봅시다. 변환 행렬의 실제 계산은 이 책의 범위 밖이므로, 다음 클래스가 어딘가의 구현을 감싼다고 하겠습니다.
class Transform {
public:
static Transform origin();
Transform combine(Transform& other);
};
C++
복사
필요한 연산은 combine()입니다. 상위 노드를 따라 지역 변환을 결합해 월드 변환을 얻는 데 사용합니다. origin()은 이동·회전·크기 변화가 없는 단위행렬(identity matrix)의 기본 변환을 반환합니다.
더티 플래그를 적용하기 전의 장면 그래프 객체를 살펴봅시다.
class GraphNode {
public:
GraphNode(Mesh* mesh)
: mesh_(mesh),
local_(Transform::origin()) {
}
private:
Transform local_;
Mesh* mesh_;
GraphNode* children_[MAX_CHILDREN];
int numChildren_;
};
C++
복사
각 노드에는 상위 노드를 기준으로 한 지역 변환이 있습니다. Mesh는 객체를 실제로 표현하는 그래픽 객체입니다. 화면에는 보이지 않고 자식들을 묶기만 하는 노드는 mesh_가 NULL일 수도 있습니다. 마지막으로 하위 노드 컬렉션이 있으며 비어 있을 수도 있습니다.
이 클래스로 만든 장면 그래프는 월드의 모든 객체를 자식이나 자손으로 갖는 최상단 GraphNode 하나입니다.
GraphNode* graph_ = new GraphNode(NULL);
// 하위 노드를 루트 노드에 추가한다...
C++
복사
그리려면 루트부터 전체 트리를 순회하면서, 각 노드의 올바른 월드 변환과 메시를 renderMesh()에 전달하면 됩니다.
void renderMesh(Mesh* mesh, Transform transform);
C++
복사
이 함수의 내부는 구현하지 않겠습니다. 특정 메시를 월드에 그리는 데 필요한 일을 한다고 합시다. 우리는 모든 노드에 대해 정확하고 효율적으로 호출하면 됩니다.
최적화되지 않은 순회
손을 ‘더럽히기(dirty)’ 위해 먼저 매번 월드 위치를 계산하는 단순한 순회를 작성하겠습니다. 최적화되지 않은 만큼 간단합니다. GraphNode에 다음 메서드를 추가합니다.
void GraphNode::render(Transform parentWorld) {
Transform world = local_.combine(parentWorld);
if (mesh_) renderMesh(mesh_, world);
for (int i = 0; i < numChildren_; i++) {
children_[i]->render(world);
}
}
C++
복사
parentWorld로 상위 노드의 월드 변환을 받습니다. 여기에 지역 변환을 결합하면 현재 노드의 월드 변환을 얻습니다. 이미 트리를 내려오면서 변환을 누적했으므로 다시 상위로 올라가며 계산할 필요가 없습니다.
월드 변환을 world에 저장하고, mesh_가 있으면 그립니다. 계산한 월드 변환을 인수로 자식들을 재귀 호출합니다. render()는 한 번에 모든 일을 하는 간단하고 깔끔한 재귀 함수입니다.
전체 장면을 그리려면 루트부터 시작합니다.
graph_->render(Transform::origin());
C++
복사
이제 더럽혀봅시다
모든 메시를 정확한 위치에 그린다는 점에서는 잘 동작합니다. 하지만 매 프레임 모든 노드에 local_.combine(parentWorld)를 실행하는 것은 비효율적입니다. GraphNode에 필드 두 개를 추가합니다.
class GraphNode {
public:
GraphNode(Mesh* mesh)
: mesh_(mesh),
local_(Transform::origin()),
dirty_(true) {
}
// 다른 메서드들...
private:
Transform world_;
bool dirty_;
// 다른 필드들...
};
C++
복사
world_는 이전 월드 변환, dirty_는 더티 플래그입니다. 새 노드는 아직 월드 변환을 계산하지 않아 지역 변환을 반영하지 못했으므로 초기 플래그는 참입니다.
움직이는 객체에 필요한 패턴이니 이동 기능도 추가합니다.
void GraphNode::setTransform(Transform local) {
local_ = local;
dirty_ = true;
}
C++
복사
setTransform()에서 플래그를 함께 켠다는 점이 중요합니다. 이것으로 끝일까요? 하위 노드도 챙겨야 합니다.
상위 노드가 움직이면 모든 자손의 월드 변환도 무효화됩니다. 그렇다고 여기서 자손의 플래그를 전부 켜지는 않겠습니다. 가능은 하지만 재귀 호출을 해야 해서 느립니다. 렌더링할 때 더 멋지게 처리할 수 있습니다.
원주 12 여기서는 if 검사가 행렬 곱셈보다 빠르다고 가정합니다. 비트 하나를 검사하는 것이 여러 부동소수점 계산보다 당연히 빠를 것 같지만, CPU는 생각보다 복잡합니다. 명령을 큐에 넣는 파이프라이닝에 많이 의존하고, 분기 예측에 실패하면 파이프라인을 다시 채우느라 사이클을 낭비합니다. CPU가 성능을 내는 방법과 방해하지 않기 위한 주의점은 데이터 지역성(17장)에서 더 자세히 다룹니다.
void GraphNode::render(Transform parentWorld, bool dirty) {
dirty |= dirty_;
if (dirty) {
world_ = local_.combine(parentWorld);
dirty_ = false;
}
if (mesh_) renderMesh(mesh_, world_);
for (int i = 0; i < numChildren_; i++) {
children_[i]->render(world_, dirty);
}
}
C++
복사
처음 구현과 크게 다르지 않습니다. 계산하기 전에 더러운지 검사하고, 계산 결과를 지역 변수가 아니라 필드에 저장합니다. 더럽지 않으면 combine()을 건너뛰고 저장한 world_를 씁니다.
핵심은 dirty 매개변수입니다. 상위 노드 중 하나라도 더러웠다면 참입니다. parentWorld를 계층 아래로 전달하며 누적하듯 더티 상태도 아래로 전달합니다.
덕분에 setTransform()에서 자손의 dirty_를 재귀적으로 켤 필요가 없습니다. 렌더링 때 상위 노드의 더티 상태를 전달해 재계산 여부를 판단합니다.
원했던 대로 지역 변환은 몇 번의 대입으로 바꾸고, 렌더링에는 이전 프레임 이후 바뀐 노드의 월드 변환만 최소한으로 계산합니다.
원주 13 GraphNode에서 render() 외에는 최신 월드 변환이 필요하지 않을 때만 가능한 구현입니다. 다른 코드도 최신 값이 필요하다면 다른 방식으로 구현해야 합니다.
dirty와 dirty_를 구별하세요 — dirty_는 이 노드의 변경 기록이고, 매개변수 dirty는 조상에서 내려온 변경까지 합친 값입니다. 계산 뒤 dirty_를 꺼도 자식에게 넘기는 dirty는 참이어야 자식도 새 부모 변환을 반영합니다. 새 시그니처의 루트 호출은 graph_->render(Transform::origin(), false);처럼 시작할 수 있습니다.
18.7 디자인 결정
구체적인 패턴이라 바꿀 부분이 많지는 않습니다.
더티 플래그를 언제 끌 것인가?
결과가 필요할 때
•
결과가 필요 없으면 계산 자체를 하지 않습니다. 파생 값을 쓰는 빈도보다 기본 값이 훨씬 자주 바뀔 때 좋습니다.
•
계산이 길면 거슬리는 멈춤이 생길 수 있습니다. 마지막 순간까지 미루면 플레이 경험을 해칠 수 있습니다. 보통은 충분히 빨라 괜찮지만 긴 작업은 더 일찍 해야 합니다.
미리 정해둔 지점에서
시간이나 게임 흐름상 미뤄둔 일을 처리하기 자연스러운 순간이 있습니다. 해적선이 항구에 정박할 때만 저장하게 할 수 있습니다. 게임 시스템과 무관하게 로딩 화면이나 컷신 뒤에서 처리할 수도 있습니다.
•
플레이 경험에 영향을 주지 않게 처리할 수 있습니다. 바쁘게 계산하는 동안 플레이어의 관심을 돌릴 화면을 보여줄 수 있습니다.
•
반대로 실제 처리 시점을 통제하지 못할 수 있습니다. 처리할 지점을 세밀하게 정해 게임에 자연스럽게 넣을 수는 있지만, 플레이어가 특정 장소로 가거나 행동하도록 강제할 수는 없습니다. 길을 잃거나 게임 상태가 꼬이면 예상보다 훨씬 오래 지연될 수 있습니다.
백그라운드로 처리한다
처음 값이 바뀔 때 타이머를 시작하고, 시간이 되면 그동안 쌓인 변경을 처리합니다.
원주 14 HCI(human-computer interaction)에서는 사용자 입력 뒤 일부러 잠시 기다렸다 응답하는 것을 이력현상(hysteresis)이라고 합니다(http://goo.gl/Jg0EMy). — 옮긴이
•
처리 빈도를 조절할 수 있습니다. 타이머 간격으로 얼마나 자주, 또는 드물게 처리할지 정합니다.
•
불필요한 일을 더 할 수 있습니다. 그동안 바뀐 상태가 적어도, 대부분 바뀌지 않은 다른 데이터까지 함께 처리할 수 있습니다.
•
비동기 작업을 지원해야 합니다. 뒤에서 처리하는 동안 플레이어도 계속 움직여야 하므로 멀티스레딩 등 동시성 기법이 필요합니다. 플레이어가 같은 상태와 상호작용할 수 있으니 안전하게 동시에 변경하도록 해야 합니다.
변경을 얼마나 세밀하게 관리할 것인가?
플레이어가 해적선을 만들고 개조할 수 있고, 서버에 자동 저장해 언제든 이어서 플레이할 수 있다고 합시다. 어느 갑판이 바뀌어 서버로 보낼지 더티 플래그로 추적합니다. 전송 데이터에는 바뀐 배의 데이터뿐 아니라 배의 어디가 바뀌었는지를 나타내는 메타데이터도 필요합니다.
더 세밀하게 관리한다
갑판의 널빤지마다 플래그를 둡니다.
•
실제로 바뀐 데이터만 처리합니다. 배에서 변경된 정확한 부분만 서버로 보냅니다.
더 듬성듬성 관리한다
갑판마다 비트를 하나 둘 수도 있습니다. 어느 부분이든 바뀌면 갑판 전체의 비트를 켭니다.
•
안 바뀐 데이터도 처리합니다. 물통 하나를 추가해도 갑판 전체를 서버로 보냅니다.
•
플래그 메모리가 줄어듭니다. 물통 열 개를 추가해도 1비트로 관리합니다.
•
고정 오버헤드가 줄어듭니다. 데이터 처리 외에 고정 작업도 필요합니다. 이 예에서는 바뀐 데이터의 위치를 나타내는 메타데이터입니다. 큰 단위로 처리할수록 메타데이터와 오버헤드가 줄어듭니다.
작게 나누면 항상 좋은 것은 아닙니다 — 작은 변경만 보내면 본문 데이터는 줄지만, ‘어디를 바꿨는지’ 표시와 플래그 수가 늘어납니다. 큰 단위는 불필요한 데이터도 보내지만 관리 비용이 작습니다. 실제 변경 빈도와 전송량을 함께 측정해야 합니다.
18.8 관련 자료
•
게임 밖에서도 흔히 쓰입니다. 책에서는 브라우저 측 웹 프레임워크인 Angular가 어떤 데이터가 바뀌어 서버로 보내야 하는지 더티 플래그로 관리하는 사례를 소개합니다.
•
물리 엔진은 움직이는 객체와 멈춘 객체를 기록합니다. 멈춘 물체는 충격을 받기 전까지 움직이지 않으므로 누군가 건드릴 때까지 처리하지 않습니다. 이런 isMoving 플래그는 힘을 받아 물리 처리가 필요한지를 알리는 더티 플래그입니다.









