티스토리 뷰

들어가며

멀티스레딩이라는 단어는 이제 개발자에게 익숙합니다. 서버 애플리케이션에서는 동시 요청을 처리하기 위해, 게임 엔진에서는 프레임 타임을 줄이기 위해 멀티스레딩을 사용합니다. 그런데 한 가지 질문이 남습니다. 왜 하필 멀티스레딩이어야 했을까요.

 

CPU의 클럭 속도가 매년 빨라지던 시절에는 멀티스레딩이 반드시 필요하지 않았습니다. 코드를 싱글스레드로 작성해도 다음 세대 CPU가 나오면 자연스럽게 성능이 올라갔기 때문입니다. 하지만 2004년을 전후로 상황이 근본적으로 바뀌었습니다. 클럭 속도를 올리는 것만으로는 더 이상 성능 향상을 기대할 수 없게 되었고, CPU 제조사들은 코어 수를 늘리는 방향으로 전략을 전환했습니다.


이 글에서는 CPU가 왜 멀티코어로 전환했는지를 물리적 한계의 관점에서 정리하고, 이 전환이 게임 개발의 아키텍처를 어떻게 변화시켰는
지를 단계별로 살펴보겠습니다. C++ 게임 개발 경험과 현재 Java 기반 백엔드를 다루고 있는 입장에서, 멀티스레딩의 출발점을 다시 확인하는 과정이 되었으면 합니다.


클럭 속도 경쟁의 끝 - 물리 법칙이 세운 벽

