2026-03-24 TIL (21일차)
알고리즘
메세지 지향 미들웨어
메세지 큐의 상위 개념인 메세지 지향 미들웨어(Message Oriented Middleware)를 알아야 합니다.
MOM은 응용 소프트웨어 간의 비동기적 데이터 통신을 돕는 소프트웨어입니다.
- 보관: 메세지 백업 기능을 통해 지속성을 제공하므로, 송수신 측이 동시에 네트워크 연결을 유지할 필요가 없습니다.
- 라우팅: 미들웨어가 직접 메세지 경로를 배정하여 하나의 메세지를 여러 수신자에게 배포할 수 있습니다.
- 변환: 송수신 측의 요구에 따라 전달하는 메세지의 형식을 변환할 수 있습니다.
메세지 큐가 바로 MOM을 구현한 시스템입니다.
메세지 큐(MQ)
자료구조의 Queue 개념을 채택하여 시스템 간에 메세지를 전달하는 통신 매개체입니다.
메세지 큐 비유
식당 홀 직원(서빙)이 손님에게 주문을 받고, 주방장에게 가서 요리가 끝날 때까지 기다린다면 다른 손님의 주문은 아예 받을 수가 없습니다. 그래서 식당에는 주문서 꽂이(Queue)가 존재합니다.
- Producer (생산자): 메세지를 만들어 큐에 넣는 주체 (예: 주문받는 홀 직원)
- Message Queue (메세지 큐): 메세지가 순서대로 쌓이는 버퍼 공간 (예: 주문서 꽂이)
- Consumer (소비자): 큐에서 메세지를 꺼내서 처리하는 주체 (예: 요리하는 주방장)
시스템 간에 데이터를 직접 주고받으며 기다리는 대신, 중간에 큐를 두어 메세지를 쌓아두고 비동기적으로 처리하는 것이 메세지 큐의 핵심입니다.
메세지 브로커 vs 이벤트 브로커
브로커: 메세지 큐에 데이터를 넣어주고 중개하는 역할을 하는 ‘주체’입니다.
| 구분 | Message Broker | Event Broker |
|---|---|---|
| 특징 | Consumer가 데이터를 가져가면 짧은 시간 내에 큐에서 삭제됨 | Consumer가 소비한 데이터를 필요한 경우 다시 소비 가능 |
| 데이터 보존 | 휘발성 강함 | 보존성 강함 (대용량 데이터 처리 유리) |
| 포함 관계 | 이벤트 브로커의 기능 수행 불가 | 메세지 브로커의 기능까지 수행 가능 |
| 대표 솔루션 | RabbitMQ, ActiveMQ, AWS SQS, Redis | Apache Kafka |
메세지 큐의 장점
현대 아키텍처에서 메세지 큐가 필수적인 이유
① 비동기 (Asynchronous) & 트래픽 완충 (Buffering) 데이터를 보내는 쪽(Producer)은 큐에 데이터를 던져넣기만 하고 바로 자기 할 일을 하러 갑니다. 트래픽이 폭주해도 메세지 큐가 거대한 댐(버퍼) 역할을 하여 데이터를 임시로 담아두므로, 서버가 터지지 않고 자신의 소화 속도에 맞춰 안정적으로 동작합니다.
② 낮은 결합도 (Decoupling) A 프로세스와 B 프로세스가 직접 연결(End-to-End)되어 있으면 한쪽의 고장이 다른 쪽으로 전파됩니다. 메세지 큐를 중간에 두면 각 서버는 ‘큐에 데이터를 넣고 빼는 법’만 알면 되므로, 서로의 상태에 영향을 받지 않고 독립적으로 동작하는 유연한 확장이 가능합니다.
③ 탄력성 (Resilience) 일부 서비스에 장애가 발생하더라도 전체 시스템이 멈추지 않습니다. 수신 측 서버가 다운되어도 송신 측은 큐에 메세지를 계속 발행할 수 있습니다.
④ 과잉/재시도 (Redundancy) 장애로 인해 메세지 처리에 실패하더라도 큐에 메세지가 보관되어 있으므로, 시스템 복구 후 안전하게 재처리(Retry)가 가능하여 데이터 유실을 막습니다.
⑤ 신뢰성 (Guarantees) 작업이 처리된 것을 명확히 확인할 수 있으며, 메세지의 안전하고 확실한 전달을 보장합니다.
⑥ 확장성 (Scalable) 클라이언트의 요청이 급증할 때, 메세지 큐에 연결되는 Producer나 Consumer 인스턴스만 수평으로 늘려(Scale-out) 간단하고 쉽게 성능을 확장할 수 있습니다.
게임 개발 예시
다수의 유저가 실시간으로 스킬을 쓰고 위치를 이동하는 네트워크 멀티 게임에서는 메세지 큐의 원리가 사용됩니다.
게임 서버 내 메세지 큐 활용 사례
네트워크 패킷 처리: 10명의 플레이어가 초당 60번씩 이동/공격 패킷을 보냅니다. 메인 게임 루프가 이를 하나씩 기다리며 처리하면 심각한 렉이 발생합니다. 따라서 네트워크 스레드는 도착한 패킷을 메시지 큐에 빠르게
Enqueue만 해두고, 게임 로직 스레드가 매 프레임마다 큐에서 패킷을Dequeue하여 상태를 동기화합니다.매치메이킹 (Matchmaking): 유저가 ‘게임 찾기’를 누르면 매치메이킹 큐에 정보가 들어갑니다. 유저는 매칭이 잡힐 때까지 상점을 둘러보거나 인벤토리를 정리할 수 있습니다(비동기). 매칭 서버는 큐에 쌓인 유저들의 점수를 비교해 백그라운드에서 방을 만듭니다.
인게임 이벤트 시스템: 캐릭터가 사망했을 때 UI, 애니메이션, 사운드, 경험치 매니저에게 일일이 명령을 내리는 대신, ‘캐릭터 사망’이라는 메세지를 이벤트 큐에 발행합니다. 그러면 각 매니저들이 알아서 메세지를 가져가 독립적으로 처리합니다. (결합도 감소)
Unreal
PrimaryActorTick.bCanEverTick
정의
- 소속:
AActor클래스의 멤버 변수인PrimaryActorTick구조체 안에 포함된bool필드입니다. - 역할: 액터가 생성될 때 엔진의 틱 목록에 이 액터를 등록을 결정합니다.
- 설정 시점: 반드시 생성자에서 설정해야 합니다. 게임 실행 중에 이 값을 바꾼다고 해서 갑자기 틱이 실행되거나 멈추지 않습니다.
- PrimaryActorTick.bCanEverTick = true;가 선언안되어 있으면 나중에 아무리 SetActorTickEnabled(true)를 호출해도 틱은 절대 실행되지 않습니다.
- 즉, true로 선언을 한번이라도 하면 나중에라도 SetActorTickEnabled 사용이 가능한데 아예 선언을 안하거나 처음부터 false로만 선언하면 SetActorTickEnabled(true)를 호출해도 실행이 안됨
등록 vs 실행
| 구분 | bCanEverTick | SetActorTickEnabled() |
|---|---|---|
| 기능 | 엔진의 틱 관리 시스템에 액터를 등록함. | 등록된 액터의 틱 함수를 실제로 호출할지 결정함. |
| 성능 | false일 경우 엔진이 참고하지도 않음 (가장 빠름). | false라도 목록에는 있어서 체크하는 비용이 미세하게 발생함. |
| 변경 | 생성자에서만 설정 가능 (정적). | 게임 플레이 중 언제든 변경 가능 (동적). |
성능 최적화
언리얼 엔진 게임 안에는 수백, 수천 개의 액터가 존재할 수 있습니다. 모든 액터의 bCanEverTick이 true라면, 엔진은 매 프레임마다 수천 개의 Tick() 함수를 호출하려고 시도하며 CPU에 부하를 줍니다.
최적화 원칙 움직이지 않는 배경, 아이템, 단순한 논리만 처리하는 액터 등 매 프레임 계산이 필요 없는 액터는 반드시 생성자에서
bCanEverTick = false;로 설정해야 합니다.
실전 사용 팁: 동적 틱 제어
만약 액터가 평소에는 쉬다가 특정 상황(예: 전투 돌입, 스킬 사용)에서만 잠깐 틱이 필요하다면 다음과 같이 구성하는 것이 정석입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
AAssignment_Actor::AAssignment_Actor()
{
// 1. 일단 틱을 쓸 '자격'을 줌
PrimaryActorTick.bCanEverTick = true;
// 2. 하지만 시작할 때는 끔. (불필요한 연산 방지)
PrimaryActorTick.bStartWithTickEnabled = false;
}
void AAssignment_Actor::OnSomethingHappened()
{
// 필요한 순간에만 동적으로 킴.
SetActorTickEnabled(true);
}
UObject
UObject에는 생성자가 없고, AActor에는 있는 이유
사실 UObject와 AActor 모두 생성자를 가질 수 있고 내부적으로는 존재합니다.
다만, 코드를 짤 때 UObject는 생성자 없이 구현하는 경우가 많고, AActor는 생성자를 거의 필수로 작성하게 되는데, 이는 역할의 차이와 엔진의 자동화 시스템 때문입니다.
UObject는 생성자가 “안 보이는” 것처럼 느껴질까? 최신 언리얼 엔진에서는 개발자의 편의를 위해 많은 코드를 숨겨두었기 때문입니다. 예전 방식에서는
UObject도FObjectInitializer를 인자로 받는 생성자를 매번 적어줘야 했지만, 지금은GENERATED_BODY()덕분에 꼭 필요한 상황(컴포넌트가 있는 액터 등)이 아니면 생략해도 엔진이 뒤에서 다 처리해 주는 것입니다.요약 “
UObject는 엔진이 알아서 기본을 챙겨주니 로직만 짜면 되지만,AActor는 컴포넌트를 조립하고 틱 설정을 하는 등 탄생 직후의 셋업이 필수라 생성자가 겉으로 드러나는 것”입니다.
UObject (최소한의 데이터)
UObject는 언리얼 시스템의 가장 밑바닥에 있는 객체입니다.
- 기본 생성자 자동 생성:
UObject를 상속받아 클래스를 만들 때 헤더에GENERATED_BODY()매크로를 넣습니다. 이 매크로가 우리가 생성자를 따로 적지 않아도 엔진이 알아서 기본 생성자를 만들어 리플렉션 시스템에 등록해 줍니다. - 컴포넌트가 없음: 월드에 배치되는 물체가 아니므로 컴포넌트(
CreateDefaultSubobject)를 생성할 일이 거의 없습니다. 단순히 데이터를 담거나 가벼운 로직만 수행한다면, 엔진이 만들어준 기본 생성자로도 충분하기 때문에 개발자가 직접 짤 일이 적은 것입니다.
AActor
AActor는 게임 월드(Level)에 실존하는 객체입니다.
- 컴포넌트 조립: 액터는 눈에 보이는 메쉬, 충돌체, 오디오 등을 가져야 합니다. 이 컴포넌트들을 메모리에 올리는
CreateDefaultSubobject<T>()함수는 반드시 생성자 안에서만 호출할 수 있습니다. - 틱 및 초기 설정:
PrimaryActorTick.bCanEverTick = true;같은 엔진 최적화 관련 설정도 생성자에서 이루어집니다. - CDO(Class Default Object) 생성: 액터는 생성자에서 설정된 값들을 바탕으로 ‘기본 복사본(CDO)’을 만듭니다. 에디터 디테일 패널에 처음에 뜨는 기본값들이 바로 이 생성자에서 결정된 값들입니다.
차이점
| 구분 | UObject (일반 데이터/로직) | AActor (월드 배치 객체) |
|---|---|---|
| 생성자 작성 | 선택 사항 (안 쓰면 엔진이 자동 생성) | 사실상 필수 |
| 핵심 작업 | 변수 초기화, 간단한 로직 준비 | 컴포넌트 생성, 틱 설정, 루트 컴포넌트 지정 |
| 존재 이유 | 엔진 시스템(GC, 리플렉션) 관리용 | 월드 상의 물리적 존재감 및 상호작용 |
UObject 레벨에 배치를 못하는 이유
이유: 생성자에서의 ‘컴포넌트 조립’ 유무입니다. 레벨에 배치된다는 것은 반드시 위치, 회전, 크기 값을 가져야 한다는 뜻입니다.
결정적인 차이: “좌표($(X, Y, Z)$)가 있는가?”
- UObject: 이름, 변수, 함수 같은 ‘정보’는 가지고 있지만, 월드 상의 ‘좌표’를 저장할 공간이 아예 없습니다. 그래서 에디터에서 레벨에 끌어다 놓으려고 해도 엔진이 어디에 위치해야하는지 몰라서 거부를 하는 것입니다.
- AActor: 액터는 좌표 정보를 가질 수 있는 루트 컴포넌트를 가질 수 있습니다.
생성자와의 관련성
- 액터의 생성자 작업:
AActor는 생성자에서CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));같은 코드를 통해 공간 정보를 담을 그릇(컴포넌트)을 만듭니다. - 조립의 결과: 이 부품이 생성자에서 성공적으로 조립되어야만, 비로소 에디터에서 레벨에 놓았을 때 이 액터는 (100, 200, 0) 위치에 존재할 수 있구나라고 엔진이 인식하는 것입니다.
- 반대로
UObject는 이런 부품 조립 과정(생성자 작업)이 기본적으로 없기 때문에, 월드라는 물리 공간에 발을 붙일 수가 없는 것입니다.
정리
| 내용 | 차이 |
|---|---|
| 레벨 배치 가능 여부 | UObject는 불가능, AActor는 가능. |
| 배치 가능/불가능 이유 | 액터는 Transform(위치 정보)을 가진 컴포넌트가 있기 때문. |
| 생성자와의 관계 | 컴포넌트를 생성자에서 조립하기 때문에 액터는 생성자가 매우 중요함. |
C++
가상 소멸자
C++에서 다형성을 활용할 때, 즉 부모 클래스의 포인터로 자식 클래스의 객체를 다룰 때 소멸자에 virtual을 안붙히면 부모의 소멸자만 호출해 버립니다.
만약 자식 클래스 내부에서 동적 할당한 메모리가 있다면, 메모리 누수가 발생합니다.
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
26
27
28
29
#include <iostream>
using namespace std;
class AAnimal {
public:
AAnimal() { cout << "동물 생성\n"; }
~AAnimal() { cout << "동물 소멸\n"; }
};
class ACat : public AAnimal {
public:
ACat() { cout << "고양이 생성\n"; }
~ACat() { cout << "고양이 소멸\n"; }
};
int main() {
AAnimal* MyPet = new ACat();
// 메모리 해제
delete MyPet;
return 0;
}
//결과
동물 생성
고양이 생성
고양이 소멸
동물 소멸
해결책: 부모 클래스에 virtual 붙이기
1
2
3
4
5
class AAnimal {
public:
AAnimal() { cout << "동물 생성\n"; }
virtual ~AAnimal() { cout << "동물 소멸\n"; }
};
virtual 키워드가 붙으면, 실제로 메모리에 생성된 자식을 찾아가서 자식의 소멸자를 먼저 부르고 부모의 소멸자를 마무리로 호출해 줍니다.