2026.01.27 (화)
MVVM / 클린 아키텍처
의존 역전 원칙

| 구분 | MVVM | 클린 아키텍처+MVVM |
|---|---|---|
| ViewModel의 역할 | 비지니스 로직+데이터요청+상태 관리 | 상태관리 및 Use Case 호출만 |
| 비지니스 로직 | ViewModel 내부에 존재함 | Use case로 완전 분리됨 |
| 데이터 소스 변경 | ViewModel 코드를 다 수정해줘야함 | Data 계층만 수정, ViewModel은 영향 없음 |
| 의존성 방향 | ViewModel -> RepositoryAPI | 외부 -> 내부만 수정 |
소프트웨어의 핵심 로직을 UI, 데이터베이스, 프레임워크와 같은 외부 요소로부터 완전히 분리하여 어떤 환경에서도 독립적으로 동작 가능하고 테스트가 가능한 구조로 만드는 것이 핵심이다 🌟
핵심 구조는 전에 팀프로젝트로 진행한 DoranDoran APP으로 기억하기 쉽도록 정리해봤다
가장 안쪽 계층이고 앱의 핵심 데이터 구조
ChatMessage 클래스class ChatMessage {
ChatMessage ({
required this.id,
required this.text,
required this.creatAt,
});
final String id;
final String text;
final DateTime creatAt;
}
사용자가 하려는 '행동' 그 자체 정의하는 것
SendMessage class SendMessage {
SendMessage(this.repository);
final ChatRepository repository;
Future<void> execute(String text) async {
if (text.isEmpty) return;
await repository.saveMessage(text);
}
}
내부(Use Case)와 외부(UI/DB)를 연결해주는 통역사
ChatViewModel가장 바깥 쪽으로 실제 화면을 그리고 데이터를 전송하는 도구
ChatView(UI), FirebaseChatRepository (구체적인 DB 구현), HTTP 클라이언트 등 실제 도구들이 위치함ListView로 채팅창을 그리거나, Firebase SDK를 사용해 데이터를 실제로 날리는 코드가 위치한다.기존의 MVVM은 주로 화면(UI)를 어떻게 깔끔하게 관리할까에 집중했다면, 클린 아키텍처는 앱 전체의 코드가 Framework에 오염되지 않고 어떻게 깔끔하게 격리할까? 에 집중한다.
기존 MVVM에서는 ViewModel이 API를 호출하고, 데이터를 가공하고, 에러를 처리하는 등 너무 많은 일을 했다면 클린 아키텍처를 통해 이 복잡한 로직을 Use Case가 가져간다.
ViewModel ->API 호출 -> 데이터 변환 -> 화면 갱신ViewModel -> Use Case 실행 -> 결과 받아서 화면 갱신기존에는 ViewModel이 화면 상태 관리 + 데이터 로직 + 외부 서버 통신을 모두 진행했음, 코드가 한 곳에 모여 있어서 편했지만, 기능이 늘어날수록 ViewModel이 커진다는 문제점이 있다.
class ChatViewModel extends StateNotifier<ChatState> {
final FirebaseFirestore _firestore = FirebaseFirestore.instance; // 직접 외부에 의존
// 메시지 전송 로직이 ViewModel 안에 직접 들어있음
Future<void> sendMessage(String text, String userId) async {
if (text.isEmpty) return; // 비즈니스 로직 1 (빈 메시지 체크)
try {
// 비즈니스 로직 2 (데이터 구조 생성 및 저장)
await _firestore.collection('messages').add({
'text': text,
'senderId': userId,
'createdAt': DateTime.now(),
});
} catch (e) {
state = state.copyWith(errorMessage: "전송 실패!");
}
}
}
예시) Firebase가 아닌 다른 DB로 바꾼다면?...
ChatViewModel의 코드를 통째로 고쳐야함
클린아키텍처를 적용하면 코드가 세 덩어리로 쪼개진다. 무엇을(Entity), 어떻게(Use Case), 어디에(Repository)로 분리하는 것이 핵심!!
// 1. Entity: 메시지의 본질
class Message {
Message({
required this.text,
required this.senderId,
required this.time
});
final String text;
final String senderId;
final DateTime time;
}
// 2. Use Case: 메시지 전송이라는 '행위' 그 자체
class SendMessageUseCase {
final ChatRepository repository; // '도구'가 아닌 '기능'의 이름에 의존
SendMessageUseCase(this.repository);
Future<void> execute(String text, String userId) async {
if (text.isEmpty) return; // 비즈니스 로직은 여기서 처리!
final message = Message(text: text, senderId: userId, time: DateTime.now());
await repository.saveMessage(message);
}
}
Firebase를 쓰든 로컬 DB로 쓰든 여기서 구현한다
// 3. Repository Implementation: 실제 Firebase로 저장하는 상세 방법
class FirebaseChatRepository implements ChatRepository {
@override
Future<void> saveMessage(Message msg) async {
await FirebaseFirestore.instance.collection('messages').add({
'text': msg.text,
'senderId': msg.senderId,
'time': msg.time,
});
}
}
이제 ViewModel은 유즈케이스를 호출만하고 화면에 보여줄 상태만 관리한다.
// 4. ViewModel: 많이 깔끔해짐
class ChatViewModel extends StateNotifier<ChatState> {
final SendMessageUseCase _sendMessageUseCase; // 유즈케이스를 주입받음
ChatViewModel(this._sendMessageUseCase) : super(ChatState());
Future<void> onSendPressed(String text, String userId) async {
// ViewModel은 "전송해줘!"라고 시키기만 함
await _sendMessageUseCase.execute(text, userId);
}
}
MVVM은 View를 위한 모델을 만드는 것, 클린 아키텍처는 도메인을 중심으로 모든 것을 분리하는 시스템이다!