본문 바로가기
Study OR Book/Book

[이펙티브 코틀린] 아이템29: 외부 API를 랩(wrap)해서 사용하라

by Baest 2025. 1. 21.

 

 

이번 챕터는 코드와 관련된 기술적인 내용 보다는 좋은 코드에 대한 가이드가 간략하게 나와 있는 것 같다.

 

API가 불안정한 이유에는 여러가지가 있을 것이고, 이때 불안정한 API를 과도하게 사용하는 것은 위험한 일이다.

만약 이러한 경우 불가피하게 API를 활용해야 한다면, 최대한 API를 로직과 분리시키는 것이 좋다.

많은 프로젝트가 잠재적으로 불안정하다고 판단되는 외부 라이브러리 API를 랩(wrap)해서 사용한다.

 

랩해서 사용 했을 때의 

 

장점

 

1. 문제가 있다면 래퍼(wrapper)만 변경하면 되므로, API 변경에 쉽게 대응 가능하다. (캡슐화하여 사용하기 때문)

2. 프로젝트의 스타일에 맞춰 API 형태 조정이 가능하다. (직렬화 등을 래퍼에서 처리 가능)

3. 특정 라이브러리에 문제 발생 시 래퍼를 수정해서 다른 라이브러리를 사용하도록 코드를 변경할 수 있다.

4. 필요한 경우 쉽게 동작 추가 및 수정이 가능하다.

 

 

단점

1. 래퍼에 대한 별도 정의가 필요하다.

2. 다른 개발자가 프로젝트를 다룰 때, 어떤 래퍼들이 있는지 따로 확인해야한다.

3. 래퍼들은 프로젝트 내부에서만 유효하므로, 문제가 생겨도 stack overflow 등에 도움을 요청하기 어렵다.

 

 

정리

1. 래퍼를 사용하기 전 장점 및 단점에 대하여 인지하고 있어야한다.

2. 라이브러리에 대한 안정성을 확인하고 싶다면, 유저 수로 어느 정도 판단이 가능하다. 

3. 신규 라이브러리 사용이 필요하다면, 클래스와 함수로 랩해서 사용하는 것을 고려해 보면 좋다.

 

 

예시

class ApiWrapper(baseUrl: String) {
    private val webClient: WebClient = WebClient.builder()
        .baseUrl(baseUrl)
        .build()

    fun get(endpoint: String, params: Map<String, String> = emptyMap()): String {
        return try {
            webClient.get()
                .uri { uriBuilder ->
                    uriBuilder.path(endpoint).apply {
                        params.forEach { (key, value) ->
                            queryParam(key, value)
                        }
                    }.build()
                }
                .retrieve()
                .bodyToMono(String::class.java)
                .block() ?: throw RuntimeException("Empty response")
        } catch (e: WebClientResponseException) {
            handleApiError(e)
            throw e
        }
    }

    private fun handleApiError(e: WebClientResponseException) {
        println("Error occurred: ${e.statusCode} - ${e.responseBodyAsString}")
    }
}

fun main() {
    val apiWrapper = ApiWrapper("https://api.example.com")
    val response = apiWrapper.get("/users", mapOf("id" to "123"))
    println(response)
}

 

 

아래와 같이 슬랙 얼럿을 보낼 때도 wrapping 하여 사용하고 있다.

@Service
class SlackWebhookClient(
    @Qualifier("monitoringWebClient")
    private val webClient: WebClient,
    private val slackWebhookProperties: SlackWebhookProperties,
) {
    fun sendMessage(
        serviceKey: String,
        slackMessage: SlackMessage,
    ): Mono<Void> {
        val webhookUrl =
            slackWebhookProperties.webhooks[serviceKey]
                ?: throw IllegalArgumentException("Webhook URL for service key '$serviceKey' not found")
        println("SlackWebhookClient webhookUrl: $webhookUrl")
        return webClient.post()
            .uri(webhookUrl)
            .body(BodyInserters.fromValue(slackMessage))
            .retrieve()
            .onStatus({ it.isError }) { response ->
                response.bodyToMono(String::class.java)
                    .flatMap { errorBody ->
                        Mono.error(
                            RuntimeException("Slack API Error: ${response.statusCode()} - $errorBody"),
                        )
                    }
            }
            .bodyToMono(Void::class.java)
    }
}