Android발행일 2024. 11. 17.원본 https://blog.naver.com/jword_/223664167912 ↗

Retrofit2 클린코드 설계 (Call사용)

Retrofit2 클린코드 설계 (Call사용) — #개발자의도구들 #retrofit2 #클린코드 #cleancode 🚨📝 📖 📒✏️💡🔍 AI스쿨 msa기반 java...

#Android Studio#Naver Blog

#개발자의도구들 #retrofit2 #클린코드 #cleancode

🚨📝 📖 📒✏️💡🔍

​

AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다

\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.

\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.

Index

  • Purpose
  • What is Retrofit
  • How to use Retrofit
  • build.gradle
  • RetrofitModule
  • Structure
  • Call
  • Test
  • Entity
  • Controller
  • Service
  • Repostior
  • Caution ⚠️
  • Question
  • Answer
  • Feedback
💡 Tips

Purpose

이전 글을 통해서 MVVM 패턴을 알아 보았고, 외부 API를 통한 실제 코드를 어떻게 설계하는지에 대해 다뤘었습니다.

​

이번에는 이전 글의 확장버전이라고 생각하시면 되는데요. Retrofit 사용 방법 부터 클린코드로 구조 설계방법까지 정리하였습니다.

​

추가로 마지막 챕터에는 Server에서 Client로 reesponse를 전달시 고려사항에 대해서도 기록하였습니다.

What is Retrofit

Retrofit은 안드로이드에서 서버로 HTTP api 통신을 위해 설계된 라이브러리입니다. Java기반으로 설계되어서 kotlin에서도 사용이 가능합니다.

​

다음과 같은 편리한 기능들을 제공합니다.

javascript 코드 예제
                                    1. 타입 안정성
2. 간편 설정
3. JSON 변환
4. 동기 비동기 요청가능

How to use Retrofit

build.gradle

javascript 코드 예제
                                    dependencies {
    // Retrofit 의존성
    implementation(com.squareup.retrofit2:retrofit:{new_version})

    // Gson 컨버터를 사용하기 위한 의존성
    implementation(com.squareup.retrofit2:converter-gson:{new_version})

    // for req, res loggin & debugging
    implementation(com.squareup.okhttp3:logging-interceptor:(new_version)")
}

최신 버전은 아래 링크에서 참고

Retrofit

https://github.com/square/retrofit

​

RetrofitModule

javascript 코드 예제
                                    package com.example.myapplication.di

@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        } // 디버깅, 로깅을 위한 설정

        val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(httpLoggingInterceptor)
            .readTimeout(60, TimeUnit.SECONDS)
            .connectTimeout(60, TimeUnit.SECONDS)
            .build()

        return Retrofit.Builder()
            .baseUrl("http://localhost:8080")
            .addConverterFactory(GsonConverterFactory.create())
            .client(okHttpClient)
            .build() // server ip, port 설정, convertor 설정 등
    }
}

Hilt & Dagger 라이브러리를 사용하여 Module로 등록 하면 편리하게 사용할 수 있습니다.

Structure

javascript 코드 예제
                                    ApiInterface : url & http method

repository(@inject ApiInterface) : 실제 retrofit 통신

ViewModel(@Inject repository) : 실제 repository 사용하여 view 전용 데이터 관리

View() {val viewModel: ViewModel} : 전용 ViewModel을 불러와 필요한 데이터 화면에 표시

이전 글에 소개된 코드를 간략하게 정리하면 위와 같습니다. 이번 글에서는 Reposiotry를 집중적으로 다뤄보도록 하죠.

Call

🧑‍💻 Repository
javascript 코드 예제
                                    package com.example.myapplication.network

import com.example.myapplication.dto.BoardResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import javax.inject.Singleton

@Singleton
interface BoardApiInterface {
    @GET("/boards")
    fun getAll(): Call<List<BoardResponse>>

    @GET("/board")
    fun getById(@Body id: Long): Call<BoardResponse>

}

현재 interface에서는 Corutine이 아닌 Call<T>방식으로 데이터를 전달받고 있습니다.

