기술면접

[cs] MVVM 예시와 함께 이해하기

쿠마냥 2024. 9. 3. 00:22

패턴에 대해 학습하다보면 MVVM에 대한 이야기가 나온다. 그때마다 적절한 레퍼런스를 찾길 바랐지만 서버만 담당하는 웹 프로그래밍 코드에서 이를 찾기는 쉽지 않았다. 따라서 오늘은 자바로 친절하게 쓰여진 안드로이드 코드를 바탕으로 MVVM을 이해해 보려 한다. 

 

(아래의 예시는 블로그에서 찾은 안드로이드 코드에 이해하기 쉽게 주석을 단 것인데, 원글을 찾아보려 구글링을 해 보아도 찾을 수 없었다. 공부용으로 잠시 참고하는 것이니, 너무 노여워 마셨으면... 글의 주인을 아시면 댓글 부탁드립니다!)

 

MVVM

  • VM은 뷰모델. UI에 들어갈 데이터를 관리한다.
  • MVP는 View-Presenter가 1대1 대응. MVVM은 1대다 가능. 값이 같다면 여러 뷰를 연결할 수 있다.

 

<activity_main.xml>

이제 코드를 살펴보자. 아래의 코드는 화면을 구성하기 위해서 필요한 설정들을 모아놓은 xml이다. 어떤 화면을 만드려고 했을까? 추측해 보자. 

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    // ViewModel 타입의 이름 viewModel을 사용하여 데이터를 넣겠다는 의미!
    <data>
        <variable
            name="viewModel"
            type="com.example.snacker.viewmodel.ViewModel" />
    </data>

		// 화면을 구성하기 위해 필요한 코드들 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".view.MainActivity">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/main_info"
            android:textSize="40sp"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.25" />


        <TextView
            android:id="@+id/user_textview"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="30sp"
            // viewModel에 winner라는 데이터가 있나 보다!
            // 이것을 여기 텍스트에 집어넣어 준다. 
            android:text="@{viewModel.winner}"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.4" />

        <Button
		        // 'ok_btnview' 버튼 기억하기
            android:id="@+id/ok_btnview"
            android:layout_width="114dp"
            android:layout_height="68dp"
            android:text="@string/main_btn"
            android:textSize="20sp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.7" />


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

 

이렇게 해서 만들어진 화면은 아래와 같다! winner는 바로 간식 당번이었다.

 

 

 

[Main Activity 클래스]

  • 이 클래스는 AppCompatActivity를 상속받으며, 데이터 바인딩과 ViewModel을 활용하여 뷰와 데이터를 연결한다.
  • 여러 개의 view가 하나의 viewModel을 공유하고 싶다면, activity에서 연결해 주면 된다. 이 액티비티는 앱 단위로 실행되는 듯하고, 여러 개의 Fragment로 쪼개어져 각자의 뷰를 관리하는 것으로 이해했지만… 확실하지 않다.
public class MainActivity extends AppCompatActivity {

    ActivityMainBinding binding; // 데이터 바인딩을 도와주는 객체 : viewmodel과 xml 연결
    ViewModel viewModel; // 뷰모델 객체 : view와 데이터(model)를 연결

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        Logger.d("Main_onCrete() 실행");
        super.onCreate(savedInstanceState);
        // activity_main.xml 파일에 현재의 액티비티를 연결하는 부분 
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 

	// 뷰모델 객체 만들고 데이터베이스 연결
        viewModel = new ViewModel(Database.getInstance());
        // 바인딩 객체에 위의 뷰모델을 연결 
        binding.setViewModel(viewModel);

	// okBtnview이라는 버튼에 클릭 리스너를 설정 -> 클릭하면 getUser()가 호출됨. 
        binding.okBtnview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Logger.d("버튼 클릭");
                viewModel.getUser();
            }
        });
    }
}

 

 

<ViewModel 클래스>

  • 데이터와 UI 간의 상호작용을 관리한다.
public class ViewModel extends BaseObservable {
// 무엇이 들어 있는지 보시라! 
    private Database database;
    private List<Person> items = new ArrayList<>();
    private String winner;

	// 읽어보면 사실상 비즈니스 로직이다. 
	// 1. 뷰모델을 생성하는 순간 데이터베이스를 연결한다. 
    public ViewModel(Database database){
        Logger.d("ViewModel 생성자 실행 | DB(Model) 참조");
        this.database = database;
        
	// 혹시 데이터베이스가 바뀌게 되면(???) 알려준다. 
    	// 데이터 베이스가 바뀔 일이 잘 상상되지 않는다...
        this.database.setOnDatabaseListener(new Database.DatabaseListener() {
            @Override
            public void onChanged() {
                Logger.d("리스너 실행");
                winner = null;
                winner = database.getWinner();
                notifyChange();
            }
        });
    }

	// db에서 유저를 가지고온다. 
    public void getUser() {
        Logger.d("db에게 user(winner)를 달라고 요청");
        database.getUser();
    }

    public String getWinner(){
        Logger.d("Winner 반환 (%s)", winner);
        return winner;
    }
}

 

 

<Database 클래스>

  • 모델에 해당하는 클래스
public class Database {
    private static Database instance;
    // Person 코드는 따로 들고오지 않았다. db 생성 시 생기는 person list. 
    private ArrayList<Person> personList = new ArrayList<>();
    private String winner;
    private DatabaseListener databaseListener;

    private Database(){
        Logger.d("Model인 Database 생성");
        personList.add(new Person(0, "최OO"));
        personList.add(new Person(1, "김OO"));
        personList.add(new Person(2, "고OO"));
        personList.add(new Person(3, "문OO"));
        personList.add(new Person(4, "윤OO"));
    }

		// 싱글톤 패턴을 함께 쓴 것을 알 수 있다!!
    public static Database getInstance() {
        Logger.d("Model에 접근 할 수 있도록 DB 인스턴스 값 요청");
        if (instance == null) {
            instance = new Database();
        }
        return instance;
    }

		// 여기가 좀 의문이다... 데이터베이스에 왜 이런 로직이 있는지? 뷰모델로 이동하는 것이 좋겠다. 
    public void getUser() {
        Logger.d("당첨자 획득");
        winner = personList.get((int)(Math.random()*5)).getName();
        notifyChange();
    }
    
    private void notifyChange() {
        if (databaseListener != null) {
            Logger.d("Model | Data 변경 되어 notify 하라고 알림");
            databaseListener.onChanged();
        }
    }

    public void setOnDatabaseListener(DatabaseListener databaseListener) {
        Logger.d("DatabaseListener 구현 객체 참조 변수 세팅 (arg1 : %s)",databaseListener.getClass().getSimpleName());
        this.databaseListener = databaseListener;
    }

    public String getWinner(){
        return winner;
    }

    public interface DatabaseListener {
        void onChanged();
    }
}

 

정리하자면 이렇다. 

  • Model (Database 클래스)
  • ViewModel (ViewModel 클래스)
  • View (XML 파일)

그렇다면 MainActivity 클래스는 무엇일까? MainActivity 클래스는 View에 가깝다고 보았다. ViewModel 객체를 생성하고 View와 ViewModel을 이어주기 때문!