해당 글은 다음 영상을 공부하며 제 방식대로 정리한 글입니다.
원 영상과 다른 부분이 있을 수 있고, 원 영상 자체가 정말 알차게 블루프린트와 C++을 섞은 설계를 잘 소개하고 있기 때문에, 원 영상을 보시는 것을 추천드립니다.
Alex Forsythe에게 좋은 영상을 만들어 준 점에 대해 감사하는 말씀을 드리고 싶네요.
원 영상: https://www.youtube.com/watch?v=VMZftEVDuCE
언리얼 엔진에서는 스크립팅을 위해 C++와 블루프린트를 모두 제공한다.
내가 유니티 개발자다 보니 이런 점은 익숙하지 않았다. 유니티는 기본적으로 C#만을 사용해서 개발을 진행하기 때문이다.
유니티와 달리 언리얼 엔진에서는, 대다수의 기능을 C++와 블루프린트 중 아무거나 골라도 구현할 수 있는 경우가 많다.
그렇기 때문에 언리얼 엔진을 처음 공부하는 입장에서, 둘 중 어떤 것을 고르는 것이 맞는 것인지, 무엇을 공부해야 할지 헤매게 되었다.
오늘 작성할 글은 Alex Forsythe가 작성한 유튜브 영상 “Blueprints vs. C++: How They Fit Together and Why You Should Use Both”을 기반으로 하고 있다. 해당 영상에서 딱 이러한 궁금증에 대해 이야기하고 있는데, 꽤 유익한 내용이 많다.
결론부터 말해서, “C++를 써야 할까, 블루프린트를 써야 할까?”라는 질문은 말이 안되는 질문이라고 한다. 둘은 상호보완적으로 설계되어 있다. 그렇기에 어느 분야에서는 C++이 좀 더 우세하고, 다른 분야에서는 블루프린트가 효율적인 선택이 된다.
따라서 올바른 질문은, “C++과 블루프린트 중 어떤 곳에 어느 것을 사용하는 게 이득인가?”를 묻는 것이다.
블루프린트와 C++의 공통점
블루프린트든 C++이든 생긴 건 다르지만, 둘은 모두 같은 행위를 할 수 있다.
모든 동작을 블루프린트로 만들 수 있는 건 아니지만, 가능한 선에서는 C++의 코드와 블루프린트 코드는 1:1 대응된다. C++로 만들 수 있는 걸 대다수 블루프린트로 구현할 수 있고, 블루프린트로 구현 가능한 건 C++로 치환 가능한 셈이다.
중요한 것은, 어떤 것을 고르든 소프트웨어 디자인을 다루게 된다는 점이다.
우리의 목표는 고수준(High level)의 기능을 구현하는 것이다.
예를 들어, 플레이어가 조종하는 탱크가 조이스틱에 따라 움직이며 사격 버튼을 누르면 포가 나가는 것이다.
그렇지만 우리가 갖고 있는 프로그래밍 언어로는 좌표를 직접적으로 수정하거나 오브젝트를 생성하는 등의 저수준(Low level)의 작업만이 가능하다. 즉, 고수준(High level)의 기능을 직접 만들 수 없다.
그래서 이를 달성하기 위해서는 로우레벨의 기능을 구현하고, 이를 조립해서 고수준 기능을 구현하게 된다.
블루프린트를 쓰든 C++를 쓰든 여러 기능을 정의하고, 이들을 조립하거나 상호작용하도록 하여 기능을 정의해야 한다. 이들 사이를 효율적으로 조립하고 상호작용하게 하다 보면, 기능이 많아질수록 그 구조를 체계적으로 짜야 헤매지 않게 된다.
로우레벨과 하이레벨, 프로그래밍과 스크립팅의 차이
이때 게임 개발에서 코드를 다루는 건 사실 여러 단계로 나누어 볼 수 있다.
- 보통 로우레벨, 미들레벨, 하이레벨로 기능을 나눌 수 있다.
- 좀 더 러프하게, 로우레벨과 하이레벨로 나눈다고 해보자.
- 여기서 로우레벨은 ‘프로그래밍’의 영역, 하이레벨은 ‘스크립팅’의 영역이라 부르도록 한다.
- 그러나 이렇게 단계를 나누어도 그 경계는 분명하지 않다. 게임 플레이에 대해서 이야기한다면, 블루프린트만으로도 C++에서 할 수 있는 많은 부분을 구현할 수 있고, 언리얼 엔진은 둘을 함께 운용할 수 있도록 상호전환이 용이하게 만들었다.
- 그렇기에 둘 사이의 경계는 정해져 있지 않아 매 기능을 구현할 때마다 어디서부터 어디까지는 블루프린트로, 나머지는 C++로 하자고 개발자가 경계를 스스로 그어야만 한다.
C++과 블루프린트가 겹치는 영역
- 물론… 경계를 긋는 것이 개발자의 몫이라고 해서, C++이 할 수 있는 걸 블루프린트가 다 할 수 있는 건 아니다. 그래픽스 프로그래밍, 외부 라이브러리와의 통합, 에디터 확장, 하드웨어 제어 등의 영역은 분명히 C++만 할 수 있는 영역이다.
- 그렇기 때문에, 여기서는 게임플레이 프로그래밍의 영역에 대해서만 다룬다. 언리얼 엔진에서 ‘게임 플레이 프로그래밍’이란 UObject 를 상속받는 구체화 클래스들을 다루는 것을 말한다. UCLASS() , UPROPERTY() , UFUNCTION() 매크로로 다루는 영역이 바로 그곳이다.
- 둘 사이를 비교할 땐 여러 가지 기준이 사용되는데, 보통 ‘성능’과 ‘설계’라는 관점으로 비교한다.
1. 성능: 블루프린트와 C++의 성능 비교
가장 둘을 비교하기 좋은 것은 ‘성능’일 것이다. 아주 흔하게 알려진 것으로, 블루프린트는 복잡한 빌드 과정이 없는 스크립팅 언어인 만큼 성능이 낮고, C++는 저수준까지 다룰 수 있어 최적화 성능이 높다는 말을 들어봤을 것이다. 그렇지만 둘 사이의 성능이 왜 차이가 나는지, 얼마나 차이가 나는지는 사실 잘 몰랐다.
분명히 C++과 블루프린트는 성능 차이가 존재한다.
- C++은 컴파일 시 바로 머신 코드(01011011…)로 변환된다.
- 반면, 블루프린트는 스크립트 언어이기 때문에, VM에서 실행될 수 있는 바이트 코드로 변환된다(VM에서 실행된다는 점에서 Java와 유사).
직접 추적해보면 알겠지만, C++ 코드는 (1) 수학 함수 부분은 인라인으로 처리해서 많은 부분을 최적화해준다. (2) 또, 매개변수 같은 걸 계산하더라도 중복하여 처리하는 부분은 컴파일러 단에서 미리 계산하여 최적화해준다.
반면, 블루프린트는 그렇게 로우레벨까지 처리할 수 없다. 바이트코드에서 매개변수에 있는 값을 레지스터에 일단 모두 넣고 함수를 “일단 호출한다”. 그래서 a라는 값이 10번 계산하면 곧이곧대로 10번을 계산하게 된다. 호출하는 함수 자체는 네이티브 레벨로 최적화된 코드를 실행하지만(보통 블루프린트 함수는 별도로 C++로 짜여진 형태로 제공되어, 그 함수 자체는 최적화가 되어 있음), 그 과정에서 local 최적화나 global 최적화 등 컨텍스트를 고려하여 최적화하는 컴파일러의 도움을 전혀 못 받는다. 이는 분명히 오버헤드로 이어지고, 유의미한 수준의 성능 저하로 이어진다.
그렇지만 중요한 건, 한 프레임에 한 번 실행하는 수준의 코드에서는 그렇게 성능 저하를 경험하기 힘들다. 1프레임에 한 번 실행한다고 보통 16ms 정도까지 걸릴 만큼 병목이 생기진 않는다. 프레임 당 한 번 실행한다고 해서 60프레임 뽑는 데 무리가 가진 않는다!
또한, 만약 최적화가 필요하다면 “블루프린트 네이티브화” 기능이라는 대안을 제공한다.
결론적으로 실행 시간의 면에서, C++가 블루프린트보다 속도가 빠른 건 맞다.
그래서 유의미한 차이를 만들어 낼 수 있는 분야에서는 C++를 쓰는 것이 옳다.
- 저수준 시스템(물론 어디까지가 저수준인지는 스스로 판단 필요)
- 대형 배열, 검색, 복잡한 Lookup 등이 포함된 데이터 프로세싱 과정
- N차원 배열, 프레임마다 실행되는 아주 무거운 작업을 포함하는 무거운 루프
- Actor Tick 함수를 사용하는 수많은 클래스 인스턴스들
그렇지만… 보통 프로그래밍에서 성능을 최적화할 땐 파레토의 법칙, 즉 ‘80-20 법칙’을 고려해야 한다. 보통 80%의 실행 시간은 20%의 코드에서 발생하기 마련이다.
그래서 무작정 블루프린트를 C++로 옮기기보단, 실제 프로파일러를 보며 시간이 많이 걸리는 부분을 집중적으로 공략해야 한다.

