개발 이야기 안하는 개발자

Good Code, Bad Code (2) _ 추상화 계층 본문

Book/Good Code, Bad Code

Good Code, Bad Code (2) _ 추상화 계층

07e 2025. 9. 4. 01:09
반응형

큰일났따 큰일났어

2장인데 벌써 머리아프다

 

머리로는 이해하는 내용들이었다.

이걸 지금 프로젝트에 적용한다고 생각하면 너무너무 큰 일이다.

일단 오늘 배운거 부터 기록해보자.

 

 

코드

문제 해결을 위해 어떻게 해결하는가도 중요하지만 해결하는 코드를 어떻게 구성하는가도 중요하다.

 

 

우리가 하는 코드 작성

복잡한 문제를 계속해서 더 작은 하위 문제로 세분화 하는 작업이 바로 코드 작성이다.

계층을 만들면서 같은 층위 내에서는 쉽게 이해할 몇 개의 개념만 다루기 때문에, 개별 코드는 복잡해 보이지 않게 한다.

 

- 상위 수준의 문제

- 알고 있어야 하는 하위 문제

- 알 필요 없는 하위 문제

 

간단하게 내가 이해한 바로 설명한다면, 만약 인벤토리를 제작해야 한다고 할때.

인벤토리를 열어서 보여준다 가 상위 문제로 둔다.

여기서 연다라는 개념은 인벤토리 위젯을 어디선가 construct 한다는 걸로 이해하면 될 것 같다.

 

알고 있어야 하는 하위 문제는 public이다.

얘를들어 인벤토리에 뭘 해야 인벤토리 내부에 아이템들이 뜨는가 로 봤을때, 당연히 연 쪽에서 데이터를 넣어줘야 하기 때문에

public Init(const TArray<int32> InItems);

정도로 있을 것이다.

 

알 필요 없는 하위 문제는 이제 Init을 통해 이루어지는 일련의 과정들, 즉 private 으로 내려가 있는 메소드들 정도로 이해하면 될것이다.
당연하게도 Init을 한다고 해서 바로 인벤토리 내부에 아이템이 뜨는게 아니다. 
내부 아이콘 위젯도 가져와야 하고, 해당 아이템들 아이디를 통해 갯수라던가 리소스 이미지라던가 가져와서 가공하는 과정들이 알 필요없는 하위 문제들로 치부하면 될 것이다.

 

 

4가지 중요성

한 번에 한두 개 정도의 계층과 몇 개의 개념만 다루면 되기 때문에 코드의 가독성이 증가한다.

세부 사항이 외부로 노출되지 않도록 보장할때, 다른 계층이나 외부에 영향을 미치지 않고 계층 내에서만 구현을 변경하기가 쉬워진다.(모듈화)

하위 계층을 보유한 상태로 새로운 문제가 들어오면다면 재사용하기 쉬워진다.

계층이 나뉘기 때문에 각 하위 문제에 대한 테스트를 하는 것이 훨씬 쉬워진다.

 

 

공개 API

public으로 구현된 부분이 공개 API 라고 부르기도 하며,

호출하는 쪽에서 공개할 개념만 정의하면 되기 때문에 코드가 제공하는 추상화 계층을 명확하게 만드는 데 도움이 된다.

 

 

함수 가독성

너무 기능이 많은 함수는 가독성이 떨어진다.

함수를 작게 만들고 수행하는 작업을 명확하게 하면 코드의 가독성과 재사용성이 높아진다.

 

 

클래스 가독성

단일 클래스에 담겨 있는 개념이 많을수록 가독성이 저하된다.

클래스 내부에 새로운 클래스(의존성 주입 패턴)를 제작해 기능을 세분화 한다.

언리얼로 따지면 Actor 내부에 있는 Component 같은 느낌이다. (MoveComponent, CameraComponent ...)

 

각 클래스의 기능을 확실하게 나누고, 공개 API를 통해 하위 문제에서 점점 위로 올라가 상위 문제를 해결하는 방식으로 해결한다.

 

 

인터페이스 가독성

계층 사이를 뚜렷이 구분하고 계층 사이에 세부 사항이 유출되지 않도록 하기 위해선 인터페이스로 정의하는 것이다.

상위 클래스가 하위 인터페이스에 의존하면서, 하위 인터페이스는 다른 클래스로 정의되는 것이다.

 

 

 

정리

이거 보면서 참 많은 걸 검색해보고, Chat GPT랑 오래 싸웠다.

언리얼에선 Single과 비슷하지만 엔진에서 관리해주는 안전한 Subsystem이란 기능이 존재한다.


책에서도 그렇고 많은 커뮤니티에서도 그렇지만 싱글톤은 가능하면 최대한 적게 사용하고 DI(의존도 주입 패턴)를 사용할 것을 권장한다.

 

