본문 바로가기
IOS App Programming/트러블 슈팅

[iOS/Clean Architecture + ReactorKit] 클린아키텍쳐 책임 분리

by B_Tori 2024. 8. 6.

ReactorKit을 사용하여 MVVM 모델로 구현을 하고 있었으며 클린아키텍처 구조로 프로젝트를 구성하고 있었다.
프로젝트의 전체적인 구조를 살펴보면 다음과 같다

Presentation
    ㄴ View(ViewController)
    ㄴ ViewModel(Reactor)
Domain
    ㄴ Model
    ㄴ Usecase 
    ㄴ Repository (프로토콜)
Data
    ㄴ DTO Model
    ㄴ RepositoryImpl (구현부)
    ㄴ Datasource

 

로그인 로직을 구현하면서 여러 데이터 소스들을 사용하고 합치는 과정들을 거치게 되었고,
이때 점점 레이어나 각 구성 요소들의 책임들을 모호하게 코드를 작성하고 있다는 생각이 들어 명확하게 책임 분리를 정리해야겠다고 생각했다.

처음에 공부할 때 전체적인 클린 아키텍처의 규칙이 있겠으나, 조금씩 사람들마다 각 구성요소에 대해 생략을 한다던지의 차이점이 있는 듯하였다. 따라서 큰 틀의 레이어가 지켜야 하는 규칙들을 꼭 지키고 그 안의 구성 요소들은 조금은 내가 이해한 대로 책임을 분리하였다.

클린 아키텍쳐의 목표를 만족할 수 있을지 사실 아직은 초기 개발 단계라 잘은 모르겠지만, 다음과 같이 분리를 하였다.

클린 아키텍쳐


클린 아키텍처의 디펜던시 룰은 안쪽으로 향하는 모습이다.
즉, 바깥의 요소들은 안의 요소를 알 수 있지만, 안의 요소들은 바깥의 요소를 알 수 없다.


그렇게 레이어를 분리하여 디펜던시를 나타내면 다음과 같은 결과가 나온다.
프레젠테이션 레이어와 데이터 레이어는 도메인 레이어를 의존하지만, 가장 안쪽에 위치한 도메인 레이어는 아무런 레이어도 의존하지 않는다.

책임 분리

나는 각 레이어의 구성요소에 대해 다음과 같이 책임을 부여하였다.

프레젠테이션 레이어

  1. 뷰(뷰컨트롤러)
    • 사용자 인터페이스(UI)를 제공하고, 사용자 입력 수신
    • UI 이벤트를 리액터에 전달하고, 리액터의 상태 변화에 따라 UI를 업데이트합니다.
  2. 리액터
    • ViewModel 역할을 하며, 상태(state)와 비즈니스 로직을 관리
    • 상태 변화에 따라 UI를 반응적으로 업데이트하고, 유즈케이스를 호출하여 비즈니스 로직을 수행

도메인 레이어

  1. 모델
    • 애플리케이션의 핵심 비즈니스 객체를 정의 (프레젠테이션 단에서 사용될 모델)
  1. 레포지토리(프로토콜)
    • 데이터 소스에 접근하기 위한 인터페이스를 정의
    • 데이터 레이어에 대한 의존성을 숨기고, 도메인 레이어에서 다양한 데이터 소스를 사용할 수 있도록 도움
<의존성 역전 원칙(DIP)>
상위 수준의 모듈이 하위 수준의 모듈에 의존하지 않고, 추상화된 인터페이스에 의존하는 원칙 고수준 모듈(도메인 레이어)이 저수준 모듈(데이터 레이어)에 의존하는 대신, 추상화된 인터페이스(레포지토리 프로토콜)에 의존하도록 한다.
  1. 유즈케이스 (프로토콜 및 구현부)
    • 특정 기능이나 비즈니스 로직을 구현
    • 도메인 모델을 활용하여 데이터 흐름을 제어하고, 레포지토리를 통해 데이터 소스와 상호작용

데이터 레이어

  1. DTO 모델
    1. 외부 API 데이터 응답 혹은 전송을 위한 객체
  1. 레포지토리 구현부
    • 레포지토리 프로토콜을 구현하여 실제 데이터 소스에 접근
    • 데이터 소스에서 반환된 값(DTO 모델)을 도메인에서 사용할 수 있도록 데이터 변환을 수행
  1. 데이터소스
    • 외부 시스템과의 데이터 통신을 담당

