(5)-1 현재 상영작 그리기 - DTO 구현을 곁들인...

HEYDAY7·2022년 10월 16일
0

GetMovie API 구체화

지난 구현에서 테스트용으로 만들어 두었던 GetMovie api를 구체화하기로 했다. 우선 get Movie api doc은 이를 참고하면 된다. 먼저 작업한 것은 Movie관련 DTO 및 domain Model을 만드는 것이었다. 코드는 간단하다. 파일의 구조는 깃을 참고하면 된다.

## MovieDTO.kt

@Serializable
data class MovieDTO(
    @SerialName("adult") val adult: Boolean,
//    @SerialName("genres") val genres
    @SerialName("id") val id: Int,
    @SerialName("imdb_id") val imdbId: String?,
    @SerialName("original_language") val originalLanguage: String,
    @SerialName("original_title") val originalTitle: String,
    @SerialName("overview") val overview: String,
    @SerialName("popularity") val popularity: Float,
    @SerialName("poster_path") val posterPath: String?,
    @SerialName("production_companies") val productionCompanies: List<CompanyDTO>,
    @SerialName("production_countries") val productionCountries: List<CountryDTO>,
    @SerialName("release_date") val releaseDate: String,
    @SerialName("runtime") val runtime: Int?,
    @SerialName("spoken_languages") val spokenLanguages: List<LanguageDTO>,
    @SerialName("status") val status: String,
    @SerialName("title") val title: String
)

@Serializable
data class CompanyDTO(
    @SerialName("name") val name: String,
    @SerialName("id") val id: Int,
    @SerialName("logo_path") val logoPath: String?,
    @SerialName("origin_country") val originCountry: String
)

@Serializable
data class CountryDTO(
    @SerialName("iso_3166_1") val isoId: String,
    @SerialName("name") val name: String
)

@Serializable
data class LanguageDTO(
    @SerialName("iso_639_1") val isoId: String,
    @SerialName("name") val name: String
)

--------------------------------------
## Movie.kt
data class Movie(
    val adult: Boolean,
//    @SerialName("genres") val genres
    val id: Int,
    val imdbId: String?,
    val originalLanguage: String,
    val originalTitle: String,
    val overview: String,
    val popularity: Float,
    val posterPath: String?,
    val productionCompanies: List<Company>,
    val productionCountries: List<Country>,
    val releaseDate: String,
    val runtime: Int?,
    val spokenLanguages: List<Language>,
    val status: String,
    val title: String
)

data class Company(
    val name: String,
    val id: Int,
    val logoPath: String?,
    val originCountry: String
)

data class Country(
    val isoId: String,
    val name: String
)

data class Language(
    val isoId: String,
    val name: String
)

이렇게 Model을 수정하고, API부분 에서는 DTO를, Repository에서는 domain model을 return 하도록 구현하면 DTO와 domain model을 분리할 수 있다. 이 과정에서 mapper를 어떻게 할지에 대한 고민에 빠졌는데 이는 아래에서 따로 다루겠다.

현재 상영작 그리기

알아봤던 API들 중 메인화면을 구성할 수 있는 요소로 현재 상영작 그리기를 택했고, 해당 API doc 부터 자세히 살펴봤다.

보아하니 아래와 같이 pagination 형태로 결과가 들어오며, get movie API와는 result에 담긴 결과물을 보았을 때 형태가 달랐다. 따라서 해당 api의 return type을 SimpleMovie라는 이름으로 다르게 하나를 더 만들었고, Pagination Model도 Generic하게 만들어줬다.

## SimpleMovieDTO.kt
@Serializable
data class SimpleMovieDTO(
    @SerialName("poster_path") val posterPath: String?,
    @SerialName("adult") val adult: Boolean,
    @SerialName("overview") val overview: String,
    @SerialName("release_date") val releaseDate: String,
    @SerialName("genre_ids") val genreIds: List<Int>,
    @SerialName("id") val id: Int,
    @SerialName("original_title") val originalTitle: String,
    @SerialName("original_language") val originalLanguage: String,
    @SerialName("title") val title: String,
    @SerialName("popularity") val popularity: Float
)

## SimpleMovie.kt
data class SimpleMovie(
    val posterPath: String?,
    val adult: Boolean,
    val overview: String,
    val releaseDate: String,
    val genreIds: List<Int>,
    val id: Int,
    val originalTitle: String,
    val originalLanguage: String,
    val title: String,
    val popularity: Float
)

## PaginationDTO.kt
@Serializable
data class PaginationDTO<T>(
    @SerialName("page") val page: Int,
    @SerialName("results") val results: List<T>,
    @SerialName("total_pages") val totalPages: Int,
    @SerialName("total_results") val totalResults: Int
)

