MVVM 아키텍처란 무엇인가요?
MVVM은 앱의 구성 요소를 세 가지 역할로 명확하게 분리하는 디자인 패턴입니다.
Model: 앱의 데이터와 로직을 담당합니다.
Repository, 데이터베이스(Room), 네트워크 API(Retrofit) 등이 여기에 해당합니다.
Model은 View나 ViewModel의 존재를 전혀 알지 못합니다.
View: 사용자 인터페이스(UI)를 담당합니다.
Activity, Fragment, XML 레이아웃 등이 여기에 해당합니다.
View는 사용자의 입력을 받아 ViewModel에 전달하고, ViewModel의 데이터 변경을 관찰(Observing)하여 UI를 업데이트할 뿐, 직접적인 로직 처리는 하지 않습니다.
ViewModel: View와 Model 사이의 중개자 역할을 합니다.
View에 표시될 데이터를 관리하고, UI 관련 로직을 처리합니다.
ViewModel은 Model에 데이터를 요청하고, View가 사용할 수 있는 형태로 만듭니다.
ViewModel은 View의 생명주기(Lifecycle)를 인지하므로, 화면 회전과 같은 구성 변경에도 데이터를 유지할 수 있습니다.
왜 MVVM 패턴을 사용해야 할까요?
관심사의 분리 : UI 로직과 비즈니스 로직이 명확하게 분리되어 코드의 가독성과 재사용성이 높아집니다.
생명주기 관리: ViewModel은 Activity/Fragment의 생명주기와 분리되어, 화면 회전 시에도 데이터를 유지하여 불필요한 API 호출 등을 방지합니다.
테스트 용이성: View(UI)에 대한 의존성이 없는 ViewModel은 단위 테스트(Unit Test)를 작성하기 매우 용이합니다.
유지보수성 향상: 각 컴포넌트의 역할이 명확하여 코드를 수정하거나 새로운 기능을 추가하기 쉬워집니다.
앱을 실행시켜 로그를 확인해보겠습니다.
예제코드
dependencies {
def lifecycle_version = "2.7.0" // 최신 안정 버전 확인
implementation libs.lifecycle.viewmodel
implementation libs.lifecycle.livedata
}
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
/**
* [View] - 사용자 인터페이스(UI)를 담당하는 부분입니다.
* - 사용자의 입력을 받아 ViewModel에 전달합니다.
* - ViewModel의 데이터 변경을 관찰(Observing)하여 UI를 업데이트합니다.
* - 직접적인 데이터 처리 로직을 포함하지 않도록 노력해야 합니다.
*/
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MVVM_Pattern";
private UserViewModel userViewModel;
private TextView nameTextView;
private ProgressBar progressBar;
/**
* --- 실행 순서 1 ---
* Activity가 생성될 때 가장 먼저 호출됩니다.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.d(TAG, "[View] MainActivity onCreate() 호출됨.");
// UI 위젯 초기화
nameTextView = findViewById(R.id.name_textview);
progressBar = findViewById(R.id.progress_bar);
// --- 실행 순서 2 ---
// ViewModelProvider를 통해 UserViewModel의 인스턴스를 가져옵니다.
// - 만약 이미 생성된 ViewModel이 있다면 그것을 반환하고,
// - 없다면 새로 생성합니다. (이때 UserViewModel의 생성자가 호출됩니다)
Log.d(TAG, "[View] ViewModelProvider를 통해 ViewModel 인스턴스 요청.");
userViewModel = new ViewModelProvider(this).get(UserViewModel.class);
// --- 실행 순서 3 ---
// ViewModel의 LiveData를 관찰(observe)하도록 설정합니다.
// 이제 LiveData의 값이 변경될 때마다 아래의 콜백이 자동으로 호출됩니다.
Log.d(TAG, "[View] ViewModel의 userName LiveData 관찰 시작.");
userViewModel.getUserName().observe(this, newName -> {
// --- 실행 순서 5 (2초 후) ---
// Repository에서 데이터 로딩이 완료되고 LiveData의 값이 변경되면 이 부분이 실행됩니다.
Log.d(TAG, "[View] LiveData 값 변경 감지! UI 업데이트 시작. 새로운 이름: " + newName);
progressBar.setVisibility(View.GONE);
nameTextView.setText(newName);
});
// --- 실행 순서 4 ---
// 초기 UI 상태를 설정합니다.
// 데이터가 아직 로드되지 않았으므로 로딩 상태를 표시합니다.
progressBar.setVisibility(View.VISIBLE);
nameTextView.setText("Loading...");
Log.d(TAG, "[View] 초기 UI 설정 완료 (로딩 중).");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "[View] MainActivity onStart()");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "[View] MainActivity onResume()");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "[View] MainActivity onPause()");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "[View] MainActivity onStop()");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "[View] MainActivity onDestroy()");
// Activity가 완전히 종료되면, 연결된 ViewModel의 onCleared()가 호출됩니다.
}
}
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
/**
* [Model] - 데이터 소스를 관리하고 비즈니스 로직을 처리하는 부분입니다.
* - 네트워크 API 호출, 로컬 데이터베이스(Room) 접근 등의 역할을 합니다.
* - ViewModel에게 필요한 데이터를 제공합니다.
* - 이 클래스는 ViewModel이나 View의 존재를 전혀 알지 못합니다.
*/
public class UserRepository {
private static final String TAG = "MVVM_Pattern";
private static UserRepository instance;
private final MutableLiveData<String> userName = new MutableLiveData<>();
// 싱글톤 패턴으로 구현 (앱 전체에서 하나의 Repository 인스턴스만 사용)
public static synchronized UserRepository getInstance() {
if (instance == null) {
instance = new UserRepository();
Log.d(TAG, "[Repository] UserRepository 인스턴스 생성 (싱글톤)");
}
return instance;
}
/**
* ViewModel이 사용자 이름을 요청할 때 호출되는 메서드입니다.
* 이 메서드는 LiveData 객체를 즉시 반환하고, 데이터 로딩은 비동기적으로 수행합니다.
*/
public LiveData<String> getUserName() {
Log.d(TAG, "[Repository] getUserName() 호출됨. 데이터 로딩 시작.");
fetchUserData(); // 비동기 데이터 로딩 시작
return userName; // 데이터가 채워지기 전의 빈 LiveData 객체를 먼저 반환
}
/**
* 실제 데이터 로딩을 시뮬레이션하는 메서드입니다.
* 여기서는 2초 후에 데이터를 생성하여 LiveData에 값을 설정합니다.
*/
private void fetchUserData() {
// 백그라운드 스레드에서 네트워크 요청을 한다고 가정
new Handler(Looper.getMainLooper()).postDelayed(() -> {
Log.d(TAG, "[Repository] 2초 후 데이터 로딩 완료. LiveData 값 업데이트 시도.");
// LiveData의 값을 업데이트합니다.
// postValue()는 백그라운드 스레드에서 안전하게 값을 변경할 수 있습니다.
userName.postValue("Android Developer");
Log.d(TAG, "[Repository] LiveData에 'Android Developer' 값 postValue 완료.");
}, 2000);
}
}
import androidx.lifecycle.LiveData;
import androidx.lifecycle.ViewModel;
import android.util.Log;
/**
* [ViewModel] - View와 Model 사이의 중개자 역할을 합니다.
* - UI 관련 데이터를 관리하고, View의 생명주기로부터 데이터를 안전하게 보호합니다.
* - Model(Repository)에 데이터 요청을 위임합니다.
* - View의 직접적인 참조를 가져서는 안 됩니다. (예: Context, Activity 등)
*/
public class UserViewModel extends ViewModel {
private static final String TAG = "MVVM_Pattern";
private final UserRepository userRepository;
private LiveData<String> userName;
/**
* ViewModel이 처음 생성될 때 호출되는 생성자입니다.
* (Activity/Fragment가 처음 생성되거나 재생성될 때 한 번만 호출됩니다)
*/
public UserViewModel() {
Log.d(TAG, "[ViewModel] UserViewModel 생성자 호출됨.");
// Repository 인스턴스를 가져옵니다. (의존성 주입을 사용하는 것이 더 좋습니다)
this.userRepository = UserRepository.getInstance();
Log.d(TAG, "[ViewModel] Repository에 데이터 요청 시작.");
// Repository에 사용자 이름을 요청하여 LiveData를 초기화합니다.
this.userName = userRepository.getUserName();
}
/**
* View(Activity/Fragment)가 관찰(observe)할 LiveData를 제공하는 getter 메서드입니다.
*/
public LiveData<String> getUserName() {
Log.d(TAG, "[ViewModel] View에서 getUserName() LiveData 요청.");
return userName;
}
/**
* ViewModel이 파괴될 때 호출됩니다. (Activity/Fragment가 완전히 종료될 때)
* 여기서 리소스 정리 작업을 수행할 수 있습니다.
*/
@Override
protected void onCleared() {
super.onCleared();
Log.d(TAG, "[ViewModel] onCleared() 호출됨. ViewModel 파괴.");
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/name_textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="User Name" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/name_textview"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
android:visibility="gone"
tools:visibility="visible"/>
</androidx.constraintlayout.widget.ConstraintLayout>
시연영상
'안드로이드 자바' 카테고리의 다른 글
| [Java][Android] 안드로이드 앱에서 특정 값을 전역으로 사용하기: BuildConfig (2) | 2025.08.02 |
|---|---|
| [Java][Android] Color.rgb() 메서드 사용해보기 (1) | 2025.07.31 |
| [Java][Android] Shared Element Transition 사용하여 화면전환 (2) | 2025.07.21 |
| [Java][Android] AnimationDrawable 로 간단한 프레임 애니메이션 만들기 (1) | 2025.07.20 |
| [Java][Android] GestureDetector로 제스처 감지하기 (1) | 2025.07.19 |