一、의존성 추가 및 네트워크 권한 설정
의존성 추가
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
// 선택 사항
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
- 첫 번째 의존성은 Retrofit, OkHttp 및 Okio 라이브러리를 다운로드하며, OkHttp 라이브러리를 수동으로 추가할 필요가 없습니다.
- 두 번째 의존성은 Retrofit 변환 라이브러리로, GSON을 사용하여 JSON 데이터를 파싱하므로 GSON 라이브러리도 함께 다운로드됩니다.
- 세 번째는 OkHttp 로그 인터셉터 관련 라이브러리로 선택 사항입니다.
네트워크 권한 추가
AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET"/>
二、Retrofit 데이터 API 부분 캡슐화
2.1 서버 응답 데이터 클래스, 예외 클래스, 예외 코드 생성
공통 응답 데이터 클래스
import com.google.gson.annotations.SerializedName
data class ApiResponse<T>(
@SerializedName("status") val status: Int = -1,
@SerializedName("result") val result: T,
@SerializedName("message") val message: String
) {
fun isSuccessful(): Boolean {
return status == 0
}
}
예외 클래스
data class ApiException(
var code: Int, override var message: String?
) : RuntimeException(message)
예외 코드 정의
import androidx.annotation.IntDef
@IntDef(
ErrorCode.SUCCESS, // 성공
ErrorCode.FAIL, // 실패
ErrorCode.NETWORK_ERROR,//네트워크 예외
ErrorCode.HOST_ERROR,//호스트 예외
ErrorCode.TIMEOUT,//타임아웃
ErrorCode.CANCEL,//취소
ErrorCode.JSON_ERROR,//데이터 파싱 예외
ErrorCode.OK,//요청 성공
ErrorCode.CREATED,
ErrorCode.FORBIDDEN,
ErrorCode.UNAUTHORIZED,//인증 실패
ErrorCode.NOT_FOUND,
ErrorCode.OTHER,//기타 오류
ErrorCode.CUSTOM_START,//사용자 정의
ErrorCode.NULL_VALUE//널 값
)
@Retention(AnnotationRetention.SOURCE)
annotation class ErrorCode {
companion object {
const val SUCCESS = 0
const val FAIL = 1
const val NETWORK_ERROR = 2
const val HOST_ERROR = 3
const val TIMEOUT = 4
const val CANCEL = 5
const val JSON_ERROR = 6
const val OK = 200
const val CREATED = 201
const val FORBIDDEN = 401
const val UNAUTHORIZED = 402
const val NOT_FOUND = 404
const val OTHER = 509
const val CUSTOM_START = 600
const val NULL_VALUE = CUSTOM_START + 1
}
}
2.2 예외 처리 캡슐화
import com.google.gson.JsonSyntaxException
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
private const val TAG = "apiRequest"
suspend fun <T> apiRequest(block: suspend () -> ApiResponse<T>): T? {
runCatching {
block()
}.onSuccess {
return it.result
}.onFailure {
when (it) {
is ApiException -> {
logW(TAG, "API 요청 예외, 코드: ${it.code} 메시지: ${it.message}")
}
is UnknownHostException,
is HttpException,
is ConnectException,
is SocketTimeoutException,
is SocketException,
is NumberFormatException,
is IllegalArgumentException,
is IllegalStateException,
is JsonSyntaxException -> {
logW(TAG, "API 요청 예외: ${it.message}")
}
else -> {
logW(TAG, "API 요청 기타 예외: ${it.message}")
}
}
return null
}
return null
}
2.3 SSLSocketClient 정의
import java.security.KeyManagementException
import java.security.KeyStore
import java.security.NoSuchAlgorithmException
import java.security.SecureRandom
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import javax.net.ssl.*
object SecureSocketClient {
@Throws(NoSuchAlgorithmException::class, KeyManagementException::class)
fun getSSLSocketFactory(): SSLSocketFactory {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, getTrustManagers(), SecureRandom())
return sslContext.socketFactory
}
private fun getTrustManagers(): Array<TrustManager> {
val trustManager: X509TrustManager = object : X509TrustManager {
@Throws(CertificateException::class)
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun getAcceptedIssuers(): Array<X509Certificate> {
return arrayOf()
}
}
return arrayOf(trustManager)
}
fun getHostnameVerifier(): HostnameVerifier {
return HostnameVerifier { _, _ -> true }
}
@Throws(Exception::class)
fun getX509TrustManager(): X509TrustManager {
var trustManager: TrustManager? = null
val trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as? KeyStore)
val trustManagers = trustManagerFactory.trustManagers
if (trustManagers.size != 1 || trustManagers[0] !is X509TrustManager) {
throw IllegalStateException("예상치 않은 기본 trust managers: $trustManagers")
}
return trustManagers[0] as X509TrustManager
}
}
2.4 사용자 정의 CustomGsonConverterFactory
import com.google.gson.Gson
import com.google.gson.JsonIOException
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonToken
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import okio.Buffer
import retrofit2.Converter
import retrofit2.Retrofit
import java.io.ByteArrayInputStream
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.lang.reflect.Type
import java.nio.charset.Charset
import kotlin.text.Charsets.UTF_8
class CustomGsonConverterFactory private constructor(val gson: Gson): Converter.Factory() {
companion object {
fun create(): CustomGsonConverterFactory {
return create(Gson())
}
private fun create(gson: Gson?): CustomGsonConverterFactory {
if (gson == null) throw NullPointerException("gson == null")
return CustomGsonConverterFactory(gson)
}
}
override fun responseBodyConverter(
type: Type,
annotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<ResponseBody, *> {
return CustomGsonResponseBodyConverter(gson, gson.getAdapter(TypeToken.get(type)))
}
override fun requestBodyConverter(
type: Type,
parameterAnnotations: Array<out Annotation>,
methodAnnotations: Array<out Annotation>,
retrofit: Retrofit
): Converter<*, RequestBody> {
return CustomGsonRequestBodyConverter(gson, gson.getAdapter(TypeToken.get(type)))
}
}
private class CustomGsonRequestBodyConverter<T>(private val gson: Gson, private val adapter: TypeAdapter<T>) : Converter<T, RequestBody> {
private val MEDIA_TYPE = "application/json; charset=UTF-8".toMediaTypeOrNull()
private val UTF_8 = Charset.forName("UTF-8")
override fun convert(value: T): RequestBody {
val buffer = Buffer()
val writer = OutputStreamWriter(buffer.outputStream(), UTF_8)
val jsonWriter = gson.newJsonWriter(writer)
adapter.write(jsonWriter, value)
jsonWriter.close()
return buffer.readByteString().toRequestBody(MEDIA_TYPE)
}
}
private class CustomGsonResponseBodyConverter<T>(private val gson: Gson, private val adapter: TypeAdapter<T>) : Converter<ResponseBody, T> {
override fun convert(value: ResponseBody): T {
val response = value.string()
val apiResponse = gson.fromJson(response, ApiResponse::class.java)
/** 먼저 code와 message를 파싱하고, code가 0이 아닌 경우 ApiException을 던져서 onFailure()로 처리합니다 **/
if (!apiResponse.isSuccessful()) {
value.close()
throw ApiException(apiResponse.status, apiResponse.message)
}
val contentType = value.contentType()
val charset = contentType?.charset(UTF_8) ?: UTF_8
val inputStream = ByteArrayInputStream(response.toByteArray())
val reader = InputStreamReader(inputStream, charset)
val jsonReader = gson.newJsonReader(reader)
value.use { _ ->
val result = adapter.read(jsonReader)
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw JsonIOException("JSON 문서가 완전히 소비되지 않았습니다.")
}
return result
}
}
}
2.5 RetrofitClient 및 비즈니스 인터페이스 생성
object ApiClient {
private val instance: Retrofit by lazy {
val logger = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
val client = OkHttpClient.Builder()
.addInterceptor(logger)
.addInterceptor(Interceptor { chain ->
val originalRequest: Request = chain.request()
val request = originalRequest.newBuilder()
.header("content-type", "application/json;charset:utf-8")
.build()
chain.proceed(request)
})
.sslSocketFactory(SecureSocketClient.getSSLSocketFactory(), SecureSocketClient.getX509TrustManager())
.connectTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build()
Retrofit.Builder().baseUrl(ApiUrls.getBaseUrl())
.client(client)
.addConverterFactory(CustomGsonConverterFactory.create())
.build()
}
// TestApiService 참조
fun getTestApiService(): TestApiService {
return instance.create(TestApiService::class.java)
}
}
三、비즈니스 인터페이스 캡슐화
3.1 데이터 부분 정의
IDataSource
interface IDataSource {
suspend fun fetchPrivacyList(): List<PrivacyData>
}
TestApiService
interface TestApiService {
@POST("privacy/fetchPrivacyList")
suspend fun fetchPrivacyList(@Body request: PrivacyRequest): ApiResponse<List<PrivacyData>>
}
RemoteDataSource
class RemoteDataSource private constructor() : IDataSource {
private lateinit var testApiService: TestApiService
companion object {
@Volatile
private var instance: RemoteDataSource? = null
fun getInstance(context: Context): RemoteDataSource {
return instance ?: synchronized(this) {
instance ?: RemoteDataSource().also {
it.testApiService = ApiClient.getTestApiService()
instance = it
}
}
}
}
override suspend fun fetchPrivacyList(): List<PrivacyData> = withContext(Dispatchers.IO) {
val request = buildPrivacyRequest() // 요청 파라미터 생성
val result = apiRequest {
testApiService.fetchPrivacyList(request)
}
result?: mutableListOf()
}
}
TestRepository
object TestRepository {
private val localDataSource: LocalDataSource by lazy {
LocalDataSource.getInstance(App.appContext)
}
private val remoteDataSource: RemoteDataSource by lazy {
RemoteDataSource.getInstance(App.appContext)
}
suspend fun fetchPrivacyList(): List {
return remoteDataSource.fetchPrivacyList()
}
}