[TIL/12] 원칙과 현실 사이, 허술함과 유연함

안건우·2025년 9월 26일

sparta_til

목록 보기
11/26

대화기능을 먼저 작성하여 스토리작업을 함께 진행하고 싶은 욕심에 GameScene의 구현을 먼저 시작했더니 코드가 꼬이기 시작했다. GameLifetimeScopeGameInitializer에 임시 코드나 나중에 삭제해야 할 코드가 계속해서 늘어나고 있었다. 이대로면 내 통제에서 벗어나게 될것임에 분명했다. 또한 미숙한 Addressables Vcontainer 사용 역시 시간을 계속해서 뺏어가고 있었다. 결국 사용자의 실제 플레이 플로우를 따라가는 것이 낫다고 판단하여, 개발 순서를 변경해 MainMenuScene부터 만들기로 결정했다.

1. AsSelf vs AsImplementedInterfaces

MainMenuLifetimeScope를 구성하는 과정에서 MainMenuViewMainMenuController를 주입할 때, MainMenuController 의 인터페이스를 생성하여AsImplementedInterfaces를 쓸지 아니면 어차피 재사용성이 떨어지는 컨트롤러이니 인터페이스 구현없이 AsSelf를 써버릴지 고민이 생겼다.

AsSelfAsImplementedInterfaces의 차이

  • AsSelf: 구체 클래스 타입(MainMenuController)으로 직접 주입을 요청할 때 사용한다.
  • AsImplementedInterfaces: 클래스가 구현한 인터페이스 타입(IMainMenuController)으로 주입을 요청할 때 사용한다.

1) 원칙주의적 관점 (인터페이스를 만드는 것이 좋다)

  • "모든 Service, Controller, Manager는 인터페이스를 가져야 한다."
    지금 당장 재사용성이 떨어져 보여도, 인터페이스를 만드는 비용은 거의 없다. 반면 이를 통해 얻는 유연성과 테스트 용이성은 매우 크다. 결합을 느슨하게 만드는 것은 습관이 되어야 한다.
  • 장점:
    • 일관성: 프로젝트 전체의 코드 구조가 통일된다.
    • 테스트 용이성: MainMenuView 테스트 시, 복잡한 MainMenuController 대신 간단한 Mock 객체를 주입하여 View의 동작만 순수하게 테스트할 수 있다.
    • 확장성: 추후 A/B 테스트나 플랫폼별로 다른 Controller를 주입해야 할 때, 기존 코드 수정 없이 대응이 가능하다.
  • 단점:
    • 파일 개수가 2배로 늘어난다.

