[디자인 패턴] 구글 아키텍처 - MVVM 패턴 이해하기

Ju-unn·2026년 5월 23일

1. MVVM이란?

정의

Model - View - ViewModel의 약자로, UI 로직과 비즈니스 로직을 분리하는 디자인 패턴입니다.

MVVM 구조

┌─────────────────────────────────────┐
│         View (UI Layer)          │  ← Activity, Fragment
│  - 사용자에게 보이는 화면           │
│  - 버튼 클릭, 입력 등 사용자 액션    │
└─────────────────────────────────────┘
              ↕️ 관찰 (Observe)
┌─────────────────────────────────────┐
│      ViewModel (중간 다리)        │
│  - UI 로직 처리                   │
│  - 데이터 가공 및 상태 관리         │
│  - LiveData로 View에 알림         │
└─────────────────────────────────────┘
              ↕️ 호출
┌─────────────────────────────────────┐
│      Model (Data Layer)          │
│  - Repository: 데이터 출처 관리    │
│  - Entity: 데이터 구조            │
└─────────────────────────────────────┘

2. MVVM이 탄생한 배경

기존 방식의 문제점 (MVC)

MVC 구조

View ↔ Controller ↔ Model

문제점

// Activity에 모든 게 섞여있는 코드 (안 좋은 예)
public class LoginActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);
        
        EditText emailInput = findViewById(R.id.emailInput);
        EditText passwordInput = findViewById(R.id.passwordInput);
        Button loginButton = findViewById(R.id.loginButton);
        
        loginButton.setOnClickListener(v -> {
            // 1. UI 처리
            String email = emailInput.getText().toString();
            String password = passwordInput.getText().toString();
            
            // 2. 검증 로직
            if (email.isEmpty()) {
                Toast.makeText(this, "이메일을 입력하세요", Toast.LENGTH_SHORT).show();
                return;
            }
            
            // 3. 네트워크 요청
            new Thread(() -> {
                // 서버 통신
                boolean success = callLoginApi(email, password);
                
                // 4. UI 업데이트 (메인 스레드에서!)
                runOnUiThread(() -> {
                    if (success) {
                        Toast.makeText(this, "로그인 성공", Toast.LENGTH_SHORT).show();
                        // 다음 화면으로 이동
                    } else {
                        Toast.makeText(this, "로그인 실패", Toast.LENGTH_SHORT).show();
                    }
                });
            }).start();
        });
    }
    
    private boolean callLoginApi(String email, String password) {
        // 서버 통신 코드...
        return true;
    }
}

이런 코드의 문제

  1. Activity가 너무 뚱뚱함 (God Object)
    • UI 처리, 검증, 네트워크, 상태 관리 모두 Activity가 함
  2. 테스트 불가능
    • Activity 없이는 로그인 로직을 테스트할 수 없음
  3. 생명주기 문제
    • 화면 회전하면 데이터 날아감
  4. 재사용 불가능
    • 다른 화면에서 같은 로직을 쓰려면 코드 복사해야 함

3. MVVM의 해결책

MVVM 방식

// 1. Model: 데이터 구조
public class LoginRequest {
    private String email;
    private String password;
    
    public LoginRequest(String email, String password) {
        this.email = email;
        this.password = password;
    }
    
    public String getEmail() { return email; }
    public String getPassword() { return password; }
}
 
public class LoginResponse {
    private boolean success;
    private String message;
    private String userId;
    
    public LoginResponse(boolean success, String message, String userId) {
        this.success = success;
        this.message = message;
        this.userId = userId;
    }
    
    public boolean isSuccess() { return success; }
    public String getMessage() { return message; }
    public String getUserId() { return userId; }
}
 
// 2. Repository: 데이터 출처 관리
public interface LoginRepository {
    LoginResponse login(LoginRequest request);
}
 
public class LoginRepositoryImpl implements LoginRepository {
    
    @Override
    public LoginResponse login(LoginRequest request) {
        // 실제로는 Retrofit 등으로 서버 통신
        // 여기서는 예시로 성공 응답 반환
        
        // 간단한 검증: 이메일에 @가 있고 비밀번호가 6자 이상이면 성공
        if (request.getEmail().contains("@") && 
            request.getPassword().length() >= 6) {
            return new LoginResponse(true, "로그인 성공", "user123");
        } else {
            return new LoginResponse(false, "로그인 실패", null);
        }
    }
}
 