javascript 코드 예제
                                    package com.example.myapplication.repository

import com.example.myapplication.dto.BoardResponse
import com.example.myapplication.network.BoardApiInterface
import dagger.hilt.android.scopes.ActivityScoped
import retrofit2.Call
import javax.inject.Inject
import javax.inject.Singleton
import retrofit2.Callback
import retrofit2.Response

@ActivityScoped
@Singleton
class BoardRepository @Inject constructor(
    private val boardApiInterface: BoardApiInterface
){
    fun getAll(callback: (Result<List<BoardResponse>>) -> Unit) {
        boardApiInterface.getAll().enqueue(object : Callback<List<BoardResponse>> {
            override fun onResponse( // call 응답 성공처리: http 응답코드로 판단
                call: Call<List<BoardResponse>>,
                response: Response<List<BoardResponse>>
            ) {
                if (response.isSuccessful) {
                    val boardResponse = response.body()
                }
            }

            override fun onFailure(p0: Call<List<BoardResponse>>, p1: Throwable) {
                TODO("Not yet implemented")
            } // call 실패 처리 : http 응답 코드로 판단
        })
    }
}

Call은 enqueue()를 사용하면 비동기적으로 request를 보낼 수 있습니다. 네트워크 요청에는 환경에 따라 상당한 시간이 걸릴 수 있어, 메인 스레드 차단이 걸릴 수 있는데 enqueue()는 이 메인 스레드를 차단하지 않고 새로운 스레드에서 동시 다발적으로 request 요청이 가능합니다.

​

Call은 응답 성공 여부를 HTTP 응답 코드로 처리합니다. 200번대의 응답은 성공으로, 그 외는 실패로 간주합니다.

javascript 코드 예제
                                    🤔 하지만, 저의 프로젝트는 응답 코드를 모두 200으로 응답하고, 오류는 클라이언트와 합의된
응답코드를 사용하고 있습니다. 이럴 때는 어떻게 해야 할까요?

Caution

이전 글을 통해 HTTP 응답 코드의 비현실적인 부분을 설명드린 바 있습니다. 이후 저는 RFC7807 스펙과 함께 All OK() 방법을 사용하였는데요.

​

이 때, 에러가 나는 상황에는 ErrorResponse를, 정상 처리할때는 (Entity)Response로 클라이언트에게 전달하였습니다.

​

하지만, Call의 경우 오직 하나의 응답만을 받을 수 있기 때문에 아래와 같이 interface를 정의해버리면, 영영 에러처리가 불가능한 상태로 빠지게 됩니다.

javascript 코드 예제
                                    package com.example.myapplication.network

import com.example.myapplication.dto.BoardResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.GET
import javax.inject.Singleton

@Singleton
interface BoardApiInterface {
    @GET("/boards")
    fun getAll(): Call<List<BoardResponse>> // ErrorResponse도 받아야 한다.

    @GET("/board")
    fun getById(@Body id: Long): Call<BoardResponse>

}

그래서 Server의 Response DTO를 전체적으로 개편하였습니다.

이미지

위는 에러 상황일때: ErrorResponse/ 아래는 전체 상황에 대하여 데이터와 ErroResponse를

Error가 나는 상황에서 ErrorResponse를 보내는게 아니라, 전체 Response에 대해서 Error와 Data를 한번에 담아서 보냅니다.

javascript 코드 예제
                                    // dto 수정사항

package com.example.myapplication.dto

data class BoardResponse(
    val boardResult: BoardResult?,
    val errorResponse: ErrorResponse?
)

package com.example.myapplication.dto

data class BoardAllResponse(
    val boardResults: List<BoardResult>,
    val errorResponse: ErrorResponse?
)

그럼 Call에서 BoardResponse내부에서 데이터와, Error를 모두 처리할 수 있습니다. 전체 목록 같이 여러 목록을 받아올때는 그에 맞는 Dto를 따로 생성하여 처리하는 것이 편한 것 같습니다.

​

Question

🤔 write Question 1.설계에 문제가 없는지 체크

Question

🙋‍♂️

​