Post

2026-04-17 TIL (39일차)

2026-04-17 TIL (39일차)

CS




컴퓨터가 이해하는 정보

명령어는 수행할 동작(명령어)수행할 대상(데이터)으로 구성됩니다. (예: “더하라. 1과 2를”, “출력해라. ‘Hello World’를” 등)

• 데이터

  • 숫자, 문자, 이미지, 동영상과 같은 정적인 정보
  • 컴퓨터와 주고받는 정보나 컴퓨터에 저장된 정보 자체를 데이터라고 통칭
  • 0과 1만으로 다양한 숫자(정수, 실수)와 문자 데이터를 표현
  • 수행할 데이터(대상)로써 명시되며, 명령어에 종속적인 정보(명령의 대상이자 재료)입니다.

• 명령어

  • 데이터를 활용하는 정보이며 실질적으로 컴퓨터를 움직이게 합니다.
  • CPU는 이 명령어를 이해하고 실행하는 부품입니다.
  • 명령어 사이클 - CPU가 이러한 명령어를 처리하는 순서




• CPU

정보를 읽어 들이고, 해석하고, 실행하는 핵심 부품입니다.

* CPU 구성

  • 산술논리연산장치(ALU, Arithmetic and Logic Unit)
    • 사칙 연산, 논리 연산과 같은 연산을 수행할 회로로 구성되어 있는 일종의 계산기
    • CPU가 처리할 명령어를 실질적으로 연산하는 요소
  • 제어장치(CU, Control Unit)
    • 명령어를 해석해 제어 신호라는 전기 신호를 내보내는 장치
    • 제어신호: 컴퓨터 각각 부품들을 동작 시킬 수 있게 하는 일종의 특별한 전기 신호 (예: 메모리 읽기/쓰기, 입출력 장치 읽기/쓰기, 인터럽트 요청 신호 등).
  • 레지스터(register) 중요
    • CPU 내부의 작은 임시 저장장치 - 데이터와 명령어를 처리하는 과정의 중간값,임시저장값을 저장
    • CPU 내에는 여러 개의 레지스터가 존재하며, 각기 다른 이름과 역할을 수행
    • CPU는 정보 처리 과정에서 거의 반드시 레지스터를 거치므로, 레지스터에 담기는 값을 관찰하면 로우 레벨(Low-Level)에서 CPU 동작을 파악할 수 있습니다.

CPU 빠른 명령어 처리를 위한 기술

멀티코어 CPU, 멀티스레드/멀티프로세서, 명령어 병렬 처리, 파이프라이닝, 슈퍼스칼라, 클럭 신호 등이 연관되어 있습니다.

• 메모리와 캐시 메모리

  • 메인 메모리(main memory)
    • 일반적으로 ‘(메인)메모리’라는 용어는 RAM을 지칭하는 경우가 많음
    • 실행 중인 프로그램을 구성하는 데이터와 명령어를 저장하는 부품 (어떤 프로그램이 실행되려면 반드시 메모리에 있어야 함)
    • 주소(address) - CPU가 원하는 정보로 접근하기 위해서는 주소가 필요 (💡 개발자 Note: C/C++의 포인터 개념과 유사하게 메모리 주소를 통해 접근함, 이유는 빠르게 접근하기 위해서)
    • 휘발성(volatile) - 전원이 공급되지 않을 때 저장하고 있는 정보가 지워지는 특성 (RAM에서 중요한 특성)
      • 메모리(RAM)는 휘발성 저장장치로, 메모리에 저장된 정보는 컴퓨터의 전원이 꺼지면 모두 삭제 (작업 중 저장을 안 하고 전원이 꺼지면 날아가는 원리)
  • 캐시 메모리(cache memory)
    • CPU가 조금이라도 더 빨리 메모리에 저장된 값에 접근하기 위해 사용하는 저장장치 (CPU 내부나 CPU-메모리 사이에 존재).
    • CPU가 자주 사용할 법한 데이터를 미리 저장해 두어, 메모리까지 가는 시간을 단축합니다.

• 보조기억장치

  • 전원이 꺼져도 저장된 정보가 사라지지 않는 비휘발성(non-volatile) 저장장치
  • 보조기억장체에 적재된 보관할 정보를 빠르게 RAM에 적재해주는 것 (저장된 내용을 안정적으로 보관하는거에 있다.)
  • CD-ROM이나 DVD, 하드 디스크 드라이브, 플래시 메모리(SSD, USB 메모리), 플로피 디스크 등
  • 보조기억장치는 보관할 프로그램을 저장
  • RAID - 보조기억장치의 정보를 안정적이고 안전하게 관리/구성하는 기술.

