TECHARTNOMAD | MAZELINE.TECH

UNREAL ENGINE

UE6 새 프로그래밍 모델 소스 코드 분석: Epic은 왜 언어와 객체 모델을 동시에 교체하려 할까요?

jplee 2026. 8. 5. 16:14

2026년 6월 17일, Epic의 엔진 개발 책임자 Marcus Wassmer는 블로그를 통해 UE6가 2027년 4분기에 Early Access에 들어간다고 확인했습니다. 그 글에서 특히 많이 인용된 문장이 있습니다. “UE6는 Actor와 Blueprint 체계를 단계적으로 지원 중단하고, Scene Graph 엔티티와 Entity Component System으로 전환합니다.”

하지만 이 문장은 이야기의 절반만 담고 있습니다.

나머지 절반은 상대적으로 눈에 띄지 않는 한 줄에 숨어 있습니다. UE6에서는 Fortnite 제작 도구(UEFN)의 스크립트 언어였던 Verse가 엔진 전체의 일급 프로그래밍 언어로 격상됩니다.

두 가지를 합쳐 보면 의미가 완전히 달라집니다.
Epic은 단순한 아키텍처 업그레이드를 하는 것이 아닙니다. 프로그래밍 언어와 객체 모델을 동시에 교체하려는 것입니다. C++는 엔진 저수준으로 내려가고, Actor/Component 체계는 단계적으로 지원 중단되며, 언어부터 런타임까지 완전히 새로운 기술 스택이 그 자리를 대신합니다.

이 기술 스택은 UE6 공개 저장소에서 이미 컴파일하고 실행할 수 있습니다. 며칠 동안 소스 코드를 정리한 끝에 전체 구조를 파악했고, 마지막에는 Verse와 Scene Graph가 실제로 어떻게 맞물리는지 예제로 설명하겠습니다.

안내: 이 글에서 인용하는 줄 수, 파일 경로, opcode 이름 등은 UE6 공개 저장소 ue6-main 브랜치의 현재 스냅샷을 기준으로 합니다. Epic은 여전히 빠르게 개발하고 있으므로 Early Access 이전에 수치와 명칭이 바뀔 수 있습니다. 글에 나온 구체적인 숫자는 “시스템의 규모와 설계 의도”를 보여 주기 위한 참고 자료입니다.

Verse 컴파일러: 약 5만 4천 줄의 C++는 무엇을 하고 있을까요?

Verse 컴파일러 코드는 Engine/Source/Runtime/VerseCompiler/에 있습니다. 파이프라인은 네 단계로 나뉩니다.

Parser는 직접 작성한 PEG 파서이며, 진입점은 VerseGrammar.h입니다. Epic은 Bison/Yacc를 쓰는 대신 C++ 템플릿으로 의존성 없는 문법 조합자 프레임워크를 구현했습니다.

// VerseGrammar.h - 각 문법 규칙은 Result<T>를 반환하며, 실패하면 즉시 중단됩니다
#define ULANG_GRAMMAR_RUN(e)               \
    {                                      \
        auto GrammarTemp = (e);            \
        if (!GrammarTemp)                  \
            return GrammarTemp.GetError(); \
    }

엄밀히 말하면 PEG(Parsing Expression Grammar)는 형식 문법이고, parser combinator는 구현 기법입니다. Epic의 방식은 “C++ 템플릿으로 작성한, 의미상 PEG와 같은 재귀 하강식 조합자”입니다. 이 조합자 덕분에 ParserPass.cpp는 약 3,000줄만으로 race{}, spawn{}, branch{} 같은 동시성 원시 문법을 포함한 Verse 전체 문법을 처리합니다.

Desugarer(약 2,080줄)는 문법 설탕을 핵심 AST로 펼칩니다. for 루프는 반복자 프로토콜 호출로, if 표현식은 decides 효과 위의 분기로 변환됩니다. 또한 Tarjan 강한 연결 요소 알고리즘으로 패키지 의존 관계를 위상 정렬합니다. Verse 모듈 시스템은 순환 의존성 검사를 기본 지원하므로, 컴파일러는 컴파일 전에 어떤 패키지들이 순환 고리를 이루는지 알 수 있습니다.

여기서 Tarjan SCC는 단순히 “순환을 감지”하는 데 그치지 않습니다. 핵심은 “그룹화”입니다. 서로 순환 의존하는 패키지를 하나의 강한 연결 요소로 묶어, 순환 내부는 하나의 단위로 컴파일하고 그룹 사이에는 위상 순서를 적용합니다. 이 방식은 순환 의존성을 무조건 오류로 처리하지 않고 “허용 가능한 순환 의존성”을 지원하는 알고리즘 기반입니다. crate 사이의 순환 의존성을 금지하는 Rust와 뚜렷하게 대비됩니다.

Rust에서 crate는 독립적으로 컴파일되는 기본 단위이며, Cargo는 크레이트(정확히는 패키지 의존 그래프) 사이의 순환 의존성을 허용하지 않습니다. 

SemanticAnalyzer는 컴파일러에서 가장 큰 단일 파일로 약 2만 1천 줄입니다. 타입 검사, 효과 추론, 동시성 의미 검증을 담당하며 효과 시스템의 핵심 로직도 여기에 있습니다.

// SemanticAnalyzer.cpp - spawn 표현식은 suspends 효과를 허용합니다
if (ExprCtx.ResultContext == ResultIsSpawned)
{
    AllowedEffects |= EEffect::suspends;
}

IRGenerator(약 2,992줄)는 타입 검사가 끝난 AST를 Verse VM 바이트코드로 바꿉니다. 바이트코드 opcode 정의는 C#으로 작성된 UBT 도구 VerseVMBytecodeGenerator.cs가 생성합니다. 이 도구는 컴파일 시 C++ opcode 구조체, 디스패처, 인라인 구현을 자동으로 만듭니다.

왜 코드 생성기를 사용할까요? opcode의 “열거형 값, 피연산자 레이아웃, 인터프리터 분기, 직렬화 로직” 네 종류 코드는 반드시 일대일로 일치해야 합니다. 수작업으로 관리하면 한 부분을 고치고 다른 부분을 빠뜨려 찾기 어려운 버그가 생기기 쉽습니다. 하나의 opcode 설명으로 네 종류 코드를 모두 생성하면 일관성을 사람의 주의력에 맡기지 않고 컴파일 단계에서 보장할 수 있습니다. Verse 효과 시스템이 “규칙을 관례가 아니라 컴파일 검사로 바꾼 것”과 같은 공학 철학입니다.

