12 분 소요

언리얼 자료구조에 대하여

자료구조란 공장 일꾼(CPU)이 창고(Memory)에서 가져온 부품(데이터)들을
‘어떤 모양의 상자’에 담아서 보관하고 운반할지를 결정하는 ‘컨테이너 시스템’입니다.

상자를 잘못 고르면, 일꾼(CPU)이 상자를 뒤지느라 시간을 다 허비하거나(Cache Miss),
아까운 창고(Memory) 공간을 낭비하게 됩니다.

언리얼 엔진이 제공하는 3대장(TArray, TMap, TSet)을
철저히 CPU와 메모리 관점에서 파헤쳐 봅시다.

이러한 컨테이너 라이브러리(UCL)는 C++ 표준 라이브러리(STL)와 비슷해 보이지만,
게임 성능(메모리 파편화 방지, 캐시 효율)을 위해 바닥부터 새로 설계된 물건들입니다.

1. TArray (가변 배열)

언리얼 엔진에서 가장 많이 쓰이고, 에픽게임즈가 가장 강력하게 권장하는 절대 존엄 자료구조입니다.

  • 비유: 부품들이 빈틈없이 일렬로 꽉 채워진 ‘컨베이어 벨트’ 또는 ‘기다란 부품 상자’.
  • 메모리 구조: 힙(Heap) 메모리에 하나의 거대한 연속된 메모리 덩어리를 할당합니다.
  • CPU 캐시 궁합: 🏆 최상 (Best)

🔍 내부 동작 원리: 왜 빠를까?

우리가 지난 2회차에서 배운 ‘공간 지역성(Spatial Locality)’의 끝판왕이기 때문입니다.

TArray<int32> Scores;
Scores.Add(10);
Scores.Add(20);
Scores.Add(30);
// ... 1000개 추가
  • 연속성: Scores의 데이터는 메모리 주소 0x100, 0x104, 0x108… 처럼 기계적으로 딱딱 붙어 있습니다.
  • 프리페치(Prefetching): CPU는 “어? 너 순서대로 읽네?”라고 눈치채고, 다음 데이터를 미리 캐시(공구함)에 가져다 놓습니다.
  • 캐시 히트: 그 결과 CPU가 Scores[0]을 읽으려고 가져오는 순간, Scores[15]까지가 ‘캐시 라인(64바이트 상자)’에 담겨 딸려옵니다.

🏗️ TArray의 숨겨진 비용: 이사(Reallocation)와 시프트(Shift)

TArray는 빠르지만, 공짜는 아닙니다.
‘가변(크기가 변함)’이라는 특성 때문에 두 가지 큰 비용이 발생할 수 있습니다.

1. 이사 비용 (Reallocation)

컴퓨터의 메모리(힙)는 고무줄처럼 늘어나지 않습니다.
현재 4칸짜리 방을 쓰고 있는데 5번째 데이터를 넣으려면 어떻게 해야 할까요?
옆집에 누가 살고 있다면 벽을 뚫을 수 없습니다.

  • 상황: 꽉 찬 배열에 Add()를 호출함.
  • 처리 과정:
    1. 새 집 찾기: 메모리 관리자(OS)에게 가서 “데이터 8개 들어갈 더 큰 방 줘!”라고 요청합니다. (보통 여유분을 포함해 2배 정도 크게 잡습니다.)
    2. 이사 (Copy): 기존 4개 데이터를 새집으로 하나하나 복사합니다.
    3. 철거 (Free): 예전 4칸짜리 집을 부숩니다.
    4. 입주: 5번째 데이터를 넣습니다.
  • 결과: 데이터가 많을수록 이사 비용은 엄청나게 비쌉니다.

💡 해결책: Reserve (예약) “나 어차피 1000개 쓸 거야”라고 미리 말해두세요.

TArray<int32> Data;
Data.Reserve(1000); // 처음부터 1000칸짜리 대저택을 빌립니다.
// 이후 1000번의 Add() 동안 이사 비용은 '0'원입니다.

2. 시프트 비용 (Shift)

