[생각] 책임지는 프로그래머

[책임지는 프로그래머] 2화 - 언리얼 엔진에서의 구현 상속

지망셍 2025. 6. 5. 08:00

프로그래밍을 공부하다 보면 꼭 듣게 되는 조언이 있다.

“구현 상속은 피하라. 인터페이스 상속과 조합(Composition)을 써라.”

처음에는 이런 말들이 좀 어렵게 느껴졌다.

“그냥 상속하면 되지, 왜 피하라고 하지?”

그래도 공부를 하다 보니 이런 조언들이 단지 문법 얘기가 아니라, 설계 철학과 유지보수의 관점에서 나온다는 걸 알게 됐다.

그런데, 요즘 언리얼 엔진을 배우면서 약간 혼란스러웠다.

언리얼에서는 구현 상속을 너무 당연하게 쓰고 있었기 때문이다.

AActor, UObject, UComponent 등 거의 모든 핵심 기능들이 상속을 전제로 만들어져 있다.

게다가 Tick(), BeginPlay() 같은 오버라이드도 기본처럼 쓰인다.

그래서 어느 순간 이런 의문이 생겼다.

“도대체 왜 일반 C++에서는 구현 상속을 피하라고 하고, 언리얼에서는 권장처럼 보이는 걸까?”

“이건 상충되는 조언일까, 아니면 맥락이 다른 걸까?”


구현 상속을 경계하라는 말의 진짜 의미

일반적인 C++ 설계 조언에서 구현 상속이 위험하다고 여겨지는 이유는 다음과 같다:

  • 자식 클래스가 부모 클래스의 내부 구현에 너무 의존하게 되고
  • 부모가 바뀌면 자식까지 줄줄이 깨지는 구조가 되기 쉽다
  • 가상 함수 호출, slicing, RTTI 등 다형성의 런타임 비용이 은근히 무겁다
  • 무엇보다, “상속은 is-a 관계일 때만 써라”라는 원칙이 자주 강조된다

이걸 요약하면 한마디로 이렇다:

상속은 강력하지만 위험한 도구니까, 신중하게 써라.

그리고 그 대안으로 자주 제시되는 게

“구현은 구성으로, 인터페이스는 상속으로.” 라는 조합이다.


언리얼 엔진은 왜 다르게 보일까?

그런데 언리얼은 왜 이렇게 상속을 많이 쓸까?

이건 단순히 코드 스타일의 차이가 아니었다.

엔진의 구조 자체가 클래스 상속을 전제로 설계되어 있었기 때문이다.

  • AActor를 상속하지 않으면 월드에 존재할 수가 없고
  • UObject를 상속하지 않으면 GC, 리플렉션, 블루프린트 연동이 안 된다
  • BeginPlay(), Tick()은 프레임워크에서 호출을 보장하는 진입점이다

즉, 언리얼에서는 상속이 단순한 선택이 아니라 시스템 통합의 전제 조건이 되는 경우가 많았다.

이걸 보면서 나는 이렇게 정리했다:

“언리얼은 프레임워크다.

그리고 프레임워크는 사용자에게 일정한 ‘틀’을 요구한다.”

그러니까 언리얼이 상속을 쓰라고 하는 건

C++의 철학과 상충되는 게 아니라, 역할이 다른 도구의 요구사항이라는 거다.


그렇다면 나는 어떻게 설계해야 할까?

이걸 정리하면서 나 자신에게 중요한 질문을 던지게 됐다.

  • 엔진이 상속을 요구하는 부분과, 내가 자유롭게 설계할 수 있는 부분은 어떻게 구분할까?
  • 엔진이 요구하는 상속을 ‘그냥 따르는 것’과, ‘이해하고 쓰는 것’은 무엇이 다를까?
  • Tick을 쓸 때마다, 이게 정말 필요한지 고민해본 적은 있었나?

이런 질문들은 단지 코드 품질의 문제가 아니었다.

“나는 지금 도구를 사용하는가, 도구에 끌려가는가?”

라는 태도의 문제였다.


이제는, “왜 이렇게 만들었는가?”를 먼저 묻고 싶다

처음에는 그저 돌아가기만 하면 됐다.

하지만 언리얼을 공부하고, 점점 더 복잡한 구조를 마주하다 보니

어떤 코드는 “왜 이렇게 짰는지”를 모르면 손대기가 어려웠다.

그래서 지금은 어떤 구조든 쓰기 전에 먼저 묻고 싶다.

  • 이 클래스는 왜 상속을 요구할까?
  • 이 구조는 어떤 메타 정보나 리플렉션을 기반으로 작동할까?
  • 이건 엔진의 책임일까, 내 책임일까?

이 질문들은 내가 더 좋은 코드를 쓰기 위해서라기보다,

“책임 있는 프로그래머가 되기 위한 질문”이라고 느껴졌다.


마치며: 도구는 책임을 대신하지 않는다

C++은 어렵고, 언리얼 엔진도 어렵다.

하지만 이 두 도구를 쓰는 이상, 우리는 코드만이 아니라 시스템 전체를 이해해야 한다.

내가 작성한 한 줄의 코드가 어떤 컴파일 타임 추상화를 요구하고,

어떤 런타임 동작을 만들어내는지,

그리고 그 동작은 누구의 책임인지.

이걸 묻는 태도야말로

내가 초보자를 지나, 진짜 중급자로 가는 첫걸음이 아닐까 생각한다.


프로그래밍은 도구를 쓰는 일이고, 그 도구는 책임의 연쇄 속에 놓인다.

우리는 그 책임을 이해하고 선택할 수 있는 프로그래머가 되어야 한다.