PART 6 · 강의 1/4
순환 참조 해결
Owner 패턴, WeakPtr 활용, Break Cycles로 순환 참조를 방지하고 해결하는 실전 전략
SECTION 01
순환 참조 탐지
순환 참조가 발생하는 일반적인 상황과 탐지 방법
흔한 순환 참조 패턴
C++
// 패턴 1: 양방향 참조
class ADialogueNPC : AActor {
UPROPERTY() ADialogueNPC* DialoguePartner; // A→B
};
// B도 A를 Partner로 → A↔B 순환
// 패턴 2: 매니저-아이템 역참조
class USkillManager : UObject {
UPROPERTY() TArray<USkill*> Skills; // 매니저→스킬
};
class USkill : UObject {
UPROPERTY() USkillManager* Manager; // 스킬→매니저 (순환!)
};
// 패턴 3: 그래프 구조
class UGraphNode : UObject {
UPROPERTY() TArray<UGraphNode*> Connections;
// 노드 A→B→C→A 순환 가능
};
순환 참조의 실제 영향
UE의 GC는 도달 가능성 기반이므로, Root Set에서 도달 불가능한 순환 그래프는 정상적으로 수집됩니다. 진짜 문제는 순환 중 하나라도 Root Set에 도달 가능하면 전체 순환이 보호되어 의도치 않은 메모리 상주가 발생하는 것입니다.
SECTION 02
Owner 패턴으로 해결
소유권 계층으로 참조 방향을 정리하는 기법
해결: 단방향 소유권 + 약한 역참조
C++
// 해결된 SkillManager-Skill 관계
UCLASS()
class USkillManager : public UObject
{
GENERATED_BODY()
// 소유자: 강한 참조 (GC 보호)
UPROPERTY()
TArray<USkill*> Skills;
void RemoveSkill(USkill* Skill)
{
Skills.Remove(Skill);
// Skill은 이제 참조 없음 → 다음 GC에서 수집
}
};
UCLASS()
class USkill : public UObject
{
GENERATED_BODY()
// 피소유자: 약한 역참조 (GC 방해하지 않음)
UPROPERTY()
TWeakObjectPtr<USkillManager> OwnerManager;
void NotifyManager()
{
if (USkillManager* Mgr = OwnerManager.Get())
{
Mgr->OnSkillCompleted(this);
}
}
};
참조 방향 설계 원칙
원칙
// 참조 방향 설계 3원칙:
1. 소유권은 단방향
부모 → 자식: UPROPERTY (강한)
자식 → 부모: TWeakObjectPtr (약한)
2. 수명이 긴 쪽이 소유
오래 사는 객체가 짧게 사는 객체를 강하게 참조
짧게 사는 객체는 약하게 역참조
3. 관찰은 약한 참조
이벤트 리스너, 캐시, UI 바인딩 등
관찰 대상을 보호하면 안 되는 경우
SECTION 03
Break Cycles 전략
이미 존재하는 순환 참조를 명시적으로 끊는 방법
명시적 참조 해제
C++
UCLASS()
class UGraphNode : public UObject
{
GENERATED_BODY()
UPROPERTY()
TArray<UGraphNode*> Connections;
// 소멸 전 순환 참조 끊기
void BreakAllConnections()
{
for (UGraphNode* Node : Connections)
{
if (Node)
{
Node->Connections.Remove(this);
}
}
Connections.Empty();
}
virtual void BeginDestroy() override
{
BreakAllConnections();
Super::BeginDestroy();
}
};
// 그래프 매니저에서 일괄 정리
void UGraphManager::ClearGraph()
{
// 모든 노드의 연결을 끊은 후
for (UGraphNode* Node : AllNodes)
{
Node->BreakAllConnections();
}
// 배열 비우기 → 참조 해제 → GC 수집 가능
AllNodes.Empty();
}
SECTION 04
순환 참조 방지 설계 가이드
처음부터 순환 참조가 발생하지 않는 구조 설계
설계 시점의 방지 전략
| 상황 | 잘못된 설계 | 올바른 설계 |
|---|---|---|
| 부모-자식 | 양방향 UPROPERTY | 부모→자식 강한, 자식→부모 약한 |
| 이벤트 리스너 | 리스너가 소스를 강한 참조 | TWeakObjectPtr 또는 AddWeakLambda |
| 그래프 구조 | 노드 간 UPROPERTY 연결 | 매니저가 소유, 노드 간 인덱스 참조 |
| UI-데이터 | 위젯이 데이터를 강하게 참조 | TWeakObjectPtr + 데이터 변경 콜백 |
SUMMARY
핵심 요약
이 강의에서 배운 내용
- 순환 참조 자체는 UE GC에서 수집 가능하지만, Root Set 연결 시 의도치 않은 보호가 문제입니다
- Owner 패턴: 소유자→강한 참조, 피소유자→약한 역참조가 표준 해결책입니다
- BreakAllConnections 패턴으로 기존 순환을 BeginDestroy 시점에 명시적으로 끊습니다
- 설계 시점에서 참조 방향을 계층적으로 설계하면 순환을 원천 방지할 수 있습니다
- 그래프 구조에서는 매니저가 모든 노드를 소유하고, 노드 간은 인덱스로 참조합니다
PRACTICE
도전 과제
배운 내용을 직접 실습해보세요
실습 1: 순환 참조 생성 및 GC 테스트
두 UObject가 UPROPERTY로 서로를 참조하는 순환 참조를 만드세요. 외부 참조를 모두 제거한 후 ForceGarbageCollection()을 호출하여 UE5 GC가 순환 참조를 처리할 수 있는지 확인하세요. Mark-and-Sweep이 순환 참조에 강한 이유를 이해하세요.
실습 2: TWeakObjectPtr로 순환 참조 방지
Parent-Child 관계에서 Parent는 UPROPERTY로, Child는 TWeakObjectPtr로 역참조하는 패턴을 구현하세요. Parent가 Destroy될 때 Child의 역참조가 자동으로 무효화되는지 확인하세요.
심화 과제: 복잡한 그래프 구조의 메모리 관리
게임 내 스킬 트리, 퀘스트 의존성 그래프 등 복잡한 참조 구조를 설계할 때의 GC 전략을 수립하세요. 소유권 방향, 약한 참조 사용 위치, AddReferencedObjects 필요 지점을 포함한 참조 아키텍처 문서를 작성하세요.