언리얼 엔진 5

[UE5] 언리얼 인터페이스(UInterface)

언린이 2026. 3. 15. 17:21
반응형

1. 인터페이스가 필요한 이유

게임 오브젝트 간의 상호작용을 구현할 때 가장 흔히 쓰는 방법은 캐스팅(Cast)입니다. 플레이어 캐릭터가 어떤 오브젝트를 향해 상호작용 키를 누를 때, 대상이 ADoor인지 AChest인지 ANpc인지 확인한 뒤 각각에 맞는 캐스팅을 수행하는 식입니다.

이 방식은 당장은 동작하지만 오브젝트 종류가 늘어날수록 조건 분기가 함께 늘어납니다. 호출 측 코드가 모든 피호출 타입을 직접 알고 있어야 하기 때문에, 두 클래스 사이에 강한 결합(Strong Coupling)이 생깁니다. 새로운 상호작용 가능 오브젝트를 추가할 때마다 호출 측 코드를 수정해야 하는 것이 대표적인 문제입니다.

UInterface는 이 문제를 해결하기 위한 언리얼 엔진의 인터페이스 시스템입니다. 호출자는 대상 오브젝트의 구체적인 타입을 몰라도, "이 오브젝트가 IInteractable 인터페이스를 구현했는가?"만 확인하고 함수를 호출할 수 있습니다. 더불어 블루프린트 클래스도 동일한 인터페이스를 구현하고 C++ 코드로부터 호출받을 수 있어, C++과 블루프린트를 동시에 지원하는 확장성 있는 아키텍처를 구성하는 데 매우 적합합니다.

2. 두 클래스 구조 — U클래스와 I클래스

UInterface를 처음 접하면 하나의 인터페이스를 선언할 때 왜 두 개의 클래스가 필요한지 의아할 수 있습니다. 이 구조를 이해하는 것이 UInterface 전체를 이해하는 출발점입니다.

에디터에서 Unreal Interface 템플릿으로 파일을 생성하면 아래와 같은 헤더가 자동으로 만들어집니다.

IInteractable.h 파일을 생성하면 다음과 같은 두 클래스가 선언됩니다.

// IInteractable.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "IInteractable.generated.h"

// 리플렉션 시스템을 위한 U-클래스. 내용을 수정하지 않습니다.
UINTERFACE(MinimalAPI, BlueprintType)
class UInteractable : public UInterface
{
    GENERATED_BODY()
};

// 실제 인터페이스 함수가 선언되는 I-클래스
class MYGAME_API IInteractable
{
    GENERATED_BODY()

public:
    // 인터페이스 함수는 이 클래스에 선언합니다.
};

위 코드처럼 두 클래스의 역할은 완전히 분리되어 있습니다.

UInteractableUObject의 리플렉션 시스템에 이 인터페이스의 존재를 등록하는 역할만 수행합니다. 이 클래스는 수정할 필요가 없으며, 함수도 선언하지 않습니다. UINTERFACE() 매크로와 UInterface 상속이 리플렉션 시스템에 필요한 메타데이터를 제공합니다.

IInteractable은 실제 인터페이스 함수가 선언되는 클래스입니다. 다른 클래스가 인터페이스를 구현할 때 상속받는 것은 이 I-클래스입니다. 이름 규칙은 U-클래스의 U 접두사를 I로 교체하는 것으로 고정되어 있으며, 언리얼 엔진이 이 규칙을 전제로 코드를 생성합니다.

UINTERFACE() 매크로에서 자주 사용되는 지정자는 다음과 같습니다.

지정자 설명
MinimalAPI 최소한의 심볼만 익스포트. 일반적으로 권장됨
BlueprintType 블루프린트에서 이 인터페이스 타입을 변수로 사용 가능하게 함
Blueprintable 블루프린트 클래스가 이 인터페이스를 구현할 수 있게 함 (UInterface는 기본적으로 Blueprintable)

 

3. 인터페이스 함수 선언 — BlueprintNativeEvent vs BlueprintImplementableEvent

인터페이스 함수를 선언할 때 UFUNCTION 지정자의 선택이 블루프린트 연동 방식을 결정합니다. 두 가지 핵심 지정자를 명확히 구분해야 합니다.

다음은 두 지정자를 모두 활용한 인터페이스 선언 예시입니다.

// IInteractable.h — I-클래스 내부
class MYGAME_API IInteractable
{
    GENERATED_BODY()

public:
    // C++ 기본 구현이 있고, 블루프린트에서 재정의 가능
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interaction")
    void OnInteract(AActor* Interactor);

    // C++ 기본 구현 없이, 블루프린트에서만 구현
    UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "Interaction")
    FText GetInteractionHint();
};

위 코드에서 두 지정자의 차이가 드러납니다.