Effects System: Verse와 주류 언어를 가르는 경계선

Effects.h를 열어 보면 컴파일러 내부에서 여러 열거형으로 Verse의 효과 경계를 정의합니다. 여기서는 사용자가 작성하는 효과 지정자(specifier)컴파일러 내부 효과 열거형을 구분해야 합니다. 둘은 같은 목록이 아닙니다.

Verse 프로그래머가 함수 시그니처에 작성하는 주요 지정자는 다음과 같습니다.

 

지정자  의미
<computes> 부작용 없는 순수 계산이며 종료를 보장합니다
<converges> 종료를 보장합니다(불변 상태를 읽을 수 있습니다)
<varies> 같은 입력이 항상 같은 출력을 만들지는 않습니다
<transacts> 가변 상태를 읽고 쓰며, 쓰기 작업을 트랜잭션으로 되돌릴 수 있습니다(함수 기본 효과)
<decides> 함수가 실패할 수 있습니다(failure context, Result<T,E>와 비슷한 “실패 시 롤백”)
<suspends> 함수가 일시 정지해 비동기 결과를 기다릴 수 있습니다
<no_rollback> 함수의 부작용을 되돌릴 수 없습니다(실제 I/O 등)

컴파일러 내부의 세분화된 효과 열거형은 다음과 비슷합니다.

// Effects.h(컴파일러 내부 열거형이며 사용자가 직접 작성하는 지정자가 아닙니다)
#define VERSE_ENUM_EFFECTS(v) \
    v(suspends)     \  // 일시 정지하고 기다릴 수 있음
    v(decides)      \  // 실패할 수 있음
    v(diverges)     \  // 종료되지 않을 수 있음
    v(reads)        \  // 가변 상태 읽기
    v(writes)       \  // 가변 상태 쓰기
    v(allocates)    \  // 메모리 할당
    v(dictates)     \  // 타입 수준 제약 정의
    v(no_rollback)  \  // 롤백 불가

사용자가 작성한 <transacts>는 내부적으로 reads | writes | allocates | diverges | dictates 같은 세부 효과의 조합으로 분해됩니다. 사용자 지정자는 여러 효과를 “묶은 것”이고, 컴파일러 내부에서는 이를 “분해”해 관리합니다. 초심자가 가장 혼동하기 쉬운 부분입니다.

효과는 비트 마스크로 구현한 EffectSet으로 조합할 수 있습니다.

// Effects.h - 미리 정의된 효과 조합
constexpr SEffectSet Transacts =
    EEffect::diverges | EEffect::reads |
    EEffect::writes | EEffect::allocates | EEffect::dictates;

constexpr SEffectSet FunctionDefault = Transacts | EEffect::no_rollback;

이 단어들은 단순한 주석이 아니라 컴파일러가 강제로 검사하는 제약입니다. suspends를 선언하지 않은 함수는 일시 정지할 수 있는 함수를 호출할 수 없고, decides를 선언하지 않은 함수는 실패 가능한 작업을 사용할 수 없습니다. 효과는 호출 스택을 따라 위로 전파됩니다. suspends 함수에 “깨끗한” 래퍼를 씌워 효과를 숨길 수 없으며, 호출자의 시그니처도 피호출자의 효과를 정직하게 반영해야 합니다. 모든 검사는 컴파일 단계에서 끝나므로 런타임 비용은 없습니다.

Verse의 기본 전략은 “롤백 가능한 부작용은 기본적으로 허용하지만, 일시 정지와 실패는 기본적으로 허용하지 않는다”입니다. 순수 함수를 기본으로 하는 Haskell과도 다르고, Result<T,E>로 오류를 명시적으로 처리하는 Rust와도 다릅니다. 게임 로직에는 Transform 읽기·쓰기, 사운드 재생, 파티클 생성 등 부작용이 자연스럽게 가득하므로 순수 함수를 강제하면 게임 프로그래머가 견디기 어렵습니다. 반면 “일시 정지”와 “실패”는 제어 흐름을 바꾸므로 명시적으로 표시하게 한 현실적인 절충안입니다.

효과 시스템은 왜 게임 엔진에 특히 중요할까요? 게임 코드의 가장 큰 확장성 병목은 개별 알고리즘이 아니라 병렬화인 경우가 많습니다. 병렬화를 막는 가장 큰 장벽은 하드웨어가 아니라, 두 함수를 동시에 실행해도 되는지 프로그래머와 컴파일러가 알기 어렵다는 점입니다. 효과 시스템은 이 질문을 컴파일러가 대신 답하게 합니다. reads/writes 표기만으로 컴파일 단계에서 두 함수 사이의 데이터 경쟁 가능성을 판단하고, 수동 잠금 없이 병렬 실행 여부를 결정할 수 있습니다.

Verse VM: 레지스터 기반 바이트코드 인터프리터입니다

Verse 런타임은 CoreUObject/Private/VerseVM/에 있고, 핵심 파일은 약 6,541줄의 VVMInterpreter.cpp입니다.

값 표현: 64비트 NaN-boxing. 모든 Verse 값은 64비트로 표현하며, IEEE 754 배정밀도 부동소수점의 NaN 인코딩 공간으로 타입을 구분합니다.

// VVMValue.h - 값 태그
static constexpr uint64 Int32Tag          = 0xffff'0000'0000'0000ull;
static constexpr uint64 PlaceholderTag    = 0x1ull;  // 지연 평가 자리표시자
static constexpr uint64 UObjectTag        = 0x3ull;  // C++ UObject 포인터
static constexpr uint64 CharTag           = 0x4ull;
static constexpr uint64 TransparentRefTag = 0x6ull;