TArray는 데이터 사이에 빈틈(구멍)이 생기는 것을 허용하지 않습니다.

  • 문제: 1000명이 일렬로 서 있는데, 맨 앞(0번) 사람을 쫓아내면(RemoveAt(0)) 어떻게 될까요?
  • 결과: 뒤에 있는 999명이 전부 한 칸씩 앞으로 걸어와서 빈자리를 채워야 합니다. (Memmove)
  • 비유: 편의점 진열대 뒤쪽 물건을 앞으로 당기는 작업과 같습니다.

💡 해결책: RemoveAtSwap 순서가 중요하지 않다면(예: 그냥 몬스터 목록), RemoveAtSwap을 쓰세요.

  1. 맨 뒤에 있는 녀석을 삭제할 빈자리(0번)로 휙 던져버립니다.
  2. 맨 뒤 칸을 삭제합니다.
  3. 이동 횟수 딱 1번. 속도는 빛처럼 빠릅니다.
  • Unreal에 최적화된 Vector
    (Unreal GC, Replication 이 적용 가능한)

2. TSet (해시 집합)

데이터의 ‘중복 방지’와 ‘빠른 존재 확인’이 필요할 때 씁니다.
(예: “현재 접속 중인 유저 ID 목록”)

  • 비유: 번호표가 붙은 ‘사물함 센터’.
  • 메모리 구조: 해시 테이블(Hash Table). 데이터가 연속적으로 붙어있지 않고, ‘해시값’에 따라 메모리 여기저기에 듬성듬성(Sparse) 퍼져 있습니다.
  • CPU 캐시 궁합: 📉 나쁨 (Poor)

🔍 내부 동작 원리: Hashing

TSet은 ‘검색(Find)’에 모든 스탯을 투자했습니다.

TArray는 데이터를 찾으려면 0번부터 순서대로 뒤져야 했죠? (일꾼이 직접 발로 뜀) 하지만 TSet에는 ‘천재 관리인(해시 함수)’가 있습니다.

TSet<int32> UniqueIDs;
UniqueIDs.Add(55);
// ... 데이터 100만 개 ...

// "55번 ID 있어?"
if (UniqueIDs.Contains(55)) { ... }
  • Hash 함수 : 특정한 입력값을 변형시켜 결과값을 내보냄
    • 그렇기에 값 검색 자체는 힘들지만
      특정 요소가 ‘포함’되어 있는지를 확인하기 쉬움
    • 다만, 다른 값이 같은 결과를 나오게 할 수 있음
      (해시 충돌!)

[검색 과정]

  1. 입력: “55번 ID 있어요?”라고 묻습니다.
  2. 해시 함수 (계산기): 천재 관리인(GetTypeHash)이 55라는 숫자를 넣고 복잡한 계산을 합니다.
    • “음… 55번은 계산해 보니 A-3 구역 사물함이군!”
  3. 즉시 이동: 일꾼은 다른 곳은 쳐다보지도 않고 A-3 사물함으로 직행합니다.
  4. 확인: 문을 열어보고 있으면 true, 없으면 false. 끝.

해시 함수(계산기): 55라는 값을 넣으면 GetTypeHash(55) 함수가 사물함 번호(인덱스)를 즉시 계산해 줍니다. (예: “A-3번 사물함으로 가세요.”)

속도: 데이터가 10개든 100만 개든, 계산기 두드리고 해당 사물함만 열어보면 끝입니다. (O(1) - 즉시 완료)

  • 반면 TArray는 최악의 경우 100만 개를 다 뒤져야 합니다. (O(N))

💥 해시 충돌 (Hash Collision) - 관리인의 ‘중복 배정’ 사고

우리는 TSet이 빠른 이유가 ‘천재 관리인(해시 함수)’이
번호표(Data)만 보고 사물함 위치(Key)를 즉시 알려주기 때문이라고 배웠습니다.

그런데, 세상에 완벽한 것은 없습니다.
이 관리인이 ‘실수’를 하는 상황을 봅시다.

1. 충돌 상황 발생

  • 상황: 현재 공장 사물함은 100개가 있습니다. (0번 ~ 99번)
    • 데이터 1: 55번 부품이 들어왔습니다.
      • 관리인: “55 나누기 100의 나머지는… 55번 사물함!”
      • (55번 부품 -> 55번 사물함 입실 완료)
    • 데이터 2: 155번 부품이 들어왔습니다.
      • 관리인: “155 나누기 100의 나머지는… 어? 또 55번 사물함이네?”
      • 문제 발생: 55번 사물함은 이미 꽉 찼습니다!

