| 포인트 | 이유 |
|---|---|
| PhaseHandles를 멤버 변수로 유지 | TSharedPtr 소멸 시 엔진이 에셋을 언로드할 수 있기 때문 |
| 단계별 순차 진행 (콜백 체인) | 병렬 로드보다 메모리 피크가 낮고 의존성 제어가 쉬움 |
| DataCache의 Key를 FPrimaryAssetId로 | 타입 안전한 검색을 위해 GetData 템플릿과 키 타입을 일치시킴 |
| GetPrimaryAssetIdList 사용 | 등록된 에셋 목록을 Asset Manager가 관리하므로 하드코딩 ID 배열 불필요 |
| UGameInstanceSubsystem 상속 | 레벨 전환에도 살아남아 데이터를 계속 유지할 수 있음 |
| 항목 | UEDAssetManager | UEDGameDataSubsystem |
|---|---|---|
| 상속 베이스 | UAssetManager (엔진 싱글톤) | UGameInstanceSubsystem (게임 인스턴스 소속) |
| 레이어 | 저수준 인프라 (Infrastructure) | 고수준 오케스트레이션 (Orchestration) |
| 핵심 질문 | "이 에셋을 어떻게 메모리에 올리나?" | "게임에 지금 어떤 데이터가 필요하고, 언제 배포하나?" |
| 관심 대상 | FSoftObjectPath, FStreamableHandle, I/O | EDataLoadPhase, DataCache, 게임 시스템 배포 |
| 생존 범위 | 엔진 전체 (에디터 포함) | 게임 인스턴스 수명과 동일 |
| 항목 | UEDAssetManager | UEDGameDataSubsystem |
|---|---|---|
| 동기 로드 | ✅ LoadAssetSync, LoadPrimaryAssetSync | ❌ 직접 수행 안 함 |
| 비동기 로드 | ✅ LoadAssetAsync, LoadPrimaryAssetsAsync | ✅ 내부에서 UEDAssetManager를 호출 |
| 에셋 캐싱 | ❌ 로드 후 포인터 반환만 | ✅ DataCache에 저장, GetData()로 접근 |
| 로드 순서 제어 | ❌ 단순 요청만 처리 | ✅ Lobby → Item → Monster 순차 진행 |
| 단계 변화 감지 | ❌ | ✅ OnPhaseChanged 델리게이트 |
| 완료 신호 | ❌ 콜백만 제공 | ✅ OnAllDataLoaded 브로드캐스트로 게임 시작 신호 |
| 핸들 수명 관리 | 호출자가 직접 관리 | PhaseHandles TMap으로 일괄 관리 |
| 상황 | 어느 클래스? |
|---|---|
| 특정 에셋 하나를 즉시 동기 로드 | UEDAssetManager |
| 인벤토리 아이콘 UI용 비동기 로드 | UEDAssetManager |
| 게임 시작 시 모든 데이터 일괄 로드 | UEDGameDataSubsystem |
| "고블린 스탯이 뭐야?" 데이터 조회 | UEDGameDataSubsystem::GetData() |
| 로딩 화면 단계 텍스트 업데이트 | UEDGameDataSubsystem::OnPhaseChanged |
| 로드 완료 후 게임 시작 트리거 | UEDGameDataSubsystem::OnAllDataLoaded |
로드 완료 보장의 차이라고 보면된다. 로드 완료가 보장된다. 항상 방어코드적어주지 않아도 됨| UEDAssetManager 직접 | UEDGameDataSubsystem 경유 | |
|---|---|---|
| 로드 완료 보장 | ❌ nullptr 가능 | ✅ OnAllDataLoaded 이후 항상 유효 |
| FPrimaryAssetId 구성 | 매번 직접 | 로드 시 1회만 |
| 핸들 수명 | 호출자가 직접 관리 | SyncManager가 일괄 관리 |
| 방어 코드 | 매번 nullptr 체크 필요 | 불필요 |
UEDSyncManager는 내부적으로 GetPrimaryAssetIdList()로 에셋 목록을 수집하는데 이 함수는 ProjectSettings -> Asset Manager에 등록된 Primary Asset 타입만 조회
메셋, 텍스처같은 리소스는 Primary Asset이 아니기 때문에 목록에 잡히지 않음 얘네들은 에셋번들로 불러와야한다.
Seconday Asset은 Primary Asset이 참조하면 자동으로 로드 된다. 번들이 이에 해당
EDAssetManager는 배달부역할만 함
MonsterData 목록줘, 비동기 로드해줘, 완료되었으니 포인터 줘
SyncManager가 언제, 무엇을, 어떤 순서로 로드할지 결정하고, EDAssetManager는 그 지시를 받아서 실제 I/O 작업만 수행합니다.
Asset Manager에 Primary Asset을 등록해놓음
-> UAssetManager 즉 부모에서 이것을 TMap 형태로 저장해놓음 PrimaryAssetMap으로
-> 이 맵이 언제 채워지냐 StartInitialLoading호출될때 Super::에서 호출되어서 자동으로 채워짐
현재 EDAssetManger는 그냥 커스텀으로 써본것뿐이지 크게 의미를 가지지는 않음
-> 로그를 찍거나, 측정시간을 가지거나 하는식, 나중에 커스텀 우선순위나 블랙리스트 같은거 등록할때는 꽤 유용하게 사용가능하다 확장성을 위한 구조라고 생각하자
EDSyncManager는 역할이 명확
-> 무엇을 어떤 순서로 로드할지 결정
-> 로드 완료된 값을 캐싱 해놓음
-> 데이터 접근 인터페이스를 제공한다.
-> UEDAssetManager에게 로드를 지시하고, 완료된 데이터를 캐시에 보관하고, 게임 코드에 배포하는 데이터 초기화 파이프라인
EDSyncManager -> EDGameDataSubsystem으로 이름 변경
PrimaryAsset은 EDGameDataSubsystem으로 로드하고, UPrimaryDataAsset상속받은 그 클래스에서 secondary asset들은 TSoftObjectPtr로 등록후 번들명을 저장해서 같이 로드할 수 있음
저장장치계층구조 : CPU와 가까울수록 속도가 빠르지만 용량이 작고 가격이 비싸다
캐시메모리 : 속도의 완충지대, CPU가 연산을 아무리 빨리해도 메모리에 데이터를 가져오는 시간이 오래 걸리면 CPU는 멍하니 기다려야 함, 이를 해결하기 위해 CPU와 메모리 사이에 위치한 SRAM 기반의 고속 저장 장치가 캐시 메모리
L1 : 코어와 가장 가깝고 빠름, 보통 코어마다 고유하게 할당
L2 : L1보다 용량은 크지만 조금 더 느림, 코어마다 할당되는 경우가 많음
L3 : 가장 용량이 큼, 여러 코어가 공유하는 형태로 사용
캐시 메모리는 용량이 작기 때문에 모든 내용을 담을 수 없다. 대신 CPU가 "앞으로 사용할 법한 데이터"를 예측하여 미리 가져다 놓는다. 이 예측의 근거가 되는 것이 바로 참조 지역성의 원리
최근에 접근했던 메모리 공간에 다시 접근하려는 경향
Tick(float DeltaTime) 함수 내에서 매 프레임 사용하는 DeltaTime 변수나 특정 액터의 참조 변수들은 한 번 사용된 후 곧 다시 사용될 확률이 높다.
접근한 메모리 공간 근처의 데이터를 다시 접근하려는 경향
TArray에 담긴 데이터들은 메모리상에 쪼르륵 모여있다. 0번 요소에 접근했다면, 다음으로 1번 요소를 사용할 확률이 높으므로 캐시는 주변 데이터를 통째로 쟁반(MBR)에 담아온다.
캐시 히트 : CPU가 필요로 하는 데이터가 캐시 메모리에 딱 들어맞아 즉시 가져오는 경우, 성능이 극대화
캐시 미스 : 예측이 틀려 캐시에 데이터가 없어 메모리(RAM)까지 직접 다녀오는 경우 이때 Cpu는 데이터가 올때까지 기다려야 하므로 성능이 하락
캐시 적중률 계산법
캐시 적중률 = 캐시 히트 횟수 / (캐시히트횟수 + 캐시 미스횟수) => 현대 컴퓨터의 적중률을 85에서 95이상
언리얼 엔진의 성능 최적화는 어떻게 하면 캐시 미스를 줄일 것인가와의 싸움
TArray사용하기
TList는 노드들이 메모리 여기저기에 흩어져 있어 공간 지역성이 엉망, CPU는 노드 하나 찾을 때마다 멀리 있는 마트(RAM)를 다녀와야 한다.
TArray는 데이터가 메모리에 연속적으로 배치, CPU가 0번 데이터를 가져올 때 주변 데이터까지 한꺼번에 캐시로 담아오기 때문에 캐시 히트 확률이 비약적으로 높아진다.
데이터 기반 설계
언리얼 엔진 5의 Mass Entity나 나나이트(Nanite)기술의 핵심은 데이터를 캐시가 가장 좋아하는 방식(연속된 배열)으로 정렬하여 하드웨어 성능을 100% 끌어내는데 있다.
많은 초보 개발자가 복잡한 수학 공식에서 최적화를 찾으려고 하지만 시니어들은 메모리 레이아웃에서 답을 찾는다. 데이터가 캐시에 예쁘게 담길 수 있도록 설계하는 것, 그것이 하드웨어를 진정으로 배려하는 엔지니어링의 정수
자기장으로 기록하는 기계식 아카이브
하드디스크는 자기적인 방식으로 데이터를 저장하는 자기 디스크의 일종, 물리적인 회전과 바늘의 움직임이 필요한 '기계식' 장치라는 점이 가장 큰 특징
플래터 : 실질적으로 데이터가 저장되는 동그란 원판, 자성 물질로 덮여있어 N극과 S극을 통해 0과 1을 기록
스핀들 : 플래터는 회전시키는 축, 회전속도는 RPM으로 나타내며, 높을수록 속도가 빠름
헤드 & 디스크 암 : 바늘처럼 생긴 헤드가 플래터 위를 미세하게 떠다니며 데이터를 읽고 씀, 모든 헤드는 암에 부착되어 일제히 이동
트랙 : 플래터 상의 동심원 하나를 의미
섹터 : 트랙을 조각낸 하드디스크의 가장 작은 전송 단위, 보통 512B에서 4096B 크기를 가짐
실린더 : 여러 겹의 플래터에서 같은 위치에 있는 트랙들을 연결한 원통 모양의 공간, 헤드를 움직이지 않고 한 번에 읽기 위해 연속된 데이터는 보통 한 실린더에 기록
반도체 기반 초고속 저장 혁명, SSD, USB메모리, SD카드 모두 플래시 메모리 기반, 전기적으로 데이터를 읽고 쓰는 반도체 방식이라 기계적인 소음이 없고 속도가 압도적으로 빠름
셀은 데이터를 저장하는 최소 단위, 셀 하나에 몇 비트를 담느냐에 따라 등급이 나뉨
SLC(Single Level Cell) : 셀당 1비트, 가장 빠르고 수명이 길지만 비쌈
MLC(Multi Level Cell) : 셀당 2비트, 속도와 가격의 균형 모델
TLC(Triple Level Cell) : 셀당 3비트 용량이 크고 저렴하여 대중적인 SSD에 쓰임
플래시 메모리의 가장 독특한 특징은 작업 단위가 다르다.
읽기/쓰기 : 페이지(Page)단위로 수행
삭제 : 페이지보다 큰 블록(Block) 단위로 수행
플래시 메모리는 덮어쓰기가 불가능 데이터 수정 시 기존 페이지는 Invalid(쓰레기 값)상태가 되고 새로운 페이지에 데이터가 적재됨, 이 쓰레기 값들이 공간을 낭비하는 것을 막기 위해 다음 과정을 거침
언리얼 엔진5의 혁신적인 기능들은 보조기억장치의 물리적 특성에 깊게 뿌리를 두고 있다.
나나이트와 SSD의 필연적 관계
나나이트는 수억개의 폴리곤을 실시간으로 불러옴, 기계적인 회전이 필요한 HDD의 탐색시간으로는 이 방대한 데이터 흐름을 감당할 수 없음
결과 : 고속 SSD가 있어야만 끊김 없는 고퀄리티 렌더링이 가능하며, 이것이 차세대 콘솔(PS5, Xbox Series X)이 SSD를 필수 장착한 이유
텍스터 팝인(Pop-in)현상의 하드웨어적 원인
HDD환경 : 헤드가 여기저기 흩어진 텍스처 섹터를 찾느라 물리적으로 움직이는 시간(탐색 시간)동안 화면에는 저해상도 텍스처가 머물게 됨
SSD환경 : 탐색 시간이 거의 0에 가깝기 때문에 데이터를 즉각적으로 RAM으로 전송할 수 있어 팝인 현상이 획기적으로 줄어듬
참조 지역성과 데이터 배치
하드 디스크 성능을 올리려면 연관된 에셋들을 플래터상의 가장 가까운 트랙(실린더)에 모아두는것이 중요 그래야 디스크 암이 덜 움직여서 탐색 시간을 단축할 수 있음, 언리얼 엔진의 빌드 과정에서 데이터패킹이 중요한 이유도 바로 이 하드웨어적 효율성 때문