NaN-boxing의 원리를 한 문장으로 설명하면 이렇습니다. 배정밀도 부동소수점에는 정상적인 부동소수점 연산으로는 나오지 않는 넓은 NaN 비트 패턴 영역이 있고, 그 안에 “이 값은 부동소수점이 아니라 태그가 붙은 포인터/정수입니다”라는 정보를 숨길 수 있습니다. 부동소수점 자체는 추가 비용 없이 바로 사용할 수 있지만, 부동소수점이 아닌 값은 비트 연산으로 풀어야 합니다. Lua 5.x, JavaScriptCore와 같은 계열의 아이디어이며, Verse는 동시성 의미를 지원하기 위해 Placeholder와 TransparentRef 태그를 추가했습니다.

Placeholder는 지연 평가의 자리표시자입니다. 어떤 Task가 다른 Task가 아직 만들지 않은 값을 읽으면 Placeholder를 받고 읽기 작업이 일시 정지됩니다. 값이 준비되면 다시 깨어납니다. 이것이 Verse에서 “비동기 대기”를 콜백 지옥이 아니라 언어 원시 기능으로 만드는 핵심입니다.

프레임과 레지스터. Verse VM은 스택식이 아니라 레지스터식입니다. 함수 호출마다 고정된 수의 레지스터를 포함한 VFrame이 만들어집니다.

// VVMFrame.h - 레지스터 규약
// Register 0 = Self
// Register 1 = Scope(제네릭/클로저 캡처)
// Register 2+ = 매개변수

이 규약은 FRegisterIndex::SELF = 0, FRegisterIndex::SCOPE = 1, FRegisterIndex::PARAMETER_START = 2처럼 하드코딩되어 있습니다.

왜 스택식이 아니라 레지스터식을 골랐을까요? JVM이나 CPython 바이트코드 같은 스택식 VM은 명령어가 더 간결하고 생성하기 쉽습니다. Lua 5.x나 Dalvik 같은 레지스터식 VM은 명령어 수가 적고 인터프리터 수준의 피프홀 최적화에 유리하며 불필요한 load/store를 줄여 보통 해석 실행이 더 빠릅니다. Epic은 “바이트코드 크기”보다 “인터프리터 처리량”을 우선했습니다. 매 프레임 대량의 로직을 실행하는 게임 런타임에는 합리적인 선택입니다.

Suspend/Resume. 바이트코드가 아직 준비되지 않은 Placeholder 읽기처럼 대기가 필요한 작업을 만나면, VM은 VBytecodeSuspension 객체를 만들고 현재 PC, 프레임, 효과 토큰을 저장한 뒤 실행을 멈춥니다. 기다리던 값이 준비되면 정지 지점이 다시 스케줄됩니다. 이것이 Verse의 suspends 효과를 런타임에서 구현하는 방식입니다. 컴파일러가 생성한 바이트코드에는 일시 정지 가능한 opcode가 포함될 수 있으며, VM은 해당 opcode를 만나면 실행 컨텍스트를 자동으로 저장하고 복원합니다.

동시성 모델: 다섯 가지 원시 기능과 협력형 스케줄러입니다

Verse의 동시성은 선점형 스레드가 아니라 Task 기반 협력형 동시성입니다. Expression.h에는 다섯 가지 원시 기능이 정의돼 있습니다.

원시 기능  의미  비유
sync {A; B} 순차 실행 일반 코드 블록
race {A; B} 병렬 실행 후 첫 완료 결과를 채택하고 나머지를 취소 Promise.race
rush {A; B} 병렬 실행 후 모두 완료되면 계속 진행 Promise.all
branch {A; B} 병렬 실행 후 첫 작업이 끝나면 반환하고 나머지는 백그라운드에서 계속 실행 경쟁 + 백그라운드
spawn {A} 시작한 뒤 결과를 기다리지 않고 즉시 계속 진행 fire-and-forget

각 원시 기능에는 race(Item:Container) { ... }처럼 컨테이너의 각 요소마다 동시 Task를 시작하는 반복 버전도 있습니다.

VM에서는 동시 실행 블록마다 VTask를 생성합니다.

// VVMTask.h
struct VTask : VValueObject
{
    bool bRunning{true};
    enum class EPhase : int8 {
        Active, CancelRequested, CancelStarted,
        CancelUnwind, Canceled
    };
    EPhase Phase{EPhase::Active};
    FOp* ResumePC{nullptr};            // 실행을 재개할 위치
    TWriteBarrier<VFrame> ResumeFrame; // 재개 시 프레임(GC 쓰기 배리어 포함)
};

ResumeFrame을 TWriteBarrier<>로 감싼 것은 임의의 장식이 아닙니다. GC에 “여기에 힙 객체 참조가 저장됐으니 할당할 때 배리어에 기록하세요”라고 알리는 장치이며, 동시성과 증분 GC가 올바르게 작동하기 위한 전제입니다.

Task는 Placeholder를 통해 동기화됩니다. 협력형 스케줄링에서도 Goroutine과 비슷한 작성 경험을 제공하지만, Go 런타임의 Goroutine은 OS 스레드 위에서 M:N 선점형 스케줄링을 하고 Verse Task는 정지 지점에서 양보하는 협력형 방식입니다. 둘 다 가볍게 작성할 수 있지만 스케줄링 모델은 본질적으로 다릅니다.

트랜잭션과 롤백. Verse의 트랜잭션 시스템은 AutoRTFM(Automatic Rollback Transactions for Failure Memory) 위에 구축됩니다. decides 함수가 실패하면 VM은 Trail(변경 로그)을 통해 트랜잭션 내부의 모든 쓰기 작업을 되돌립니다. Trail은 대입 전 값을 기록하고, 롤백 시 역순으로 복원합니다. Verse 프로그래머는 decides만 선언하면 되고, 컴파일러가 트랜잭션 경계 코드를 자동 생성합니다.

AutoRTFM에 대해서는 두 가지를 분명히 해야 합니다.

  • 전체 이름은 Automatic Rollback Transactions for Failure Memory이며, Runtime Transactional Memory가 아닙니다. 목적은 “실패 롤백”이지 멀티스레드 동시성 제어가 아닙니다.
  • Epic은 Clang 분기를 유지하며, 컴파일러가 C++ 코드를 정확한 undo 작업을 기록할 수 있는 형태로 자동 변환합니다. 이를 통해 C++와 Verse가 같은 트랜잭션 의미를 공유합니다. no_rollback 효과가 필요한 이유도 여기에 있습니다. 파일 쓰기나 네트워크 패킷 전송 같은 실제 I/O는 되돌릴 수 없으므로 트랜잭션 밖에 명시적으로 격리해야 합니다.

