Retrofit2 클린코드 설계 (Call사용)
Retrofit2 클린코드 설계 (Call사용) — #개발자의도구들 #retrofit2 #클린코드 #cleancode 🚨📝 📖 📒✏️💡🔍 AI스쿨 msa기반 java...
#개발자의도구들 #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에서도 사용이 가능합니다.
다음과 같은 편리한 기능들을 제공합니다.
1. 타입 안정성
2. 간편 설정
3. JSON 변환
4. 동기 비동기 요청가능
How to use Retrofit
build.gradle
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)")
}
최신 버전은 아래 링크에서 참고
https://github.com/square/retrofit
RetrofitModule
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
ApiInterface : url & http method
repository(@inject ApiInterface) : 실제 retrofit 통신
ViewModel(@Inject repository) : 실제 repository 사용하여 view 전용 데이터 관리
View() {val viewModel: ViewModel} : 전용 ViewModel을 불러와 필요한 데이터 화면에 표시
이전 글에 소개된 코드를 간략하게 정리하면 위와 같습니다. 이번 글에서는 Reposiotry를 집중적으로 다뤄보도록 하죠.
Call
| 🧑💻 Repository |
|---|
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>방식으로 데이터를 전달받고 있습니다.
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번대의 응답은 성공으로, 그 외는 실패로 간주합니다.
🤔 하지만, 저의 프로젝트는 응답 코드를 모두 200으로 응답하고, 오류는 클라이언트와 합의된
응답코드를 사용하고 있습니다. 이럴 때는 어떻게 해야 할까요?
Caution
이전 글을 통해 HTTP 응답 코드의 비현실적인 부분을 설명드린 바 있습니다. 이후 저는 RFC7807 스펙과 함께 All OK() 방법을 사용하였는데요.
이 때, 에러가 나는 상황에는 ErrorResponse를, 정상 처리할때는 (Entity)Response로 클라이언트에게 전달하였습니다.
하지만, Call의 경우 오직 하나의 응답만을 받을 수 있기 때문에 아래와 같이 interface를 정의해버리면, 영영 에러처리가 불가능한 상태로 빠지게 됩니다.
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를 한번에 담아서 보냅니다.
// 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
| 🙋♂️ |
|---|