예를 들어, 블루프린트(왼쪽 파랑 막대)에서 4ms가 걸리고 전체를 C++로 재작성해 0.2ms로 20배만큼 성능을 개선했다고 해도(오른쪽 노랑 막대), 굳이 그럴 필요 없이 사실 하나의 함수만 C++로 재작성했어도 거의 똑같은 효과를 얻을 수 있는 경우가 있겠다.
그래서 결국 중요한 건 컨텍스트(맥락; Context)이다. C++가 빠르다곤 해도 유의미한 차이가 있는지는 상황에 따라 다르고, 이것도 직접 프로파일러를 보면서 분석해 보아야 한다.
2. 클래스 설계에서의 C++ & 블루프린트
물론 성능의 이슈가 아니더라도, 시스템 일부를 C++로 작성하고 싶을 수도 있다.
C++에서는 클래스 정의란 클래스가 무엇을 담당해야 하는지 확립한 다음, 책임을 정확하게 처리하기 위한 필요한 속성과 함수가 무엇인지 팡가하는 것을 의미한다. 공개 인터페이스로 표시되어야 할 속성과 함수, 비공개 구현으로 숨겨야 할 속성 및 함수를 파악하는 것도 포함한다.
C++에선 헤더(.h)와 구현부(.cpp)로 나누는데 이건 블루프린트에서도 거의 똑같다. 클래스 설정에서 함수 목록, 속성, 구성요소를 정의하고, 이벤트 그래프와 함수 그래프에서 함수 구현을 포함한다.
그래서 새 클래스와 다른 유형을 정의하는 방면에서는 거의 동등하다고 볼 수 있다.
문제는 종속성
둘 사이의 진짜 차이점은 유형(Type) 간의 종속성에서 발생한다.
c++에서는 클래스, 열거형, 구조체를 만들 때마다 새로운 유형이 생기는데, 기능을 구현하다 보면 이들 사이에서 서로 알아야 하는 지점이 생기게 된다. 그것이 바로 코드베이스의 ‘종속성’이다.
종속성은 가능하면 단방향이 되는 것이 좋다. 예를 들어, 무기와 미사일이 있다고 하면, 무기는 미사일을 알아야 하지만, 미사일은 무기를 모르는 편이 좋다. 그래야 하는 이유는 설계 원칙 때문인데, 서로가 서로를 알게 되면 커플링(Coupling)이라고 하여 둘 중 하나를 수정하면 다른 하나도 영향을 받아 수정해야 하므로 비효율적이기 때문이다.
무기에서 미사일을 하나만 발사할 수 있다는 조건이 걸리더라도, 미사일이 무기를 아는 일은 최대한 자제해야 한다. 이를 해결하기 위해 보통 델리게이트(Delegate)를 두는데, 언리얼 엔진에서는 이를 이벤트 디스패처라고 부른다. 미사일이 무기가 갖고 있는 미사일이 몇 개인지 알고 싶어도, 직접적으로 참조하기보다 델리게이트를 통해 간접적으로 확인하도록 해야 종속성을 줄일 수 있다.
어쨌거나, 이렇게 둘을 분리함으로서 서로 알아야 할 영역을 줄이고 종속성을 최소화할 수 있다.
프로젝트가 커질수록 종속성 문제는 점점 커지는데, 이들을 잘 분리하는 것이 코드 수정의 위험이라던가 많은 리스크를 줄이는 역할을 한다.
이런 분리를 위해서 C++에서는 모듈을 사용할 수 있다.
C++의 모듈 기능
프로젝트가 복잡해지면 기능과 시스템을 별도 모듈로 분리할 수 있는데, 한 모듈의 클래스가 다른 모듈의 클래스를 참조하려면 두 모듈 간에 명시적인 종속성이 있어야 한다.
public class CobaltCore : ModuleRules
{
public CobaltCore(ReadOnlyTargetRules Target)
: base(Target)
{
PublicDependencyModuleNames.Add("Core");
PublicDependencyModuleNames.Add("CoreUObject");
PublicDependencyModuleNames.Add("Engine");
PublicDependencyModuleNames.Add("CobaltWeapons");
}
}
이때 이 다른 모듈에 참조되는 클래스는 ‘모듈의 공개 API’의 일부로 내보내야 한다. 모듈은 일반적으로 단방향으로만 참조할 수 있기에 계층형 아키텍처로 구성된다.
ex) 게임 모듈 하위에 무기 모듈을 두는 식으로 설계할 수 있다.
이렇게 설계하고, 무기 모듈은 이것의 상위에 있는 모듈을 참조할 수 없게 제약을 거는 것이다.
이는 스스로 부과하는 제약 조건으로, 설계 원칙을 위반하는 코드를 작성하려고 하면 “명시적인 종속성이 아닌 모듈의 코드를 사용하려고 한다”는 링커 오류가 발생한다. 즉, 프로그래머가 실수해도 귀신 같이 에러로 찾아준다.

