2025/07/25 TIL

민트맛치킨·2025년 7월 25일

Unreal

목록 보기
22/26

디자인 패턴

1. 생성 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 단 하나만 존재하도록 보장, 어디서나 접근 가능
#include "CoreMinimal.h"

// 프로젝트 전체에서 단 하나의 인스턴스만 존재하는 클래스
class FMyGameSingleton
{
private:
    // 정적 유일 인스턴스 포인터
    static TUniquePtr<FMyGameSingleton> Instance;

    // 생성자를 private으로: 외부에서 직접 생성(new) 금지
    FMyGameSingleton()
    {
        UE_LOG(LogTemp, Warning, TEXT("MyGameSingleton created!"));
    }
public:
    // 싱글톤 인스턴스 반환(없으면 새로 만듬, 항상 같은 객체 반환)
    static FMyGameSingleton& Get()
    {
        if (!Instance)
        {
            Instance = MakeUnique<FMyGameSingleton>();
        }
        return *Instance;
    }
    // 필요시 동작 확인용 소멸자
    ~FMyGameSingleton()
    {
        UE_LOG(LogTemp, Warning, TEXT("MyGameSingleton destroyed!"));
    }
    // 예제 함수: 싱글톤 통합 기능
    void DoSomething()
    {
        UE_LOG(LogTemp, Warning, TEXT("Singleton doing something."));
    }
};

// 정적 인스턴스 초기화(필수)
TUniquePtr<FMyGameSingleton> FMyGameSingleton::Instance = nullptr;

// 사용 예시 - 어디서든 Get()으로 전역 유일 객체 호출
void ExampleUsage()
{
    FMyGameSingleton::Get().DoSomething();
}

팩토리 패턴

  • 객체 생성을 별도의 메서드로 캡슐화하여 생성 로직을 한 곳에서 관리
#include "CoreMinimal.h"

// 무기 기본 클래스(인터페이스 역할)
class Weapon
{
public:
    virtual FString GetWeaponName() const = 0;
    virtual ~Weapon() {}
};

// 구체 무기 클래스: 검
class Sword : public Weapon
{
public:
    virtual FString GetWeaponName() const override
    {
        return TEXT("Sword");
    }
};

// 구체 무기 클래스: 활
class Bow : public Weapon
{
public:
    virtual FString GetWeaponName() const override
    {
        return TEXT("Bow");
    }
};

// 생성할 무기 종류 구분용 enum
enum class EWeaponType
{
    Sword,
    Bow
};

// 팩토리 클래스로 무기 객체 생성을 캡슐화
class WeaponFactory
{
public:
    // 무기 타입에 따라 다양한 클래스 생성
    static TUniquePtr<Weapon> CreateWeapon(EWeaponType WeaponType)
    {
        switch (WeaponType)
        {
        case EWeaponType::Sword:
            return MakeUnique<Sword>(); // 검 무기 생성
        case EWeaponType::Bow:
            return MakeUnique<Bow>();   // 활 무기 생성
        default:
            return nullptr;
        }
    }
};

// 사용 예시 - 타입만 넘기면 생성 로직 한 곳에서 관리
void FactoryPatternExample()
{
    TUniquePtr<Weapon> MySword = WeaponFactory::CreateWeapon(EWeaponType::Sword);
    if (MySword)
    {
        UE_LOG(LogTemp, Warning, TEXT("Created: %s"), *MySword->GetWeaponName());
    }
}

빌더 패턴

  • 복잡한 객체를 단계별로 생성할 수 있게 함
  • 생성 과정과 표현을 분리하여 동일한 생성 절차에서 다른 표현 만들 수 있음
#include "CoreMinimal.h"

// 결과 데이터: 캐릭터 속성 정보(이름, 무기, 방어구)
struct FCharacterData
{
    FString Name;
    FString Weapon;
    FString Armor;
};

// 빌더용 추상 인터페이스: 각 과정을 분리하여 구현을 유연하게 함
class CharacterBuilder
{
public:
    virtual ~CharacterBuilder() {}
    virtual void SetName(const FString& InName) = 0;
    virtual void SetWeapon(const FString& InWeapon) = 0;
    virtual void SetArmor(const FString& InArmor) = 0;
    virtual FCharacterData GetResult() const = 0;
};

// 실제 빌더 구현: 내부적으로 조립 단계별 저장
class WarriorBuilder : public CharacterBuilder
{
private:
    FCharacterData Data;
public:
    // 이름 지정
    virtual void SetName(const FString& InName) override
    {
        Data.Name = InName;
    }
    // 무기 지정
    virtual void SetWeapon(const FString& InWeapon) override
    {
        Data.Weapon = InWeapon;
    }
    // 방어구 지정
    virtual void SetArmor(const FString& InArmor) override
    {
        Data.Armor = InArmor;
    }
    // 완성된 구조체 반환
    virtual FCharacterData GetResult() const override
    {
        return Data;
    }
};