BlueprintNativeEvent는 C++에서 _Implementation 접미사를 붙인 함수로 기본 동작을 정의하고, 블루프린트에서 선택적으로 재정의할 수 있습니다. C++과 블루프린트 모두에서 구현 가능한 가장 범용적인 방식입니다.

BlueprintImplementableEvent는 C++에서 기본 구현을 작성하지 않으며, 블루프린트에서만 구현됩니다. 블루프린트 전담 구현이 필요한 함수에 사용합니다. 이 지정자를 사용한 함수를 C++ 클래스에서 구현하려 하면 컴파일 오류가 발생합니다.

4. 인터페이스 구현 — C++ 클래스에 적용하기

인터페이스를 구현하는 C++ 클래스는 I-클래스를 다중 상속받고, BlueprintNativeEvent로 선언된 함수의 경우 _Implementation 접미사 버전을 override합니다.

다음은 AChest 액터가 IInteractable을 구현하는 예시입니다.

// AChest.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "IInteractable.h"      // I-클래스 헤더 포함
#include "AChest.generated.h"

UCLASS()
class MYGAME_API AChest : public AActor, public IInteractable
{
    GENERATED_BODY()

public:
    // BlueprintNativeEvent 함수의 C++ 구현은 _Implementation으로 선언
    virtual void OnInteract_Implementation(AActor* Interactor) override;
};
// AChest.cpp
#include "AChest.h"
#include "Engine/Engine.h"

void AChest::OnInteract_Implementation(AActor* Interactor)
{
    // 상자 열기 로직
    GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Yellow, TEXT("보물 상자를 열었습니다!"));
}

위 코드처럼 헤더에서는 virtual void OnInteract_Implementation(...) override로 선언하고, cpp에서 실제 동작을 구현합니다. 원래 인터페이스에 선언된 OnInteract()는 직접 구현하지 않으며, 언리얼 엔진의 코드 생성기가 이 함수를 자동으로 처리합니다.

5. Execute_ 함수 호출 방식 — 블루프린트 대응의 핵심

UInterface의 가장 중요한 특징 중 하나는 Execute_ 접두사를 붙인 정적 함수 호출 방식입니다. 이 방식을 반드시 사용해야 하는 이유를 이해하는 것이 실무에서 가장 중요합니다.

문제의 핵심은 다음과 같습니다. 인터페이스를 C++로만 구현한 클래스라면 일반 포인터 캐스팅(Cast<IInteractable>)으로 함수를 호출할 수 있습니다. 하지만 블루프린트 클래스가 인터페이스를 구현한 경우, C++ 캐스팅이 항상 nullptr을 반환합니다. 블루프린트 클래스의 인터페이스 구현은 C++ vtable이 아닌 블루프린트 VM에 존재하기 때문입니다.

Execute_ 함수는 이 두 가지 경우를 모두 처리합니다. C++ 구현이면 C++ 함수를 호출하고, 블루프린트 구현이면 블루프린트 이벤트 그래프를 실행합니다.

다음은 플레이어 캐릭터가 바라보는 대상에게 상호작용을 트리거하는 코드입니다.

// APlayerCharacter.cpp
void APlayerCharacter::TryInteract()
{
    AActor* TargetActor = GetActorInFocus(); // 레이캐스트로 대상 획득

    if (!TargetActor)
    {
        return;
    }

    // C++/블루프린트 구현 모두를 올바르게 감지하는 방법
    if (TargetActor->Implements<UInteractable>())
    {
        // Execute_ 호출: C++ 구현과 블루프린트 구현 모두 동작
        IInteractable::Execute_OnInteract(TargetActor, this);
    }
}

위 코드에서 두 가지 핵심 패턴을 확인할 수 있습니다.

첫째, 인터페이스 구현 여부 확인은 Implements<U클래스>() 함수를 사용합니다. Cast<I클래스>() 대신 이 방법을 사용해야 블루프린트 클래스도 올바르게 감지됩니다.

둘째, 함수 호출은 I클래스::Execute_함수명(대상객체, 인자...) 형식의 정적 함수로 수행합니다. 첫 번째 인자로 반드시 인터페이스를 구현한 UObject*를 전달해야 합니다.

아래 표는 호출 방식별 동작 차이를 정리한 것입니다.

호출 방식 C++ 구현 블루프린트 구현
Cast<IInteractable>(Actor)->OnInteract(...) 동작함 nullptr 반환, 크래시 위험
IInteractable::Execute_OnInteract(Actor, ...) 동작함 동작함
Actor->Implements<UInteractable>() 감지됨 감지됨

 

6. 블루프린트와의 연동

UInterface는 블루프린트에서 두 가지 방식으로 사용됩니다. 블루프린트 클래스에서 직접 인터페이스를 구현하거나, C++로 구현된 인터페이스를 블루프린트에서 호출하는 방식입니다.

