Post

2026-04-10 TIL (34일차)

2026-04-10 TIL (34일차)

CS

• 디자인 패턴 (객체 지향)

패턴명핵심 키워드한 줄 요약
Factory (팩토리)생성 대행“객체를 만드는 법”
객체 생성 로직을 밖으로 노출하지 않고, 공장(Factory) 클래스에게 생성을 떠넘기는 패턴입니다.
Strategy (전략)행동 교체“객체의 행동을 갈아 끼우는 법”
무기(칼, 총, 활)를 런타임에 동적으로 스왑(Swap)하듯이, 알고리즘(행동)을 캡슐화하여 쉽게 갈아 끼우는 패턴입니다.
Observer (옵저버)구독과 알림“객체의 변화를 관찰하고 알리는 법”
유튜브 구독처럼, 어떤 데이터가 변경되었을 때 이를 쳐다보고 있는 모든 객체(구독자)에게 자동으로 신호를 쏴주는 패턴입니다.
Proxy (프록시)대리인 (접근 제어)“무거운 객체 대신 가벼운 대리인을 세우는 법”
용량이 크거나 접근이 민감한 원본 객체 대신 가짜(Proxy)를 먼저 쥐여주고, 진짜 데이터가 필요해지는 순간에만 원본을 호출(Lazy Load)하는 패턴입니다.
Iterator (이터레이터)순회 (탐색)“자료구조의 내부를 몰라도 순회하는 법”
배열이든, 트리든, 리스트든 내부 구조(STL)에 신경 쓰지 않고 Next()만 호출해서 순서대로 쭉 훑어볼 수 있게 해주는 패턴입니다.


UI 아키텍처 패턴 (데이터와 화면의 분리)

패턴명핵심 구조특징 및 요약
MVCModel - View - Controller“가장 클래식한 기본형”
사용자 입력이 컨트롤러로 들어옵니다. 컨트롤러가 모델을 업데이트하고 뷰를 갱신합니다. (단점: 뷰와 모델의 의존성이 남아있음)
MVPModel - View - Presenter“화면과 데이터를 완벽히 단절”
뷰와 모델이 서로를 아예 모릅니다. 중간 관리자인 프리젠터(Presenter)가 뷰의 요청을 받고 모델을 수정한 뒤, 다시 뷰를 직접 업데이트합니다. (특징: 뷰와 프리젠터가 1:1 매칭됨)
MVVMModel - View - ViewModel“데이터 바인딩(Data Binding)의 마법”
뷰가 뷰모델을 ‘구독(Observer)’합니다. 뷰모델의 데이터가 바뀌면, 코드를 짜지 않아도 화면(View)이 알아서(자동으로) 바뀝니다. (언리얼 UMG, 웹 프론트엔드에서 가장 많이 씀)


코딩테스트

• 2차원 배열 길찾기 Tip

방향 배열(dy, dx) 분리이유

cpp //N,S,W,E int dir[]={-1,1,-1,1}; 하나의 배열만 사용하면 문제가 발생합니다. 배열에서 값을 꺼내왔을 때, 이 값을 y에 더해야 할지 x에 더해야 할지 알 수 없습니다.

  • 만약 북쪽(‘N’)을 나타내는 -1을 꺼냈다고 가정해 봅시다.
    • 북쪽으로 가려면 세로(y)는 -1이 되고 가로(x)는 0이 되어야 합니다.
    • 서쪽(‘W’)의 -1을 꺼내면 반대로 세로(y)는 0, 가로(x)는 -1이 되어야 합니다.

결국 값을 꺼내고 나서도 “이게 상하 이동이야, 좌우 이동이야?” 하고 if-else문을 많이 사용하는 일이 발생합니다.

제를 해결하기 위해, 각 방향으로 이동할 때 발생하는 y의 변화량과 x의 변화량을 각각 분리해서 쌍(Pair)으로 만들어 줍니다.

1
2
int dy[] = {-1, 1, 0, 0}; 
int dx[] = {0, 0, -1, 1};


사용방법

두 줄만으로 완벽한 2차원 이동이 구현가능

1
2
3
4
5
int idx = Getdir('N'); // 0 반환