이처럼 서로 다른 데이터(55, 155)가 우연히
똑같은 사물함 번호를 배정받는 사고를 ‘해시 충돌(Hash Collision)’이라고 합니다.

2. 언리얼의 해결책: “빈 곳 찾기” (Linear Probing)

언리얼 엔진(UE)은 이런 상황에서 당황하지 않고
‘개방 주소법(Open Addressing)’이라는 방식을 주로 사용합니다.
쉽게 말해 “눈치껏 옆 칸에 넣기”입니다.

  1. 충돌: 155번 부품을 들고 55번 사물함에 갔더니 꽉 찼습니다.

  2. 이동: “에이, 옆 칸(56번) 비었나?” -> 확인.

  3. 입실: 56번이 비어있다면 거기에 155번 부품을 넣습니다.

  • 만약 56번도 찼다면? 57번으로 갑니다. 빈방 나올 때까지 계속 옆으로 갑니다.

  • 개방 주소법과 다른 방법?
    • ‘체이닝’
      해당 시점에서 Vector 와 같은 배열을 생성하여
      같은 값들을 저장해둔 후, 거기서 검색
      (이건 이거대로 중복값이 많아질 수록 문제 발생)
      (O(n)에 가까워짐)
  • 어찌 되었든, 해시 충돌 자체가 자주 발생하는 것이 그리 좋은 상황이 아님

3. 대가: 성능 떡락의 주범 (O(1) → O(N))

문제는 나중에 데이터를 찾을 때 발생합니다.

  • 질문: “야, 155번 부품 가져와.”
    • 관리인: “계산해 보니 55번 사물함이야. 가봐.”
    • 일꾼(CPU)의 고생길:
      1. 55번 사물함을 엽니다. -> “어? 55번 부품이네? 내가 찾는 게 아니야.” (실패 1)
      2. “아, 아까 자리 없어서 옆에 뒀나?” 하고 56번을 엽니다. -> “어? 다른 놈이네?” (실패 2)
      3. 57번을 엽니다… 58번을 엽니다…
      4. 결국 찾을 때까지 사물함을 하나하나 다 열어봐야 합니다.

운이 좋으면 한 방에 찾지만(O(1)), 충돌이 너무 많이 나서 데이터들이 줄줄이 소세지처럼 밀려 있으면,
사실상 TArray처럼 모든 사물함을 다 뒤지는 꼴(O(N))이 됩니다. 이것이 해시 충돌이 무서운 이유입니다.

⚠️ 치명적 단점: 순회(Iteration)가 느림

TSet이나 TMapfor 문으로 돌리면 왜 TArray보다 느릴까요?

이것도 사물함 비유로 완벽하게 이해할 수 있습니다.

  • TArray (컨베이어 벨트): 부품 1번부터 100번까지 벨트 위에 빈틈없이 꽉 채워져 있습니다. 일꾼은 제자리에서 줍기만 하면 됩니다. (속도: KTX)
  • TSet (사물함):
    • 1번 데이터: 101호
    • 2번 데이터: 505호 (중간에 400개 비어있음)
    • 3번 데이터: 909호 (또 비어있음)

[순회 시 상황]

일꾼(CPU)이 1번 데이터를 처리하고 “다음 거!”를 외칩니다.

하지만 바로 옆 사물함은 ‘구멍(Hole, 빈 데이터)’입니다. 그 옆도 비어있고, 그 옆도 비어있습니다.

  • CPU: “101호 처리 완료. 다음… 어? 비었네? 다음… 비었네? 으악! 505호까지 뛰어가야 해!”
  • 메모리 관점: 데이터가 띄엄띄엄 있으니, 지난번에 배운 ‘캐시 라인(64바이트 상자)’에 데이터가 1~2개밖에 안 들어옵니다. 계속 창고(RAM)를 왔다 갔다 해야 하니 캐시 미스(Cache Miss)가 폭발합니다.

3. TMap (키-값 쌍) - 공장의 ‘자재 관리 대장’

TSet이 단순히 “이 번호의 사물함이 사용 중인가?”(존재 여부)만 확인한다면,

