본문 바로가기

카테고리 없음

프로젝트 아키텍처 변경 기록

목차

  1. 서론
  2. 기존 아키텍처
  3. 예상 목표 아키텍처
  4. 문제 발생
    1. 첫번째 해결책
    2. 두번째 해결책
    3. 세번째 해결책(해결!)
  5. 결론

 

서론

기존 App 의 기능을 이용하여 간단한 MacOS Application 을 만들 때 접하게 된 문제이다.

먼저 iOS Application 을 개발할 때는 필요한 기능들을 Domain 모듈(하나의 Target)에 구현했었다.

MacOS App 에서 Domain 모듈의 기능들을 이용하기 위해 모듈을 Import 할 때, 기존에 여러 기능들을 하나의 Domain 모듈로 관리했기 때문에 필요없는 기능들과 그에 따른 의존성까지 포함하게 되었다.

이에 따라, 기존의 여러 기능들을 포함하고 있는 하나의 Domain 모듈을 쪼개기로 결심했다.

 

기존 아키텍처

먼저 기존 아키텍처에 대해서 짧게 설명하자면,

 

Domain 모듈이 소유한 것 :

  • 각 기능에 대한 Use Cases
  • 각 Use Case 에 대한 인터페이스와 구현
  • Repository 패턴에 따른 인터페이스

Infrastructure 모듈 :

  • Repository 패턴에 따른 인터페이스의 구현
  • 여러 외부 기능/기술에 대한 의존성을 가짐

 

변경 전 기존 아키텍처

 

하나의 Domain 모듈 안에서 각 기능을 표현하는 Use Cases 가 정의되어 있었고 Use Case 구현을 위해 필요에 따라 여러 Repository 인터페이스 또는 Use Case 인터페이스를 의존했다.

하나의 Domain 모듈 안에서 여러 기능이 정의되어 있기 때문에 일부 기능(일부 Use Case)만 사용하고 싶어도 전체 Domain 모듈을 Import 해야한다.

 

예상 목표 아키텍처 (일부)

 

위 이미지처럼 모듈을 분리하면 문제가 쉽게 해결된다. 그런데...

 

Circular Dependency !

실제로 Domain 모듈을 분리하는건 쉽게 수행할 수 있었다. 그러나 빌드 단계에서 문제를 맞닥뜨리게 되는데...

Circular Dependency

하나의 Domain 모듈일 때는 필요에 따라 여러 Repository 또는 다른 Use Case 들을 참조하며 Use Case 를 구현했다. 그 이유는, Use Case 란 사용자 입장에서 작성되었고 목표를 수행하기 위해 필연적으로 여러 Entity 를 참조할 수 밖에 없는 상황이 존재하거나 다른 연관된 Use Case 를 실행해야 하는 상황이 존재하기 때문이었다.

그러나 기능에 따라 Domain 모듈을 분리하고 각 모듈에서 필요한 의존성을 설정하다보니 각 모듈이 서로를 필요로하는 Circular Dependency 가 생겨버렸다.

첫번째 해결책과 문제점

첫번째로, 각 Domain 모듈에 대해 Interface 모듈과 Implementation 모듈로 나눠서 정의하는 방법이 떠올랐다. Implementation 모듈은 절대 다른 Implementation 모듈을 참조하지 않고 필요한 Interface 모듈만 참조한다. 따라서 구현 부분에서 발생했던 Circular Dependency 문제를 해결할 수 있다. 또한 모듈을 사용하기 위해서는 Interface 모듈만 Import 하면 된다.

이걸 그림으로 나타내면 아래와 같다.

해결책 1

이 방법으로 해결이 되나 싶었지만 다른 문제가 발생했다.

현재 프로젝트에선 `Swinject`라는 의존성 주입 프레임워크를 사용하고 있고, 각 모듈에서 모듈을 사용하는데 필요한 실제 인스턴스를 생성하는 방법을 정의한 `Assembly` 를 제공하는 방식으로 의존성 주입이 이루어지고 있다.

현재 프로젝트의 의존성 주입 방법

 

하지만 첫번째 해결책을 적용할 경우 이 Assembly 를 정의할 곳이 애매해진다. 각 케이스 별로 이유를 설명하자면,

  1. Interface 모듈에 위치: Assembly 는 구현 객체를 참조해야 하므로 구현을 모르는 Interface 모듈에서는 정의될 수 없다.
  2. Implementation 모듈에 위치: 모듈에 대한 Assembly 를 사용해야하기 때문에 Interface 모듈뿐만 아니라 Implementation 모듈도 Import 해야하고 Interface 모듈이 따로 있는데 구현 모듈을 직접 의존한다는 점 자체도 좋은 방향이 아니다.
  3. 별도의 모듈에 위치: Assembly 모듈을 따로 정의하고 Interface 와 Implementation 을 Import 하여 Assembly 를 제공할 수 있지만 이 경우 구현 모듈에 위치한 구현 객체와 생성자가 public 으로 정의되어야 한다.

