MVVM, 클린 아키텍처

박진·2026년 1월 27일

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


오늘의 공부 내용 이미지 💫


구분MVVM클린 아키텍처+MVVM
ViewModel의 역할비지니스 로직+데이터요청+상태 관리상태관리 및 Use Case 호출만
비지니스 로직ViewModel 내부에 존재함Use case로 완전 분리됨
데이터 소스 변경ViewModel 코드를 다 수정해줘야함Data 계층만 수정, ViewModel은 영향 없음
의존성 방향ViewModel -> RepositoryAPI외부 -> 내부만 수정

🛸 클린 아키텍처

소프트웨어의 핵심 로직을 UI, 데이터베이스, 프레임워크와 같은 외부 요소로부터 완전히 분리하여 어떤 환경에서도 독립적으로 동작 가능하고 테스트가 가능한 구조로 만드는 것이 핵심이다 🌟

🔹 핵심 구조

핵심 구조는 전에 팀프로젝트로 진행한 DoranDoran APP으로 기억하기 쉽도록 정리해봤다

1. Entities : 순수 데이터 본질

가장 안쪽 계층이고 앱의 핵심 데이터 구조

  • 예시 : ChatMessage 클래스
  • 내용 : 메시지 보낸 내용, 보낸 사람 ID, 전송 시간 등 아주 기본적인 정보만 담는 곳
  • 특징 : 이 코드는 Firebase를 쓰든 내 서버를 쓰든 절대 변하지 않는 순수한 Dart 클래스
class ChatMessage {
  ChatMessage ({
   required this.id,
   required this.text,
   required this.creatAt,
   });
   
   final String id;
   final String text;
   final DateTime creatAt;
   }

2. Use Case : 구체적인 기능 로직

사용자가 하려는 '행동' 그 자체 정의하는 것

  • 예시 : SendMessage
  • 내용 : 사용자가 앱에서 수행하려는 구체적인 행위(비지니스 로직)를 정의한다.
  • 특징 : 데이터를 어떻게 가져오는지 모르고, 단지 저장하라는 명령만 내린다. 엔티티를 사용해서 앱의 기능을 수행하며 데이터가 어떻게 흐를지 결정한다.
class SendMessage {
  SendMessage(this.repository);
  
  final ChatRepository repository;
  
  Future<void> execute(String text) async {
    if (text.isEmpty) return;
    await repository.saveMessage(text);
    }
  }

3. Interface Adapters : MVVM의 ViewModel

내부(Use Case)와 외부(UI/DB)를 연결해주는 통역사

  • 예시 : ChatViewModel
  • 내용 : 사용자가 화면에서 버튼을 누르면 유즈케이스를 실행시키고, 결과가 오면 화면에 보여줄 '상태'를 바꾼다
  • 기존 MVVM과 연결 : 이 계층이 MVVM의 ViewModel과 Repository 인터페이스가 위치하는 곳
  • 주로 사용 및 역할 : Riverpod, Provider, 등 여기서 상태 관리를 담당함. 외부 데이터(JSON 등)를 내부에서 사용하기 좋은 엔티티 형태로 바꾸거나 그 반대의 역할을 수행한다.

4. Frameworks & Drivers : 실제 기술 도구

가장 바깥 쪽으로 실제 화면을 그리고 데이터를 전송하는 도구

  • 예시 : ChatView(UI), FirebaseChatRepository (구체적인 DB 구현), HTTP 클라이언트 등 실제 도구들이 위치함
  • 내용 : Flutter의 ListView로 채팅창을 그리거나, Firebase SDK를 사용해 데이터를 실제로 날리는 코드가 위치한다.
  • 특징 : 언제든 교체될 수 있는 영역이다.

🛩 MVVM과 클린 아키텍처 🛸

기존의 MVVM은 주로 화면(UI)를 어떻게 깔끔하게 관리할까에 집중했다면, 클린 아키텍처는 앱 전체의 코드가 Framework에 오염되지 않고 어떻게 깔끔하게 격리할까? 에 집중한다.

🔸 ViewModel의 일거리

기존 MVVM에서는 ViewModel이 API를 호출하고, 데이터를 가공하고, 에러를 처리하는 등 너무 많은 일을 했다면 클린 아키텍처를 통해 이 복잡한 로직을 Use Case가 가져간다.

  • 기존 : ViewModel ->API 호출 -> 데이터 변환 -> 화면 갱신
  • 이후 : ViewModel -> Use Case 실행 -> 결과 받아서 화면 갱신

🔸 기존 MVVM 방식 (ViewModel이 모든 일을 할 때)

기존에는 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의 코드를 통째로 고쳐야함

🔸 클린 아키텍처 + MVVM (역할을 분담할 때)

클린아키텍처를 적용하면 코드가 세 덩어리로 쪼개진다. 무엇을(Entity), 어떻게(Use Case), 어디에(Repository)로 분리하는 것이 핵심!!

① Domain 계층 (가장 안 쪽: 순수로직)

// 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);
  }
}

② Data 계층 (바깥쪽: 실제 기술)

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,
    });
  }
}

③ Presentation 계층 (바깥쪽: MVVM의 ViewModel)

이제 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를 위한 모델을 만드는 것, 클린 아키텍처는 도메인을 중심으로 모든 것을 분리하는 시스템이다!

0개의 댓글