TMap은 그 사물함 안에 ‘실제 물건(Value)’까지 보관하는 자료구조입니다.
게임 개발에서 TArray 다음으로 가장 많이 쓰입니다.

  • 비유: ‘부품 관리 대장’. (부품 번호[Key] $\rightarrow$ 실제 부품의 위치 및 정보[Value])
  • 메모리 구조: TPair<Key, Value>라는 짝꿍 데이터를 저장하는 해시 테이블입니다.
    • 내부적으로는 TSet과 거의 똑같은 알고리즘(해시 함수, 충돌 처리)을 사용합니다.
  • CPU 캐시 궁합: 📉 나쁨 (Poor) (TArray 대비)

🔍 TSet vs TMap: 형제 관계

이 둘은 사실상 같은 유전자를 가진 형제입니다. 목적만 다릅니다.

  • **TSet:** "55번 열쇠(Key), 우리 공장에 있어?" → **존재 여부 확인용 (Check)**
  • TMap<Key, Value>: “55번 열쇠(Key) 줄 테니까, 금고 안에 있는 돈(Value) 꺼내줘.” → 데이터 검색 및 조회용 (Retrieve)

⚖️ TArray vs TMap: CPU 관점의 승부 (면접 단골 질문!)

초급자가 가장 많이 하는 실수 중 하나가 “데이터 찾을 거니까 무조건 TMap이 빠르겠지?”라고 생각하는 것입니다.

정답은 “데이터 개수가 적으면(약 10~50개 미만) TArray가 더 빠르다”입니다. 왜 그럴까요?

1. TMap 검색 과정 (택시 타기)

  • 과정: 해시 함수 계산(복잡한 수식) → 메모리 주소 점프 → 데이터 확인.
  • 비유: “택시 앱 켜고, 목적지 입력하고, 택시 기다려서 타고 가기.”
  • 특징: 거리가 멀어도(데이터가 많아도) 요금과 시간은 비슷하지만, 기본요금(초기 비용)이 비쌉니다.

2. TArray 검색 과정 (전력 질주)

  • 과정: 0번부터 그냥 무식하게 훑기. (for문 혹은 FindByPredicate)
  • 비유: “그냥 두 다리로 뛰어가기.”
  • 특징: 거리가 멀면(데이터 1만 개) 힘들어서 느리지만, 가까운 거리(데이터 30개)는 택시 부르는 시간보다 뛰어가는 게 훨씬 빠릅니다.
    • 비결: 지난번에 배운 캐시 히트(한 번에 64바이트씩 가져옴)와 프리페치 덕분에, CPU가 미친 듯이 빠른 속도로 훑어버립니다.

💡ex) 인벤토리 구현

유저 인벤토리 아이템이 30~50칸이라면?

굳이 복잡하고 메모리 많이 먹는 TMap을 쓰는 것보다
TArray를 쓰고 FindByPredicate로 찾는 게 성능상으로도, 메모리상으로도 이득일 수 있습니다.

  • 이전에 솔로 게임에서 구조체 + TArray 방식으로 구현

4. 정리

언리얼 엔진에서 자료구조를 선택하는 절대 기준은 없습니다. 상황에 따라 다릅니다.

  • 고민하기 어렵다면 TArray를 쓰세요
    • 많은 상황에서 정답입니다.
    • CPU 캐시가 가장 좋아하며, 메모리 낭비가 가장 적습니다.
    • 데이터를 순서대로 처리(순회)해야 한다면 훨씬 이득입니다.
  • TMap / TSet은 이럴 때 쓰세요.
    • 데이터가 많고(수백 개 이상),
    • 특정 데이터를 ‘콕 집어’ 찾는 일(검색)이 매우 빈번할 때.
    • 단, 순회(Loop) 성능은 TArray보다 떨어진다는 것을 명심하세요.
  • 기본 보관 + 순회 : TArray!
  • 검색 및 데이터 포함 여부 : TMap / TSet!

면접 내용 복습

1. Struct vs Class

표준 C++ (Standard C++) 관점

표준 C++에서 structclass는 기술적으로 거의 동일합니다.

C++ 창시자 비야네 스트롭스트룹은 C 언어와의 호환성을
유지하면서 객체지향을 도입하기 위해 struct를 확장했기 때문입니다.

A. 기술적 차이 (접근 지정자 및 상속)