위 방법들 중 3번 방식으로 구현하는게 가장 적절해 보였다. 구현 모듈의 객체가 public 으로 공개된다 해도 구현 모듈만 따로 사용할 일은 없을 것이기 때문이다.

하지만 더 나은 방향이 있을거라 생각했고 아키텍처 레벨에서 구현 모듈을 직접 사용하는 것을 제한하고 싶은 마음도 있었기에 다른 방안을 더 고민해 보았다.

두번째 해결책과 문제점

두번째 해결책은 Circular Dependency 의 원인이 근본적으로 모듈간 참조에 있다고 보고 Domain 모듈 사이의 참조를 모두 없애는 것이었다.

모듈간 참조가 전혀 없도록 구성하면 Domain 모듈에 대해 응집도를 높이고 결합도를 낮추는 효과도 있다.

해결책 2

하지만 기존에 다른 Domain 모듈을 참조하는 방식으로 Use Case 가 구현 되어 있었기 때문에 위 해결책대로 변경한다면 Use Case 구현이 변경되어야 했다.

이 때 변경이 필요했던 Use Case 가 어떻게 변경되었는지 예시를 들어보자면,

// 기존 구현
public protocol SetDailyReminderUseCase {
    // - 파라미터로 넘어온 시간에 로컬 알림이 설정된다.
    // - 알림을 설정할 때 필요한 추가 정보(선호 사운드, 현재 설정값 등)는 구현에 맡긴다.(구현에서 다른 도메인에 접근 가능)
    func execute(time: Time)
}

// 변경 된 구현
public protocol SetDailyReminderUseCase {
    // - 파라미터로 넘어온 시간에 로컬 알림이 설정된다.
    // - 알림을 설정할 때 필요한 추가 정보를 모두 넘겨주어야 한다.(구현에서 다른 도메인에 접근 불가)
    func execute(time: Time, preferredSound: Sound, userInfo: UserInfo)
}

 

위와 같이 구현에서 다른 도메인에 접근할 수 없게 되자 Use Case 를 실행할 때 필요한 정보들을 모두 파라미터로 넘겨주도록 변경되었다.

하지만 과연 이렇게 Use Case 를 변경하는게 올바른 방향인지 의문이 들었다. Use Case 는 사용자의 관점에서 작성되어야 하는것이 아니였던가? 알림을 설정하고자 하는 사용자에게 "알림을 설정하고 싶으면 설정할 시간을 알려주세요" 라고 하는 대신 "알림을 설정하고 싶으면 설정할 시간과 자주 사용했던 알림 소리와 내 정보 화면에서 저장했던 정보들을 알려주세요" 라고 하는게 맞는걸까?

나는 기존 구현 방향이 맞다고 생각했다. 그래서 학습을 통해 Use Case 와 Domain 에 대한 이해를 넓히며 다른 해결 방안을 구상했다.

(다른 Domain 에 대한 참조를 제거하면서 만난 또 다른 문제도 있지만 이 단락에서는 일단 넘어간다.)

(결국 채택한!) 세번째 해결책

세번째 해결책은 Domain 모듈과 Use Case 를 분리하는 방법이었다.

해결책 3

 

Domain-Driven, Use Case, iOS Clean Architecture 등등... 계속 자료를 찾아보고 이해가 높아지면서 일반적인 iOS Clean Architecture 와 내가 추구하는 방향이 약간 다르다는것을 알게 되었다. 일반적인 iOS Clean Architecture 에서는 Use Case 를 비즈니스 규칙의 일부분으로 보고 있지만 나는 사용자가 앱을 사용하는 인터페이스 정도로만 보고있어서 생긴 차이점 같다.

이에 따라 Domain 과 Use Case 를 나누었다. Domain 모듈은 독립적인 기능을 수행하는 서비스의 역할을, Use Case 모듈은 사용자가 앱을 사용함에따라 Application 으로부터 1차적으로 요청을 받는 역할을 하도록 했다. Use Case 를 수행할 때 여러 기능이 필요하다면(e.g. 데이터 저장 & 메세지 발송 & 알림 등록 등...) 여러 Domain 모듈을 참조할 수 있다.

 

결과적으로 아래와 같이 전체적인 그래프가 형성되었다.

Module graph

 

 

결론

  • 결국 마지막 방법을 채택함으로써 현재로서는 충분히 확장이 쉽고 필요한 만큼 모듈이 나뉘었다고 생각한다.
  • 여러 개념들(Domain, Use Case, Module, Dependency 등)에 대해 다시 깊게 생각해보는 계기가 되었고, 다른 아키텍처를 접해도 전보다 더 익숙하게 이해할 수 있을 것 같다.