Scene Graph: Actor가 사라진 뒤 게임 세계는 누가 맡을까요?

지금까지 Verse 언어 계층을 분석했습니다. 이제 약 12만 줄의 C++, 638개 파일, 6개 모듈로 이루어진 Engine/Plugins/EntityFramework/를 살펴보겠습니다. Verse 코드가 실제로 실행되는 “운영체제”에 해당합니다.

Entity: 비어 있는 컨테이너입니다

Entity.h를 열면 verse::entity는 UBaseEntity를 상속하며, 핵심 데이터는 배열 두 개뿐입니다.

// Entity.h - UBaseEntity의 핵심 데이터
UPROPERTY()
TArray<TObjectPtr<UObject>> Components;     // Component 목록

UPROPERTY()
TArray<TObjectPtr<UObject>> OwnedEntities;  // 자식 Entity 목록

정말 이 두 배열뿐입니다. Entity 자체에는 Transform도, Tick도, 렌더링 로직도 없습니다. Component 목록과 자식 Entity 목록을 보관하는 일만 합니다. Component 목록을 보관하고 Transform이 필수 내장인 Unity GameObject와도 다릅니다. UE6에서는 Transform조차 선택형 Component입니다.

이 차이는 중요합니다. Unity에서는 3D 공간에 존재하지 않는 순수 로직 객체도 Transform을 강제로 가져야 하는 역사적 부담이 있습니다. UE6는 Transform을 선택형 Component로 내리면서 순수 데이터·순수 로직 노드가 공간 관련 비용 없이 Scene Graph에 존재할 수 있게 했습니다. 따라서 Entity는 광원, 소리, AI 행동 트리, UI 요소 등 무엇이든 표현할 수 있고 서로 다른 기반 클래스를 상속할 필요가 없습니다. Entity는 순수한 “Scene Graph 노드”이며 모든 행동은 Component가 제공합니다.

Component: 6단계 생명주기입니다

Component.h는 엄격한 순서의 여섯 단계 생명주기를 정의합니다.

// Component.h - Component 생명주기
//  1. OnInitialized
//  2. OnAddedToScene
//  3. OnBeginSimulation
//  4. OnEndSimulation
//  5. OnRemovingFromScene
//  6. OnUninitializing

소스에서는 EEntityNotificationState 열거형으로 Entity와 Component가 현재 어느 단계인지 추적합니다. Created부터 TearedDown까지 총 20개 상태가 있습니다. Initializing 단계에 Component를 추가해도 엔진이 새 Component의 Initialized → AddedToScene → BeginSimulation 과정을 보장하고, 호스트 Entity와 같은 상태까지 동기화합니다.

UE5에서는 BeginPlay 도중 Component를 추가하면 다음 프레임이 되어야 Tick을 받는 등 상태가 “반 박자 늦게” 따라올 수 있습니다. UE6에서는 엔진이 상태 동기화를 보장하고, 새 Component가 호스트와 같은 생명주기 단계까지 자동으로 보충됩니다.

Entity 트리와 이벤트 전파

Entity는 트리를 구성하며 각 Entity에는 Parent와 Children이 있습니다. 이 트리가 씬 이벤트의 전파 방향을 결정합니다.

// Entity.h - 씬 이벤트 전파
bool SendUp(TInterfaceInstance<verse::scene_event> SceneEvent);    // 버블링
bool SendDown(TInterfaceInstance<verse::scene_event> SceneEvent);  // 브로드캐스트

SendUp은 Parent 체인을 따라 위로 버블링하고, SendDown은 Children 체인을 따라 아래로 브로드캐스트합니다. 트리 구조를 따라 전파한다는 점에서 Web DOM 이벤트 모델과 비슷합니다. 자식 Entity의 OnDamage 이벤트를 부모에게 올릴 수 있고, 부모 Entity의 OnPause 이벤트를 모든 자식에게 내릴 수도 있습니다. 직접 순회 코드를 작성할 필요가 없습니다.

다만 DOM 이벤트는 “캡처 → 대상 → 버블링”의 3단계와 stopPropagation/preventDefault 같은 완전한 체계를 갖고 있습니다. Scene Graph의 SendUp/SendDown은 방향이 명확한 두 API로 의미가 더 단순합니다. 직관을 얻기 위한 비유일 뿐, 정확한 의미는 소스 코드를 기준으로 봐야 합니다.

실행 엔진: Tick은 사라지고 DAG가 살아났습니다

UE5의 AActor::Tick(float DeltaTime)에는 치명적인 문제가 있습니다. 모든 Actor의 Tick 순서가 암묵적입니다. TickGroup과 AddTickPrerequisiteActor로 의존 관계를 수동 관리해야 하므로, 조금만 실수하면 “물리가 끝나기도 전에 애니메이션이 위치를 읽는” 경쟁 상태가 생길 수 있습니다.

UE6는 Execution Engine으로 이 모델을 완전히 다시 작성했습니다. 핵심은 ExecutionPhase.h입니다.

// ExecutionPhase.h - 실행 단계는 방향성 비순환 그래프입니다
class FExecutionPhase final
{
    // 동기화 노드(콜백 없이 의존 순서만 지정)
    FUpdateConnector RegisterSyncNode(FName Name, bool Conditional,
        const FUpdateDependencies& Dependencies);

    // 함수 노드(매 프레임/간격 호출)
    FUpdateConnector RegisterFunction(TExecuteFn Function,
        const FUpdateDependencies& Dependencies, float Interval = 0.0f);

    // 객체 노드(멀티스레드 일괄 처리 지원)
    FUpdateConnector RegisterObject(UObject* Object,
        TUObjectExecuteFn Function,
        const FUpdateDependencies& Dependencies,
        float Interval = 0.0f, uint8 MaxThreadWidth = 1);
};

FExecutionPhase 내부는 방향성 비순환 그래프(DAG)입니다. 노드는 RunAfter/RunBefore로 의존 관계를 선언하고, 엔진은 런타임에 자동으로 위상 정렬합니다. UE6에는 PrePhysics → StartPhysics → DuringPhysics → EndPhysics → PostPhysics → EndFrame의 6개 Phase가 미리 정의돼 있습니다.

