디자인 패턴
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로 통합, 한 번의 함수 호출로 전체 제어
- 주의: 퍼사드가 지나치게 커지면 단일 클래스에 모든 변경점이 집중, 작은 하위 시스템 직접 제어가 필요하면 불편