// 3. ViewModel: UI 로직 처리
public class LoginViewModel {
    private LoginRepository repository;
    
    // 로그인 상태를 저장하는 LiveData (실제 안드로이드에서는 LiveData 사용)
    private String loginStatusMessage = "";
    private boolean isLoginSuccess = false;
    
    public LoginViewModel(LoginRepository repository) {
        this.repository = repository;
    }
    
    // View(Activity)가 호출할 메서드
    public void login(String email, String password) {
        // 1. 입력값 검증
        if (email == null || email.isEmpty()) {
            loginStatusMessage = "이메일을 입력하세요";
            isLoginSuccess = false;
            notifyView(); // View에 알림
            return;
        }
        
        if (password == null || password.isEmpty()) {
            loginStatusMessage = "비밀번호를 입력하세요";
            isLoginSuccess = false;
            notifyView();
            return;
        }
        
        // 2. Repository를 통해 로그인 요청
        LoginRequest request = new LoginRequest(email, password);
        LoginResponse response = repository.login(request);
        
        // 3. 결과 처리
        isLoginSuccess = response.isSuccess();
        loginStatusMessage = response.getMessage();
        notifyView();
    }
    
    // View에 상태 변경 알림 (실제로는 LiveData가 자동으로 해줌)
    private void notifyView() {
        System.out.println("[ViewModel] 상태 변경 알림: " + loginStatusMessage);
    }
    
    // View가 상태를 확인하는 메서드
    public String getLoginStatusMessage() {
        return loginStatusMessage;
    }
    
    public boolean isLoginSuccess() {
        return isLoginSuccess;
    }
}
 
// 4. View: UI 처리만
public class LoginActivity {
    private LoginViewModel viewModel;
    
    public void onCreate() {
        // ViewModel 생성
        LoginRepository repository = new LoginRepositoryImpl();
        viewModel = new LoginViewModel(repository);
        
        // UI 초기화는 여기서...
    }
    
    // 버튼 클릭 시
    public void onLoginButtonClick(String email, String password) {
        // ViewModel에 위임
        viewModel.login(email, password);
        
        // 결과 확인 (실제로는 LiveData를 observe)
        if (viewModel.isLoginSuccess()) {
            System.out.println("✅ " + viewModel.getLoginStatusMessage());
            // 다음 화면으로 이동
        } else {
            System.out.println("❌ " + viewModel.getLoginStatusMessage());
            // 에러 메시지 표시
        }
    }
}

4. MVVM의 장점

1. 관심사의 분리 (Separation of Concerns)

View      → "화면에 보여주는 것만 신경 쓴다"
ViewModel → "UI에 필요한 데이터 처리만 신경 쓴다"
Model     → "데이터 가져오기/저장만 신경 쓴다"

2. 테스트 용이성

// ViewModel은 Activity 없이도 테스트 가능!
@Test
public void testLogin_Success() {
    // Given (준비)
    LoginRepository mockRepository = new LoginRepositoryImpl();
    LoginViewModel viewModel = new LoginViewModel(mockRepository);
    
    // When (실행)
    viewModel.login("test@email.com", "password123");
    
    // Then (검증)
    assertTrue(viewModel.isLoginSuccess());
    assertEquals("로그인 성공", viewModel.getLoginStatusMessage());
}
 
@Test
public void testLogin_EmptyEmail() {
    LoginRepository mockRepository = new LoginRepositoryImpl();
    LoginViewModel viewModel = new LoginViewModel(mockRepository);
    
    viewModel.login("", "password123");
    
    assertFalse(viewModel.isLoginSuccess());
    assertEquals("이메일을 입력하세요", viewModel.getLoginStatusMessage());
}

3. 생명주기 안정성

Activity가 파괴되어도 → ViewModel은 살아있음
화면 회전해도 → ViewModel의 데이터는 유지됨

4. 재사용성

// 같은 ViewModel을 여러 Activity/Fragment에서 사용 가능
LoginViewModel viewModel = new LoginViewModel(repository);
 
// LoginActivity에서 사용
// LoginFragment에서도 사용
// QuickLoginDialog에서도 사용

5. MVC vs MVP vs MVVM 비교

비교표

특징MVCMVPMVVM
View-Logic 연결Controller가 직접Presenter가 직접LiveData로 자동
View 참조Controller가 View 참조Presenter가 View 참조ViewModel은 View 모름
테스트 용이성어려움가능쉬움
생명주기 관리Activity가 직접복잡함AAC가 자동 처리
양방향 바인딩없음없음있음 (DataBinding)