항목 싱글톤 서브시스템 DI
정의 전역 단일 인스턴스, static Get() 접근 엔진 관리 단일 인스턴스, GetSubsystem() 접근 의존성 외부 주입 (생성자/Setter)
관리 주체 개발자 (초기화/파괴 수동) Unreal 엔진 (자동 Initialize/Deinitialize) 외부 코드 (수동 주입)
결합도 강한 결합 (특정 클래스 의존) 약간 결합, 인터페이스로 느슨 가능 느슨한 결합 (인터페이스 의존)
테스트 용이성 어려움 (Mock 불가) 쉬움 (인터페이스+Mock 가능) 매우 쉬움 (Mock 주입 쉬움)
Unreal 적합성 PIE, 멀티플레이어 문제 위험 엔진과 통합, 안전, 확장성 좋음 수동 작업 많음, Blueprint 복잡
용도 간단한 전역 유틸 (권장 안 함) 기능별 분리 (인벤토리, 네트워크 등) 테스트/유연성 중시 시
//싱글톤 예제

UCLASS()
class USerSingleton : public UObject {
    GENERATED_BODY()
public:
    static USerSingleton* Get() { return Instance; }
    void AddItem(int32 ItemId) {}
private:
    static USerSingleton* Instance;  // 개발자 관리
};
USerSingleton* USerSingleton::Instance = nullptr;

// 사용
void AMyActor::BeginPlay() {
    USerSingleton::Get()->AddItem(123);  // 강한 결합
}

 

//서브 시스템 예제

UCLASS()
class UInventorySubsystem : public UGameInstanceSubsystem {
    GENERATED_BODY()
public:
    void AddItem(int32 ItemId) {}
};

// 사용
void AMyActor::BeginPlay() {
    UInventorySubsystem* Sub = GetGameInstance()->GetSubsystem<UInventorySubsystem>();
    Sub->AddItem(123);  // 엔진 관리
}

 

// DI 예제

UCLASS()
class AMyActor : public AActor {
    GENERATED_BODY()
public:
    UPROPERTY() TScriptInterface<IInventory> Inventory;
    void SetInventory(TScriptInterface<IInventory> InInv) { Inventory = InInv; }
    void BeginPlay() {
        Inventory->AddItem(123);  // 외부 주입
    }
};

// 주입
AMyActor* Actor = GetWorld()->SpawnActor<AMyActor>();
Actor->SetInventory(GetGameInstance()->GetSubsystem<UInventorySubsystem>());

 

 

// 서브 시스템과 DI 혼합

UINTERFACE()
class IInventory : public UInterface {
    GENERATED_BODY()
public:
    UFUNCTION() virtual void AddItem(int32 ItemId) = 0;
};

UCLASS()
class UInventorySubsystem : public UGameInstanceSubsystem, public IInventory {
    GENERATED_BODY()
public:
    void AddItem(int32 ItemId) override {}
};

// 사용
UCLASS()
class AMyActor : public AActor {
    GENERATED_BODY()
public:
    UPROPERTY() TScriptInterface<IInventory> Inventory;
    void BeginPlay() {
        Inventory = GetGameInstance()->GetSubsystem<UInventorySubsystem>();
        Inventory->AddItem(123);  // 느슨한 결합
    }
};

 

우선 싱글톤은 관리를 개발자가 해야해서 사용 안하는걸 권장한다.

엔진에서 관리하게 Subsystem을 사용하는걸 더 권장하는 편이다. 

 

왠만하면 DI를 사용하는걸 권장하는 느낌이고 Subsystem과 혼합해서 쓰는건 내가 땡깡부려서 억지로 AI가 만들었나 싶더라.

Subsystem의 기능들을 Interface로 만드는게 좀 억지스럽다.

        UMyGameInstanceSubsystem* MySubsystem = GameInstance->GetSubsystem<UMyGameInstanceSubsystem>();
        if (MySubsystem)
        {
            MySubsystem->DoSomething();
        }

이 코드가 언리얼에서 권장하는 Subsystem 사용 법이다.

싱글톤과 비슷하게 강한 결합을 하는 느낌이지만, 어쨌든 엔진에서 해당 서브 시스템의 Life Cycle을 관리해주니까.

 

언리얼에선 DI 보단 Subsystem을 더 권장하는 느낌이 강하고, 실제로 사람들도 DI 보단 많이 쓰는것 같다.

억지로 DI 를 사용하려고 노력하기 보다는 새로운 객체를 만들고 계층을 구분해야 하는 상황이 발생한다면 그때 Interface로 변경한 다음에 사용하려고 해야겠다.

반응형