🧙🏻‍♀️ PRND iOS팀의 UseCase 활용기

Panini
PRND
Published in
9 min readDec 23, 2022

PRND iOS팀에서는 Clean Architecture를 도입해 사용하고 있고, 계층 간 관심사 분리를 통해 많은 이점을 체감하고 있습니다.

그중에서도 도메인 계층의 UseCase를 PRND iOS팀 내에서 해석한 방식으로 사용하면서 느꼈던 부분들을 소개해보려고 합니다.

꼭 Clean Architecture의 모든 Layer를 갖추지 않더라도 다양하게 활용할 수 있으니 가볍게 읽어보세요!

UseCase가 뭘까?

밥 아저씨의 설명에 의하면

UseCase 계층은 어플리케이션에 특화된 비즈니스 규칙을 포함하며, UseCase는 Entity와 주고받는 데이터의 흐름을 조정하고, Entity가 UseCase의 목표를 달성하기 위해 범용 비즈니스 규칙을 사용하도록 지시한다.

Wikipedia에서는,

소프트웨어의 사용 시나리오 시스템이 외부 요청 (ex. 사용자 입력)을 수신하고 이에 응답하는 잠재적 시나리오

(뭐라는 거지.. 그리고 물어 물어보니 팀마다 프로젝트마다 해석하는 방식이 조금씩 다른 것 같은데…)

반복된 논의와 시행착오를 통해, PRND iOS 팀에서는 모바일 어플리케이션의 UseCase를 다음과 같이 정의해서 사용하고 있습니다.

  • 시스템의 동작을 개발자나 프로그램이 아닌 사용자의 관점에서 표현한 시나리오로, 시스템이 제공해야 하는 기능을 묘사한다.
  • 어플리케이션의 주요 비즈니스 로직을 한가지씩 나누어 담는 곳.
  • Presentation Layer (ex. ViewModel, Interactor, Reactor 등)에서 활용할 비즈니스 로직들을 캡슐화
  • PresentationLayer가 외부 Layer (ex. Remote, Local)와 소통할 수 있는 창구

📌 위 내용은 PRND iOS팀 내에서 기준으로 삼기 위해 정의한 내용들로, 실제로 서비스나 팀마다 사용방식이 매우 다양합니다!

/// - Important: 아래 등장하는 예시 코드들의 컨벤션은 PRND iOS팀의 컨벤션이 아닙니다! 😀
/// 또한, 이해를 돕기 위한 코드로 컴파일이 되지 않을 수 있습니다 🤣

UseCase 장점 맛보기

🐻 서버 팀 : 여기 이 API를 쓰세요! Response 모양은 다음과 같습니다!

🐹 모바일 팀 : 앱에서 사용하기에 응답 형태가 좀 애매한데... 이걸 고쳐 달라고 해야 하나..? 서버가 앱 화면을 위해 수정이 일어나는 게 맞나? 더 애매한데? ViewModel에서 모양을 바꿔서 사용해야 하나?

많은 팀에서 경험해봤을 법한 흐름인데요!! (강요)

UseCase를 이용하면, 외부 상황 (ex.서버 API, Local DB 등)을 Domain Layer에서 입맛대로 손쉽게 변형해서 사용할 수 있습니다. Presentation Layer를 위한 중간 서버처럼 활용하는 느낌입니다!

특히, 서버가 MSA(Micro Service Architecture) 형태라면 더욱더 효과적입니다! 작은 기능을 갖는 서비스들을 어플리케이션에 필요한 형태로 합치고 변형하고 지지고 볶아 사용할 수 있어서, 서비스 복잡도를 보다 낮출 수 있습니다.

🚫 코드 중복을 방지

핵심 비즈니스 로직을 캡슐화하고, 재사용 할 수 있도록 함으로써 같은 코드가 여러 곳에 산재하지 않도록 합니다.

🐜 책임을 각자 나눠 갖기 때문에, 커다란 객체를 피할 수 있음!

  • 자칫하면 모든 역할을 Presentation Layer에 맡겨서, 아주 비대한 비즈니스 객체(ViewModel, Reactor, Interactor..)를 만들어낼 수도 있습니다. UseCase에 책임을 한가지씩 위임해서 이 문제를 해결할 수 있습니다!
  • 외부 세계 (서버, DB 등)에 의존하지 못하도록 강제되기 때문에 자동으로 작아지는 Presentation 객체!

✅ Testability 올릴 수 있음!

  • 한가지 책임만 갖고, 상태를 갖지 않기 때문에 테스트 범위가 명확해집니다!

💪🏼 변화에 강하다!

  • 외부 계층에서 일어나는 변화(서버, DB, 프레임워크, UI 등의 변화)가 UseCase (DomainLayer)의 변경을 일으키지 않는다.
  • UseCase 내부에서 생기는 로직의 변화가 Presentation과 같은 다른 계층에 영향을 주지 않는다.
  • 여러 기능이 한곳에 모여있지 않고, 한 개의 UseCase가 단 하나의 비즈니스 로직만 담당하기 때문에, 관련 없는 요구사항으로 인해 변경될 여지가 사라진다.

본격 UseCase 활용기

기본

먼저, UseCase를 어디서 어떻게 만들어서 사용하는지 볼까요? 차량 정보를 조회해서 보여주는 화면이 있다고 합시다.

UseCase를 사용하지 않고서는, ViewModel에서 차량 정보를 요청해서 화면에 뿌려주곤 했습니다.

ViewModel Request As-Is

CarRequest는 서버 의존적인 인스턴스이기 때문에, ViewModel (PresentationLayer)에서 CarRequest를 알게 하고 싶지 않습니다.