// Director: 조립 순서와 절차 고정, 다양한 타입 생성 가능
class CharacterDirector
{
public:
    // 기사 캐릭터 빌드 절차
    void ConstructKnight(CharacterBuilder& Builder)
    {
        Builder.SetName(TEXT("Brave Knight"));
        Builder.SetWeapon(TEXT("Sword"));
        Builder.SetArmor(TEXT("Plate Armor"));
    }

    // 궁수 캐릭터 빌드 절차
    void ConstructArcher(CharacterBuilder& Builder)
    {
        Builder.SetName(TEXT("Swift Archer"));
        Builder.SetWeapon(TEXT("Bow"));
        Builder.SetArmor(TEXT("Leather Armor"));
    }
};

// 사용 예시 - 빌더와 디렉터 조합으로 다양한 조립 결과 얻기
void BuilderPatternExample()
{
    WarriorBuilder Builder;
    CharacterDirector Director;
    // 기사 캐릭터 생성
    Director.ConstructKnight(Builder);
    FCharacterData KnightData = Builder.GetResult();
    UE_LOG(LogTemp, Warning, TEXT("Name:%s Weapon:%s Armor:%s"), *KnightData.Name, *KnightData.Weapon, *KnightData.Armor);
    // 궁수 캐릭터 생성
    Director.ConstructArcher(Builder);
    FCharacterData ArcherData = Builder.GetResult();
    UE_LOG(LogTemp, Warning, TEXT("Name:%s Weapon:%s Armor:%s"), *ArcherData.Name, *ArcherData.Weapon, *ArcherData.Armor);
}
패턴언제 쓰기 좋은가언제 쓰지 않는 게 좋은가
싱글톤- 전역에서 한 번만 생성, 모두가 동일 자원 필요할 때
- 설정/매니저/환경 등 단일 관리 객체
- 테스트 코드에서 전역 의존성 문제
- 멀티플레이 등에서 동시 사용자별 분리 필요할 때
팩토리- 다양한 타입의 객체를 상황에 따라 쉽게 생성해야 할 때
- 생성 로직이 복잡하거나 반복될 때
- 생성할 객체 종류가 거의 바뀌지 않는 경우
- 너무 단순 객체일 땐 오히려 코드가 불필요하게 복잡해짐
빌더- 복잡한 초기화(단계별 조립, 옵션 다양) 필요 객체
- 동일한 생성 절차로 여러 변형 필요할 때
- 객체 구조가 매우 단순, 필수설정만 있으면 되는 경우
- 불필요한 클래스/코드 증가가 부담이 될 때

  • 싱글톤

    • 예시: 게임 글로벌 세팅, 오디오 매니저, 세션 관리
    • 주의: 전역상태/의존성 증가, 테스트 어려움
  • 팩토리

    • 예시: 무기, 몬스터, UI 위젯 등 다형성 있는 객체 일괄 생성
    • 주의: 오히려 단순 객체에 사용하면 오버엔지니어링
  • 빌더

    • 예시: 커스텀 캐릭터, 복잡한 아이템/퀘스트, 여러 파츠를 조립해야 할 때
    • 주의: 빌더가 불필요하게 많아지거나, 개체 구조가 단순하면 비효율

2. 구조 패턴

어댑터 패턴

  • 호환되지 않는 인터페이스를 가진 클래스들을 함께 작동할 수 있도록 변환
#include "CoreMinimal.h"

class LegacyMoveSystem
{
public:
    // 레거시 이동 시스템(인터페이스 다름)
    void MoveToPosition(const FVector& Dest)
    {
        UE_LOG(LogTemp, Warning, TEXT("Legacy 시스템: 위치 이동 (%s)"), *Dest.ToString());
    }
};

class IMover
{
public:
    virtual ~IMover() {}
    // 새 표준화된 이동 인터페이스
    virtual void Move(const FVector& Dest) = 0;
};

class LegacyMoveAdapter : public IMover
{
private:
    LegacyMoveSystem* Legacy = nullptr;
public:
    // 레거시 시스템을 외부에서 주입
    LegacyMoveAdapter(LegacyMoveSystem* InLegacy) : Legacy(InLegacy) {}
    // 표준 인터페이스 호출 → 레거시 방식으로 내부 변환 호출
    virtual void Move(const FVector& Dest) override
    {
        if (Legacy)
        {
            Legacy->MoveToPosition(Dest);
        }
    }
};

void AdapterPatternExample()
{
    LegacyMoveSystem* OldSystem = new LegacyMoveSystem();
    IMover* Mover = new LegacyMoveAdapter(OldSystem);
    Mover->Move(FVector(100, 50, 0)); // 표준 인터페이스로 사용
    delete Mover;
    delete OldSystem;
}

데코레이터 패턴

  • 객체에 동적으로 새로운 기능을 추가할 수 있게 하는 패턴
  • 기능 확장이 필요할 때 서브클래싱 대신 사용함
#include "CoreMinimal.h"

class AttackComponent
{
public:
    virtual ~AttackComponent() {}
    // 공격 실행 인터페이스
    virtual void Attack() = 0;
};