// 컴파일 에러
CobaltWeapons\Private\Missile.cpp line(7): Cannot open include file: 'CobaltPlayerController.h'
// 링커 에러
unresolved external symbol ACobaltPlayerController::GetPrivateStaticClass referenced in function AMissile::Explode.
이렇게 명시적인 모듈로 분리해 설계하는 방식을 사용하면, 다음과 같은 장점이 있다.
- 빌드 시간 관리
- 코드 베이스 각 부분에 대한 책임 소재 명확히 함
- 이론적으로는 인지 부하를 줄여준다.
하나의 모듈 내에서 작업하고 있다면, 명시적인 모듈 참조가 있지 않는 한, 다른 모든 모듈의 코드는 신경 쓰지 않아도 된다.
물론.. 모듈 경계는 양날의 검으로, 위험성이 있다.
- 장점: 새로운 타입이나 의존성을 추가할 때 설계를 고려하도록 강제한다.
- 단점: 새로운 타입이나 의존성을 추가할 때 설계를 고려하도록 강제한다.
즉, 확장할 때마다 계속 귀찮은 작업이 하나 더 더해진다는 뜻.
물론 모듈 시스템을 현명하게 사용한다면, 대규모 프로젝트를 체계적으로 관리할 수 있다.
소스 코드에서 프로젝트 빌드가 성공적으로 완료되면, 이러한 경계가 위반되지 않았다고 확신할 수 있다.
블루프린트엔 모듈이 없다
그러나 블루프린트는 이러한 코드의 경계가 없다. C++로 따지자면, 단일 모듈로 구성된다.
블루프린트에선 모든 타입은 다른 모든 타입을 자유롭게 참조할 수 있다.
고수준의 스크립팅에선 문제가 안되지만, 게임의 핵심 시스템을 설계할 땐 이런 경계를 강제할 도구가 부족하기 때문에 코드 베이스가 지나치게 강력하게 결합될 가능성이 커진다.
물론 Reference Viewer를 사용하면 이런 참조를 비주얼적으로 볼 수 있다. 이를 통해 참조 관계를 관리할 수는 있지만, C++만큼의 경계를 구축하기는 어렵다.
블루프린트는 C++ 클래스를 참조할 수 있지만, C++ 클래스는 블루프린트 객체를 참조할 수 없다.
블루프린트는 그에 해당하는 UObject 클래스가 생성되기 때문에 우회적으로나마 C++에서 참조하는 것 자체는 가능하지만, AActor로 참조할 뿐이므로 구체 클래스를 알 수 없다. 따라서 함수 호출이 불가능하다. 함수 이름을 찾아 동적 호출은 가능하지만… 그건 애시당초 엄청난 성능 낭비가 생기는 데다가 OOP 설계 철학에도 맞지 않다.
이 점 때문에, 설계할 땐 보통 ‘C++ 클래스 간의 상호작용’으로 설계하고, 비주얼적인 효과 등은 블루프린트 클래스로 구체화하여 처리하는 것이 정석이다.
그런데 만약 “나는 진짜 프로그래머니까 모든 클래스를 C++로 구현한다”고 고집이라도 부리면? 블루프린트를 아예 안 쓰고 C++로 구현한다고 가정하자.
그렇게 되면 에셋 참조부터 주의해야 하는데, 에디터 전용 에셋 참조나 필수적인 플러그인, 엔진 수준 기능을 하드코딩으로 가져오려 할 땐 괜찮지만, 게임 오브젝트를 하드코딩으로 참조하려 하면 문제가 생기게 된다.
- ConstructorHelpers::FObjectFinder 를 이용해 하드코딩한 경로로 에셋을 가져오게 되면, 에셋에 대한 직접적인 참조가 발생한다. 즉, 게임이 시작되는 순간 그 에셋을 가져와 메모리에 영구적으로 로드하게 된다.
- 프로젝트 진행 방향에서도 문제가 있다. 어떤 에셋을 사용할지의 문제는 상위 수준의 결정이다. 그런데 C++ 코드 단의 기능 구현은 로우 레벨에서의 구현을 목표로 한다. 그러다 보니… C++ 코드에서 구현하는 건 설계적으로 하나의 클래스에 너무 많은 기능을 던져주는 셈이 된다.
그래서 블루프린트에서 에셋을 참조하도록 하는 편이 훨씬 낫다. 시각적 피드백도 즉시 받을 수 있어 작업이 편리하고, 에셋 참조가 효율적으로 관리되어 에셋을 원활히 불러오고 내보낼 수 있다.
결론적으로 캐릭터 외형을 결정하는 것은 블루프린트로, 게임 세계를 구현하는 것은 C++로 분리하는 편이 훨씬 깔끔해 진다.