대표적인 컴파일러 수준의 차이는 기본 가시성(Default Visibility)입니다.

특징 Struct (구조체) Class (클래스)
기본 접근 지정자 public private
기본 상속 유형 public 상속 private 상속
  • 이러한 가시성은 OOP의 ‘캡슐화(은닉성)’에 해당함
// 표준 C++ 예시
struct FData {
    int A; // public으로 간주됨
};

class UManager {
    int B; // private으로 간주됨
public:
    int C;
};

struct DerivedStruct : BaseStruct {}; // public 상속
class DerivedClass : BaseClass {};    // private 상속

B. 기능적 동일성

많은 분들이 오해하는 것과 달리, 표준 C++의
Struct도 생성자, 소멸자, 멤버 함수, 가상 함수(Virtual Function), 다형성(Polymorphism)을 모두 가질 수 있습니다.

C. 의미론적 차이 (Semantics)

기능은 같지만, 전 세계 C++ 프로그래머들은
약속(Convention)에 따라 용도를 구분합니다.

  • POD (Plain Old Data): struct는 주로 데이터만 담고 있는 경우(C 스타일)에 사용합니다.
  • 불변성(Invariant) 관리: classprivate 멤버를 통해 내부 상태를 보호하고, 메서드를 통해서만 상태를 변경하도록 강제할 때 사용합니다.

2언리얼 엔진(Unreal C++) 관점

언리얼 엔진은 표준 C++ 위에 거대한 프레임워크(UObject Architecture)를 얹었기 때문에, 둘의 차이가 극명하게 갈립니다.

핵심은 리플렉션(Reflection)과 메모리 관리입니다.

A. 메모리 관리와 수명 주기 (Memory & Lifecycle)

이것이 가장 중요한 차이입니다. “데이터가 어디에 저장되고 누가 지우는가?”

  1. UCLASS (Class)
    • Heap 할당 & 간접 참조: 무조건 힙(Heap) 메모리에 생성되며, 우리는 오직 포인터()를 통해서만 접근할 수 있습니다.
    • Garbage Collection (GC): 언리얼의 GC 시스템이 관리합니다. 더 이상 참조되지 않는 객체는 GC가 알아서 메모리를 해제합니다.
    • 무거움 (Heavyweight): UObject를 상속받으면 기본적인 오버헤드(메타데이터 등)가 큽니다. 수백만 개의 AActor를 생성하면 성능 저하가 옵니다.
  2. USTRUCT (Struct)
    • Value Type (값 타입): 스택(Stack)에 생성되거나, 클래스 내부에 메모리 블록으로 포함(Inline)됩니다. 포인터로 관리하기보다 값 자체로 들고 다닙니다.
    • No GC: GC 시스템이 직접 관리하지 않습니다. Struct를 포함하고 있는 부모(예: UCLASS)가 사라지면 같이 사라집니다.
    • 가벼움 (Lightweight): 메타데이터가 적고 메모리 구조가 단순합니다. 수만 개의 파티클 데이터 등을 처리할 때 유리합니다.

B. 캐시 지역성 (Cache Locality) - 성능 심화

게임 성능 최적화(특히 CPU)에서 가장 중요한 것은 데이터가 메모리에 연속적으로 존재하는가입니다.

  • **TArray (Good):**
    • 구조체 배열은 메모리상에 데이터가 일렬로 붙어서(Contiguous) 저장됩니다.
    • CPU 캐시 히트(Cache Hit)율이 높아 연산 속도가 매우 빠릅니다. (데이터 지향 설계, DOTS와 유사한 이점)
  • TArray<UMyClass> (Bad for Cache):
    • 클래스 배열은 “주소값(포인터)”들의 배열입니다.
    • 실제 데이터는 힙 메모리 여기저기에 흩어져 있습니다(Fragmentation).
    • 접근할 때마다 ‘포인터 추적(Pointer Chasing)’이 발생하여 CPU 캐시 미스(Cache Miss)가 자주 발생합니다.

C. 리플렉션과 블루프린트 (Reflection & Blueprint)