CPU 성능 향상의 역사를 이해하려면 두 가지 법칙을 함께 봐야 합니다. 하나는 무어의 법칙(Moore's Law)이고, 다른 하나는 데나드 스케일링(Dennard Scaling)입니다.

Gordon Moore가 1965년에 예측한 무어의 법칙은 "반도체 칩의 트랜지스터 집적도가 약 2년마다 두 배로 증가한다"는 관찰이었습니다. 이 예측은 수십 년간 놀라울 정도로 정확하게 맞아떨어졌고, 실제로 트랜지스터 수는 꾸준히 늘어왔습니다. 하지만 무어의 법칙이 보장하는 것은 트랜지스터의 수이지, 성능 그 자체는 아닙니다. 트랜지스터 수의 증가가 곧 성능 향상으로 이어지던 시기가 있었는데, 그 연결 고리가 바로 데나드 스케일링이었습니다.

1974년 Robert Dennard가 발표한 데나드 스케일링은 트랜지스터를 더 작게 만들면 전력 밀도가 일정하게 유지된다는 이론입니다. 트랜지스터가 작아지면 전압도 낮아지고, 전류도 줄어들어 전력 소비가 면적에 비례해 감소한다는 것입니다. 이 덕분에 트랜지스터를 더 많이 집적하면서도 동시에 클럭 주파수를 올릴 수 있었습니다. 1990년대와 2000년대 초반, Intel과 AMD가 벌인 클럭 속도 경쟁(GHz War)은 이 데나드 스케일링이 유효했기에 가능했습니다.


그런데 2004년경을 기점으로 이 관계가 무너졌습니다. 트랜지스터를 더 작게 만들어도 전력 밀도가 줄어들지 않는 현상이 나타난 것입니다. 누설 전류(leakage current)가 공정 미세화와 함께 급격히 증가했고, 이로 인해 트랜지스터가 작아져도 전력 소비는 오히려 늘어나는 상황이 벌어졌습니다. Intel Pentium 4 Prescott(2004)은 이 문제의 상징적인 사례였습니다. 90nm 공정에서 3.8GHz까지 클럭을 끌어올렸지만, 발열이 심각한 수준에 이르러 후속 제품인 Tejas 프로젝트가 취소되는 결과로 이어졌습니다.

CPU의 동적 전력 소비는 P ∝ C × V² × f 라는 관계를 따릅니다. 여기서 C는 커패시턴스, V는 전압, f는 주파수입니다. 주파수를 올리려면 전압도 함께 올려야 하는데, 전력은 전압의 제곱에 비례하므로 주파수를 선형으로 올리면 전력은 그보다 훨씬 가파르게 증가합니다. 이것이 전력 장벽(Power Wall)으로 불리는 물리적 한계입니다.


결국 CPU 제조사들은 클럭 속도를 높이는 전략을 사실상 포기하고, 코어 수를 늘리는 방향으로 전환했습니다. Intel은 Pentium D(2005)를 시작으로 듀얼코어 프로세서를 출시했고, 2006년에는 Core 2 Duo로 본격적인 멀티코어 시대를 열었습니다. 이 전환은 소프트웨어에 근본적인 변화를 요구했습니다. 코어가 아무리 많아도 프로그램이 하나의 스레드에서만 실행된다면 나머지 코어는 유휴 상태로 남기 때문입니다.



게임 루프의 진화 - 싱글스레드에서 잡 시스템까지

CPU의 멀티코어 전환이 가장 직접적인 영향을 미친 분야 중 하나가 게임 개발입니다. 게임은 매 프레임 제한된 시간(60fps 기준 16.6ms) 안에 입력 처리, 물리 시뮬레이션, AI 연산, 렌더링을 모두 완료해야 하기 때문에, CPU 성능에 민감할 수밖에 없습니다. 멀티코어가 표준이 되면서 게임 엔진의 스레딩 모델도 단계적으로 발전해 왔습니다.

싱글스레드 게임 루프

Quake, Doom 시대의 게임 엔진은 하나의 메인 루프에서 모든 것을 순차적으로 처리했습니다. 입력을 받고, 게임 상태를 업데이트하고, 화면을 그리는 과정이 하나의 스레드 안에서 반복되는 구조입니다. 이 방식은 구조가 단순하고 디버깅이 용이하다는 장점이 있었습니다. 당시에는 CPU 클럭이 매년 빨라지고 있었기 때문에, 같은 코드도 다음 세대 하드웨어에서는 더 빠르게 실행되었고, 굳이 복잡한 멀티스레딩을 도입할 동기가 크지 않았습니다.

렌더링 스레드 분리

2005~2006년에 출시된 콘솔들이 전환의 계기가 되었습니다. Xbox 360은 3코어 Xenon CPU를, PS3는 비대칭 멀티코어 구조의 Cell Broadband Engine을 탑재했습니다. 이 하드웨어에서 성능을 제대로 끌어내려면 단일 스레드로는 한계가 분명했습니다.

이 시기에 가장 먼저 분리된 것이 렌더링 스레드입니다. 메인 스레드에서 게임 로직을 처리하는 동안, 렌더링 스레드에서는 이전 프레임의 결과를 바탕으로 GPU 커맨드를 생성하고 제출하는 구조입니다. 두 스레드가 한 프레임씩 어긋나며 파이프라인을 형성하기 때문에, 이론적으로는 단일 스레드 대비 거의 2배에 가까운 처리량을 얻을 수 있었습니다. 물론 두 스레드 사이의 동기화 지점에서 병목이 발생할 수 있어 실제 성능 향상은 그보다 낮은 경우가 많았지만, 멀티코어를 활용하는 첫 번째 실용적 패턴으로 자리를 잡았습니다.

태스크 기반 병렬 처리 - 잡 시스템의 등장

렌더링 스레드를 분리하는 것만으로는 4코어, 8코어 이상의 CPU를 효과적으로 활용하기 어려웠습니다. 각 스레드에 고정된 역할을 배분하는 방식은 코어 수가 늘어날 때 확장성이 떨어지기 때문입니다. 물리 담당 스레드, AI 담당 스레드, 오디오 담당 스레드를 각각 만들면 코어가 4개일 때는 적당하지만, 코어가 16개가 되면 대부분의 코어가 놀게 됩니다.

이 문제를 해결하기 위해 등장한 것이 잡 시스템(Job System)입니다. 작업을 스레드에 직접 할당하는 대신, 작업을 작은 단위(Job)로 쪼개어 큐에 넣고, 워커 스레드 풀이 큐에서 작업을 꺼내 처리하는 구조입니다. Intel TBB(Threading Building Blocks) 라이브러리가 이 패턴을 대중화하는 데 기여했습니다.


게임 업계에서는 Naughty Dog의 GDC 2015 발표가 큰 영향을 미쳤습니다. "The Last of Us" 엔진에서 사용한 파이버(Fiber) 기반 잡 시스템은, 기존의 스레드 기반 병렬 처리에서 한 단계 더 나아간 접근이었습니다. 파이버는 OS 스레드보다 가벼운 실행 단위로, 작업이 대기 상태에 들어가면 파이버를 중단하고 같은 워커 스레드에서 다른 파이버를 실행할 수 있습니다. 이를 통해 워커 스레드가 유휴 상태에 빠지는 시간을 최소화하고, 코어 활용률을 극대화할 수 있었습니다.

ECS와 데이터 지향 설계 - 아키텍처 수준의 병렬화

잡 시스템이 "작업을 어떻게 분배할 것인가"에 대한 답이었다면, ECS(Entity Component System)와 데이터 지향 설계(Data-Oriented Design)는 "데이터를 어떻게 구조화해야 병렬 처리가 자연스러워지는가"에 대한 답입니다.

전통적인 객체 지향 게임 엔진에서는 게임 오브젝트가 자신의 상태와 행동을 캡슐화합니다. 이 구조에서는 오브젝트 간 데이터 의존성이 복잡하게 얽혀 병렬 처리가 어렵습니다. 반면 ECS에서는 엔티티(Entity)가 데이터를 소유하지 않고, 컴포넌트(Component)가 순수한 데이터만을 담으며, 시스템(System)이 특정 컴포넌트 조합을 가진 엔티티들을 일괄 처리합니다.

이 구조의 핵심 이점은 시스템 간 데이터 의존성이 명확하다는 점입니다. PositionSystem이 Position 컴포넌트를 읽고 쓰고, PhysicsSystem이 Velocity와 Collider 컴포넌트를 처리할 때, 두 시스템이 같은 컴포넌트를 동시에 수정하지 않는다면 안전하게 병렬 실행할 수 있습니다. Unity의 DOTS(Data-Oriented Technology Stack)와 Unreal Engine의 Mass Entity 시스템이 이 패러다임을 채택하고 있으며, 엔진 수준에서 의존성을 분석하고 병렬 실행을 자동으로 스케줄링합니다.

데이터 지향 설계는 캐시 효율 측면에서도 유리합니다. 같은 타입의 컴포넌트들이 메모리에 연속적으로 배치되기 때문에 CPU 캐시 히트율이 높아지고, 이는 멀티스레딩의 성능 이점을 더욱 극대화합니다. 컨텍스트 스위칭 시 캐시 오염 문제가 줄어들기 때문입니다.



멀티스레딩이 실질적으로 큰 효과를 발휘하는 경우

멀티스레딩은 모든 상황에서 효과적인 것은 아닙니다. 병렬화의 이점이 극대화되려면 몇 가지 조건이 충족되어야 합니다. 작업 간 데이터 의존성이 낮고, 각 작업의 연산량이 스레드 생성 및 동기화 비용을 상회해야 합니다. 게임 개발에서 이 조건을 잘 충족하는 대표적인 영역들을 정리해 보겠습니다.

물리 연산 병렬화

게임 내 수천 개의 강체(Rigid Body)가 충돌하고 반응하는 물리 시뮬레이션은 멀티스레딩의 대표적인 수혜 영역입니다. Havok Physics와 NVIDIA PhysX 같은 물리 엔진들은 내부적으로 태스크 병렬 처리를 적극 활용합니다.

물리 연산이 병렬화에 적합한 이유는 공간 분할(Spatial Partitioning)에 있습니다. 월드를 격자 또는 트리 구조로 분할하면, 서로 먼 영역에 있는 오브젝트들은 독립적으로 충돌 검출과 해석을 수행할 수 있습니다. 오브젝트 간의 물리적 상호작용은 인접한 오브젝트 사이에서만 발생하므로, 충분히 떨어진 영역의 연산은 데이터 의존성 없이 병렬 처리가 가능합니다. 이런 특성 덕분에 코어 수에 비례하는 성능 향상을 기대할 수 있는 영역입니다.

AI 연산 분산

수백 명의 NPC가 동시에 활동하는 게임에서 각 NPC의 행동 트리(Behavior Tree)를 평가하는 작업은 상당한 CPU 시간을 소모합니다. NPC의 의사결정은 대부분 자신의 상태와 주변 환경 정보만을 기반으로 이루어지기 때문에, NPC 간 데이터 의존성이 낮습니다. 이 독립성 덕분에 NPC들의 AI 틱을 여러 스레드에 분배하면 거의 선형적인 성능 향상을 얻을 수 있습니다.

다만 모든 NPC의 AI를 매 프레임 평가하는 것은 비효율적일 수 있습니다. 실무에서는 LOD(Level of Detail) 개념을 AI에도 적용하여, 플레이어에게서 먼 NPC는 몇 프레임에 한 번씩만 평가하는 방식을 병행하기도 합니다. 멀티스레딩과 이런 최적화 기법을 함께 적용하면 수천 NPC의 동시 활동도 현실적인 프레임 타임 안에서 처리할 수 있습니다.

비동기 에셋 로딩

오픈 월드 게임에서 플레이어가 이동할 때 새로운 지역의 텍스처, 메시, 사운드 파일을 실시간으로 로딩해야 합니다. 이 작업을 메인 스레드에서 처리하면 로딩 중 프레임이 멈추는 현상(히칭, hitching)이 발생합니다.

비동기 에셋 스트리밍은 백그라운드 스레드에서 디스크 I/O와 리소스 디코딩을 처리하고, 준비가 완료되면 메인 스레드에서 안전하게 교체하는 방식입니다. 이 사례는 I/O 대기 시간을 다른 유용한 작업으로 채운다는 점에서, CPU 바운드 병렬화와는 성격이 다릅니다. 디스크에서 데이터를 읽는 동안 CPU는 유휴 상태에 놓이므로, 이 시간에 메인 스레드의 게임 로직이 계속 실행될 수 있도록 분리하는 것입니다.

오디오 처리

게임의 오디오 처리는 실시간성이 매우 중요합니다. 일반적으로 오디오 버퍼는 5ms 단위로 채워져야 하며, 이 주기를 놓치면 소리가 끊기거나 팝 노이즈가 발생합니다. 게임 로직의 프레임 타임은 16ms에서 33ms 사이를 오가며 변동하는데, 오디오가 이 변동에 영향을 받으면 안 됩니다.

이런 이유로 오디오 믹싱과 공간 음향 처리는 전용 스레드에서 독립적으로 실행되는 것이 일반적입니다. 오디오 스레드는 높은 우선순위로 실행되어 다른 작업에 의해 밀리지 않도록 보장하며, 메인 스레드와는 락프리(lock-free) 큐를 통해 최소한의 동기화만으로 통신합니다. 게임 로직에서 "이 위치에서 이 사운드를 재생하라"는 명령을 큐에 넣으면, 오디오 스레드가 이를 꺼내 처리하는 구조입니다.


마치며

CPU의 클럭 속도 향상이 물리적 한계에 도달하면서 멀티코어가 표준이 되었고, 게임 엔진은 이 변화에 적응하기 위해 스레딩 모델을 단계적으로 발전시켜 왔습니다. 싱글스레드 게임 루프에서 렌더링 스레드 분리로, 다시 잡 시스템으로, 그리고 ECS 기반의 아키텍처 수준 병렬화까지, 각 단계는 당시 하드웨어의 제약과 요구에 대한 엔지니어링 응답이었습니다.

이 과정을 돌아보면서 다시 확인하게 된 점이 있습니다. 멀티스레딩은 단순히 "스레드를 여러 개 만들어 작업을 나누는 것"이 아니라, 데이터 구조와 아키텍처 설계가 뒷받침되어야 비로소 효과를 발휘한다는 것입니다. 물리 연산이 병렬화에 적합한 것은 공간 분할이라는 데이터 구조 덕분이고, ECS가 자동 병렬 실행을 가능하게 하는 것은 컴포넌트 기반의 데이터 배치 덕분입니다.

다음 글에서는 멀티스레딩의 비용 측면을 살펴보겠습니다. 스레드를 늘린다고 항상 성능이 좋아지는 것은 아닙니다. 컨텍스트 스위칭 오버헤드, CPU 바운드와 I/O 바운드 작업의 차이, 그리고 적정 스레드 수를 결정하는 원칙에 대해 정리해 보겠습니다.


참고 출처

  • Intel 64 and IA-32 Architectures Software Developer Manuals
  • AMD64 Architecture Programmer's Manual
  • Moore, G. (1965). "Cramming more components onto integrated circuits". Electronics, 38(8).
  • Dennard, R. et al. (1974). "Design of Ion-Implanted MOSFET's with Very Small Physical Dimensions". IEEE Journal of Solid-State Circuits.
  • Amdahl, G. (1967). "Validity of the single processor approach to achieving large scale computing capabilities". AFIPS Conference Proceedings.