6-1. 블루프린트 클래스에서 인터페이스 구현하기

블루프린트 클래스가 C++ 인터페이스를 구현하려면, 블루프린트 에디터의 Class Settings에서 Implemented Interfaces 목록에 해당 인터페이스를 추가합니다. UINTERFACE() 매크로에 BlueprintType이 지정되어 있어야 이 목록에 인터페이스가 표시됩니다.

인터페이스를 추가하면 My Blueprint 패널의 Interfaces 섹션에 해당 함수가 나타납니다. BlueprintNativeEvent로 선언된 함수는 우클릭 → Implement event를 통해 이벤트 노드로 오버라이드할 수 있습니다.

6-2. TScriptInterface를 이용한 UPROPERTY 선언

에디터에서 인터페이스 타입의 레퍼런스를 UPROPERTY로 노출하려면 TScriptInterface<I클래스> 래퍼 타입을 사용합니다.

다음은 인터페이스 레퍼런스를 에디터에서 할당할 수 있게 노출하는 예시입니다.

// 에디터에서 직접 인터페이스 구현 오브젝트를 할당할 수 있습니다.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Interaction")
TScriptInterface<IInteractable> InteractTarget;

// 코드에서 사용 시
void APlayerCharacter::TryInteractWithTarget()
{
    if (InteractTarget)
    {
        // TScriptInterface는 내부적으로 UObject*와 IInteractable*을 모두 보관합니다.
        IInteractable::Execute_OnInteract(InteractTarget.GetObject(), this);
    }
}

위 코드처럼 TScriptInterface는 내부에 UObject*와 I-클래스 포인터를 함께 보관하며, GC(Garbage Collection) 시스템이 레퍼런스를 올바르게 추적할 수 있게 합니다.

6-3. GetAllActorsWithInterface를 이용한 일괄 조회

특정 인터페이스를 구현한 모든 액터를 월드에서 수집할 때는 UGameplayStatics::GetAllActorsWithInterface를 사용합니다.

다음은 월드의 모든 상호작용 가능 오브젝트를 수집하여 순회하는 예시입니다.

#include "Kismet/GameplayStatics.h"

void AGameManager::ActivateAllInteractables()
{
    TArray<AActor*> InteractableActors;

    // U-클래스를 StaticClass()로 전달하여 인터페이스 구현 액터 수집
    UGameplayStatics::GetAllActorsWithInterface(
        GetWorld(),
        UInteractable::StaticClass(), // 반드시 I-클래스가 아닌 U-클래스를 사용
        InteractableActors
    );

    for (AActor* Actor : InteractableActors)
    {
        IInteractable::Execute_OnInteract(Actor, this);
    }
}

위 코드에서 GetAllActorsWithInterface의 두 번째 인자에는 I-클래스(IInteractable)가 아닌 U-클래스(UInteractable)의 StaticClass()를 전달해야 합니다. I-클래스는 UObject가 아니기 때문에 StaticClass() 함수를 갖지 않습니다.

7. 일반 C++과의 비교

일반 C++ 인터페이스와 UInterface는 다중 상속이라는 공통 기반을 공유하지만, 동작 방식에 결정적인 차이가 있습니다.

순수 C++ 방식에서는 인터페이스 구현 확인을 dynamic_cast로 수행하고, 함수 호출은 vtable을 통한 가상 함수 디스패치로 이루어집니다.

// 순수 C++ 인터페이스 방식
class IInteractable
{
public:
    virtual void OnInteract(Actor* Interactor) = 0; // 순수 가상 함수
    virtual ~IInteractable() = default;
};

// 호출 측
IInteractable* Target = dynamic_cast<IInteractable*>(SomeObject);
if (Target)
{
    Target->OnInteract(Player); // vtable 통한 직접 호출
}

이 방식은 C++ 클래스끼리만 동작합니다. 블루프린트 VM에서 구현된 함수는 C++ vtable에 등록되지 않으므로 이 방법으로는 호출할 수 없습니다.

UInterface는 리플렉션 시스템을 통해 이 한계를 극복합니다.

// UInterface 방식 — C++과 블루프린트 구현 모두 처리
if (SomeActor->Implements<UInteractable>())
{
    // 리플렉션 기반 디스패치: C++ vtable 또는 블루프린트 VM으로 자동 라우팅
    IInteractable::Execute_OnInteract(SomeActor, Player);
}

또한 순수 C++ 인터페이스를 GC 관리 객체(UPROPERTY)로 저장하려면 별도의 처리가 필요하지만, TScriptInterface<> 래퍼를 사용하면 GC 연동이 자동으로 처리됩니다. 순수 가상 함수로 구현을 강제하는 C++ 방식과 달리, UInterface는 구현이 선택적이므로 유연성이 높지만 구현 누락에 대한 컴파일 타임 보장이 없다는 트레이드오프가 존재합니다.

