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")
}
기본 구조
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)
}
}
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>
}
인터셉터
로깅 인터셉터
private val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
}
인증 인터셉터
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)
}
토큰 갱신 인터셉터
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
}
}
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)
}
}
}
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
}
}
}
}
파일 업로드
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)
// 처리
}
}
캐싱
OkHttp 캐시 설정
val cacheSize = 10 * 1024 * 1024L // 10 MB
val cache = Cache(context.cacheDir, cacheSize)
val okHttpClient = OkHttpClient.Builder()
.cache(cache)
.build()
캐시 제어 인터셉터
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()
}
에러 처리
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)
}
}
간단한 HTTP 요청
Retrofit 없이 간단한 요청:
val url = URL("https://www.example.com/data")
val connection = url.openConnection() as HttpsURLConnection
connection.connect()
val inputStream = connection.inputStream
// 데이터 읽기
결론
Retrofit은 Android에서 REST API 통신을 간편하게 처리할 수 있게 해줍니다. 인터셉터를 활용하여 인증, 로깅, 캐싱 등을 효율적으로 구현하고, Repository 패턴으로 데이터 레이어를 깔끔하게 분리하세요.
Comments