
클라이언트 아키텍처에 대한 단상 - '서버'가 진짜 '도메인' 아닐까?
개인 광고 영역
개발에서 아키텍처(Architecture)는 무엇일까? AI는 이렇게 답한다.
소프트웨어 시스템의 기본적인 구조와 구성 요소들 간의 관계, 그리고 이를 통제하는 원칙과 지침. 미래의 변경과 확장에 어떻게 대응할지를 결정하는 청사진(Blueprint).
모두가 동의하겠지만, 미래의 변경과 확장에 유연하게 대응하는 것이 아키텍처의 핵심이다. 더 빠른 개발 속도와 안정성은 덤이다.
그렇다면 오늘날 우리가 클라이언트 개발에 적용하는 아키텍처는 이 목적에 정말 충실할까? 혹시 유행하는 패턴을 ‘좋은 코드’라는 명목 아래 무비판적으로 따르고 있지는 않을까? 이 글에서는 클린 아키텍처의 상징과도 같은 ‘UseCase’의 딜레마를 통해 클라이언트 아키텍처를 좀 더 넓은 시각으로 바라보고자 한다.
이 글에서는
- 유행처럼 번지는 클라이언트 아키텍처, 특히 UseCase 패턴의 실효성에 대해 의문을 제기한다.
- 단순히 데이터를 전달하는 역할에 그치는 UseCase의 딜레마를 실제 코드 예시와 함께 살펴본다.
- 관점을 전환하여, ‘서버’를 중심으로 클라이언트 아키텍처를 바라보는 새로운 시각을 제안한다.
- 궁극적으로 ‘좋은 아키텍처’란 무엇이며, 비즈니스 가치를 높이는 실용적인 설계는 어떤 것인지 함께 고민해 본다.
모바일 아키텍처, 언제부터 필수였을까?
안드로이드 초창기에는 아키텍처보다 하드웨어의 한계를 넘는 것이 중요했다. static
을 남발한 코드 설계와 Activity
하나에 수천 줄의 코드를 넣는 게 당연했다. 그 시절에도 디자인 패턴은 있었지만, 지금처럼 ‘설계 원칙’을 따르는 개발이 중심은 아니었다.
시간이 흘러 하드웨어 성능이 좋아지고, 구글이 MVP, 클린 아키텍처 가이드를 제시하면서 패러다임이 바뀌었다. 이제는 SOLID 원칙을 지키며 MVVM, 클린 아키텍처를 적용하는 것이 ‘잘하는 개발’의 척도처럼 여겨진다.
하지만 이 과정에서 “클린 아키텍처를 적용해야 한다”는 명제가 “멀티 모듈은 필수다”, “UseCase는 반드시 써야 한다”는 오해로 이어지기도 한다. 클린 아키텍처는 레이어의 논리적 분리를 말하는 것이지, 물리적인 모듈 분리를 강제하지 않는다. 애매한 모듈 분리는 오히려 생산성을 해칠 뿐이다.
가장 큰 딜레마는 역시 ‘UseCase’다.
UseCase는 정말 필요할까?: ‘단순 전달’의 딜레마
UseCase는 비즈니스 로직을 캡슐화하고, 단일 책임 원칙(SRP)을 지키는 훌륭한 도구임이 맞다. ViewModel의 코드도 명확해진다.
UseCase는 필요할까?: ‘단순 전달’의 딜레마
UseCase는 비즈니스 로직을 캡슐화하여 재사용성을 높이고, SOLID의 단일 책임 원칙(SRP)을 지키는 훌륭한 도구인것은 맞다.
하지만 실제 어떨까? ViewModel부터 순서대로 알아보자.
- UseCase를 활용하는 ViewModel
class SessionViewModel(
private val fetchSession: FetchSessionUseCase,
private val updateSession: UpdateSessionUseCase,
) : ViewModel() {
// 생성자만 봐도 ViewModel의 역할이 명확히 보인다.
}
- Repository를 직접 활용하는 ViewModel
class SessionViewModel(
private val sessionRepository: SessionRepository,
) : ViewModel() {
// 생성자만으로는 역할을 파악하기 어려워 코드를 더 봐야 한다.
}
분명 1번 코드가 의도를 파악하기 쉽다. 하지만 FetchSessionUseCase
의 구현부를 들여다보면 어떨까?
internal class FetchSessionUseCaseImpl(
private val sessionRepository: SessionRepository,
) : FetchSessionUseCase {
override suspend operator fun invoke(): SessionEntity =
sessionRepository.fetchSession()
}
여러분의 코드도 이렇지 않은가? Repository의 함수를 그대로 호출하는, 단순 전달(Forwarding)만 하는 UseCase가 프로젝트에 수없이 많을 것이다. 이 boilerplate를 위해 우리는 얼마나 많은 시간을 쓰고 있을까?
안드로이드 공식 가이드 - link조차 도메인 레이어는 ‘선택적(Optional)’ 이라고 말한다. 필자의 생각도 같다. UseCase 없이 Repository를 직접 사용한다고 해서 도메인 레이어가 사라지는 것이 아니다. Entity는 여전히 존재하고, 데이터 흐름의 논리적인 레이어 구분은 유효하다. 그냥 ‘UseCase’라는 클래스만 생략되었을 뿐이다.
내가 진짜 하고 싶은 이야기: 관점의 전환
그럼 UseCase는 언제 유용할까? 공식 가이드의 설명에 매우 동의한다.
여러 Repository의 데이터를 조합해야 할 때. 예를 들어, 뉴스 데이터와 기자 데이터를 결합하는 경우.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) { /* ... */ }
이런 복잡한 비즈니스 로직이 클라이언트에 얼마나 될까? 이제 시야를 더 넓혀보자. 우리가 만드는 앱의 데이터와 비즈니스 로직은 대부분 어디에 있는가? 바로 서버다.
A팀과 B팀의 환경
두 팀이 있다고 가정해보자.
A팀
: 서버가 BFF(Backend for Frontend) 패턴으로 구성되어 있다. 클라이언트의 ‘홈 화면’에 필요한 모든 데이터를 서버가 하나의 API(GET /home)로 완벽하게 가공해서 내려준다.B팀
: 서버가 범용적인 REST API를 제공한다. ‘홈 화면’을 구성하기 위해 클라이언트가 GET /users/me, GET /products/recommend, GET /events/live API를 각각 호출하고 조합해야 한다.
A팀 클라이언트에게 복잡한 도메인 레이어가 필요할까? 아니다. 서버가 이미 UseCase 역할을 해준 셈이다. 이 팀의 클라이언트는 사실상 거대한 뷰 레이어(View Layer) 이며, 데이터 가공을 위한 최소한의 Data Layer만 있으면 충분하다.
반면 B팀 클라이언트에서는 여러 데이터를 조합하고 가공하는 UseCase가 강력한 힘을 발휘한다. 이처럼 ‘올바른 아키텍처’는 우리 팀의 서버 환경과 API 정책에 따라 결정되는 것이지, 절대적인 정답이 있는 것이 아니다.
서버가 ‘도메인’이라면
클라이언트에서 아무리 Domain, Data 레이어를 정교하게 나눠봤자, 대부분의 로직은 서버가 내려주는 데이터를 그대로 화면에 보여주는 것이다. 클라이언트의 로직은 ‘View의 비즈니스 로직’에 가까운 경우가 많다.
그런데도 우리는 왜 이렇게 복잡한 구조를 고집할까? “재사용성을 위해”라고 말하지만, 단순 전달만 하는 UseCase가 정말 재사용에 얼마나 기여할까?
최근 IT 업계에서 “클라이언트 개발은 너무 느리다!”는 말이 자주 들린다. API가 늦게 나와서? 배포가 부담스러워서? 어쩌면 우리가 스스로 만든 과도한 엔지니어링(Over-engineering)의 덫에 빠져 속도를 잃고 있는 것은 아닐까?
마무리
AI가 코드를 ‘딸깍’하고 만들어주는 시대에, 우리의 진짜 가치는 정해진 패턴을 그대로 따르는 것이 아니라 상황에 맞는 최적의 설계를 ‘선택’하고 ‘집중’하는 능력에 있다.
지금 우리가 작성하는 코드가 정말 비즈니스에 유연함을 더하고, 동료와의 협업을 편하게 만드는 코드인지 끊임없이 자문해봐야 한다. 무조건적인 아키텍처 도입은 정답이 아니다. 상황에 맞는 코드가 비즈니스적으로 가장 유리하다.
Comments