Feign 오류를 ErrorDecoder와 도메인 예외로 분리하기
예약 서비스가 쿠폰 서비스에 쿠폰 유효성을 물어본다고 해 보자. 쿠폰이 만료됐다면 쿠폰 서비스는 자기 서비스의 오류 코드와 메시지를 응답한다. 그런데 예약 서비스의 여러 메서드가 이 응답을 각자 파싱하기 시작하면 같은 변환 코드가 퍼지고, 한 곳은 만료 코드를 처리하고 다른 곳은 빠뜨리는 식의 차이가 생긴다.
예전 프로젝트에서 실제로 부딪힌 문제도 비슷했다. Feign 호출마다 상대 서비스의 오류 코드와 메시지를 예약 서비스가 이해하는 오류로 바꾸는 코드가 반복됐다. 이를 FeignExceptionUtils로 모아 중복을 줄이려 했다. 원문은 당시 구현에 초점을 두고 있었지만, 다시 정리하면서 공통 변환의 위치와 책임을 더 분명히 나눌 필요가 있다고 봤다.
원격 오류와 공개 API 계약 분리
상대 서비스의 오류 응답은 그 서비스 내부의 계약이다. 예약 API를 호출한 사용자에게 그 JSON이나 메시지를 그대로 전달하면, 쿠폰 서비스의 응답 변경이 예약 API의 외부 계약까지 흔들 수 있다. 원격 오류 본문에는 내부 서비스명, 디버그 정보, 입력값이 섞일 수도 있다.
따라서 응답은 경계마다 의미를 바꿔야 한다.
쿠폰 서비스의 HTTP 응답
→ Feign ErrorDecoder가 원격 오류 분류
→ 예약 서비스 예외로 변환
→ 예약 API의 공개 오류 응답으로 변환
예를 들어 쿠폰 서비스의 COUPON_EXPIRED 코드를 예약 도메인의 CouponUnavailableException으로 매핑할 수 있다. 외부 응답은 예약 서비스가 정한 안정적인 코드와 사용자가 이해할 수 있는 메시지를 사용한다. 원격 본문 전체를 사용자에게 내보내지는 않는다.
ErrorDecoder와 도메인 서비스의 역할
Feign의 ErrorDecoder는 HTTP 응답이 2xx가 아닐 때 이를 애플리케이션 예외로 바꿀 수 있는 확장 지점이다. Spring Cloud OpenFeign에서도 클라이언트별 설정이나 빈으로 ErrorDecoder를 연결할 수 있다. 여기서는 HTTP 상태와 약속된 원격 오류 코드를 읽어 “원격 도메인 거절”인지 “일시적인 서버 오류”인지 분류하는 역할을 맡긴다.
그 다음 서비스 계층은 그 실패가 현재 업무에서 어떤 뜻인지 결정한다. 만료된 쿠폰은 사용자에게 다른 쿠폰을 고르도록 안내할 수 있지만, 쿠폰 서비스의 503은 일시적인 의존 서비스 장애로 다뤄야 한다. 하나의 FeignExceptionUtils가 HTTP 파싱, 재시도 결정, 도메인 예외 선택, API 응답 문구까지 모두 맡으면 책임이 다시 뭉친다.
| 실패 종류 | 예시 | 예약 서비스에서의 처리 |
|---|---|---|
| 업무상 거절 | 쿠폰 만료, 대상 사용자 아님 | 도메인 예외로 변환하고 자동 재시도하지 않음 |
| 원격 서버 오류 | HTTP 502·503 | 멱등한 호출에 한해 제한된 재시도 검토 |
| 응답 없는 통신 실패 | 연결 실패, 읽기 시간 초과 | 네트워크 장애로 분류하고 타임아웃·호출 단계 기록 |
| 계약 밖 응답 | 알 수 없는 오류 코드, 잘못된 JSON | 안전한 기본 예외로 격리하고 진단 정보 남김 |
HTTP 상태가 성공 범위가 아닌 응답은 ErrorDecoder로 들어오지만, DNS 실패나 연결 거부처럼 응답 자체가 없으면 다른 예외 경로를 탄다. 둘을 같은 “Feign 오류”로만 기록하면 원격 업무 거절과 인프라 장애를 구분하기 어렵다.
요청 성격에 맞춘 재시도 정책
조회 요청이 일시적 타임아웃으로 실패했다면 짧은 재시도가 도움이 될 수 있다. 반면 예약 생성이나 결제 요청은 서버에서 이미 처리했는데 응답만 돌아오지 않았을 수 있다. 이때 무조건 다시 보내면 예약이나 결제가 중복될 수 있다. 재시도를 허용할 호출에는 멱등 키를 함께 보내고, 최대 횟수와 전체 호출 시간을 제한해야 한다.
Spring Cloud OpenFeign의 재시도 기본값은 순수 Feign과 다를 수 있다. 현재 문서는 기본 Retryer.NEVER_RETRY 구성을 설명한다. 프로젝트 버전과 클라이언트 설정을 확인하지 않고 공통 재시도 정책을 전역으로 켜면, 생각하지 못한 쓰기 요청까지 다시 실행될 수 있다. 그래서 오류 디코더와 재시도기는 목적이 다른 설정으로 분리하는 편이 이해하기 쉽다.
로그에는 호출한 클라이언트와 메서드, HTTP 상태, 추적 ID, 계약된 오류 코드 정도를 남기면 장애를 찾는 데 도움이 된다. 요청·응답 본문이나 인증 헤더 전체를 그대로 기록하는 방식은 피한다. 원격 메시지가 필요한 경우에도 길이를 제한하고 민감한 값이 제거됐는지 확인해야 한다.
처음 FeignExceptionUtils를 만들었던 목적은 반복을 줄이는 것이었다. 지금 기준에서 더 나은 공통화는 모든 실패를 한 예외로 덮는 것이 아니라, 원격 응답을 한 번 안전하게 해석하고 업무 의미는 호출 서비스가 결정하게 두는 것이다. 그렇게 해야 원격 서비스의 변경을 격리하면서도 운영 로그에서 장애 원인을 잃지 않는다.
참고 문서: OpenFeign ErrorDecoder 인터페이스와 오류 처리, Spring Cloud OpenFeign 레퍼런스
당시 기록: Feign Client 예외 처리 공통화