본문 바로가기
안드로이드 자바

[Java][Android]ViewModel, LiveData, Repository 순서 추적하기

by teamnova 2025. 8. 14.
728x90

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>

 

시연영상