언리얼 헤더 툴(UHT)이 코드를 분석할 때 처리 방식이 다릅니다.

  • UCLASS:
    • 블루프린트에서 Object Reference로 취급됩니다.
    • Valid 체크가 가능합니다 (Null일 수 있음).
    • 함수(UFUNCTION) 호출의 주체가 됩니다.
  • USTRUCT:
    • 블루프린트에서 구조체 핀으로 취급됩니다.
    • Null 개념이 없습니다 (항상 기본값이라도 존재함).
    • UFUNCTION을 내부에 가질 수 없습니다.
      • (C++에서는 멤버 함수를 만들 수 있지만, 블루프린트에서 호출 가능한 UFUNCTION 매크로는 붙일 수 없습니다. 대신 별도의 BlueprintFunctionLibrary를 통해 조작해야 합니다.)

사용시 주의점

// [위험!] 구조체에 UObject 포인터를 담을 때 주의해야 함
USTRUCT()
struct FBadStruct
{
    GENERATED_BODY()

    // UPROPERTY()를 빼먹으면 GC가 이 포인터를 모름!
    // 참조 중인데도 GC가 MyActor를 메모리에서 지워버릴 수 있음 -> Dangling Pointer 크래시
    AActor* MyActor; 

    // 올바른 예: UPROPERTY를 붙여야 GC가 참조 카운팅을 함
    UPROPERTY()
    AActor* SafeActor;
};
USTRUCT(BlueprintType)
struct FItemData
{
    GENERATED_BODY()

    UPROPERTY()
    int32 Count;

    // [가능] C++ 내부에서만 쓰는 함수는 얼마든지 작성 가능
    void AddCount(int32 Amount) { Count += Amount; }

    // [불가능] 구조체 멤버 함수에는 UFUNCTION을 붙일 수 없음! 컴파일 에러 발생.
    // UFUNCTION(BlueprintCallable) 
    // void Reset() { Count = 0; } 
};

정리

  1. 데이터 중심(Data-Oriented)인가?Struct (F 접두사)
  • 예: 인벤토리 아이템 정보, 스킬의 데미지 계수, 설정 옵션, 벡터 수학.
  • 대량 생성 및 반복문 처리가 필요하면 Struct가 빠릅니다.
  1. 객체 중심(Object-Oriented)인가?Class (U/A 접두사)
  • 예: 무기(Weapon) 그 자체, 몬스터 AI, 게임 모드, UI 위젯.
  • 상태 변화가 복잡하고, 월드에 존재하며, 이벤트(BeginPlay 등)를 받아야 한다면 Class입니다.
  1. 블루프린트와 통신해야 하는가?
  • 데이터를 핀(Pin)으로 주고받고 싶으면 Struct.
  • 함수를 호출하고 레퍼런스를 저장하고 싶으면 Class.

2. 프로세스와 스레드

프로세스(Process)와 스레드(Thread)의 차이는
운영체제(OS)와 시스템 프로그래밍의 가장 핵심적인 개념입니다.

단순히 “실행 단위”라는 정의를 넘어, 메모리 구조, 자원 공유 방식, 그리고 컨텍스트 스위칭(Context Switching) 비용의 관점에서 명확하게 구분해야 합니다.

“프로세스는 공장(Factory) 그 자체이고, 스레드는 그 공장 안에서 일하는 일꾼(Worker)들입니다.”

  • 공장(프로세스): 독립적인 건물을 씁니다. 다른 공장의 자원을 함부로 가져다 쓸 수 없습니다.
  • 일꾼(스레드): 같은 공장 안에서 도구와 자재(메모리)를 공유하지만, 각자 맡은 작업 공간(스택)은 따로 있습니다.

프로세스 (Process)

“실행 중인 프로그램(Program in execution)”

프로그램(코드 덩어리)이 메모리에 적재되어 CPU의 할당을 받을 수 있는 활동적인 상태를 말합니다.
운영체제로부터 시스템 자원을 할당받는 가장 큰 작업 단위입니다.

  • 독립성: 각 프로세스는 완벽하게 분리된 메모리 공간을 가집니다.
  • 메모리 구조: Code, Data, Heap, Stack 영역을 모두 개별적으로 가집니다.
  • 통신: 다른 프로세스에 접근하려면 IPC (Inter-Process Communication)라는 별도의 복잡하고 느린 통신 방법(파이프, 소켓 등)을 사용해야 합니다.
  • 안정성: 한 프로세스가 충돌(Crash)하여 죽더라도, 다른 프로세스에는 영향을 주지 않습니다. (예: 크롬 탭 하나가 멈춰도 브라우저 전체는 꺼지지 않음)