// 두 배열의 0번째 값을 각각 더해버림!
y += dy[idx]; // y += -1 (위로 한 칸)
x += dx[idx]; // x += 0  (가로는 안 움직임)


vector의 tip

vector<string>은 겉보기에는 1차원 리스트 같지만, 내부적으로 ‘문자의 배열(string)’들을 담고 있는 ‘배열(vector)’이기 때문에 사실상 2차원 배열처럼 사용할 수 있습니다.

개념

  • 구조: vector<string> vec; -> vec[행][열]
  • 원리:
    • vec[i] : 벡터의 i번째에 들어있는 문자열(string) 전체에 접근합니다.
    • vec[i][j] : i번째 문자열 안에 있는 j번째 문자(char) 하나에 직접 접근합니다.


• 정적배열의 2차원과 2차원 벡터의 차이점

차이점

구분정적 2차원 배열 (int arr[N][M])2차원 벡터 (vector<vector<int>>)
형태완벽한 사각형 (모든 행의 열 개수가 동일)🧩 테트리스 모양 (행마다 열 개수가 다를 수 있음)
크기처음 정하면 절대 바꿀 수 없음언제든지 .push_back()으로 늘릴 수 있음
[i] 접근i번째 행의 시작 메모리 주소를 가리킴i번째 행에 있는 1차원 데이터 덩어리 전체를 반환
[i][j] 접근i행 j열의 특정 값을 반환i행 j열의 특정 값을 반환

그림으로 보는 차이점

사진 자료

정적 배열 : 사각형
2차원 벡터: 테트리스 모양처럼 값이 늘어날때마다 달라짐

Unreal

• Tip

헤더(h) 파일에서 함수를 선언하고, 블록 지정 후 Alt + Enter를 누르면 cpp 파일에 함수의 구현부를 자동으로 뼈대까지 싹 다 만들어 줍니다!

• 메타 데이터(Metadata)

  • 언리얼 엔진에서는 컴파일할 때 UPROPERTY(), UFUNCTION() 같은 매크로를 읽어서 클래스의 이름, 변수 타입, 크기 등의 정보를 수집해 둡니다.
  • 매개변수: 매크로 괄호 () 안에 들어가는 옵션들이 바로 메타데이터입니다.
    • 예를 들어 UPROPERTY(EditAnywhere, BlueprintReadWrite)라고 적으면, 엔진은 이 메타데이터를 읽고 파악합니다.

• 가비지 컬렉션 주기를 줄이면?

  • 짧아진 만큼 가비지 컬렉션을 불러서 부하가 생깁니다.

• UPROPERTY()의 유무 차이

Uobject 생성

1
2
3
4
5
6
7
8
9
10
11
12
// UMyTestObject.h
public:
	void InitObject(FString InName);

	// 객체가 사라질 준비를 할 때 호출됨
	virtual void BeginDestroy() override;

	// 가비지 컬렉션이 나를 지울 때 호출됨
	virtual void FinishDestroy() override;

private:
	FString MemoName;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UMyTestObject.cpp
void UMyTestObject::InitObject(FString InName)
{
	MemoName = InName;
}

void UMyTestObject::BeginDestroy()
{
	Super::BeginDestroy(); 
	UE_LOG(LogTemp, Warning, TEXT("GC 시작 : %s 객체가 사라질 준비가 됐습니다."), *MemoName);
}

void UMyTestObject::FinishDestroy()
{
	Super::FinishDestroy();
	UE_LOG(LogTemp, Warning, TEXT("GC 완료 : %s 객체가 메모리에서 완전히 사라졌습니다."), *MemoName);
}

Actor 생성

1
2
3
4
5
6
7
// AMyGCObserver.h
public:

	UPROPERTY()
	class UMyTestObject* SafeObject;

	class UMyTestObject* DangerObject;
1
2
3
4
5
6
7
8
9
10
11
12
13
// AMyGCObserver.cpp (BeginPlay)
void AMyGCObserver::BeginPlay()
{
	Super::BeginPlay();
	
	SafeObject = NewObject<UMyTestObject>(this);
	SafeObject->InitObject(TEXT("SafeObject_Member"));

	DangerObject = NewObject<UMyTestObject>(this);
	DangerObject->InitObject(TEXT("DangerObject_Member"));

	UE_LOG(LogTemp, Log, TEXT("가비지 컬렉션 작동 시작.."));
}