UE5 Tick과의 본질적 차이는 다음과 같습니다.

항목 UE5 Tick UE6 Execution Phase

의존 관계 관리 암묵적(TickGroup + Prerequisite) 명시적(RunAfter/RunBefore DAG)
병렬화 수동(TaskGraph) 자동(Object 노드 MaxThreadWidth)
조건부 실행 수동 bool 검사 Conditional Sync Node 내장
동적 등록 BeginPlay에서 바인딩 런타임 Register/Unregister

가장 중요한 차이는 의존 관계가 명시적이라는 점입니다. UE5에서는 같은 그룹에 있는 두 Actor의 실행 순서가 불확실합니다. UE6에서 B.RunAfter(A)를 작성하면 B는 반드시 A 뒤에 실행됩니다. 경험과 TickGroup 미세 조정에 의존하던 순서를 등록 단계부터 검증 가능한 의존 그래프로 바꾼 것입니다. A가 B를 의존하고 B가 다시 A를 의존하는 순환이 생기면, 런타임에서 무작위로 문제를 일으키는 대신 위상 정렬 단계에서 즉시 드러납니다.

SceneGraphAPI: Entity를 조작하는 유일한 진입점

UE6 소스에는 명확한 규칙이 있습니다. Scene 구조를 수정할 때 Entity 메서드를 직접 호출하지 말고 UE::SceneGraphAPI 네임스페이스를 사용해야 합니다. 직접 호출하면 Undo/Redo 스택, Outliner 갱신, 뷰포트 다시 그리기 같은 에디터 갱신이 일어나지 않기 때문입니다.

// Entity.h - SceneGraphAPI의 핵심 작업
namespace UE::SceneGraphAPI
{
    TNotNull<verse::entity*> CreateEntity(...);
    bool AddEntity(...);
    bool RemoveOwnedEntity(...);
    verse::component* CreateComponent(...);
    verse::component* GetOrCreateComponentByType(...);

    void BeginBatchOperation(TNotNull<UWorld*> InWorld);  // 에디터 갱신 일시 정지
    void EndBatchOperation(TNotNull<UWorld*> InWorld);    // 한 번에 갱신
}

BeginBatchOperation/EndBatchOperation은 특히 중요합니다. 수천 개의 Entity를 한 번에 만들 때 Batch로 감싸면 에디터가 한 번만 갱신되므로, Entity를 하나 추가할 때마다 Outliner를 다시 그리는 치명적인 비용을 피할 수 있습니다.

Verse와 Scene Graph는 어떻게 맞물릴까요?

지금까지 두 시스템을 각각 분석했습니다. 이제 두 시스템이 연결되는 방식을 살펴보겠습니다. UE6 새 아키텍처에서 가장 핵심적인 설계입니다.

ECS의 기반: Fragment, Archetype, Processor

UE6의 ECS는 Runtime/MassEntity/에 구현돼 있습니다. Mass Entity는 원래 Epic AI 팀이 대규모 군중과 교통 시뮬레이션을 위해 개발했으며, 2021년 《The Matrix Awakens》 데모에서 처음 공개됐습니다. 핵심 개념은 세 가지입니다.

Fragment——순수 데이터이며 Actor Component를 대신합니다.

// EntityFragments.h
USTRUCT()
struct FTransformFragment : public FMassFragment
{
    GENERATED_BODY()
    UPROPERTY()
    FTransform Transform;
};

USTRUCT()
struct FHealthFragment : public FMassFragment
{
    GENERATED_BODY()
    UPROPERTY()
    float CurrentHealth = 100.0f;
};

Archetype——Fragment의 조합 템플릿입니다. 같은 Archetype을 사용하는 모든 엔티티가 동일한 메모리 레이아웃을 공유하므로 Processor가 연속 메모리를 순회할 수 있고 CPU 캐시에 유리합니다. OOP의 “객체별 가상 함수 호출”과 비교했을 때 ECS가 갖는 핵심 성능 이점입니다.

Processor——순수 로직이며 Actor::Tick을 대신합니다.

// MassProcessor.h
UCLASS(abstract, ...)
class UMassProcessor : public UObject
{
    UE_API virtual void ConfigureQueries(...);
    UE_API virtual void Execute(FMassEntityManager& EntityManager,
        FMassExecutionContext& Context);
};

ECS가 빠른 이유를 한 문장으로 정리하면 이렇습니다. 전통 OOP는 각 객체가 데이터와 가상 함수 테이블을 가진 객체 배열을 순회하며 캐시 미스와 간접 점프를 반복합니다. ECS는 같은 종류의 데이터를 연속으로 배치하고 하나의 Processor가 파이프라인처럼 훑습니다. 엔티티 수가 수백 개에서 수만·수십만 개로 늘면 차이는 차원이 달라집니다. UE6가 Actor 체계를 퇴역시키려는 자신감도 여기에서 나옵니다. 새 모델은 대량의 동질 엔티티를 처리할 때 태생적으로 더 잘 확장됩니다.

NPC 하나를 처음부터 만들기: Verse → ECS 전체 흐름을 네 단계로 살펴보겠습니다

아키텍처 그림보다 실행 가능한 코드 한 조각이 더 설득력 있습니다. NPC의 데이터 정의부터 동시성 스케줄링까지 전체 생명주기를 따라가며 Verse와 Scene Graph의 협력 방식을 살펴보겠습니다.

1단계: Fragment 정의(C++ 측)

Actor/Component 체계에서는 NPC를 만들기 위해 ACharacter를 상속하고 UHealthComponent, UBehaviorTreeComponent를 붙인 뒤 Tick을 재정의해야 합니다. ECS 체계에서 NPC는 Fragment 몇 개의 조합일 뿐입니다.

// NPCFragments.h - C++ 측 순수 데이터 정의
USTRUCT()
struct FHealthFragment : public FMassFragment
{
    GENERATED_BODY()
    UPROPERTY()
    float CurrentHealth = 100.0f;
    UPROPERTY()
    float MaxHealth = 100.0f;
};

