안드로이드/개발관련(Kotlin)

[Android] HttpsURLCoonection을 사용한 인터넷 통신

닉네임못짓는사람 2025. 11. 30. 02:33
반응형
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를 역직렬화 하여 반환하고,

이외의 경우 에러를 발생시켜 호출부에서 처리하도록 한다.

반응형