[안드로이드] 빈혈 도메인 모델과 쓸모없는 유스케이스, 그리고 비대한 뷰모델
안드로이드 개발을 하다보면 "코드가 이상한데 .. 뭐가 이상한거지? 어디를 어떻게, 뭘 기준으로 손 봐야하지?"라는 생각이 들 때가 많습니다.
특히 도메인 모델, 유스케이스, 뷰모델 같은 구조적인 문제에 있어서 그런 생각이 많이 드는 것 같아요.
이번 글은 그러한 고민들을 바탕으로 참고 자료를 찾아보다 발견한 드로이드나이츠 강의 - 빈혈 도메인 모델과 쓸모없는 유스케이스 그리고 비대한 뷰모델에 대해 생각해보기
를 보고 정리한 글입니다. 구조적 문제들을 중심으로, 객체지향 프로그래밍의 핵심 원리를 다시 살펴보면서 더 나은 아키텍처 설계를 위한 방향성을 정리해보았습니다.
참고로 드로이드나이츠란 대한민국 최대 안드로이드 개발자들만을 위한 컨퍼런스입니다!
여기 참고할 자료, 영상 많고 좋아요 .. 다들 정리 많이 해주길 ㅎㅎ .. .. 기다릴게요
객체지향 프로그래밍의 핵심 개념 다시 보기
우선 본론으로 들어가기 전에 객체지향 프로그래밍(OOP)에 대해 먼저 간단히 짚고 넘어가겠습니다.
1. 캡슐화
- 객체는 데이터와 데이터를 조작하는 메서드가 함께 존재해야 합니다.
- 외부에서 직접 데이터를 조작하지 못하도록, 메서드(getter, setter)를 통해서만 접근하도록 설계합니다.
ex) 은행 계좌 클래스의 잔액은 private / 입금, 출금 메서드로만 잔액을 조작
2. 데이터 은닉
- 캡슐화의 연장선에 있는 개념으로, 내부 구현을 감춥니다.
- 접근 제어자(private, protected 등)을 활용해 외부 접근을 제한합니다.
- 필요 시 공개해야하는 데이터는 public 메서드를 통해 간접적으로 접근하도록 설정합니다.
ex) 자동차 클래스에서 엔진 내부 작동 방식은 감추고, 사용자는 startEngine() 같은 메서드만 호출 가능합니다.
3. 데이터 추상화
- 객체의 복잡한 내부 구현을 숨기고, 사용자에게 꼭 필요한 정보만 제공하는 개념입니다.
- 추상화를 통해 사용자는 복잡한 내부 로직이나 구현 방법을 몰라도 클래스 사용이 가능합니다.
ex) TV의 리모컨 버튼을 누르면 TV가 켜지지만, 내부적으로 전자 회로가 어떻게 작동하는지는 사용자가 알 필요가 없어요.
4. 다형성
- 하나의 인터페이스를 통해 서로 다른 구현을 가질 수 있는 객체들을 다룰 수 있는 개념입니다.
- 같은 이름의 메서드나 연산자가 여러 형태로 동작 가능한 것을 의미하며,
👉 메서드 오버로딩: 같은 이름의 메서드를 매개변수에 따라 정의할 수 있습니다.
메서드 오버라이딩: 상속받은 메서드를 하위 클래스에서 재정의하여 동작을 변경할 수 있습니다.
ex) 동물 클래스의 소리내기() 메서드는 하위 클래스인 개, 고양이에서 각각 멍멍, 야옹으로 동작을 다르게 구현 가능할 수 있습니다.
위와 같이 객체지향 프로그래밍의 주요개념에 대해서 알아 보았는데요, 이번 글에서는 캡슐화를 위주로 다뤄볼게요.
그럼 이제 개발하면서 겪는 구조적 문제점들을 하나씩 봅시다.
문제 1: 빈혈 도메인 모델
빈혈 도메인이란 무엇인가?
겉으로 보기에는 다른 도메인 모델처럼 다른 구조들과 관계를 가지고 있지만, 행위가 없는 도메인 모델을 의미합니다.
객체인데 행위가 없고, 끽해야 getter랑 setter만 있는 단순한 데이터 컨테이너에 불과한, 가방 코드인 셈이죠.
👉 즉, 도메인 모델에 행위가 없음을 의미
문제 2: 쓸모 없는 유스케이스
단순히 repository의 메서드를 호출하고 끝나는 usecase를 비즈니스 로직을 가지고 있다고 말할 수 있을까요?
이런 구조는 비즈니스 로직을 서버에만 위임하는 아키텍처에서 주로 발생합니다.
또, "usecase에 로직이 없으니 안만든다? 이건 또 코드 일관성에 어긋난다" 라는 생각에도 발생합니다.
문제 3: 비대한 뷰모델
요구사항이 있을 때 이 로직은 ViewModel에 구현할지, UseCase에 둘지, Data Class로 정리할지, 아니면 아예 서버에서 구현할지 .. 고민이 참 많습니다.
로직이 어디에 위치해야 할지 명확하지 않을 때, 많은 로직이 주로 ViewModel로 몰리게 됩니다.
이는 단일 책임 원칙(SRP)를 위반하는 대표적인 예죠.
결과적으로 ViewModel은 과도하게 커지거나 복잡해집니다.
👉 가독성과 유지보수성이 급격히 떨어지게 됨.
위와 같이 문제들을 짚었을 때, 각각의 문제에서 생각해볼 요소들이 있죠?
우선, 우리는 ViewModel의 책임을 다시 한번 생각해볼 필요가 있습니다.
ViewModel의 책임
ViewModel은 UI 관련 로직을 처리하고 데이터를 관리하는 것이 주요 역할이에요.
따라서, 도메인 로직까지 포함하게 된다면, 불필요하게 비대해지며, 테스트도 어려워지게 되겠죠.
ViewModel은 View의 Model로, UI에 필요한 데이터를 준비하는데 집중해야 합니다.
그럼 이제 도메인 모델과 유스케이스의 역할에 대해서도 생각해봅시다.
- 도메인 모델과 유스케이스는 동일한 책임을 가질 수 있지만, 관점이 다릅니다.
도메인 모델의 책임
- 하나의 객체에 집중된 비즈니스 로직을 스스로 처리해야 합니다.
예를 들어 Order 객체는 cancel() 같은 메서드를 통해 자신의 상태를 바꾸는 책임을 가져야 하며, 데이터만 담긴 구조체가 되어선 안 됩니다.
객체지향의 캡슐화 원칙을 지키기 위한 핵심이죠.
유스케이스의 책임
- 여러 도메인 모델이 협력하는 시나리오나, 사용자의 요청 흐름을 처리하는 역할입니다.
유스케이스는 사용자(Actor)의 관점에서 동작하며, 내부 구현보다는 절차적인 흐름에 집중을 하는거죠.
예를 들어, PlaceOrderUseCase는 Order, Inventory, Payment 모델들을 조합해 하나의 시나리오를 완성합니다.
유스케이스는 여러 도메인 모델의 협력을 조정하며, 사용자의 명령을 처리하는 절차적 계층입니다.
👉 작고 명확한 책임은 도메인 모델에, 복잡한 흐름과 모델 간 조율은 유스케이스에 담당시키는 것을 추천합니다.
위와 같이 정리를 하긴했지만, 사실은 모든 조직과 프로젝트에 적용 가능한 단 하나의 정확한 정답은 없다고 합니다.
어떨 때는 도메인에, 어떨 때는 유스케이스에 책임을 분배하며 각자의 제품과 상황에 맞는 가이드라인을 정하여 개발을 진행하는 것을 추천하는데요.
예를들어,
카카오 지그재그 앱의 아키텍처 가이드라인으로는
- 프레젠테이션 레이어에 도메인 로직을 구현하지 않기
-> 도메인 로직이 무엇인지에 대한 정확한 정의가 필요함
ViewModel에는
1. 데이터를 뷰에 표현하거나
2. 사용자 상호작용을 위한
로직만을 구현함.
또, 뷰에 표현되어야 하는 UI 상태를 기억해야 함. - 단일 도메인 모델에 대한 비즈니스 로직은 도메인 모델이 책임지도록 함.
도메인 모델은 스스로 필요한 로직을 구현해야 함.
빈혈 도메인 모델을 피하기 위해서 정의함.
ViewModel에서 도메인 모델을 직접 다루는 로직을 구현해서는 안됨.
로직 구현 우선 순위는
1. 도메인 모델
2. 유스케이스(로직이 도메인 모델에 책임이 없다고 느낄때만) - 여러 도메인 모델에 대한 비즈니스 로직은 유스케이스가 해결할 수 있음
도메인 로직은 아래 기준에 따라 유스케이스로 구현됨
- 여러 도메인 모델이 참조되는 복잡한 비즈니스 로직
- 프레젠테이션 레이어에서 직접 데이터 레이어를 참조하지 않도록 하는 wrapper 레이어 조직
-> 특별한 비즈니스 로직이 추가되어 있지 않다면, SAM 인터페이스로 구현
-> SAM이란? 단 하나의 추상 메서드만 가지는 인터페이스
위와 같이 각 구조의 책임과 역할에 대해 정리를 해보았는데요.
정리를 해보았지만, 아직 명확히 어떻게 분리를 해야겠다 라는 생각은 잘 안드실겁니다.
왜냐하면 사실 이 영상의 결론은
정답은 없지만, 고민해야 한다.
이기 때문이에요.
뭔가 허무한 결론이지만, 이 영상이 주고자 하는 핵심 메시지는 바로 생각해보기에요.
도메인 모델, 유스케이스, 뷰모델이 각각 어떤 책임을 져야 하는지에 대한 정답은 없습니다. 하지만 객체지향의 원칙을 생각하며, 빈혈 도메인 모델, 비대한 뷰모델, 쓸모없는 유스케이스를 피하기 위해 각 레이어의 책임을 명확히 분리하려는 노력은 필요합니다. 이 노력 자체가 아키텍처의 품질을 결정한다고 생각해요.
위 글을 읽고 책임 분리에 대해 생각해보며, 각자의 조직, 프로덕트, 팀 문화에 맞는 명확한 가이드라인을 만드는 과정을 모두 가져보셨으면 좋겠습니다.