USTRUCT()
struct FBehaviorStateFragment : public FMassFragment
{
    GENERATED_BODY()
    UPROPERTY()
    int32 State = 0;  // 0=Idle, 1=Patrol, 2=Combat, 3=Fleeing
};

FTransformFragment는 엔진에 이미 내장돼 있습니다. 여러 Fragment를 Archetype으로 조합하면 엔진이 연속 메모리 레이아웃을 자동으로 할당합니다. 상속도, 가상 함수도, Tick도 없습니다. Fragment는 순수 struct이고 모든 필드가 UPROPERTY()이므로 곧바로 직렬화하고 네트워크 복제할 수 있습니다.

2단계: Verse에서 엔티티 생성

UE6 코드 생성기는 각 FMassFragment에 대응하는 Verse 타입 바인딩을 자동으로 만듭니다. OpNewObject 명령어는 Verse 클래스에 NativeRepresentation 플래그가 있는지 런타임에 검사하고, 플래그가 있으면 순수 Verse 힙 객체 대신 C++ UObject를 생성합니다. 따라서 Verse 코드에서 ECS 엔티티를 직접 조작할 수 있습니다.

# NPC.verse - Verse 측 NPC 엔티티 생성
# <decides>는 실패 가능성을 뜻합니다(공간이 이미 점유된 경우 등)
# <transacts>는 ECS 상태 변경처럼 롤백 가능한 부작용을 뜻합니다
InitNPC<decides><transacts>(Position:vector3, PatrolPath:[]vector3):entity =
    var NPC:entity = CreateEntity(
        FTransformFragment{Transform := MakeTransform(Position)},
        FHealthFragment{CurrentHealth := 100.0, MaxHealth := 100.0},
        FBehaviorStateFragment{State := 1}  # 1 = Patrol
    )
    # 생성에 실패하면 decides가 트랜잭션 롤백을 자동으로 실행합니다
    # if-failed-then-cleanup을 직접 작성할 필요가 없습니다
    NPC

<decides> 표기에 주목해야 합니다. 공간 점유 등의 이유로 CreateEntity가 실패하면 Verse 트랜잭션 시스템이 해당 트랜잭션의 부작용을 자동으로 되돌립니다. UE5에서는 if (SpawnActorFailed) { DestroyActor(); RefundResources(); } 같은 정리 코드를 직접 작성해야 하지만, Verse에서는 “실패 시 롤백”이 언어의 원시 기능입니다.

3단계: Processor 정의(Verse 측)

Processor는 효과 표기가 붙은 Verse 함수이며, ECS 스케줄러가 프레임 단위로 호출합니다.

# NPCBehavior.verse - Verse 측 Processor 로직
# <reads> + <writes>는 이 함수가 어떤 Fragment를 읽고 쓰는지 알려 줍니다
# 두 Processor가 같은 Fragment를 쓴다면 병렬 실행할 수 없습니다
PatrolProcessor<reads(TransformFragment, BehaviorStateFragment),
                writes(TransformFragment)>
    (Entities:[]entity, DeltaTime:float):void =

    for (NPC : Entities):
        var State = GetFragment[NPC, FBehaviorStateFragment].State
        var Transform = GetFragment[NPC, FTransformFragment].Transform

        if (State = 1):  # Patrol 상태
            var NewPos = MoveAlongPath(Transform.Position, DeltaTime)
            SetFragment[NPC, FTransformFragment]{
                Transform := MakeTransform(NewPos)
            }

reads와 writes는 문서용 주석이 아닙니다. 다른 Processor도 writes(TransformFragment)를 선언한 상태에서 스케줄러가 둘을 병렬 실행하려 하면, 컴파일러 또는 스케줄러가 안전하지 않은 실행을 거부합니다.

4단계: 동시성 구성

마지막으로 Verse의 동시성 원시 기능을 사용해 여러 Processor를 한 프레임의 실행 계획으로 구성합니다.

# WorldTick.verse - 프레임별 최상위 스케줄링
TickWorld(DeltaTime:float):void =
    # rush: 세 Processor가 서로 다른 엔티티 그룹에서 병렬 실행됩니다
    # 효과 시스템은 서로 다른 Fragment를 조작할 때 충돌이 없음을 보장합니다
    rush:
        PatrolProcessor(PatrolEntities, DeltaTime)
        CombatProcessor(CombatEntities, DeltaTime)
        PhysicsProcessor(AllPhysicsEntities, DeltaTime)

    # 위 세 작업이 모두 끝난 뒤, 그 결과에 의존하는 Processor를 실행합니다
    # sync 블록의 Processor는 위 결과를 읽을 수 있습니다
    sync:
        AnimationProcessor(AllNPCs, DeltaTime)
        AudioProcessor(AllNPCs, DeltaTime)

rush는 ECS의 여러 Processor 병렬 실행으로, sync는 의존 체인으로 직접 매핑됩니다. 이 네 단계를 거치면 NPC는 데이터 정의부터 프레임별 스케줄링까지 깊은 상속 체인도, 가상 함수 호출도, 수동 잠금도 필요하지 않습니다. Fragment는 순수 데이터이고 Processor는 순수 로직이며, 동시성 안전은 효과 시스템이 컴파일·스케줄 단계에서 보장합니다.

효과 시스템 = ECS 병렬 안전의 컴파일 단계 보장

Verse 효과 시스템과 Scene Graph가 가장 깊이 연결되는 지점입니다.

  • reads(Fragment) → 해당 Processor가 Fragment를 읽기만 하므로 같은 Fragment를 읽기만 하는 다른 Processor와 병렬 실행할 수 있습니다.
  • writes(Fragment) → 해당 Processor가 Fragment를 쓰므로 같은 Fragment를 읽거나 쓰는 다른 Processor와 병렬 실행할 수 없습니다.
  • allocates → 함수가 새 엔티티를 만들어 Archetype 메모리를 할당한다는 뜻이며, GC 쓰기 배리어가 필요합니다.
  • suspends → 함수가 비동기 작업을 기다릴 수 있으므로, 스케줄러가 전체 프레임을 막지 않고 해당 함수만 정지할 수 있습니다.

