오타 때문에 500 에러가 발생한 문제
상황
유저 서비스에서 주문 서비스를 호출하기 위해 Feign Client를 사용했다.
그런데 사용하는 과정에서 500에러가 발생했다.
나중에 찾아보니 엔드포인트 과정에서 오타가 있었다.
정상 엔드포인트
@GetMapping("/order-service/{userId}/orders")
내가 잘못 작성한 엔드포인트
@GetMapping("/oders-service/{userId}/orders")
문제 현상
실제 주문 서비스에서는 /oders 라는 엔드포인트가 없음.
주문 서비스에서는 404를 반환한다.
그러나 호출한 유저 서비스에서는 500 Internal Server Error 로 보인다.
Feign 자체가 받은 HTTP 오류를 잡아 내부에서 예외로 변환하며, 이를 잡지 않으면 상위로 500이 터진다.
간단한 예제
@FeignClient(name = "order-service")
public interface OrderClient {
// ❌ 오타가 있는 엔드포인트
@GetMapping("/oders-service/{userId}/orders")
List<OrderResponse> getOrder(@PathVariable Long id);
}
List<OrderResponse> orderList = orderServiceClient.getOrders(userDto.getUserId());
userDto.setOrders(orderList);
정리
Feign은 HTTP 오류(400, 404 등) -> FeignException으로 변환한다.
try/catch가 없으면 스프링은 기본적으로 500으로 응답한다.
그래서 유저서비스에서는 무조건 500이 터지는 것처럼 보인다.
결과적으로 엔드포인트 요청이 잘못되어서 오류가 발생했는지 어디서 오류가 발생한건지 찾기가 힘들다.
try/catch 예외처리
해결 시도
Feign 예외을 try/catch로 잡아서 내부에서 원래 주문 서비스가 준 상태 코드를 확인하도록 처리해보았다.
List<OrderResponse> orderList = null;
try {
orderList = orderServiceClient.getOrders(userId);
} catch (FeignException e) {
log.error(e.getMessage());
}
List<OrderResponse> orderList = orderServiceClient.getOrders(userDto.getUserId());
userDto.setOrders(orderList);
결과
유저 서비스에서도 주문 서비스에서 보낸 404예외가 잘 전달이 되어서 응답이 될거고,
500예외가 아닌 원래 의도한 HTTP 상태 코드를 살릴 수 있게 되었다.
간단하고 빠르게 해결 가능하지만 feignclient를 사용하는 모든 로직에서 try/catch를 사용하게 되면 코드 중복이 생긴다..
그리고 아직 위에 코드에서는 예외 처리에 대한 공통 예외처리나, 메시지를 적용하지 않았지만 클라이언트에게 보여주는 메시지 형식이나 텍스트를 바꾸려고 하면 많은 부분을 수정해야 하고 유지보수가 굉장히 비효율적으로 극단적이게 된다..
따라서 아래에서 소개하는 ErrorDecoder를 활용하는 편이 좋다.
ErrorDecoder 구현
@Component
@RequiredArgsConstructor
public class FeignErrorDecoder implements ErrorDecoder {
private final Environment env;
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()) {
case 400:
break;
case 404:
if (methodKey.contains("getOrders")) {
return new ResponseStatusException(HttpStatus.valueOf(response.status()),
env.getProperty("order-service.exception.order-is-empty"));
}
break;
default:
return new Exception(response.reason());
}
return null;
}
}
List<OrderResponse> orderList = orderServiceClient.getOrders(userDto.getUserId());
userDto.setOrders(orderList);
ErrorDecoder는 예외를 자동 변환을 해준다. 즉 feignclient를 사용하는 서비스 계층에서는 예외처리를 잡지 않아도 된다.
try/catch 문을 사용하지 않아도 된다. 실제로 위 코드 case 404: 라인 아래에 if 문에다가 브레이크포인트를 잡고 디버그 모드로 활성화 하여 테스트를 진행하면 해당 브레이크 포인트 시점으로 잡히는 것을 확인할 수 있다.
그리고 추가적으로 나 같은 경우는 yml 파일에서 예외 메시지를 관리하고 있어 나중에 추가적으로 메시지 변경사항이 있을 때 yml파일만 수정하고, 서버를 재시작하지 않아도 바로 적용할 수 있게 RabbitMQ로 파이프라인을 구성해두었다.
try/catch 문과 비교를 해보자면
| try/catch | ErrorDecoder | |
| 예외 처리 위치 | 각 컨트롤러/서비스 메서드마다 작성 | 전역 공통 처리 |
| 중복 여부 | 매우 높음 (메서드마다 반복) | 없음 (단 한 파일) |
| 유지 보수성 | 낮음 | 매우 높음 |
| HTTP 상태 코드 처리 | 직접 e.status() 꺼내야 함 | ErrorDecoder에서 공통 처리 |
| 컨트롤러 코드 | 지저분해짐 | 깔끔함 |
| 추천 옵션 | ❌ (임시 방편) | ✔ 권장 (공식 문서도 이 방식을 사용) |
정리
Feign은 HTTP오류를 FeignException으로 변환하여 던진다.
try/catch가 없으면 스프링에서 500을 반환해, 원래 오류 코드가 덮어져서 가려진다.
try/catch로 감싸면 원래 상태코드를 클라이언트에 그대로 전달할 수 있지만 중복 코드가 많다.
그래서 ErrorDecoder를 구현해 전역 공통 예외 처리하는 것이 가장 좋은 방법이라 생각한다.
ErrorDecoder를 사용하면 각 API는 Exception을 서비스 계층에서 신경쓰지 않아도 되며, 유지보수성이 증가한다.
'BACKEND & SERVER > Spring Boot MSA' 카테고리의 다른 글
| [Spring Boot] Spring Kafka Consumer에서 JSON 처리하기 (0) | 2025.11.26 |
|---|---|
| [Spring Boot] Kafka 기반 주문 서비스와 상품 서비스의 비동기 재고 차감 (0) | 2025.11.22 |
| [Spring Boot] 스프링 부트 슬랙 API 연동하고 이메일 인증하기 (1) | 2025.11.11 |
| [Spring Boot] JitPack | MSA Project GateWay Swagger 구현 (0) | 2025.11.05 |
| [Spring Boot] JitPack | MSA 프로젝트에서 공통 관심사 분리하기 (0) | 2025.11.03 |