이 원칙을 따라 설계하면 위와 같은 구조가 되는데, 고수준의 기능을 블루프린트에서 구현하고, 기반이 되는 저수준의 구조는 C++에서 설계하는 식이다. 고수준의 블루프린트는 저수준의 C++ 클래스를 단방향으로 참조하며, 이 흐름은 반대로는 흘러가지 않는다.
전통적인 구조로, 적당한 기술을 갖춘 팀이 적당히 복잡한 게임을 만들 때 유용한 구조다.
그렇지만 언리얼 엔진의 구조는 상당히 유연하다. 그렇기에 다양한 변주가 생길 수 있다.

대표적인 예시로, 모든 데이터 유형을 C++로 가져올 필요가 없으므로 일부 함수만 가져올 수도 있다.
- UBlueprintFunctionLibrary를 확장하는 클래스를 만들면, static BlueprintCallable 함수를 만들면 어디서든 네이티브 코드를 활용할 수 있다.
- 다음과 같이 개별 함수만 C++ 코드로 가져와서 만들어주고, 나머지는 블루프린트에서 구현할 수도 있다.

결론
언리얼 엔진은 블루프린트나 C++이나 둘 다 같은 걸 구현할 수는 있지만, 언리얼 엔진을 처음 접한다면 블루프린트를 먼저 사용해 보면서 배우는 것이 좋다. 블루프린트로 배우는 것은 C++에 직접적으로 적용할 수 있는 것이 많다.
블루프린트는 이런 장점이 있다.
- 에셋이나 시각 효과: C++ 소스코드에선 에셋이 어떻게 보일지 알 수 없지만, 블루프린트는 즉각적으로 에셋을 보면서 편집 가능하다.
- 스크립트 기반 시퀀스 처리에 유리: 시간에 따라 어떤 것을 처리해야 할 때, 그래프로 아주 직관적으로 표현이 가능하다.
- Iteration 속도 증가: 개발 과정에서는 테스트하고 고치고 하는 반복 과정이 많은데 이를 보통 Iteration이라고 부른다. 이런 과정이 블루프린트는 상당히 간단하다.
- 접근성: 프로그래머 뿐만 아니라 다양한 사용자들이 접근할 수 있다.
- 기능 발견의 용이성(UX 측면에서의 discoverability): 언리얼을 처음 써봐도 쓰다 보면 에디터에서 어떤 어떤 기능이 있는지 알 수 있다. 그런 만큼 작동 방식 파악이 훨씬 빠르고 편하다.
C++은 다음과 같은 분야에서 장점이 있다.
- 최고의 런타임 성능: 컴파일 최적화를 거치면 쓸모없는 오버헤드가 완전히 사라진다.
- 프로젝트의 기반이 되는 핵심 코드 작성에 적합: 모듈을 통해 종속성 관리를 하기 쉽고, 또 C++ 자체는 텍스트이기 때문에 엔진과 종속적이지 않다.
- 엔진 기능 제공:
- 외부 라이브러리 사용: 사용하고 싶은 C/C++ 라이브러리를 자유롭게 빌드하여 프로젝트에 플러그인으로 포함이 가능하다(C++에서만 가능!!).
- 차이(diff) 확인과 병합의 용이성: C++은 차이 분석이나 병합이 쉬워 버전 관리에서 활용하기 좋다.
사실 그래서 거의 모든 콘텐츠를 구현할 때 C++와 블루프린트 중 어떤 걸 사용할 지 고민해야 하는데, 이때 고려할 사항은 이렇다.
- 성능이 중요한 부분인가?
- 기능이 고수준(High level)과 저수준(Low level) 중 어느 쪽에 가깝나? 프로젝트 전체 디자인엔 뭐가 맞을까?
- 유지보수 측면에서 뭐가 유리할까?
- 프로젝트 관리 측면, 일정 측면, 예산 측면, 팀 구성 면 등에서 어떤 것이 실용적일까?
취미 수준에선 원하는 걸 써도 상관없다. 그렇지만 프로젝트를 본격적으로 진행할 땐, 이러한 점들을 잘 고려하며 진행해야 한다.
그리고 이런 것들을 고려하려면, 둘 다 한 번씩은 써보는 편이 좋다고 한다. 두 쪽을 다 써봐야, 어디에서 어떤 걸 쓸지 감이 더 잘 잡히기도 하고, C++ 개발자라면 블루프린트를 만져보면서 어떤 기능들이 있는지 훨씬 빨리 파악할 수 있을 것이다.
'Unreal Engine 5' 카테고리의 다른 글
| 언리얼 엔진 - 향상된 입력 시스템(Enhanced Input System) (0) | 2025.12.04 |
|---|---|
| 언리얼 엔진 5에서 잘못 지은 클래스명 변경하기 (0) | 2025.11.28 |
| UE5 머티리얼 인스턴스(Material Instance) (0) | 2025.04.11 |
| UE5 언리얼 엔진에서 Asset란? check와 ensure의 차이점 (4) | 2024.07.13 |
| GetWorld와 GetLevel은 어떻게 구현되어 있을까? (0) | 2024.04.25 |
댓글