Rust의 borrow checker와 뿌리는 비슷하지만 접근법은 다릅니다. Rust는 개별 데이터의 소유권과 생명주기로 데이터 경쟁을 막고, Verse는 “함수가 접근하는 Fragment 집합” 단위의 효과 선언으로 병렬 실행 가능성을 판단합니다. 둘 다 컴파일 단계의 병렬 안전을 구현하지만, Verse는 의도를 선언하면 컴파일러와 스케줄러가 검증하는 방식이며 ECS의 일괄 처리 모델에 맞게 더 굵은 단위를 사용합니다.

MCP: AI가 UE6에 들어오는 게이트

MCP(Model Context Protocol)는 Verse가 노출한 엔진 기능을 외부 AI에 개방합니다. 소스는 Programs/UnrealConsole/Private/MCP/에 있습니다.

// UCMcpIntegration.h
namespace UE::UnrealConsole::MCP {
    void Init();     // 모든 MCP 도구 등록
    bool RegisterToolsetClass(UClass* ToolsetClass);
}

Epic은 UE6 발표에서 개방형 MCP 인프라로 엔진 기능을 노출하고, 개발자가 Claude, Codex 등 여러 선도 모델을 조합해 엔진 콘텐츠 제작에 활용할 수 있게 하겠다고 분명히 밝혔습니다. 여기서 “엔진 기능”에는 Verse API도 포함됩니다. AI가 Verse 코드를 생성해 엔티티를 만들고 Fragment를 수정하며 Processor를 스케줄할 수 있습니다.

한 문장으로 정리하면 MCP는 AI가 UE6에 들어오는 문이고, Verse는 AI가 문 안에서 사용하는 언어입니다. 개발자가 Verse로 Fragment와 Processor를 정의하면 MCP가 해당 API를 외부 AI에 공개하고, AI가 MCP를 통해 Verse API를 호출해 콘텐츠 생성을 돕습니다. Verse → ECS → MCP → AI → Verse로 이어지는 폐쇄형 순환 구조입니다.

AI가 엔진 메모리를 직접 고치는 대신 “결정론적 API”를 사용하는 이유도 중요합니다. MCP는 모델에 원시 포인터를 넘기는 것이 아니라 타입과 권한 경계가 있는 구조화된 도구 호출을 제공합니다. Verse 효과 시스템과 트랜잭션 롤백을 결합하면 AI가 만든 작업이 실패하더라도 “실패 시 롤백”으로 안전하게 되돌릴 수 있습니다. 이는 AI가 프로젝트를 안전하게 수정하기 위한 공학적 전제입니다. 효과 시스템은 사람뿐 아니라 AI를 위한 안전망이기도 합니다.

4계층 아키텍처 전체 모습

지금까지 분석한 모듈을 모두 합치면 다음과 같습니다.

┌──────────────────────────────────────────┐
│  MCP                                     │  AI 인터페이스 계층
│  Verse API를 Claude/Gemini/Codex에 노출   │
├──────────────────────────────────────────┤
│  Verse                                   │  언어 계층
│  효과 시스템(병렬 안전) + 동시성 원시 기능 │
│  + 트랜잭션 메모리(롤백) + GC(메모리 관리) │
├──────────────────────────────────────────┤
│  Scene Graph + ECS (Mass Entity)         │  런타임 계층
│  Fragment(데이터) + Archetype(레이아웃)   │
│  + Processor(로직) + EntityManager(스케줄)│
│  + ExecutionPhase(DAG) + SceneGraphAPI   │
├──────────────────────────────────────────┤
│  UE6 Core (렌더링/물리/네트워크/오디오...) │  엔진 계층
└──────────────────────────────────────────┘

마이그레이션 관점: 기존 UE5 프로젝트는 어떻게 해야 할까?

대부분의 독자에게는 “소스 코드가 얼마나 아름다운가”보다 지금 가진 UE5 프로젝트가 새 아키텍처 때문에 한 번에 잘려 나갈지가 더 현실적인 문제입니다. 몇 가지를 판단해 보겠습니다.

  • “하룻밤에 교체”되는 것이 아니라 “장기간 공존”합니다. Epic은 Actor/Blueprint가 UE6 초기 버전에서도 유지되며 새 프레임워크가 “충분히 성숙”한 뒤 단계적으로 지원 중단되고 변환 도구가 제공된다고 밝혔습니다. Early Access는 2027년 말이고 정식 버전은 그보다 12~18개월 뒤이므로 프로젝트에는 수년의 전환 기간이 있습니다.
  • C++는 사라지는 것이 아니라 “아래 계층으로 내려갑니다.” C++는 엔진 저수준과 고성능 모듈을 맡고 Verse가 gameplay 계층을 담당합니다. “gameplay에서 C++의 위치가 C++에서 어셈블리의 위치와 비슷해집니다”라고 이해하는 편이 정확합니다. 퇴출이 아니라 계층 분리입니다.
  • Mass/ECS는 완전히 새로운 개념이 아닙니다. Mass Entity는 UE5부터 실험적 플러그인으로 제공됐고, 이미 군중·교통·탄막 같은 대규모 엔티티 작업에 사용하는 팀이 있습니다. UE5에서 미리 Mass를 익히는 것이 UE6 런타임 모델로 부드럽게 넘어가는 가장 저렴한 방법입니다.
  • Verse는 지금도 배울 수 있습니다. Verse는 UEFN에서 이미 수년간 공개됐고, 문법·효과 시스템·failure context 같은 핵심 개념도 지금 익힐 수 있습니다. UE6가 나온 뒤 처음 배우면 학습 곡선이 마이그레이션 기간에 몰리므로 위험이 더 커집니다.

현실적으로 말씀드리면, 지금 시작해 2027~2029년까지 이어질 새 프로젝트라면 “UE6로 다시 작성하는 데 베팅”하기보다 “UE5에서부터 UE6에 친화적인 구조를 채택”하는 편이 좋습니다. 데이터와 로직을 분리하고, 깊은 상속 체인을 줄이며, 핵심 시스템에는 Mass와 데이터 지향 구조를 우선 고려하세요. 실제로 이전하든 하지 않든 프로젝트 구조가 더 건강해집니다.

위험과 미해결 질문: 이 아키텍처가 앞으로 답해야 할 것은 무엇일까?