8. 유니티 엔진과의 비교

유니티에서 인터페이스와 가장 유사한 기능은 C# 인터페이스(interface 키워드)입니다. 유니티에서는 MonoBehaviour를 포함한 모든 C# 클래스가 일반 인터페이스를 자연스럽게 구현할 수 있으며, 구현 여부 확인은 GetComponent<IInteractable>() 또는 is 키워드로 수행합니다.

// 유니티 C# 방식
IInteractable target = hitObject.GetComponent<IInteractable>();
if (target != null)
{
    target.OnInteract(player); // 직접 가상 함수 호출
}

언리얼 UInterface와 유니티 C# 인터페이스의 핵심 차이점은 다음 세 가지입니다.

이중 클래스 구조: 유니티는 단일 interface 타입만 선언하면 됩니다. 언리얼은 리플렉션 등록을 위한 U-클래스와 실제 함수 선언을 위한 I-클래스가 항상 쌍으로 필요합니다.

블루프린트 연동: 유니티의 C# 인터페이스는 스크립트 레이어에만 존재합니다. 언리얼 UInterface는 리플렉션 시스템을 통해 블루프린트 VM과 연결되므로, C++로 작성된 인터페이스를 블루프린트 클래스에서 구현하고 C++ 코드에서 호출하는 혼합 구조가 가능합니다.

호출 방식: 유니티는 인터페이스 레퍼런스를 통해 직접 함수를 호출합니다. 언리얼은 C++/블루프린트 구현을 모두 처리하려면 반드시 Execute_ 정적 함수를 통해야 합니다.

9. 주의사항

9-1. 순수 가상 함수 강제화 방법

UInterface는 기본적으로 구현을 강제하지 않습니다. C++ 구현을 반드시 요구하려면 _Implementation 함수를 헤더에서 순수 가상으로 선언합니다.

// I-클래스 내부에서 순수 가상으로 선언하여 구현 강제
class MYGAME_API IInteractable
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interaction")
    void OnInteract(AActor* Interactor);

    // 이 방식으로 C++ 구현을 강제할 수 있습니다.
    // 단, 블루프린트 클래스에는 적용되지 않음에 주의하셔야 합니다.
    virtual void OnInteract_Implementation(AActor* Interactor) = 0;
};

위 코드처럼 순수 가상 선언은 C++ 클래스에 대해서만 컴파일 타임 강제화가 적용되며, 블루프린트 클래스에는 적용되지 않습니다.

9-2. Cast vs Implements 혼용 금지

Cast<IInteractable>(Actor)는 해당 Actor가 C++에서 IInteractable을 상속한 경우에만 성공합니다. 블루프린트 클래스가 인터페이스를 구현한 경우에는 항상 nullptr을 반환하므로, 코드베이스에서 Implements<UInteractable>()Execute_ 패턴을 일관성 있게 사용하는 것이 중요합니다.

9-3. GetAllActorsWithInterface의 성능 비용

이 함수는 월드의 모든 액터를 순회합니다. 매 프레임 호출하면 성능 문제가 발생할 수 있으므로, 반드시 캐싱하거나 이벤트 기반으로 사용하셔야 합니다.

9-4. BlueprintImplementableEvent와 C++ 공존 불가

BlueprintImplementableEvent로 선언된 함수에 C++ _Implementation 구현을 작성하면 컴파일 오류가 발생합니다. C++ 기본 구현이 필요하면 반드시 BlueprintNativeEvent를 사용해야 합니다.

9-5. TScriptInterface와 Raw 포인터 혼용 주의

IInteractable* 형태의 raw 포인터를 UPROPERTY로 저장하면 GC 시스템이 추적하지 못합니다. 에디터에서 할당하거나 GC 관리가 필요한 인터페이스 레퍼런스는 반드시 TScriptInterface<IInteractable>을 사용하셔야 합니다.

 

 

UInterface는 서로 다른 클래스 계층에 속한 오브젝트들이 공통된 행동 규약을 따르도록 만드는 언리얼 엔진의 핵심 설계 도구입니다. 특히 Execute_ 호출 방식과 Implements<> 확인 패턴을 일관성 있게 적용하면, C++과 블루프린트 구현을 모두 투명하게 처리하는 확장성 있는 시스템을 구축할 수 있습니다. 상호작용 시스템, 데미지 수신 인터페이스, 저장 가능 오브젝트 표식 등 다양한 게임플레이 아키텍처에서 클래스 간 결합도를 낮추는 데 적극 활용하시기 바랍니다.

반응형