• 입출력장치

  • 입출력장치(input/output device)
    • 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환하는 장치
    • 입력장치 - 마우스, 키보드, 마이크 등
    • 출력장치 - 스피커, 모니터, 프린터 등
    • 보조기억장치는 메모리를 보조하는 임무를 수행하는 특별한 입출력장치 (보조기억장치 역시 데이터를 보관하며 메모리를 보조하는 특별한 입출력장치의 일종으로 봅니다.)
    • 주변장치(peripheral device) - 보조기억장치와 입출력장치를 통칭
    • GPU도 입출력장치의 일종으로 볼 수 있습니다.




• 메인 보드와 버스

핵심 부품들(CPU, 메모리, 보조기억장치 등)을 하나로 연결해 주는 요소입니다.

  • 메인 보드 (또는 마더 보드)
    • 컴퓨터의 핵심 부품을 비롯한 여러 부품들을 연결할 수 있는 슬롯과 연결 단자로 구성된 기판 (핵심 부품을 비롯한 여러 부품을 꽂아 연결할 수 있는 슬롯과 단자로 구성된 거대한 판자입니다.)
  • 버스 (Bus)
    • 버스(bus) - 각 컴퓨터 부품들이 정보를 주고받는 통로
      • 시스템 버스(system bus) - 핵심 부품들을 연결하는 가장 중요한 통로로, 사람에 비유하면 척추와 같은 역할을 합니다.




• 저장장치의 계층 구조

저장장치들은 CPU와의 거리, 용량, 성능을 기준으로 피라미드 형태의 계층 구조를 가집니다.

[계층 순서 (CPU와 가까운 순)]

레지스터캐시 메모리메인 메모리(RAM)보조기억장치

  1. CPU와 가까운 저장장치는 빠르고, 멀리 있는 저장장치는 느림
  2. 속도가 빠른 저장장치는 용량이 작고, 가격이 비쌈

피라미드의 위로 올라갈수록 (레지스터 방향):

  • 속도: 빠름
  • 용량: 작음
  • 가격: 비쌈

피라미드의 아래로 내려갈수록 (보조기억장치 방향):

  • 속도: 느림
  • 용량: 큼
  • 가격: 저렴함




Unreal

강의


1. 스마트한 포인터 관리: TObjectPtr

언리얼 엔진 5부터 원시 포인터(*)를 대체하기 위해 도입된 ‘접근 제어 핸들(스마트 핸들)’입니다.

  • 왜 사용하는가? * 에디터 상에서 지연 로딩(Lazy Loading) 및 오브젝트 추적 등 관리 효율과 개발 생산성을 극대화해 줍니다.
    • 주의: 게임 런타임 성능이 향상되는 것은 아닙니다. 패키징(빌드) 시 다시 가벼운 원시 포인터로 변환되어 동작합니다.
  • 최적의 사용법 (위치에 따른 구분)
    • 헤더 파일(.h)의 UPROPERTY 멤버 변수: TObjectPtr<T> 사용 (권장)
    • 함수 매개변수나 지역 변수: T* (원시 포인터) 사용. 잠깐 쓰고 버릴 변수까지 무거운 핸들링 비용을 쓸 필요가 없기 때문입니다.
  • for문 순회 팁
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    // TObjectPtr은 원시 포인터로의 암시적 변환을 지원합니다. (내부적으로 로직이 한번 돌아서)
    for (TObjectPtr<USceneComponent>& Component : Components) 
    {
        // ...
    }
    // 객체 대신 순수 메모리 주소만 가볍게 빼서 쓰는 아래 방식이 성능상 더 좋습니다.
    for (USceneComponent* Comp : Components) 
    {
        // ...
    }
    


2. 안전한 클래스 참조: TSubclassOf vs UClass

에디터에서 기획자나 개발자가 “어떤 종류의 클래스를 스폰(Spawn)할지” 결정할 때 사용하는 타입입니다.

  • UClass* (위험): 세상의 모든 클래스(위젯, 게임모드 등)가 다 들어올 수 있어 범위가 너무 넓습니다.
  • TSubclassOf<T> (안전벨트): “특정 클래스(T)를 상속받은 클래스들만” 에디터 목록에 띄워줍니다.
    • (예: TSubclassOf<AActor>로 지정하면 액터가 아닌 것은 아예 보이지 않아 안전합니다.)
  • StaticClass(): C++ 코드 내에서 특정 클래스의 설계도(UClass) 주소를 직접 가져올 때 사용합니다.
1
GetWorld()->SpawnActor<AActor>(USceneComponent::StaticClass());


