상황: Auction 서비스에서 Payment 서비스를 REST로 호출할 때, Payment 서비스 내부에서 발생한 "예치금 잔액 부족" 등의 커스텀 예외 메시지가 클라이언트에게 전달되지 않고 500 Internal Server Error로만 나타나는 문제 발생.
// [Payment 서비스 - 도메인 내부 로직]
public void hold(int amount) {
if (balance - holdingAmount < amount) {
// 실제로는 여기서 비즈니스 예외가 발생함
throw new CustomException(ErrorType.INSUFFICIENT_BALANCE);
}
}
// [Auction 서비스 - API 클라이언트]
public DepositHoldResponseDto holdDeposit(int amount, String memberPublicId, Long auctionId) {
DepositHoldRequestDto request = new DepositHoldRequestDto(amount, memberPublicId, auctionId);
// (문제) 에러 응답(4xx/5xx)에 대한 별도 핸들링이 없음
// RestClient는 기본적으로 실패 시 RestClientResponseException을 던짐
SuccessResponseDto<DepositHoldResponseDto> response = restClient.post()
.uri("/deposits/hold")
.body(request)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
return response.data();
}
REST 통신은 텍스트 기반이므로, 호출된 서비스에서 발생한 Java 예외 객체를 직접 전달할 수 없음
RestClient는 4xx, 5xx 응답을 받으면 예외를 던지지만, 응답 본문(Body)에 담겨온 상세 에러 JSON(ExceptionResponseDto)을 자동으로 파싱해주지 않는다.Payment 서비스가 "잔액 부족(400)" 에러를 뱉어도, 이를 호출한 Auction 서비스는 단순히 "REST 호출이 실패했다"는 사실만 인지한다. 결과적으로 실제 원인은 사라지고 클라이언트에게는 500 서버 에러가 노출된다.Payment 서비스 로그: CustomException: 예치금 잔액이 부족합니다. (정상 발생)Auction 서비스 로그: 500 Internal Server Error (원인 불명)공통 에러 핸들러를 도입하여 HTTP 에러 응답 본문의 에러 코드를 파싱하고, 원래의 비즈니스 예외를 그대로 복원
ErrorType에 역직렬화 메서드 추가에러 코드(예: 4002)를 가지고 다시 ErrorType enum을 찾을 수 있는 기능을 추가한다.
public enum ErrorType {
INSUFFICIENT_BALANCE(400, 4002, "예치금 잔액이 부족합니다."),
// ...
public static Optional<ErrorType> findByCode(int code) {
return Arrays.stream(values())
.filter(type -> type.code == code)
.findFirst();
}
}
InternalApiErrorHandler) 구현응답 본문을 읽어 원래의 에러 코드와 메시지를 추출한 뒤, CustomException을 재생성한다.
@Component
@RequiredArgsConstructor
public class InternalApiErrorHandler {
private final ObjectMapper objectMapper;
public void handle(HttpRequest request, ClientHttpResponse response) throws IOException {
String body = new String(response.getBody().readAllBytes());
try {
// JSON 응답을 ExceptionResponseDto로 역직렬화
ExceptionResponseDto errorResponse = objectMapper.readValue(body, ExceptionResponseDto.class);
// 에러 코드로 원래 ErrorType 복원
ErrorType errorType = ErrorType.findByCode(errorResponse.code())
.orElse(ErrorType.INTERNAL_SERVER_ERROR);
// 원래 서비스의 메시지를 담아 예외 재생성
throw new CustomException(errorType, errorResponse.message());
} catch (Exception e) {
throw new CustomException(ErrorType.INTERNAL_SERVER_ERROR, "내부 서비스 호출 실패");
}
}
}
@Service
@RequiredArgsConstructor
public class PaymentApiClient {
private final RestClient restClient;
private final InternalApiErrorHandler errorHandler;
public DepositHoldResponseDto holdDeposit(...) {
return restClient.post()
.uri("/deposits/hold")
.body(request)
.retrieve()
// 모든 에러 응답에 대해 공통 핸들러 적용
.onStatus(HttpStatusCode::isError, errorHandler::handle)
.body(new ParameterizedTypeReference<>() {})
.data();
}
}