Jetpack ViewModel에 대해 알아보자 2

milkbottle·2024년 4월 28일
0

ViewModel

ViewModel의 유래 (많이 뇌피셜)

앞서 MVVM 에 대한 개념을 알아보았다.

사실 "왜 MVVM을 쓸까?" 라고 한다면 REST API를 좀 더 쉽고 간편하게 연동하려고 쓰는 이유가 크다.

MVVM이 없다면 button에 클릭 리스너를 넣고.. 그 안에 이벤트 핸들러에 일일히 서버 주소와 상태를 변경하는 함수를 다 작성하고..

View에 해당하는 Activity.kt의 역할이 너무 커지게 된다.

그래서 캡슐화를 하다 보니 자연스레 ViewModel이 나온 것이다.

ViewModel의 역할

REST API를 위해 ViewModel이 탄생한 것이므로, 서버(or 목업데이터)와 클라이언트 간에 상호작용을 통해 데이터가 CRUD가 되는 행동을 해야한다.

이를 위해서는 ViewModel 안에 다음과 같은 기능을 구현해야한다.

  1. API를 요청하는 함수
  2. API에 대한 반환 데이터를 저장하는 Model

https://안녕/postsGET요청을 하면 (게시글id, 작성일자, 게시글제목)에 대한 리스트를 받을 수 있는 API가 있다고 하자.

위를 기반으로 ViewModel을 만든다면 다음과 같다.

class PostListViewModel : ViewModel() {
    private val _postsLiveData = MutableLiveData<List<Post>>()
    val postsLiveData: LiveData<List<Post>>
        get() = _postsLiveData

    // ViewModel에 해당하는 데이터를 초기화하거나 가져오는 메서드
    fun loadPosts() {
        // 여기에 게시글을 불러오는 코드가 존재한다.
        // 예를 들어, 네트워크에서 게시글을 가져오거나 로컬 데이터베이스에서 불러오는 등의 작업이 이루어진다.
        val loadedPosts: List<Post> = // 게시글을 가져오는 로직;
        _postsLiveData.value = loadedPosts // LiveData에 게시글 목록 설정
    }
}

이런 느낌으로 구현될 것이다.

1번 API 요청함수는 loadPosts()가, 2번 Model은 _postsLiveData가 대응된다.

하지만 여기선 문제점이 있다.

loadPosts() 부분에 서버 url인 https://안녕/posts을 넣고 요청하는 그런 코드를 직접 모두 구현한다면?

kotlin은 절차지향 언어가 아니다.

Activity.kt의 역할을 줄이기 위해 ViewModel을 도입한건데, 이래서야 ViewModel이 또 커지게 된다.

APIService의 도입

그래서 ApiService라는 개념이 도입된다.

API의 baseUrl을 설정해주고, 매 API마다 GET 요청인지, DEL 요청인지.. 등 Method를 설정해주고, API의 endpoint도 쉽게 매핑할 수 있다.

object ApiService {
    private const val BASE_URL = "https://안녕/"

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    private val apiService: ApiInterface by lazy {
        retrofit.create(ApiInterface::class.java)
    }

    fun getPosts(): Call<List<Post>> {
        return apiService.getPosts()
    }
}

interface ApiInterface {
    @GET("posts")
    fun getPosts(): Call<List<Post>>
}

이를 ViewModel에 적용하면 다음처럼 바뀐다.

class PostListViewModel : ViewModel() {
    private val _postsLiveData = MutableLiveData<List<Post>>()
    val postsLiveData: LiveData<List<Post>>
        get() = _postsLiveData

    fun loadPosts() {
        val call = ApiService.getPosts()

        call.enqueue(object : Callback<List<Post>> {
            override fun onResponse(call: Call<List<Post>>, response: Response<List<Post>>) {
                if (response.isSuccessful) {
                    val loadedPosts = response.body()
                    _postsLiveData.value = loadedPosts
                } else {
                    // Handle error
                    Log.e("ApiService", "Failed to fetch posts: ${response.code()}")
                }
            }

            override fun onFailure(call: Call<List<Post>>, t: Throwable) {
                // Handle failure
                Log.e("ApiService", "Failed to fetch posts: ${t.message}")
            }
        })
    }
}