Dangling Pointer가 발생해야하는데?

시간이 지나 가비지 컬렉션이 작동하면, GC는 UPROPERTY()가 없는 DangerObject를 ‘아무도 안 쓰는 쓰레기’로 판단하고 메모리에서 날려버립니다. 그런데 여기서 문제가 발생합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// AMyGCObserver.cpp (Tick)
void AMyGCObserver::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	bool bSafeIf = SafeObject != nullptr;
	bool bSafeValid = IsValid(SafeObject);

	bool bDangerIf = DangerObject != nullptr;
	bool bDangerValid = IsValid(DangerObject);


	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Green, FString::Printf(TEXT("Safe(UPROPERTY) -> if : %s, IsValid : %s"), bSafeIf ? TEXT("True") : TEXT("False"), bSafeValid ? TEXT("True") : TEXT("False")));
            //한줄로 편하게 계속 갱신되게 하는 메세지 코드(Tip)
	GEngine->AddOnScreenDebugMessage(-1, 0.0f, FColor::Red, FString::Printf(TEXT("Danger(UPROPERTY) -> if : %s, IsValid : %s"), bDangerIf ? TEXT("True") : TEXT("False"), bDangerValid ? TEXT("True") : TEXT("False")));

}

가비지 컬렉션이 실행했는데도 Tick에서 Danger 출력값은 true,true로 갱신을 해도 삭제됬다고 인식을 못하고 있는 결과가 나옴

해결책

IsValidLowLevel() 사용하기

Danger if 출력은 true가 나오는데 IsValidLowLevel의 값은 false가 나옵니다.
단순히 포인터가 비어있는지만 검사하는 게 아니라, 엔진 깊숙한 곳까지 들어가 “이 메모리 주소가 진짜 유효한 UObject 형태를 갖추고 있는가?”를 검사합니다.

TWeakObjectPtr

Danger의 출력값이 둘다 false가 나옵니다

UPROPERTY()를 안 쓸 거라면, 일반 생포인터(*) 대신 약한 포인터를 사용해야 합니다.
TWeakObjectPtr은 가비지 컬렉션이 객체를 날려버리는 것을 막지는 않지만, 객체가 날아가는 순간 스스로를 갱신하여 댕글링 포인터가 되는 것을 막아주는 스마트 포인터입니다.

언리얼 생성자에 초기화 하는 이유

언리얼 엔진에서 C++ 클래스의 생성자는 단 한 번, CDO가 메모리에 올라갈 때만 호출됩니다. 즉, 게임 플레이 중에 SpawnActor로 수백 개의 몬스터를 스폰하더라도 생성자는 호출되지 않습니다. 대신 이미 만들어진 CDO의 메모리를 통째로 복사해 옵니다.

따라서 생성자에서 변수를 초기화해 두면, CDO에 그 값이 영구적으로 각인되고, 이후 생성되는 모든 인스턴스는 추가적인 계산 없이 그 초기값을 그대로 물려받게 되어 성능에 엄청난 이점을 가져다줍니다.




COD를 이용한 최적화 원리

CDO는 단순한 ‘템플릿’ 이상의 최적화 기능을 제공합니다.

메모리 절약

수많은 객체가 스폰되더라도, 값이 변경되지 않은 변수들은 개별 메모리에 중복해서 저장하지 않고 원본인 CDO를 참조하는 방식으로 메모리 사용량을 획기적으로 줄입니다.

빠른 초기화

객체를 스폰할 때마다 메모리를 새로 할당하고 변수를 하나하나 세팅하는 대신, 미리 완성된 CDO의 메모리 블록을 통째로 덮어쓰기(Memcpy) 하므로 스폰 속도가 매우 빠릅니다.

델타 직렬화

게임을 저장하거나 네트워크로 데이터를 전송할 때, 객체의 ‘모든 정보’를 보내지 않습니다. 오직 CDO의 기본값과 비교해서 ‘달라진 값(Delta)’만 저장하거나 전송합니다. (예: 체력 100인 몬스터가 90이 되면, 전체 스탯이 아닌 ‘체력 90’이라는 차이점만 저장하므로 용량이 대폭 감소합니다.)