서버에 클라가 붙는것도 IPC의 일종

스레드 (Thread)

“프로세스 내에서의 실행 흐름 단위”

하나의 프로세스 안에서 생성되며, 프로세스의 자원을 공유하는 경량화된 실행 단위입니다.

  • 공유: 프로세스의 Code, Data, Heap 영역을 모든 스레드가 공유합니다. (전역 변수, 동적 할당 메모리 접근 가능)
  • 독립성: Stack(스택)과 PC(Program Counter, 레지스터) 만은 각 스레드가 독립적으로 가집니다.
  • 통신: 메모리를 공유하므로 별도의 통신 기법 없이 데이터 접근이 즉각적입니다. 하지만 동기화(Synchronization) 문제(동시 접근 이슈)가 발생합니다.
  • 안정성: 하나의 스레드에 오류가 발생해 프로세스가 종료되면, 같은 프로세스 내의 모든 스레드가 함께 종료됩니다.

기술적 심층 비교

A. 메모리 구조의 차이

  • Process: { Code, Data, Heap, Stack } X N개
  • Thread: { Code, Data, Heap } (공유) + { Stack, Register } X N개
    • Q: 왜 스택(Stack)은 따로 가질까요?
      • A: 스택은 함수 호출과 지역 변수, 리턴 주소를 저장하는 공간입니다. 스레드가 “독립적인 실행 흐름”을 가지려면 함수를 호출하고 돌아오는 흐름이 개별적이어야 하므로 스택은 반드시 독립적이어야 합니다.
    • Q: 왜 PC(레지스터)는 따로 가질까요?
      • A: PC는 “다음에 실행할 명령어 주소”를 가리킵니다. 스레드 A는 10번 줄을 실행 중이고, 스레드 B는 100번 줄을 실행 중일 수 있어야 하므로 CPU 레지스터 상태는 독립적이어야 합니다.

B. 컨텍스트 스위칭 (Context Switching) 비용

CPU가 하나의 작업에서 다른 작업으로 넘어갈 때 발생하는 오버헤드입니다.

  • 프로세스 간 전환 (Heavy):
    • 가상 메모리 주소 체계가 완전히 바뀝니다.
    • CPU 캐시 메모리(L1, L2…)를 비워야 할 수도 있습니다(Cache Flush).
    • TLB(페이지 테이블 캐시)가 초기화됩니다. 매우 느립니다.
  • 스레드 간 전환 (Light):
    • 같은 프로세스 내의 스레드끼리 전환할 때는 메모리 주소 공간(Code, Data, Heap)이 그대로 유지됩니다.
    • 오직 레지스터(PC, SP 등) 값만 저장하고 복구하면 됩니다. 훨씬 빠릅니다.

멀티 스레딩과 멀티 프로세싱

  • 멀티 스레딩 (Multi-threading):
    • 게임은 성능을 위해 멀티 스레드를 적극 사용합니다.
    • Main Thread (Game Thread): 게임 로직(액터 이동, 입력 처리, 블루프린트 실행)을 담당.
    • Render Thread: 게임 스레드가 “무엇을 그려라”라고 명령하면, 실제 GPU에 보낼 명령을 처리.
    • Worker Threads: 물리 연산, 애니메이션 계산, 오디오 처리, 파일 로딩 등.
  • 멀티 프로세싱 (Multi-processing):
    • 게임 클라이언트 자체는 하나의 프로세스입니다.
    • 하지만 게임 서버를 띄울 때는 안정성을 위해 하나의 물리 서버에 여러 개의 “게임 서버 프로세스(데디케이티드 서버 인스턴스)”를 띄우기도 합니다. 하나가 죽어도 다른 채널(프로세스)은 살아야 하기 때문입니다.

정리하자면?

  • 프로세스: 안정성이 중요하고 독립적인 작업이 필요할 때 (예: 크롬 브라우저 탭, 서버 인스턴스).
  • 스레드: 성능과 응답 속도가 중요하고 데이터를 빈번하게 공유해야 할 때 (예: 게임 엔진 내부, 고성능 연산).

댓글남기기