ViewModel에 url에 대한 정보도 없고, GET요청인지, DEL요청인지 이런 정보도 없다.

캡슐화가 잘 되었기 때문에, 개발자 입장에서는 ApiService에 API와 관련된 기능을 구현하고 ViewModel에서 땡겨 쓰면된다.

하지만!! 또 문제점이 존재한다.

Repository의 도입

실제 개발에서는 API가 클라이언트보다 개발이 느릴 수도 있다.

그렇다면 클라이언트에서는 API연동이 아니라 목업연동을 통해 테스트를 진행해야한다.

그래서 API 데이터와 목업 데이터를 하나의 저장소(repository)라고 보고, 클라이언트와 연결해주는 매개체가 필요하다.

그것이 바로 Service이다.

아까 API를 통해 데이터를 가져오는 구현부가 APIService안에 있었다.

그렇다면 모킹을 통해 데이터를 가져오는 것은? MockService라고 할 수 있다.

Service라는 인터페이스에서 게시글을 불러오는 순수함수가 생기고,

Service를 상속해서 APIServiceMockService 가 순수함수를 오버라이딩해 구현하는 그림이 떠오를 것이다.

  1. ViewModel이 Repository에 "나 데이터 줘" 라고 한다.
  2. Repository는 Service를 통해 실제저장소(Mock or 백엔드서버)에 데이터를 가져온다.
  3. Repository는 가져온 데이터를 ViewModel에게 전달한다.
  4. ViewModel은 전달받은 데이터를 View에게 그릴 수 있도록 상태값을 변경한다.

이런 Flow를 거치게 된다.

Repository 코드는 다음과 같아진다.

Repository는 Service를 통해 목업이나 서버에 접속하여 데이터를 가져온다.

class PostRepository(private val service: Service) {
    fun getPosts(callback: (Result<List<Post>>) -> Unit) {
        service.getPosts(callback)
    }
}

Service는 목업이나 서버에 접속하여 데이터를 가져오는 인터페이스이다.

interface Service {
    fun getPosts(callback: (Result<List<Post>>) -> Unit)
}

ApiService 목업에 접속해 데이터를 가져오는 구현부를 가지고 있다.

object ApiService : Service {
    private const val BASE_URL = "https://안녕/"

    private val retrofit: Retrofit by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    private val apiService: ApiInterface by lazy {
        retrofit.create(ApiInterface::class.java)
    }

    override fun getPosts(callback: (Result<List<Post>>) -> Unit) {
        val call = apiService.getPosts()

        call.enqueue(object : Callback<List<Post>> {
            override fun onResponse(call: Call<List<Post>>, response: Response<List<Post>>) {
                if (response.isSuccessful) {
                    callback(Result.success(response.body()))
                } else {
                    // Handle error
                    callback(Result.failure(Exception("Failed to fetch posts: ${response.code()}")))
                }
            }

            override fun onFailure(call: Call<List<Post>>, t: Throwable) {
                // Handle failure
                callback(Result.failure(t))
            }
        })
    }
}

object MockService : Service {
    override fun getPosts(callback: (Result<List<Post>>) -> Unit) {
        // 목업 데이터를 반환합니다.
        callback(Result.success(MockService.getMockPosts()))
    }
}

interface ApiInterface {
    @GET("posts")
    fun getPosts(): Call<List<Post>>
}

MockService는 목업에 접속해 데이터를 가져오는 구현부를 가지고 있다.

object MockService : Service {
    // 여기에 목업 데이터를 정의합니다.
    private val mockPosts = listOf(
        Post(1, "2024-04-01", "첫 번째 게시물"),
        Post(2, "2024-04-02", "두 번째 게시물"),
        Post(3, "2024-04-03", "세 번째 게시물")
    )

    override fun getPosts(callback: (Result<List<Post>>) -> Unit) {
        // 목업 데이터를 반환합니다.
        callback(Result.success(mockPosts))
    }
}

0개의 댓글