class BasicAttack : public AttackComponent
{
public:
    // 기본 공격만 실행
    virtual void Attack() override
    {
        UE_LOG(LogTemp, Warning, TEXT("기본 공격"));
    }
};

class AttackDecorator : public AttackComponent
{
protected:
    AttackComponent* Wrapped; // 내부에 기존 기능을 보관
public:
    AttackDecorator(AttackComponent* InWrapped) : Wrapped(InWrapped) {}
    // 원본 기능 실행(덧붙일 데코용)
    virtual void Attack() override
    {
        if(Wrapped) Wrapped->Attack();
    }
};

class FireAttackDecorator : public AttackDecorator
{
public:
    FireAttackDecorator(AttackComponent* InWrapped) : AttackDecorator(InWrapped) {}
    // 기본 공격 후 화염 효과 추가
    virtual void Attack() override
    {
        AttackDecorator::Attack();
        UE_LOG(LogTemp, Warning, TEXT("화염 추가 공격"));
    }
};

class IceAttackDecorator : public AttackDecorator
{
public:
    IceAttackDecorator(AttackComponent* InWrapped) : AttackDecorator(InWrapped) {}
    // 기본 공격 후 빙결 효과 추가
    virtual void Attack() override
    {
        AttackDecorator::Attack();
        UE_LOG(LogTemp, Warning, TEXT("빙결 추가 공격"));
    }
};

void DecoratorPatternExample()
{
    // 기본 공격에 불, 빙결 효과를 동적으로 덧붙임
    AttackComponent* Attack = new BasicAttack();
    Attack = new FireAttackDecorator(Attack);  // 불 효과 추가
    Attack = new IceAttackDecorator(Attack);   // 빙결 효과 추가
    Attack->Attack(); // 기본 → 불 → 빙결 순 호출됨
    delete Attack;
}

퍼사드 패턴

  • 복잡한 서브시스템들을 하나의 간단한 인터페이스로 통합하여 제공하는 패턴
  • 복잡함을 숨기고 사용하기 쉽게 만들 수 있음
#include "CoreMinimal.h"

class SoundSystem
{
public:
    void PlayAttackSound()
    {
        UE_LOG(LogTemp, Warning, TEXT("공격 사운드 재생"));
    }
};

class ParticleSystem
{
public:
    void PlayAttackEffect()
    {
        UE_LOG(LogTemp, Warning, TEXT("공격 파티클 재생"));
    }
};

class CameraSystem
{
public:
    void PlayShake()
    {
        UE_LOG(LogTemp, Warning, TEXT("카메라 흔들림 효과"));
    }
};

class AttackEffectFacade
{
private:
    SoundSystem Sound;
    ParticleSystem Particle;
    CameraSystem Camera;
public:
    // 하나의 함수로 여러 효과 시스템을 통합 실행
    void PlayAllAttackEffects()
    {
        Sound.PlayAttackSound();
        Particle.PlayAttackEffect();
        Camera.PlayShake();
    }
};

void FacadePatternExample()
{
    AttackEffectFacade Facade;
    Facade.PlayAllAttackEffects(); // 한 줄로 여러 효과
}
패턴언제 쓰기 좋은가언제 쓰지 않는 게 좋은가
어댑터- 외부 라이브러리, 레거시 코드 등 기존 인터페이스와 호환이 안 될 때
- 표준화된 내부 코드와 외부 시스템 연결
- 인터페이스가 이미 일치하는 경우
- 어댑터 계층이 남발되면 관리 어려움
데코레이터- 객체에 동적으로 여러 기능/옵션을 조합하고 싶을 때
- 기능 확장이 잦고, 조립식 추가가 필요한 시스템
- 기능 분기가 거의 없는 단순 객체
- 데코레이터 중첩으로 성능/관리 복잡할 때
퍼사드- 여러 하위 시스템의 복잡한 구현을 간단한 인터페이스로 제공할 때
- 팀 협업, 블루프린트 등에서 통합제어 필요
- 하위 시스템 자체가 충분히 단순할 때
- 퍼사드에 모든 책임이 집중되는 경우

  • 어댑터

    • 예시: 외부 라이브러리의 좌표 이동 함수(LegacyMoveSystem::MoveToPosition)를 게임 표준 인터페이스(IMover::Move)로 감싸서 통합 사용
    • 주의: 어댑터 계층이 과도하면 코드 추적과 디버깅이 복잡, 불필요한 추상화 지양
  • 데코레이터

    • 예시: 무기 공격에 불‧빙결 등 여러 효과를 런타임에 조합해서 적용, 능동적으로 옵션을 덧붙여야 할 때 활용
    • 주의: 중첩 데코레이터 남발 시 성능 저하와 관리 난이도 증가, 메모리/참조 관리 주의
  • 퍼사드

    • 예시: 사운드, 파티클, 카메라 진동 등 다양한 시스템의 "공격효과"를 AttackEffectFacade로 통합, 한 번의 함수 호출로 전체 제어
    • 주의: 퍼사드가 지나치게 커지면 단일 클래스에 모든 변경점이 집중, 작은 하위 시스템 직접 제어가 필요하면 불편

0개의 댓글