책임 있는 분석이라면 장점만 이야기해서는 안 됩니다. 이 아키텍처에는 아직 충분히 답하지 못한 문제가 남아 있습니다.

  • 디버깅과 관측 가능성. 효과 시스템, 트랜잭션 롤백, 협력형 Task, 레지스터 VM은 모두 정확성과 성능을 위한 추상화입니다. 하지만 추상화 계층이 늘수록 문제가 생겼을 때 원인을 찾기 어려워집니다. 깊은 호출 속 decides가 실패해 전체 트랜잭션이 롤백됐을 때 어느 단계가 실패를 일으켰는지 어떻게 확인할까요? 중단점, 재생, 효과 시각화 같은 도구 체인이 언어의 야심을 따라갈 수 있는지가 성패를 좌우합니다.
  • 성능 예측 가능성. 협력형 스케줄링은 잠금을 피하지만, 양보하지 않는 긴 작업 하나가 같은 스케줄 컨텍스트의 다른 Task를 지연시킬 수 있습니다. NaN-boxing은 부동소수점 비용을 없애지만 다른 값에는 언패킹 비용을 추가합니다. 데모에서는 아름다운 절충이 실제 프로젝트의 GC 정지와 캐시 변동 속에서도 잘 작동할지는 대규모 검증이 필요합니다.
  • 생태계와 인재. Verse는 완전히 새로운 언어이며 함수형 논리 + 효과 시스템 패러다임은 C++/Blueprint에 익숙한 팀에 실제 학습 비용을 요구합니다. 서드파티 플러그인, 튜토리얼, Stack Overflow 답변, AI 학습 데이터도 처음부터 다시 쌓아야 합니다. Epic은 AI 지원을 해법 중 하나로 보지만, AI 생성 코드의 품질과 유지보수성이라는 새로운 문제도 생깁니다.
  • “두 가지를 동시에 교체”하는 복합 위험. 언어와 객체 모델을 함께 교체하면 어느 한 계층의 문제도 전체 마이그레이션을 지연시킬 수 있습니다. Epic이 UE4→UE5보다 어렵다고 인정한 근본적인 이유입니다. 보상은 클 수 있지만 실행 위험도 실제로 두 배가 됩니다.

이 문제들을 꺼내 놓는 것은 비관하려는 목적이 아닙니다. 아키텍처에 걸린 판돈이 큰 만큼, 의심을 품고 진지하게 검토할 가치가 있기 때문입니다.

이 아키텍처를 진지하게 봐야 하는 이유

Verse 컴파일러, Verse VM, EntityFramework, Mass Entity를 모두 살펴본 뒤 내릴 수 있는 판단은 다음과 같습니다.

첫째, 이것은 “UE5 위에 스크립트 계층 하나를 얹은 것”이 아닙니다. Verse 효과 시스템은 컴파일러 전 단계에 깊이 통합돼 있습니다. Parser가 효과 표기를 해석하고, SemanticAnalyzer가 효과 호환성을 검사하며, IRGenerator가 효과 추적 바이트코드를 생성하고, VM이 런타임에 효과 토큰을 전달합니다. 도구 체인 전체가 효과 시스템을 중심으로 다시 설계됐습니다. 효과 시스템은 Scene Graph의 병렬 안전과 직접 연결됩니다. reads/writes 표기로 컴파일러와 스케줄러가 Processor의 병렬 실행 가능성을 판단합니다. 언어와 런타임이 서로를 정의합니다.

둘째, 동시성 모델이 현실적이고 완성도 있게 구성돼 있습니다. race/rush/branch/spawn은 리소스 로딩, 경쟁 검색, 백그라운드 작업, 다중 병렬 처리처럼 게임 개발에서 자주 쓰는 동시성 패턴을 포괄합니다. 효과 추적을 기본 지원하고 ECS의 다중 Processor 스케줄링으로 직접 매핑되므로 프로그래머가 스레드 풀, mutex, TaskGraph를 직접 관리할 필요가 없습니다.

셋째, 트랜잭션 메모리는 게임 로직의 가장 골치 아픈 문제 중 하나를 해결합니다. 게임 로직에는 “어떤 작업을 시도하고 실패하면 전부 되돌리는” 패턴이 자주 나옵니다. 건물 배치를 예로 들면 공간 검사 → 자원 차감 → 엔티티 생성 중 한 단계라도 실패할 경우 모든 변경을 취소해야 합니다. Verse 트랜잭션 시스템은 이 패턴을 언어 원시 기능으로 만들고 컴파일러가 롤백 로그를 자동 관리합니다. ECS에서는 여러 엔티티의 Fragment를 원자적으로 수정하는 것과 같습니다. 전부 성공하거나 전부 롤백됩니다.

넷째, 소스 코드가 이미 준비돼 있습니다. VerseCompiler + VerseVM + EntityFramework + Mass Entity 모듈이 공개 저장소에 들어 있고 컴파일·실행·디버깅할 수 있습니다. Epic은 UE6 소스 공개 초기부터 새 프로그래밍 모델 전체를 넣었으며 단순한 PPT만 보여 준 것이 아닙니다.

다섯째, AI가 이 아키텍처의 일급 시민입니다. MCP는 나중에 덧붙인 외부 플러그인이 아니라 설계 단계부터 준비된 인터페이스입니다. Verse로 엔진 기능을 정의하고 MCP로 AI에 공개하면, AI가 Verse 코드를 생성해 엔진을 조작합니다. UE6는 “AI가 게임 개발에 참여합니다”라는 가정을 기반으로 설계됐으며 “AI가 어쩌면 조금 쓸모 있을 수도 있습니다”라는 수준이 아닙니다.

Epic이 언어와 객체 모델을 동시에 교체하려는 이유는 C++가 느리거나 Actor가 나빠서가 아닙니다. 언어 계층(Verse)과 런타임 계층(Scene Graph + ECS)을 함께 재설계하면 둘 사이의 인터페이스를 극한까지 최적화할 수 있기 때문입니다. 효과 시스템이 병렬 안전을 보장하고, 동시성 원시 기능이 스케줄러에 매핑되며, 트랜잭션 메모리가 Fragment 원자 작업에 연결됩니다. 언어나 런타임 중 하나만 바꿔서는 만들 수 없는 구조입니다.