Blog

ENGINEERING NOTE

[Unreal Engine] 참조 연결의 분리

해당 포스팅은 Stuart Butler , Tom Oliver의 Game Development Patterns with Unreal Engine 5를 바탕으로 작성한 내용입니다.위 UML 시퀀스 다이어그램은 플레이어 입력부터 투사체 생성, 적 처치, 점수 반영까지의 흐

Unreal Engine
해당 포스팅은 Stuart Butler , Tom Oliver의 Game Development Patterns with Unreal Engine 5를 바탕으로 작성한 내용입니다.

참조 연결의 분리 필요성

위 UML 시퀀스 다이어그램은 플레이어 입력부터 투사체 생성, 적 처치, 점수 반영까지의 흐름을 표현하고 있다.

겉보기에는 정상적으로 동작하는 구조처럼 보이지만, 객체 간 참조 관계가 지나치게 얽혀 있어 여러 설계적 문제를 내포하고 있다. 이 글에서는 해당 다이어그램이 가지는 문제점을 참조 연결 관점에서 정리한다.

1️⃣ Character 클래스의 책임이 과도하게 집중된다

Character는 발사 로직(Fire()), 투사체 초기화(Init()), 적 처치 처리(KilledEnemy()), 점수 증가 요청까지 담당한다. 하나의 객체가 전투, 무기, 보상 시스템 전반을 모두 관여하는 구조가 된다.

이는 단일 책임 원칙을 명확히 위반하는 구조이다. 결과적으로 Character는 변경에 매우 취약한 중심 객체가 된다.

2️⃣ 객체 간 참조가 순환 구조를 형성한다

Character는 Projectile을 생성하고 초기화한다. Projectile은 적을 처치하면 다시 Character의 KilledEnemy()를 호출한다. Character는 다시 PlayerController의 AddPoints()를 호출한다.

이로 인해 객체 간 참조가 트리 구조가 아닌 순환 그래프 구조를 형성한다. 순환 참조는 의존성 추적을 어렵게 만들고 유지보수 비용을 급격히 증가시킨다.

3️⃣ 확장 시 변경 범위가 과도하게 커진다

무기 교체 시스템을 추가하거나, 여러 종류의 투사체를 도입하거나, AI 캐릭터가 동일한 공격 로직을 사용하게 할 경우를 가정한다. 이 구조에서는 Character, Projectile, PlayerController 모두 수정 대상이 된다.

기능 추가가 곧 대규모 수정으로 이어지는 구조는 확장에 매우 불리하다.



참조 연결 분리의 인프라 구축

객체 간 참조를 분리하기 위해서는 단순히 클래스를 나누는 것만으로는 부족하다.

어떤 방향으로, 어떤 방식으로 통신할 것인지에 대한 인프라 규칙가 먼저 정의되어야 한다. 아래는 트리 구조 기반 설계에서 참조 연결을 안전하게 분리하기 위해 사용되는 대표적인 세 가지 수단이다.

1. 퍼블릭 함수(Public Function)

퍼블릭 함수 호출은 올바른 타입의 레퍼런스를 이미 확보했을 때 사용하는 가장 기본적인 통신 방식이다. 호출자는 대상 객체의 정확한 타입을 알고 있어야 하며, 직접 참조를 통해 함수를 호출한다. 이 방식은 구조적으로 트리의 상위에서 하위로 내려가는 방향에 적합하다.

상위 객체가 하위 객체의 생명주기를 관리하는 경우, 퍼블릭 함수 호출은 명확하고 안전한 선택이 된다. 다만 하위 객체가 상위 객체를 직접 호출하는 구조에서는 강한 결합을 유발하므로 주의가 필요하다.

2. 이벤트 델리게이트(Event Delegate)

이벤트 델리게이트는 퍼블릭 함수 호출과 정반대 방향의 통신을 담당한다. 하위 객체는 자신을 소유하거나 관리하는 상위 객체의 타입을 알 필요 없이 이벤트를 발생시킨다. 상위 객체는 해당 이벤트를 구독함으로써 필요한 반응을 처리한다.

이 방식은 트리의 하위에서 상위로 신호를 전달하는 구조에 적합하다. 하위 객체가 상위 객체를 직접 참조하지 않으므로 의존성이 단절된다. 상태 변화, 결과 보고, 알림 전달에 매우 효과적인 구조이다.

3. 인터페이스(Interface)

인터페이스는 특정 클래스 타입에 대한 직접적인 링크 없이 통신하기 위한 수단이다. 호출자는 대상 객체의 구체적인 타입을 알지 못해도 인터페이스 계약만 만족하면 호출이 가능하다. 이로 인해 서로 다른 분기(branch)에 위치한 객체 간 통신이 가능해진다.

인터페이스는 보통 트리의 옆 방향, 즉 형제 노드나 다른 계층 간 통신에 사용된다. 정확한 구현체에 의존하지 않으므로 구조 확장에 매우 유리하다. 시스템 전반의 결합도를 낮추는 핵심 인프라 역할을 한다.



분리 설계를 적용시켜보기

분리 설계 적용 이후의 구조 변화

분리 설계를 적용한 UML 다이어그램에서는 객체 간 참조 방향이 명확한 트리 구조로 정리된다. 모든 객체는 자신보다 상위 계층을 직접 참조하지 않는다.

  • AIUnit은 충돌 및 사망 시점에 단순히 자신의 상태 변화만을 외부에 알린다.
  • AIController는 AIUnit의 이벤트를 수신하여 이를 상위 계층으로 전달한다.
  • GameMode는 점수 증가 여부를 판단하는 유일한 책임 주체로 동작한다.

이로 인해 전투 로직, 게임 규칙, UI 로직이 명확히 분리된다.

시퀀스 흐름의 정리

1. Character가 공격을 수행한다. 1. Projectile이 생성되고 적과 충돌한다. 1. AIUnit은 사망 이벤트를 발생시킨다. 1. AIController가 해당 이벤트를 수신한다. 1. GameMode가 점수 증가를 결정한다. 1. PlayerController를 통해 점수 변경이 전달된다. 1. ScoreWidget이 UI를 갱신한다.

이 흐름에서 각 객체는 자신의 책임 범위 내 행동만 수행한다. 어느 객체도 시스템 전체를 인지하지 않는다.

참조 연결 분리가 가져온 효과

분리 설계를 통해 다음과 같은 효과를 얻을 수 있었다.

  • 하위 객체가 상위 객체를 직접 호출하지 않는다.
  • 점수 시스템은 전투 로직과 분리된다.
  • UI는 게임 규칙 변경에 영향을 받지 않는다.
  • AI, 플레이어, UI 로직이 독립적으로 확장 가능해진다.

이 구조에서는 기능 추가 시 수정 범위가 국소화된다. 새로운 적 타입이나 점수 규칙이 추가되어도 기존 전투 로직은 변경되지 않는다.

🔥 설계 관점에서의 핵심 정리
  • 제어는 위에서 아래로 퍼블릭 함수로 전달한다.
  • 결과 보고는 아래에서 위로 이벤트 혹은 델리게이트로 전달한다.
  • 계층을 넘는 통신은 인터페이스로 해결한다.
이 규칙을 지키는 것만으로도 구조는 자연스럽게 정리된다!