## Pagination.kt
data class Pagination<T>(
    val page: Int,
    val results: List<T>,
    val totalPages: Int,
    val totalResults: Int
)

그리고서는 api 코드도 작성해줬다.


## MovieApi
suspend fun getMovieNowPlaying(
        page: Int,
        region: String,
        language: String
    ): PaginationDTO<SimpleMovieDTO>

## KtorMovieApi
override suspend fun getMovieNowPlaying(
        page: Int,
        region: String,
        language: String
    ): PaginationDTO<SimpleMovieDTO> =
        networkService.get(
            "$url/movie/now_playing?region=$region&language=$language"
        )

여기까지는 매우 순탄하게 작업이 흘러갔다. 하지만 여기서 mapping issue 갈 발생했다!

Mapping

DTO와 Domain model을 구분한 것, mapper를 쓰려고 하는 것 두 가지 모두 Layer를 분리시킬 수 있다는 좋은 장점을 가진 방식이라는 것을 알았지만, 그것보다도 사실 회사에서 작업하던 방식이 익숙해서 이렇게 진행하려 했던 것이다. 다만 짧은 지식의 나는 Mapper를 실제로 어떻게 구성하고 주입해줘야하는지에 대해 확실히 기억이 나지 않았다. 그래서 이거저거 삽질을 많이 해봤는데 모두 실패했다 ㅠㅠ. 결국 오늘 작업에서는 파일을 따로 분리하지 못하고 RepositoryImpl에 코드로 일단 때려넣게 되었다...

## MovieRepository (해당 코드 추가)
suspend fun getMovieNowPlaying(
        page: Int,
        region:String = "KR",
        language: String = "ko"
    ): Pagination<SimpleMovie>

## MovieRepositoryImpl (전체 구성을 보여주기 위해 코드 전체를 가져옴)
class MovieRepositoryImpl @Inject constructor(
    private val movieApi: MovieApi
) : MovieRepository {

    override suspend fun getMovie(movieId: Int): Movie =
        movieApi.getMovie(movieId).toMovie()

    override suspend fun getMovieNowPlaying(
        page: Int,
        region:String,
        language: String
    ): Pagination<SimpleMovie> =
        movieApi.getMovieNowPlaying(page, region, language).toPagination { it.toSimpleMovie() }
}

// 여기부터가 mapper 파트
fun MovieDTO.toMovie() = Movie(
    adult,
    id,
    imdbId,
    originalLanguage,
    originalTitle,
    overview,
    popularity,
    posterPath,
    productionCompanies.map { it.toCompany() },
    productionCountries.map { it.toCountry() },
    releaseDate,
    runtime,
    spokenLanguages.map { it.toLanguage() },
    status,
    title
)

fun CompanyDTO.toCompany() = Company(
    name, id, logoPath, originCountry
)

fun CountryDTO.toCountry() = Country(
    isoId, name
)

fun LanguageDTO.toLanguage() = Language(
    isoId, name
)

// SimpleMovie
fun SimpleMovieDTO.toSimpleMovie() = SimpleMovie(
    posterPath,
    adult,
    overview,
    releaseDate,
    genreIds,
    id,
    originalTitle,
    originalLanguage,
    title,
    popularity
)

// Pagination
fun <T, R> PaginationDTO<T>.toPagination(transformer: (T) -> (R)) = Pagination(
    page,
    results.map { transformer(it) },
    totalPages,
    totalResults
)

결과

아래 좌측 사진은 네이버에 검색해서 나온 현재상영작, 오른쪽 사진은 현재 만들고 있는 앱에서 오늘 작성한 api로 현재 상영중인 영화를 불러온 결과이다. 양쪽 다 첫 페이지만을 가져온 상태라 정확하지는 않지만 서로 겹치는게 많이 보이는 걸로 보아 가져오는 데에는 성공한 것 같다.(해당 api를 call 하는 코드를 보고 싶다면 이 commit에서 확인해보자) 이렇게 해당 api는 정상적으로 동작하는 것을 확인하였으니 다음 순서로는 오늘 미처 구현하지 못했던 Mapper를 개별 파일로 만들어 효과적으로 주입하는 방식을 알아보고(의외로 쉬울수도 있을 것 같긴한데... 잘안되는... 원래 개발이 그런거 같다.) 된다면 SimpleMovie를 그려주는 UI 정도를 작업해볼까 싶다.

해당 작업을 진행중인 branch를 남겨둔다.

오늘도 이만...

profile
(전) Junior Android Developer (현) Backend 이직 준비생

0개의 댓글