상호작용

프레젠테이션 레이어

  • 뷰(뷰컨트롤러) : 사용자로부터 입력을 받아 리액터에 전달
  • 리액터 : 상태를 관리하여 상태 값에 따라 뷰를 업데이트하며, 필요에 따라 유즈케이스를 호출하여 비즈니스 로직을 수행

도메인 레이어

  • 레포지토리는 데이터 소스에 접근하기 위한 인터페이스를 제공함
  • 프레젠테이션에서 호출한 유즈케이스는 레포지토리 인터페이스를 통해 데이터 소스에 접근하여 데이터를 조작함

데이터 레이어

  • 실제 구현된 레포지토리에서 데이터소스와 상호작용하여 DTO를 받아오면 알맞은 도메인 모델로 변환하여 반환해 줌
  • 데이터 소스는 외부 API와 통신한 결괏값을 레포지토리에게 넘겨줌

 

로그인 로직을 구현해 보자

우선 우리의 로그인 로직은 다음과 같았다.

  • SNS 로그인을 이용
  • SNS 로그인 정보과 FCM 토큰값을 서버에 전달하면 서버에서는 회원가입 이력과 액세스토큰 및 리프레시 토큰을 내려줌

따라서 정리하면 다음과 같은 과정이다.

  1. SNS 로그인을 한번 SNS 로그인 측에 요청을 해서 응답을 받아오고,
  2. FCM 토큰 값도 FCM서버에 요청을 해서 받아오면
  3. 해당 값을 합쳐서 우리 서버로 Login 요청을 보냄

3번의 값을 합치는 부분이 어디서 이뤄지느냐를 고민하면서 클린아키텍처의 책임 분리를 명확히 해야겠다는 생각을 하게 되었다.

각각의 데이터 소스들 및 레포지토리들은 하나의 작업을 하도록 분리를 하여 어떤 유즈케이스에서든 재사용될 수 있도록 하는 것이 좋아 보였기에 해당 과정에서 합치는 과정을 진행한다면 단일 책임원칙을 지키지 못할 듯하였다.

 

그렇다면 리액터와 유즈케이스가 남아있었는데,
리액터에서 로그인값과 fcm값을 상태로 관리하고 이 둘의 값이 정상적으로 들어온다면 서버로 요청을 보내는 과정과
유즈케이스에서 합쳐서 요청을 보내고 서버 결괏값만 프레젠테이션으로 반환하는 과정 두 가지이다.

 

결론적으로 값을 합치는 로직은 UseCase에서 진행하였다.
해당 이유는 UseCase가 하나의 비즈니스 로직을 담당하는 역할이고,

내가 생각하기에 해당 과정은 "로그인" 과정으로 다음과 같이 비지니스 규칙을 정의할 수 있었다.
유저 로그인 -> "SNS 로그인 결과와 FCM 토큰을 결합하여 서버에 요청하는 것"
즉 결과를 합쳐서 서버에 요청하는 거 자체가 하나의 비즈니스 로직 자체라고 생각되었기 때문에 Usecase에서 진행을 하였다.

 

또한 리액터의 상태값의 경우는 나는 SwiftUI에서처럼 뷰의 상태값을 관리한다라고 생각되기 때문에 단순히 서버 요청을 보낼 타이밍을 잡기 위해 State를 활용하는 것이 맞는지에 대한 생각이 들었기 때문에 뷰에서 사용되지 않을 state라면 잘못된 설계 같다는 생각이 들었다.

 

그래서 로그인 로직의 흐름을 그림으로 정리해 보면 다음과 같다.
글씨가 조금 더럽기는 하지만... 실제 로그인 로직 적용 전 그려본 정리 로직이다.

 

느낀 점

클린 아키텍처를 작성하다 보면 클린아키텍처의 목적과 방향성을 잃고 코드를 작성하게 되는 일이 쉬운듯하다.

특히 지금처럼 여러 로직들이 합쳐져서 만들어지는 유즈케이스의 경우 더욱 그런 듯하다.

따라서 익숙해질 때까지 데이터의 흐름을 직접 그려보는 것이 필수적인 과정이 될 듯하다. 

댓글