3. 언리얼 컨테이너 3대장 (메모리 레이아웃)

  • TArray (선형 배열): 데이터가 메모리에 일렬로 붙어있습니다. 순차적으로 훑을 때(Iteration) CPU 캐시 히트율이 극대화되어 가장 빠릅니다. (C++의 std::vector와 유사)
  • TSet / TMap (해시 기반): 데이터를 순서 없이 저장하지만, 고유한 키(Key) 값으로 데이터를 ‘단번에 찾아낼 때(조회)’ 압도적으로 빠릅니다. (중복 허용 X, C++의 std::unordered_set, std::unordered_map와 유사)


4. TArray

데이터 넣기 (Add vs Emplace)

  • Add(Value): 데이터를 임시로 만들고 복사해서 집어넣습니다. (단순한 타입인 int32 등에 사용하면 안전합니다.)
  • Emplace(Value): 데이터가 들어갈 자리를 미리 파놓고, 그 자리에서 직접 객체를 조립합니다. (복사 비용이 없어 구조체나 복잡한 클래스에 사용하면 성능상 이득입니다.)
  • Insert(Value, Index): 특정 인덱스 위치에 값을 밀어 넣습니다.
  • AddUnique(Value): 동일한 값이 없을 때만 넣습니다.

Tip: AddUnique는 값을 넣을 때마다 배열 전체를 뒤져야 해서 데이터가 많아질수록 느려집니다(O(N)). 애초에 중복이 없는 데이터 모음이 필요하다면 TSet을 쓰는 것이 성능상 훨씬 좋습니다.

탐색 및 검증

  • Num(): 현재 배열에 들어있는 실제 데이터의 개수를 반환합니다. (주로 for문의 조건으로 사용)
  • Find(Value): 값이 위치한 인덱스 번호를 반환합니다. 없으면 INDEX_NONE(-1)을 반환합니다.
  • Contains(Value): 특정 값이 배열에 존재하는지 확인합니다. (bool 반환)

데이터 지우기

  • Remove(Value): 해당 값을 가진 모든 요소를 지웁니다.
  • RemoveSingle(Value): 처음 발견된 딱 하나만 지웁니다.
  • RemoveAt(Index): 특정 순번(Index)의 데이터를 지웁니다.

주의: 인덱스를 초과하면 크래시가 나므로 반드시 if(IntArray.IsValidIndex(Index))로 검증 후 삭제해야 합니다.

  • Empty(): 배열을 싹 비웁니다.


EAllowShrinking

언리얼의 배열은 데이터를 지워도 “나중에 또 들어올지 모르니” 확보해둔 메모리(Capacity/Slack)를 유지하는 습성이 있습니다. 이 잉여 메모리를 지우고 싶을 때 사용합니다.

1
IntArray.Shrink(); // 데이터는 놔두고, 남는 잉여 여백 메모리만 OS에 반환하여 잘라냅니다.


람다(Lambda)와 함수형 프로그래밍

복잡한 구조체(Functor)를 굳이 만들지 않고, C++ 코드 중간에 이름 없는 1회용 함수를 끼워 넣는 문법입니다.

캡처 블록 []의 의미

람다 함수 외부의 변수를 함수 안으로 어떻게 가져올 것인가를 결정합니다.

  • []: 외부 변수 사용 안 함 (가장 가볍고 안전)
  • [=]: 외부 변수를 복사해서 가져옴 (값만 사용할 때, 원본 영향 없음)
  • [&]: 외부 변수를 참조로 가져옴 (람다 안에서 원본 값을 수정해야 할 때)
  • [this]: 현재 클래스의 멤버 변수나 함수를 람다 안에서 쓰고 싶을 때
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TArray<int32> IntArray = { 10, 50, 20, 90, 30 };

// [1] Sort (사용자 지정 정렬) - 내림차순 정렬
IntArray.Sort([](const int32 A, const int32 B) {
    return A > B; 
});

// [2] RemoveAll (조건부 일괄 삭제) 
IntArray.RemoveAll([](int32 Value) {
    return Value == 5; 
});

// [3] FilterByPredicate (필터링) 
TArray<int32> ReturnIntArray = IntArray.FilterByPredicate([](const int32 FindValue) {
    return FindValue < 9; 
});




5. TSet

중복을 허용하지 않으며, 특정 값이 존재하는지 빠르게 찾을 때(O(1)) 사용합니다.

데이터 추가 및 탐색 (TArray와의 결정적 차이)

  • Add(Value): 데이터를 추가합니다. 중복된 값을 넣으면 무시됩니다.
  • Contains(Value): 값이 존재하는지 확인합니다. (bool 반환)
  • Find(Value): TArray는 인덱스 번호를 반환하지만, TSet은 값의 ‘포인터(주소)’를 반환합니다. (메모리 구조가 순차적이지 않기 때문입니다.)
    1
    2
    
    int32* FoundPtr = IntArraySet.Find(20); 
    if (FoundPtr != nullptr) { /* 값 찾음! */ }
    