🎬 각 패턴의 흐름

MVC

User → View → Controller → Model
                ↓
              View 업데이트

MVP

User → View → Presenter → Model
         ↑        ↓
         └────────┘ (양방향 참조)

MVVM

User → View → ViewModel → Model
         ↓ Observe
       LiveData (자동 업데이트)

6. MVVM의 핵심: LiveData (관찰자 패턴)

LiveData가 해결하는 문제

// 기존 방식: View가 계속 확인해야 함 (Polling)
while (true) {
    if (viewModel.hasNewData()) {
        updateUI(viewModel.getData());
    }
    Thread.sleep(100);
}
 
// LiveData 방식: 데이터가 변경되면 자동으로 알려줌 (Push)
viewModel.loginResult.observe(this, result -> {
    // 데이터가 변경되면 자동으로 이 코드가 실행됨!
    if (result.isSuccess()) {
        showSuccessMessage();
    } else {
        showErrorMessage(result.getMessage());
    }
});

LiveData의 장점

  1. 자동 UI 업데이트: 데이터 변경 시 UI가 자동으로 갱신
  2. 생명주기 인식: Activity가 백그라운드에 있으면 업데이트 안 함
  3. 메모리 누수 방지: Activity 종료 시 자동으로 구독 해제

7. 구글이 MVVM을 권장하는 이유

구글의 AAC (Android Architecture Components)

구글은 MVVM을 쉽게 구현할 수 있도록 도구를 제공합니다:

  1. ViewModel: 생명주기에 안전한 데이터 저장소
  2. LiveData: 관찰 가능한 데이터 홀더
  3. DataBinding: XML에서 직접 ViewModel 사용
  4. Room: SQLite를 쉽게 사용하는 라이브러리

AAC 전체 구조

┌──────────────────────────────────────┐
│  UI Layer (Activity/Fragment)        │
│  - DataBinding으로 ViewModel 연결     │
└──────────────────────────────────────┘
              ↕️ LiveData
┌──────────────────────────────────────┐
│  ViewModel (AAC ViewModel)           │
│  - LiveData로 상태 관리               │
│  - 생명주기 자동 처리                 │
└──────────────────────────────────────┘
              ↕️
┌──────────────────────────────────────┐
│  Repository                          │
│  - 데이터 출처 통합 관리               │
└──────────────────────────────────────┘
        ↙️              ↘️
┌──────────────┐  ┌──────────────┐
│ Remote Data  │  │ Local Data   │
│ (Retrofit)   │  │ (Room)       │
└──────────────┘  └──────────────┘

8. 실무에서 사용하는 이유

1. 코드 유지보수성

// 나쁜 예: Activity에 모든 게 섞여있음
class BadLoginActivity {
    // UI 코드
    // 네트워크 코드
    // 검증 코드
    // 상태 관리 코드
    // ... 2000줄
}
 
// 좋은 예: 각자 역할 분리
class LoginActivity {        // 100줄 (UI만)
}
class LoginViewModel {       // 100줄 (UI 로직)
}
class LoginRepository {      // 50줄 (데이터)
}

2. 협업 용이성

담당자 A → View (UI 디자인)
담당자 B → ViewModel (로직 구현)
담당자 C → Repository (API 연동)
 
각자 독립적으로 작업 가능!

3. 테스트 자동화

// 로그인 로직 테스트 (Activity 없이도 가능!)
@Test
public void testLoginLogic() {
    LoginViewModel vm = new LoginViewModel(mockRepo);
    vm.login("test@email.com", "pass");
    assertTrue(vm.isLoginSuccess());
}

핵심 정리

MVVM의 핵심 3줄 요약

  1. View: UI만 담당, ViewModel을 관찰
  2. ViewModel: UI 로직 담당, View를 모름
  3. Model: 데이터 담당, UI와 완전히 분리

왜 MVVM을 사용하나?

  • ✅ 관심사의 분리 → 코드가 깔끔해짐
  • ✅ 테스트 용이성 → 버그가 줄어듦
  • ✅ 생명주기 안정성 → 앱이 안정적임
  • ✅ 재사용성 → 개발 속도가 빨라짐
profile
한줄한줄 기록해보자

0개의 댓글