2) 실용주의적 관점 (필요할 때만 만든다)

  • "구현체가 하나뿐이고 앞으로도 그럴 것이 확실하다면, 인터페이스는 과잉 설계(Over-engineering)다."
    YAGNI(You Ain't Gonna Need It) 원칙에 따라, 1:1 관계가 명확한 클래스 사이에 불필요한 추상화 계층을 만들 필요는 없다. AsSelf()는 이런 경우를 위해 존재한다.
  • 장점:
    • 코드의 양과 파일 수가 줄어든다.
    • 클래스 간의 관계 파악이 더 직관적이다.
  • 단점:
    • 나중에 확장이 필요해지면 결국 인터페이스를 추출하는 리팩토링이 필요하다.
    • 프로젝트 내에서 인터페이스 사용 규칙이 모호해져 일관성이 깨질 수 있다.

결론

IMainMenuController라는 인터페이스를 생성하고, MainMenuView가 이를 주입받도록 AsImplementedInterfaces를 사용하기로 결정했다. 재사용성이 떨어지는 인터페이스임은 분명하지만, 이유는 다음과 같다.

  1. 디자인 패턴 일관성: 이게 제일 컸다 내 프로젝트 내의 모든 Service, Controller, Manager는 모두 인터페이스를 구현하고 있었다. 그걸 확인하고 나니 계속해서 일관성을 지키고 싶었다.
  2. 도메인의 강한 결합 방지: 사실 인터페이스에 대한 사용이 나올때마다 지겹도록 주입받는 내용이다. 사실 아직까지 나는 인터페이스를 통한 느슨한 결합이 주는 이점에 대해 확실한 체화가 되어있지 않다. 그렇다면 결국 계속해서 써보면서 느끼는게 정답이 아닐까 하는 생각이 들었다.

※ 이 과정에서 추가로 공부한 [Inject] 용례

  • MonoBehaviour에서는 생성자 주입이 불가능하므로 필드/프로퍼티 주입을 위해 필수로 사용해야 한다.
  • 일반 클래스에서 생성자가 여러 개일 경우, DI 컨테이너가 어떤 생성자를 사용해야 할지 알려주는 지시자 역할을 한다.

2. 페이드 아웃 문제와 단일 책임 원칙(SRP)에 대한 고찰

그렇게 MainMenuScene을 실행하니 페이드 아웃이 되지않고 검은색 트랜지션 캔버스가 화면을 덮고 있었다. 원인은 금방 찾을 수 있었다. TransitionServiceFadeAndLoadSceneAsync 라는 메서드를 사용해서 InitializerSceneTransitionCanvas를 조절하여 페이드인/아웃과 씬 전환 기능을 수행한다. 게임 시작 시 GameInitializer -> GameManager 순으로 로직이 호출되는데, BootstrapSceneLoaderMainMenuScene을 로드할 때 TransitionService를 거치지 않고 Addressables 에셋을 직접 로드하고 있었다. 당연히 TransitionCanvas의 알파 값은 조절되지 않았다.
이 지점에서 근본적인 의문이 들었다.

"TransitionService가 페이드 효과와 씬 전환, 두 가지 역할을 동시에 맡는 것은 SRP(단일 책임 원칙) 위반이 아닌가?"

이 의문은 PlayerDataRepository의 패턴을 통해 해소되었다.
내 프로젝트의 PlayerDataRepositoryInventoryPlayerStat 두 테이블을 동시에 관리한다. 엄격한 SRP 관점에서 보면, 이는 InventoryRepositoryPlayerStatRepository로 분리해야 하는 설계다.

하지만 '단일 기능'의 경계를 어떻게 설정하느냐에 따라 관점이 달라질 수 있다.
테이블 관리라는 개별 기술로 보면 이는 분명한 SRP 위반이다. 하지만 도메인의 개념으로 보면 PlayerDataRepository의 책임은 'PlayerData'라는 도메인 개념을 구성하는 것이다. 이 관점에서 보면 InventoryPlayerStat은 'PlayerData'를 구성하는 하위 요소일 뿐이다. 이를 TransitionService에 적용하면, 이 서비스의 책임은 '페이드 효과'와 '씬 로딩'이라는 개별 기능이 아니라, '씬과 씬 사이의 연결'이라는 하나의 도메인을 책임지는 것이라고 볼 수 있다.

그래서 해결책은?

처음에는 GameInitializer, GameManager, BootstrapSceneLoader 중 어디에서 TransitionService를 호출할지 고민했다. 하지만 훨씬 간단한 해결책을 선택했다. TransitionCanvas의 초기 알파 값을 0으로 변경했다. 이는 편의를 위한 해결이 아니었다. MainMenuScene은 다른 씬에서 '전환'되는 단계가 아닌 '시작' 단계다. 내가 만든 FadeAndLoadSceneAsync 메서드는 이름 그대로 화면과 화면의 연결을 담당해야지, 초기 화면을 시작하는 메서드가 아니다. 만약 추후 MainMenuScene 앞에 인트로 씬이 생긴다면, 시작 씬을 인트로 씬으로 변경하고 그곳에서 FadeAndLoadSceneAsync("MainMenuScene")을 호출하면 된다. 사실 최근에 VContainer와 Addressables을 사용하며 수도없이 복잡한 문제와 복잡한 해결책을 마주했어서 이렇게 해결을 하려고 하니 조금 찝찝한 감이 들었다. 하지만 아무리 생각해도 이것이 도메인 관점에서 더 논리적인 메서드 활용법이라는 결론을 내렸다.

결론

  1. 사용자 플로우에 따른 개발 순서 설정의 중요성 초기 개발 방향이 혼란스러울 때, 사용자의 동선을 따라가는 것이 얼마나 중요한지 확실히 깨달을 수 있었다. 아마 앞으로도 나는 내 프레임워크를 계속해서 발전시켜 꾸준히 사용하겠지만 맨땅에서 시작하더라도 이 깨달음은 꽤 큰 것이 될것 같다.
  2. SOLID 원칙, 특히 SRP는 절대적인 규칙이 아니라고 생각한다. 물론 엄격한 분리와 고전적인 원칙에 따른 설계가 필요한 프로젝트와 장소가 있을것이다. 하지만 허술한 설계와 유연한 설계의 차이는 분명히 존재한다고 생각한다. 절대, 앞으로도 정답을 찾을 수 없는 분야가 아닐까 그런 생각이 든다.

0개의 댓글