기술면접
[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을 이어주기 때문!