예시 API 응답으로 수신하는 code 및 그에 따른 반환값

when (response.code) {
    1000 -> 성공 (데이터 반환)
    1007 -> 기처리 (데이터 반환)
    2000 -> 구분해야하는 실패1
    2001 -> 구분해야하는 실패2
    else -> 그 외 모든 응답 코드(실패)
}

 

예시 상황에서, External Client class에서 Exception을 직접 던지는게 나을까? 아니면 return 기반으로 가는게 나을까? 둘 다 사용하는 hybrid로 가는게 나을까?

External Client class에서 Exception을 직접 던지는 경우 예시)

후술하겠지만 이렇게 처리하는 것은 좋지 않다.

AbcClient {
    fun post() {
        val response = webClient.post()
            ...
            .onErrorMap(TimeoutException::class.java) {
                MyException(Level.ERROR, ...)
            }
            .awaitSingle()

        when {
            response.data?.pgError?.isFooError() == true -> {
                throw MyException(Level.ERROR, ...)
            }
            response.isBarError() -> {
                throw MyException(Level.ERROR, ...)
            }
        }

        if (!response.isSuccess()) {
            throw MyException(Level.ERROR, ...)
        }

        return response
    }
}

 

 

 

고려해야 할 것

실패 케이스는 크게 3가지다 : [WebClient 에러, 파라미터 에러, 응답 실패]

  • '기처리' 같은 케이스는 1. 상위 메서드에서 기처리를 성공으로 볼지 실패로 볼지가 다 다를 수가 있고, 2. 실패 케이스와 달리 성공과 동일한 데이터를 상위에 반환해야 한다. 즉, '기처리' 케이스는 Exception으로 처리 할 수 없고 반드시 return을 사용해야 한다.
  • 응답 실패 중 몇몇 케이스는 상위 메서드에서 구분 해야 하는 경우가 있다.
  • 대체로 상위 메서드에서는 [catch 실패/return 실패]를 구분하고 싶지 않아 한다. WebClient 단에서 실패했든, 응답은 제대로 왔는데 실패코드를 반환한 것이든, 상위 메서드 입장에서는 동일한 실패다. => 이건 애초에 불가능한 전제다. 왜냐면...
  • 중요한 처리로직 인 경우, caller는 callee를 믿지 않는 것이 좋다. (따라서 심지어 callee가 Exception을 반환하지 않겠다고 명시해두었더라도 catch 해야 한다)
  • Client class에서는 DTO를 반환한다. VO를 만들어서 반환하는 것은 어색하고, 그럴거라면 ClientService로 wrapping 하는게 맞다.

 

 

방안1 ) 성공 제외 모두 Exception으로 (불가능)

// callee
파라미터 에러 -> throw
WebClient 에러 -> throw
응답 실패 -> throw
응답 기처리 -> throw   --- 이 부분이 불가능하다.
응답 성공 -> return

throw Exception을 return 처럼 정보 반환 용도로 사용해서는 안된다

 

방안2 ) 모두 return 기반으로 (불가능)

// callee
파라미터 에러 -> return
WebClient 에러 -> return
응답 실패 -> return
응답 성공/기처리 -> return

callee가 자기 자신을 try-catch로 감싸고 있어 Exception 날리가 없다고 말해도, caller는 callee를 믿으면 안된다.

  • Client 메서드는 보통 API 응답 DTO를 반환하는 것을 생각해보면, 파라미터 에러, WebClient 에러 시에는 어떤 코드를 반환하는 것이 불가능하다. (API 서버에서 반환하지도 않는 코드로 세팅해서 리턴해야 하나? 이상하다.)
  • 에러를 값으로 다루고 싶다면 상위 메서드에서 runCatching을 쓰는 식으로 접근해야지, Client 레벨에서 메서드 전체를 try-catch 해서 return 기반으로 쓰는건 별 의미가 없다.

 

방안3 ) 응답 실패 코드 중 일부는 Exception으로, 일부는 return으로 (bad)

// callee
파라미터 에러 -> throw
WebClient 에러 -> throw
응답 실패 -> throw
응답 성공/기처리 -> return

응답 실패 코드 중 일부는 Exception으로, 일부는 return으로 처리하는 것은 가장 나쁘다. callee에서 일부 실패 코드에 대해 Exception을 던진다 해도, 상위 메서드에서 response.code에 접근 가능한 이상, 되도록 모든 응답 code값에 대한 if 처리를 고려하는게 분기처리 일원화 관점에서 더 타당하다.

// 1000, 1007을 제외한 response.code가 오면 Client에서 throw Exception
val response = client.post()
when (response.code) {
    1000 -> ...
    1007 -> ...
    else -> // 어떤 미흡한 보완방어로직
}
이렇게 처리하는 것은 좋지 않은데
response.code 값에 따른 동작이
일부는 client 안에, 일부는 바깥에 존재하기 때문이다.
when 구문에서 모든 response.code 케이스에 접근 가능한데
callee를 믿고 이에 대한 처리를 해주지 않는다는 것이 어색하게 느껴진다.
else에 보완방어로직을 넣을 수는 있지만 
애초에 보완 할 필요가 없게끔 구조 자체를 개선하는 것이 더 낫다.

 

방안4 ) WebClient 에러는 Exception으로, 응답 성공/실패는 return으로 (good)

  • 어차피 caller는
    • 중요 로직인 경우 반드시 catch를 써야만 하고
    • 중요하지 않은 로직인 경우 직접 catch하지 않고 GlobalExceptionHandler에서 처리하도록 놔둘 수 있으며
  • callee는 모든 실패를 throw 할 수 없고 정보 반환을 위해 반드시 return 해야하는 케이스가 있으니 (e.g., 기처리)

 

어차피 callee는 return, throw를 둘 다 써야 하고 / caller는 if, catch를 둘 다 써야 한다.

 

아래와 같이 처리하는게 최선으로 보인다.

// callee
파라미터 에러 -> throw
WebClient 에러 -> throw
응답 실패/성공/기타성공 -> return
// caller
try {
    val response = callee()
    when (response.code) { ... }
catch (...) {
    // 또는 GlobalExceptionHandler에서 잡도록 pass
}

 

그렇다면 응답 실패를 해석하고 Exception으로 바꿔서 던지는 것은 어디서 해야 하는가?

ClientService가 있다면 여기서 하고,

ClientService가 없다면 각 사용처에서 private method로 처리한다.