따라서, CarRequest 관련 로직을 수행해줄 UseCase를 이용해 격리해주려고 합니다. 아래와 같이 GetCarUseCase Protocol과 구현체를 정의합니다.

UseCase Basic Usage

그리고, ViewModel에서는 GetCarUseCase를 이용해 같은 로직을 수행합니다.

ViewModel Request To-Be

위와 같이 가장 기본적으로는 Repository와 같은 창구를 통해서 서버나 DB 등의 외부 서비스를 호출해 원하는 로직을 수행하고 반환하는 방식으로 구현할 수 있습니다.

다시 말해 Presentation Layer (ViewModel, Reactor, Interactor …)에서 외부 계층을 분리해줄 수 있습니다!

💡 Presentation Layer UnitTest에 Mocking이 필요하므로 UseCase를 추상화해서 사용합니다!

💡 UseCase 객체는 상태를 갖지 않고, 입력을 이용해 로직 실행 후 출력을 반환하는 역할만 수행합니다.

이전에도 서버 요청을 책임지는 Service, Repository 와 같은 객체들을 만들어 사용하던 편이었기 때문에, UseCase 도입 초반에는 그냥 통로 역할만 하는 객체를 일일이 만드는 게 오히려 귀찮기도 했습니다. 하지만 사용 케이스가 점점 많아질수록 위임할 수 있는 책임들이 생겼습니다! 😺

Validation / Error 처리

이번에는 차량 상세 정보 요청 전/후로 아래와 같은 두 가지 Validation이 필요한 경우가 생겼다고 합시다!

  1. 요청 시 id가 비어있는 경우 에러 처리
  2. 성공 응답이지만, 필수 정보가 비어있는 경우 에러 처리

기존의 ViewModel이라면, 아래와 같이 각 에러 처리를 ViewModel에서 해줘야 합니다. 또한, CarRequest를 사용하는 모든 곳에서 공통으로 필요한 validation이라면 차량 정보를 요청하는 모든 곳에 중복코드가 생길 수밖에 없죠.

ViewModel Error Handling As-Is

아래 코드와 같이 UseCase 구현 내부에서 원하는 시점에 Validation Logic을 수행하고, 필요한 경우 Error로 반환할 수 있습니다.

UseCase Validation Example

GetCarUseCase를 사용하는 지점에서는 해당 Validation 자체에는 관여하지 않고, Presentation에 필요한 정보만 받아 처리할 수 있게 되었습니다. ViewModel이 보다 간결해졌습니다! 😊

ViewModel Error Handling To-Be

외부 인터페이스 변경에 대응하기

차량 정보를 가져오기 위해 사용자 Id가 필요해졌습니다! 이제 적당한 서버 사용자 API나 로컬 DB에 저장된 UserId를 가져와서 차량 정보 서비스를 호출해야 합니다.

위에서 내린 UseCase의 정의에 따라 사용자의 입장에서 생각해보면, 외부 세상은 달라졌지만, 차량 정보 요청은 여전히 하나의 UseCase로 표현되어야 합니다.

Combine Multiple Services

위와 같이 GetCarUseCase 의 요구사항에는 변함없이, 여러 서비스를 차례로 연결해 사용할 수 있겠습니다.

외부 상황은 변했지만, ViewModel의 코드에는 변경이 일어나지 않습니다! 차량 정보를 가져오는 방식이 변경됨에 따라 해당 책임을 갖는 GetCarUseCase에만 변경이 일어났습니다. 😆🎉🎉

또 다른 응용 사례로는 여러 서비스를 호출하고 응답들을 입맛대로 합쳐, Presentation에서 요구하는 하나의 결과로 만들어줄 수도 있을 겁니다!

핵심은 서버나 DB 등과 같은 바깥세상의 서비스들을 엮어서 필요한 하나의 출력으로 만들어 줄 수 있다는 점이죠! 우리는 API 종속적 사고에서 UseCase 기반 사고를 하게 되었습니다 😀

Before

before

After

after

간단한 예시에 대해 몇 가지 UseCase 활용 방식을 적용해보았습니다. 정리해보면 위와 같은 코드 변화가 생겼음을 알 수 있습니다.

  • ViewModel에서 어플리케이션 비즈니스 로직 몇 가지를 UseCase로 분리해냈고,
  • UseCase를 추상화해서 ViewModel의 단위테스트를 용이하게 해줬습니다.
  • 또, 외부 세계의 변경은 ViewModel에 영향을 주지 않습니다.

위에서 소개한 사례 이외에도 캐싱이나 응답 지지고 볶기 등 다양한 응용을 통해 UseCase를 유용하게 사용하고 있습니다! 기존에는 화면 단위로 필요한 API를 모아둔 Service 객체를 만들어두고 사용하는 API-Driven 방식에 익숙했었는데, 서버 요청과 같은 작업도 도메인 로직의 일부로 가정하고 분리하면서 UseCase(Domain)-Driven으로 설계된 결과물에 만족하고 있습니다.

UseCase를 다른 방식으로 활용하고 있는 팀의 이야기도 너무 궁금합니다! 또, 위 내용에서 잘못되었거나, 개선할 수 있는 부분도 언제든 공유 주시면 너무너무 반가울 것 같습니다. 🤗

PRND iOS팀에서는 Clean Architecture의 울타리 안에서 UseCase라는 이름으로 사용하고 있지만, 얼마든지 다른 이름으로 많은 곳에서 다양하게 사용할 수 있을 것 같아 공유해봤는데 🤓 많은 팀에 새로운 아이디어가 되길 바랍니다!

--

--