DEV Community

dss99911
dss99911

Posted on Originally published at dss99911.github.io

Android 네트워킹과 Retrofit 가이드

Android 네트워킹과 Retrofit 가이드

Android에서 REST API 통신을 위한 Retrofit 사용법을 알아봅니다.

기본 설정

의존성 추가

dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
}
Enter fullscreen mode Exit fullscreen mode

기본 구조

OkHttpClient: 실제 네트워크 통신 담당

  • 타임아웃 설정
  • 인터셉터 (헤더 추가, 로깅 등)

Retrofit: OkHttpClient와 앱을 연결

  • Base URL 설정
  • Converter (Gson 등) 설정
  • Service 인터페이스 생성

Service: API 정의

Retrofit 초기화

object ApiClient {
    private const val BASE_URL = "https://api.example.com/"

    private val okHttpClient: OkHttpClient by lazy {
        OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .addInterceptor(loggingInterceptor)
            .addInterceptor(authInterceptor)
            .build()
    }

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

    val apiService: ApiService by lazy {
        retrofit.create(ApiService::class.java)
    }
}
Enter fullscreen mode Exit fullscreen mode

API Service 인터페이스

interface ApiService {
    @GET("users")
    suspend fun getUsers(): Response<List<User>>

    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): Response<User>

    @GET("search")
    suspend fun searchUsers(
        @Query("query") query: String,
        @Query("page") page: Int,
        @Query("limit") limit: Int = 20
    ): Response<SearchResult>

    @POST("users")
    suspend fun createUser(@Body user: User): Response<User>

    @PUT("users/{id}")
    suspend fun updateUser(
        @Path("id") userId: String,
        @Body user: User
    ): Response<User>

    @DELETE("users/{id}")
    suspend fun deleteUser(@Path("id") userId: String): Response<Unit>

    @FormUrlEncoded
    @POST("login")
    suspend fun login(
        @Field("email") email: String,
        @Field("password") password: String
    ): Response<AuthResponse>

    @Multipart
    @POST("upload")
    suspend fun uploadFile(
        @Part file: MultipartBody.Part,
        @Part("description") description: RequestBody
    ): Response<UploadResponse>

    @Headers("Cache-Control: max-age=3600")
    @GET("static-data")
    suspend fun getStaticData(): Response<Data>
}
Enter fullscreen mode Exit fullscreen mode

인터셉터

로깅 인터셉터

private val loggingInterceptor = HttpLoggingInterceptor().apply {
    level = if (BuildConfig.DEBUG) {
        HttpLoggingInterceptor.Level.BODY
    } else {
        HttpLoggingInterceptor.Level.NONE
    }
}
Enter fullscreen mode Exit fullscreen mode

인증 인터셉터

private val authInterceptor = Interceptor { chain ->
    val originalRequest = chain.request()
    val token = TokenManager.getToken()

    val newRequest = originalRequest.newBuilder()
        .addHeader("Authorization", "Bearer $token")
        .addHeader("Content-Type", "application/json")
        .build()

    chain.proceed(newRequest)
}
Enter fullscreen mode Exit fullscreen mode

토큰 갱신 인터셉터

class TokenRefreshInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val response = chain.proceed(chain.request())

        if (response.code == 401) {
            synchronized(this) {
                // 토큰 갱신
                val newToken = refreshToken()
                if (newToken != null) {
                    TokenManager.saveToken(newToken)

                    // 원래 요청 재시도
                    val newRequest = chain.request().newBuilder()
                        .header("Authorization", "Bearer $newToken")
                        .build()
                    return chain.proceed(newRequest)
                }
            }
        }

        return response
    }
}
Enter fullscreen mode Exit fullscreen mode

Repository 패턴

class UserRepository(
    private val apiService: ApiService
) {
    suspend fun getUsers(): Result<List<User>> {
        return try {
            val response = apiService.getUsers()
            if (response.isSuccessful) {
                Result.success(response.body() ?: emptyList())
            } else {
                Result.failure(ApiException(response.code(), response.message()))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun getUser(id: String): Result<User> {
        return try {
            val response = apiService.getUser(id)
            if (response.isSuccessful && response.body() != null) {
                Result.success(response.body()!!)
            } else {
                Result.failure(ApiException(response.code(), response.message()))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

ViewModel에서 사용

class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {

    private val _users = MutableLiveData<List<User>>()
    val users: LiveData<List<User>> = _users

    private val _error = MutableLiveData<String>()
    val error: LiveData<String> = _error

    fun loadUsers() {
        viewModelScope.launch {
            repository.getUsers()
                .onSuccess { userList ->
                    _users.value = userList
                }
                .onFailure { exception ->
                    _error.value = exception.message
                }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

파일 업로드

fun uploadImage(uri: Uri) {
    viewModelScope.launch {
        val file = File(uri.path!!)
        val requestFile = file.asRequestBody("image/*".toMediaType())
        val body = MultipartBody.Part.createFormData("file", file.name, requestFile)

        val description = "Image description".toRequestBody("text/plain".toMediaType())

        val response = apiService.uploadFile(body, description)
        // 처리
    }
}
Enter fullscreen mode Exit fullscreen mode

캐싱

OkHttp 캐시 설정

val cacheSize = 10 * 1024 * 1024L // 10 MB
val cache = Cache(context.cacheDir, cacheSize)

val okHttpClient = OkHttpClient.Builder()
    .cache(cache)
    .build()
Enter fullscreen mode Exit fullscreen mode

캐시 제어 인터셉터

val cacheInterceptor = Interceptor { chain ->
    val response = chain.proceed(chain.request())

    val cacheControl = CacheControl.Builder()
        .maxAge(1, TimeUnit.HOURS)
        .build()

    response.newBuilder()
        .header("Cache-Control", cacheControl.toString())
        .build()
}
Enter fullscreen mode Exit fullscreen mode

에러 처리

sealed class NetworkResult<T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Error<T>(val code: Int, val message: String?) : NetworkResult<T>()
    data class Exception<T>(val e: Throwable) : NetworkResult<T>()
}

suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResult<T> {
    return try {
        val response = apiCall()
        if (response.isSuccessful) {
            NetworkResult.Success(response.body()!!)
        } else {
            NetworkResult.Error(response.code(), response.message())
        }
    } catch (e: IOException) {
        NetworkResult.Exception(e)
    } catch (e: Exception) {
        NetworkResult.Exception(e)
    }
}
Enter fullscreen mode Exit fullscreen mode

간단한 HTTP 요청

Retrofit 없이 간단한 요청:

val url = URL("https://www.example.com/data")
val connection = url.openConnection() as HttpsURLConnection
connection.connect()
val inputStream = connection.inputStream
// 데이터 읽기
Enter fullscreen mode Exit fullscreen mode

결론

Retrofit은 Android에서 REST API 통신을 간편하게 처리할 수 있게 해줍니다. 인터셉터를 활용하여 인증, 로깅, 캐싱 등을 효율적으로 구현하고, Repository 패턴으로 데이터 레이어를 깔끔하게 분리하세요.


Originally published at https://dss99911.github.io

Top comments (0)