GetMutableDefault

CDO는 원칙적으로 읽기 전용(Read-Only) 마스터 템플릿입니다. 하지만 개발 과정에서 부득이하게 이 원본 값을 런타임에 직접 수정해야 할 때가 있습니다. 이때 사용하는 특수한 함수가 GetMutableDefault<T>()입니다.

  • 기능: 특정 클래스의 CDO에 접근할 수 있는 수정 가능한(Mutable) 포인터를 반환합니다.
  • 주의점: 이 함수로 CDO의 값을 변경하면, 이후에 스폰되는 모든 해당 클래스의 객체들은 변경된 값을 기본값으로 가지고 태어납니다. (실무에서는 시스템 기획 데이터 변경 등 매우 제한적인 상황에서만 조심스럽게 사용해야 합니다.)




게임모드에서 TestMyActor의 CDO를 수정하게 된다면?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void AMyGameModeBase::BeginPlay()
{
    Super::BeginPlay();
    // 1. 일반 액터 스폰
    ATestMyActor* ActorA = GetWorld()->SpawnActor<ATestMyActor>(); // 100
    UE_LOG(LogTemp, Warning, TEXT("ActorA Init Health : %d"), ActorA->Health);

    // 2. 스폰된 인스턴스(ActorA)의 체력을 50으로 수정
    ActorA->Health = 50;
    UE_LOG(LogTemp, Warning, TEXT("ActorA Modi : %d"), ActorA->Health);

    //CDO 생성
    ATestMyActor* MyActorCDO = GetMutableDefault<ATestMyActor>();
    UE_LOG(LogTemp, Warning, TEXT("CDO Init Health : %d"), MyActorCDO->Health);

    //CDO 수정
    MyActorCDO->Health = 200;
    UE_LOG(LogTemp, Warning, TEXT("CDO Modfi Health : %d"), MyActorCDO->Health);

    //CDO  바꾼다음에 인스턴스 생성
    ATestMyActor* ActorB = GetWorld()->SpawnActor<ATestMyActor>();
    UE_LOG(LogTemp, Warning, TEXT("ActorB Init Health : %d"), ActorB->Health);
    UE_LOG(LogTemp, Warning, TEXT("ActorA Current HP : %d"), ActorA->Health);

}

5번 과정(ActorB의 스폰 직후 체력)의 결과를 분석합니다.


▶️ 에디터에서 [처음] 플레이 버튼(PIE)을 눌렀을 때

결과가 100, 50, 100, 200, 100, 50으로 나옵니다. 분명 코드 위에서 CDO의 체력을 200으로 바꿨는데, 직후에 스폰된 ActorB의 체력이 여전히 100입니다.

  • 이유: 언리얼의 SpawnActor는 현재 레벨이 시작될 때 미리 만들어둔 ‘임시 복사본 CDO (Instance Default)’를 참조하여 생성하기 때문입니다. 코드 런타임 중간에 원본 CDO를 바꿨다고 해서, 이미 진행 중인 게임 씬의 스폰 시스템에 즉시 동기화되지는 않습니다.

▶️ 에디터에서 [두 번째] 플레이 버튼(PIE)을 눌렀을 때

결과가 100, 50, 200, 200, 100, 50으로 나옵니다. 놀랍게도 3번 과정(CDO Init Health)의 결과가 처음부터 200으로 시작합니다.

  • 이유: 언리얼 에디터는 실행을 중지해도 백그라운드에서 완전히 꺼지지 않습니다(에디터 프로세스가 메모리를 계속 잡고 있음). 첫 번째 플레이 때 GetMutableDefault메모리 깊숙한 곳에 있는 진짜 CDO 원본을 200으로 수정해 버렸기 때문에, 플레이를 껐다가 다시 켜도 그 변경 사항이 에디터 메모리에 그대로 남아있는 것입니다.

결론 에디터 환경에서 GetMutableDefault로 CDO를 수정하면 에디터를 껐다가 켜기 전까지 메모리가 수정된 상태로 유지됩니다. 따라서 런타임에 코드나 블루프린트로 원본 CDO를 수정하는 짓은 예기치 못한 치명적 버그를 낳을 수 있으므로 조심해야합니다.

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