interface HttpClient {
companion object {
const val DEFAULT_CONNECTION_TIMEOUT = 15_000
const val DEFAULT_READ_TIMEOUT = 15_000
}
fun<IN: Any?, OUT: Any> doRequest(
method: RequestMethod,
path: String,
outClass: KClass<OUT>,
headers: List<Header> = emptyList(),
body: IN? = null
): OUT
fun <OUT : Any> doRequest(
method: RequestMethod,
path: String,
outClass: KClass<OUT>,
headers: List<Header> = emptyList()
): OUT = doRequest<Any, OUT>(method, path, outClass, headers, body = null)
}
enum class RequestMethod {
GET, POST, PUT, DELETE
}
interface를 선언해 구현체를 유연하게 사용할 수 있도록 만든다.
기본 함수로는 doRequest가 있는데, body가 있는것과 없는 함수로 추가했는데,
body가 있는 함수의 경우 사용시 IN의 데이터 타입을 명시해주거나
body를 null로 명시적으로 추가해줘야 하기 때문에 편의성을 위해 위와 같이 구성했다.
class MyHttpClient private constructor(
private val baseUrl: String,
private val connectTimeout: Int,
private val readTimeout: Int,
private val connectionFactory: ConnectionFactory,
private val converter: Converter
) : HttpClient
class Builder {
private var baseUrl: String = ""
private var connectTimeout: Int = HttpClient.DEFAULT_CONNECTION_TIMEOUT
private var readTimeout: Int = HttpClient.DEFAULT_READ_TIMEOUT
private var connectionFactory: ConnectionFactory = {
URL(it).openConnection() as HttpURLConnection
}
private var converter: Converter = MyJsonConverter()
fun setBaseUrl(url: String): Builder {
baseUrl = url
return this
}
fun setConnectTimeout(timeout: Int): Builder {
connectTimeout = timeout
return this
}
fun setReadTimeout(timeout: Int): Builder {
readTimeout = timeout
return this
}
fun setConnectionFactory(factory: ConnectionFactory): Builder {
connectionFactory = factory
return this
}
fun setConverter(converter: Converter): Builder {
this.converter = converter
return this
}
fun build(): MyHttpClient = MyHttpClient(
baseUrl,
connectTimeout,
readTimeout,
connectionFactory,
converter
)
}
구현체의 생산자와 Builder는 위와같이 추가, Builder패턴 사용을 위해 private constructor로 추가했다.
converter는 HttpClient 내부에서 Request, Response의 데이터 <-> json 변환을 담당한다.
retrofit처럼 외부에서 converter를 설정 가능한 방법으로 구현한다.
override fun <IN : Any?, OUT : Any> doRequest(
method: RequestMethod,
path: String,
outClass: KClass<OUT>,
headers: List<Header>,
body: IN?
): OUT = request(method, path, outClass, headers, body)
override fun <OUT : Any> doRequest(
method: RequestMethod,
path: String,
outClass: KClass<OUT>,
headers: List<Header>
): OUT = request(method, path, outClass, headers, null)
private fun<IN, OUT: Any> request(
method: RequestMethod,
path: String,
outClass: KClass<OUT>,
headers: List<Header>,
body: IN?
) : OUT {
val connection = createConnection(method, path, headers, body)
val response = connection.handleCommonResponse(outClass)
return response
}
내부 구현은 위와 같이 되어있다.
doRequest 함수는 내부적으로 request함수를 실행하고, 매 요청시마다 connection을 생성한다.
private fun<IN> createConnection(
method: RequestMethod,
path: String,
headers: List<Header>,
body: IN?,
apply: (HttpURLConnection) -> Unit = {}
): HttpURLConnection {
val url = "${baseUrl}$path"
return connectionFactory(url).apply {
requestMethod = method.name
connectTimeout = this@MyHttpClient.connectTimeout
readTimeout = this@MyHttpClient.readTimeout
setRequestProperty(Headers.CONTENT_TYPE_HEADER_KEY, Headers.CONTENT_TYPE_HEADER_VALUE)
setRequestProperty(Headers.ACCEPT_HEADER_KEY, Headers.ACCEPT_HEADER_VALUE)
headers.forEach { setRequestProperty(it.key, it.value) }
val jsonPayload = body?.let { converter.serialize(it) }
if (!jsonPayload.isNullOrBlank()) {
doOutput = true
outputStream.use { it.write(jsonPayload.toByteArray(Charsets.UTF_8)) }
}
apply(this)
}
}
이때 생성자의 connectionFactory를 사용하는데, 필요에 따라 여기서 HttpURLConnection 생성 방법을 정한다.
이를 통해 테스트에서 유연한 URLConnection 주입이 가능하다.
private fun<OUT: Any> HttpURLConnection.handleCommonResponse(outClass: KClass<OUT>): OUT {
val responseCode = responseCode
val isSuccess = responseCode == HttpURLConnection.HTTP_OK
val stream = if (isSuccess) inputStream else errorStream
val response = stream.bufferedReader().use { it.readText() }
disconnect()
if (isSuccess) {
return converter.deserialize(response, outClass)
} else {
throw IOException(response)
}
}
통신 결과는 connection.responseCode를 호출하면 서버에서 응답을 받을때까지 대기한다.
이 작업은 synchronous하게 진행되기 떄문에 main thread에서 호출하지 않도록 주의해야한다.
이후 응답받은 결과의 responseCode를 확인해서 200인 경우 response를 역직렬화 하여 반환하고,
이외의 경우 에러를 발생시켜 호출부에서 처리하도록 한다.
'안드로이드 > 개발관련(Kotlin)' 카테고리의 다른 글
| [Android] 라이브러리 없이 직렬화/역직렬화 구현 (0) | 2025.11.30 |
|---|---|
| [Android] 라이브러리 없이 클라이언트에서 테스트 서버 구현 (0) | 2025.11.30 |
| [Android] jvmTarget 지정 방식이 deprecated 된 이유 (0) | 2025.11.29 |
| [Android] TYPESAFE_PROJECT_ACCESSORS (1) | 2024.10.09 |
| [Android] Android Compose navigation 사용 시 recomposition 이슈 (0) | 2024.09.24 |