TSet의 순회와 안전한 삭제 (Iterator)

배열처럼 for(int32 Value : IntArraySet) 형식으로 순회할 수도 있지만, 순회 도중에 데이터를 삭제해야 한다면 무조건 ‘이터레이터(Iterator)’를 사용해야 합니다.

  • 이유: 순차적인 for문 도중 요소를 지우면 구조가 망가져 크래시가 나기 때문입니다.
1
2
3
4
5
6
7
8
9
// 이터레이터(It)를 사용한 순회 및 조건부 삭제
for (TSet<int32>::TIterator It = IntArraySet.CreateIterator(); It; ++It) 
{
    if (*It < 60) 
    {
        It.RemoveCurrent(); // 현재 위치의 데이터를 안전하게 삭제!
    }
}
// 읽기 전용으로 쓸 거면 TConstIterator를 사용합니다.

집합 수학 연산 (교집합, 합집합)

  • Union(SetB): 두 Set을 합친 결과를 반환합니다. (합집합)
  • Intersect(SetB): 두 Set의 공통된 값만 반환합니다. (교집합)

메모리 최적화와 TArray로의 변환 (Compact & Shrink)

데이터를 지우면 메모리 중간중간에 구멍(Hole)이 생깁니다. 이를 정리하는 강력한 기능입니다.

  • Compact(): 중간에 이가 빠진 빈 공간(Hole)을 무시하고, 유효한 데이터들을 앞으로 쭉 땡겨서 연속적으로 정렬합니다.
  • Shrink(): Compact로 인해 뒤로 밀려난 잉여 메모리를 OS에 반환(삭제)합니다.
1
2
3
4
5
SetC.Compact(); 
SetC.Shrink(); 
// 위 두 개를 세트로 거치고 나면 메모리가 빈틈없이 연속적이게 되므로,
// 아래처럼 TArray로 깔끔하게 변환할 수 있습니다.
TArray<int32> MyArray = SetC.Array();




6.TMap

학번으로 학생 이름을 찾듯, 고유한 ‘키(Key)’를 통해 ‘값(Value)’을 빠르게 찾을 때 사용합니다.

데이터 추가 및 조회

  • Add(Key, Value) / Emplace(Key, Value): 맵에 데이터를 넣습니다. (성능상 이점은 TArray와 동일)
  • Find(Key): 키값으로 검색하며, 해당 Value의 ‘포인터’를 반환합니다.
1
FString* FoundItem = ItemMap.Find(101);

데이터 접근과 삽입 (FindOrAdd vs [] 연산자)

이 두 가지의 차이를 명확히 아는 것이 크래시 방지의 핵심입니다.

  • FindOrAdd(Key) (안전+편리): 키가 있으면 해당 값을 주고, 없으면 기본값을 새로 만들어서(Add) 그 ‘참조자(&)’를 줍니다. (주로 값을 새로 덮어쓸 때 씁니다.)
    1
    2
    
    FString& ItemRef = ItemMap.FindOrAdd(104);
    ItemRef = TEXT("Bow"); // 104번 키의 값이 "Bow"로 설정/변경됨. (포인터는 안됨)
    
  • [] 연산자 (위험): * FString Name = ItemMap[105];
1
FString Name = ItemMap[105];
  • 해답: 만약 105라는 ‘키’ 자체가 Map에 등록되어 있지 않다면, 언리얼은 가차 없이 크래시(Assertion Failed)를 냅니다.
  • 해결책: []로 접근하기 전에는 반드시 ItemMap.Contains(105)로 키가 존재하는지 먼저 확인해야 합니다.

TMap의 2가지 순회 방식 차이점 (범위 기반 vs 이터레이터)

TSet과 마찬가지로 순회 중 ‘삭제(Remove)’ 여부에 따라 방식을 골라야 합니다.

  • 읽기/수정만 할 때 (범위 기반 for문): 지우는 기능은 없지만 코드가 짧고 빠릅니다
1
2
3
4
for (const TPair<int32, FString>& Pair : ItemMap) 
{
    // Pair.Key 와 Pair.Value 로 접근
}
  • 순회 중에 지울 일이 있을 때 (이터레이터): 지워진 곳에 접근하는 위험을 막아줍니다.
1
2
3
4
for (TMap<int32, FString>::TIterator It = ItemMap.CreateIterator(); It; ++It) 
{
    // It.RemoveCurrent(); 로 안전한 삭제 가능
}

TMap 메모리 최적화

TMap 역시 구조상 해시 테이블이므로, 요소가 지워진 후 Compact()Shrink()를 사용하여 메모리를 압축하고 최적화할 수 있습니다.

This post is licensed under CC BY 4.0 by the author.