Post

External Client class에서 Exception을 던지는게 좋을까?

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

1
2
3
4
5
6
7
8
9
10
11
required(param)           // 에러 발생 가능

webClient...              // 에러 발생 가능

when (response.code) {    // 실패 발생 가능
    1000 -> // 성공코드 (데이터 반환)
    1007 -> // 기처리코드 (데이터 반환)
    2000 -> // 시스템에서 구분해야 하는 실패코드1
    2001 -> // 시스템에서 구분해야 하는 실패코드2
    else -> // 기타 실패로 처리해도 되는 나머지 모든 응답 코드
}

선택지

위와 같은 상황일 때, External Client class에서

  1. 1000일 때만 return하고, 나머지 케이스는 모두 Exception으로 처리하는게 나을까?
  2. 1000을 비롯한 모든 실패 응답 까지도 return 기반으로 가는게 나을까? (절대 Exception 던지지 않음)
  3. 일부는 Exception으로, 일부는 return으로, 즉 둘 다 사용하는 hybrid로 가는게 나을까?

상위 메서드에서 [catch 실패/return 실패]를 구분하지 않고 한 쪽으로 몰아서 처리하면 좋겠다는 생각 때문에
1, 2에 대해서 고민 할 수 있는데, 결국 이는 불가능하거나, 이상한 방향으로 진행된다.
결과적으로 3을 선택해야만 한다.

고려해야 할 실패 케이스

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

선택지 비교

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 예시 코드
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.data!!
    }
}
1
2
3
4
5
6
// callee
파라미터 에러 -> throw
WebClient 에러 -> throw
응답 실패 -> throw
응답 기처리 -> throw   --- 이 부분이 불가능하다.
응답 성공 -> return
  • 1007을 exception으로 처리하려는 경우?
  • 1007을 return으로 처리하려는 경우?
    • 여기서 1000, 1007은 둘 다 response.data가 존재하지만, response.data를 반환하게 되면 상위 메서드에서 1000인지 1007인지 알 수 없다. ‘1007 기처리’ 같은 케이스는 상위 메서드에서 기처리를 성공으로 볼지 실패로 볼지가 다 다를 수가 있어 반드시 response.code를 반환해야만 한다.
    • 하지만 response를 반환하면 상위 메서드에서 response.code에 접근 가능하므로, 전체 code에 대한 처리가 이중으로 들어갈 가능성이 있어 좋지 않은 리턴값 설계다.
  • 1000, 1007만 return으로 처리하되 response.code를 반환하지 않고 기처리 여부 boolean 필드를 만들어서 반환하는 것은?

성공 제외 모든 응답을 Exception으로 처리하려는 생각은 결국 이상한 방향으로 진행 될 가능성이 높다.

방안2 ) 1000을 비롯한 모든 실패 응답 까지도 모두 return 기반으로 (불가능)

1
2
3
4
5
// 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)

1
2
3
4
5
// callee
파라미터 에러 -> throw
WebClient 에러 -> throw
응답 실패 -> throw
응답 성공/기처리 -> return
1
2
3
4
5
6
7
// 1000, 1007을 제외한 response.code가 오면 Client에서 throw Exception
val response = client.post()
when (response.code) {
    1000 -> ...
    1007 -> ...
    else -> // 어떤 미흡한 보완방어로직
}

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

즉 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를 둘 다 써야 한다.

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

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

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

ClientService가 있다면 여기서 하고, ClientService가 없다면 각 사용처에서 private method로 처리한다.

This post is licensed under CC BY 4.0 by the author.