Model - View - ViewModel의 약자로, UI 로직과 비즈니스 로직을 분리하는 디자인 패턴입니다.
┌─────────────────────────────────────┐
│ View (UI Layer) │ ← Activity, Fragment
│ - 사용자에게 보이는 화면 │
│ - 버튼 클릭, 입력 등 사용자 액션 │
└─────────────────────────────────────┘
↕️ 관찰 (Observe)
┌─────────────────────────────────────┐
│ ViewModel (중간 다리) │
│ - UI 로직 처리 │
│ - 데이터 가공 및 상태 관리 │
│ - LiveData로 View에 알림 │
└─────────────────────────────────────┘
↕️ 호출
┌─────────────────────────────────────┐
│ Model (Data Layer) │
│ - Repository: 데이터 출처 관리 │
│ - Entity: 데이터 구조 │
└─────────────────────────────────────┘
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. 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());
// 에러 메시지 표시
}
}
}
View → "화면에 보여주는 것만 신경 쓴다"
ViewModel → "UI에 필요한 데이터 처리만 신경 쓴다"
Model → "데이터 가져오기/저장만 신경 쓴다"
// 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());
}
Activity가 파괴되어도 → ViewModel은 살아있음
화면 회전해도 → ViewModel의 데이터는 유지됨
// 같은 ViewModel을 여러 Activity/Fragment에서 사용 가능
LoginViewModel viewModel = new LoginViewModel(repository);
// LoginActivity에서 사용
// LoginFragment에서도 사용
// QuickLoginDialog에서도 사용
| 특징 | MVC | MVP | MVVM |
|---|---|---|---|
| View-Logic 연결 | Controller가 직접 | Presenter가 직접 | LiveData로 자동 |
| View 참조 | Controller가 View 참조 | Presenter가 View 참조 | ViewModel은 View 모름 |
| 테스트 용이성 | 어려움 | 가능 | 쉬움 |
| 생명주기 관리 | Activity가 직접 | 복잡함 | AAC가 자동 처리 |
| 양방향 바인딩 | 없음 | 없음 | 있음 (DataBinding) |
User → View → Controller → Model
↓
View 업데이트
User → View → Presenter → Model
↑ ↓
└────────┘ (양방향 참조)
User → View → ViewModel → Model
↓ Observe
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());
}
});
구글은 MVVM을 쉽게 구현할 수 있도록 도구를 제공합니다:
┌──────────────────────────────────────┐
│ UI Layer (Activity/Fragment) │
│ - DataBinding으로 ViewModel 연결 │
└──────────────────────────────────────┘
↕️ LiveData
┌──────────────────────────────────────┐
│ ViewModel (AAC ViewModel) │
│ - LiveData로 상태 관리 │
│ - 생명주기 자동 처리 │
└──────────────────────────────────────┘
↕️
┌──────────────────────────────────────┐
│ Repository │
│ - 데이터 출처 통합 관리 │
└──────────────────────────────────────┘
↙️ ↘️
┌──────────────┐ ┌──────────────┐
│ Remote Data │ │ Local Data │
│ (Retrofit) │ │ (Room) │
└──────────────┘ └──────────────┘
// 나쁜 예: Activity에 모든 게 섞여있음
class BadLoginActivity {
// UI 코드
// 네트워크 코드
// 검증 코드
// 상태 관리 코드
// ... 2000줄
}
// 좋은 예: 각자 역할 분리
class LoginActivity { // 100줄 (UI만)
}
class LoginViewModel { // 100줄 (UI 로직)
}
class LoginRepository { // 50줄 (데이터)
}
담당자 A → View (UI 디자인)
담당자 B → ViewModel (로직 구현)
담당자 C → Repository (API 연동)
각자 독립적으로 작업 가능!
// 로그인 로직 테스트 (Activity 없이도 가능!)
@Test
public void testLoginLogic() {
LoginViewModel vm = new LoginViewModel(mockRepo);
vm.login("test@email.com", "pass");
assertTrue(vm.isLoginSuccess());
}