UI 종속성 문제 : Layer와 Slot의 계층 구조에서의 설계 문제

KWONYEONGMIN·2024년 10월 16일

언리얼

목록 보기
11/15

개요

ProjectXZ의 모듈러 시스템에서 UI를 구성하는 과정에서 구조적 종속성 문제가 발생했다. UXZModuleCustomizingLayer(Layer), UXZModuleSelectSlot(Slot), UXZModuleSelectSlotItem(SlotItem), FXZModuleSelectSlotInfo(SlotInfo) 클래스에서 상위와 하위 클래스 간의 불필요한 결합에서 발생하는 종속성 문제와 이를 해결한 과정에 대해서 정리하려고 한다.




문제 상황


SlotItemSlotInfo 구조체와 함께 Slot의 데이터를 초기화한다.
SlotItemListView에 들어가는 데이터 항목을 나타내며, SlotInfo는 해당 항목에 대한 구체적인 모듈 정보를 담고 있고, Layer의 UMG에서 데이터가 설정되기 때문에 Layer에 선언되어 있다.
(SlotItem은 Slot에 SlotInfo는 Layer에 선언되어 있음)
하지만 Slot에서 SlotItem을 사용해 데이터를 초기화하는 과정에서 SlotItem이 SlotInfo를 멤버 변수로 가지고 있기 때문에, 간접적으로 Layer에 의존하게 된다.
결국 SlotItemSlot의 데이터를 관리하는 역할을 하지만, Layer에 필요한 SlotInfo를 직접 참조하는 구조 때문에, SlotLayer 사이의 결합도가 높아진다.
이러한 구조는 순환 참조 문제가 있고 추후에 SlotLayer가 독립적으로 확장되거나 변경될 때 문제가 발생할 가능성이 있다.

정리하자면
1. Layer는 ListView로 Slot을 관리
2. SlotItem은 Slot의 데이터를 초기화하기 때문에 Slot에 선언
3. SlotInfo는 Layer에서 Slot에 초기화할 데이터를 넣어주기 때문에 Layer에서 선언
4. 이때 SlotItem이 SlotInfo를 멤버 변수로 가지게 됨 ⇒ 문제의 시작
5. Layer는 상위의 개념이기 때문에 Slot, SlotItem을 알아도 상관 없지만, Slot은 Layer의 하위 개념이기 때문에 Layer에 대한 정보를 알면 안됨


왜 종속성이 문제가 되느냐 ?

  • 코드의 재사용성이 저하된다.
    • LayerSlot 간의 결합도가 높아지면 Slot을 다른 시스템에서 사용할 수 없게 된다.
  • 계층적 설계 위반
    • 하위 클래스는 상위 클래스에 대해 의존성이 없어야 한다.
    • 하위 클래스에서 상위 개념의 요소를 직접 참조하면 계층 구조가 뒤틀리면서 설계가 복잡해지고 유지보수 비용이 증가한다
  • 순환 참조 위험
    • 상위 클래스와 하위 클래스가 서로 의존하는 경우, 순환 참조가 발생할 가능성이 크다.



해결 방법

이전 코드

class PROJECTXZ_API UXZModuleSelectSlotItem : public UObject
{
    GENERATED_BODY()
    
public:
   ...

public:
    UXZModuleSelectSlotItem() {  }
    void InitializeData(FXZModuleSelectSlotInfo NewSlotInfo);
    
     FORCEINLINE const FXZModuleSelectSlotInfo GetSlotInfo() const { return SlotInfo; }
     ...

private:
    void CalculateItemIndexRange();
    
    FXZModuleSelectSlotInfo SlotInfo;
	  ...
};

수정된 코드

class PROJECTXZ_API UXZModuleSelectSlotItem : public UObject
{
    GENERATED_BODY()
    
public:
  ...

public:
    UXZModuleSelectSlotItem() {  }
    void InitializeData(EModularMeshType NewModuleType);
    
    FORCEINLINE const FModuleIndexInfo GetIndexInfo() const { return IndexInfo; }
    FORCEINLINE const EModularMeshType GetModuleType() const { return ModuleType; }

private:
    ...

    void CalculateItemIndexRange();
     ...
    EModularMeshType ModuleType;
};
  • 단순하게 SlotItem에서 SlotInfo를 멤버 변수로 가지고 있지 않도록 하였다.
  • SlotInfo 자체를 멤버 변수로 가지고 있지 않고, 가지고 있는 데이터를 기본형 데이터로 만들어 주었다. 그 과정에서 필요 없는 멤버 변수(MaterialInstance)를 삭제하고 코드를 정리하였다 !
void UXZModuleCustomizingLayer::NativeConstruct()
{
	Super::NativeConstruct();

	const int ModuleNum = SelectSlotsInfo.Num();
	TArray< UXZModuleSelectSlotItem*>  SlotsArray;

	for ( int i = 0; i < ModuleNum; ++i )
	{
		UXZModuleSelectSlotItem* SlotItem = NewObject<UXZModuleSelectSlotItem>(this);
		SlotItem->InitializeData(SelectSlotsInfo[i].ModuleType);
		SlotsArray.Add(SlotItem);
	}

	SlotListView->SetListItems(SlotsArray);
}
  • 필요한 데이터는 ModuleType 밖에 없었다 !! ㅎㅎ
  • 그리고 SloItem에 SlotInfo 데이터를 Initailize해주었다.



느낀점

다음부터는 초기 설계 단계에서 이런 계층구조간의 종속성 문제를 미리 생각하여 설계 해야겠다. 지금은 멤버 변수로 포함시키지 않음으로써 간단하게 해결됐지만, 훨씬 복잡한 시스템을 구현해야할 경우,
클래스 간의 결합도를 낮추기 위해 인터페이스를 통해 상위와 하위 간의 의존성을 최소화해야 할 것 같다

profile
Hello World

0개의 댓글