- OpenFeign에서는 HTTP 통신 과정에서 발생할 수 있는 다양한 예외 상황을 처리하기 위한 여러 메커니즘을 제공합니다. - 주로 ErrorDecoder를 통한 전역적 예외 처리와 try-catch를 통한 개별 예외 처리 방식을 활용할 수 있습니다. - 또한 @ControllerAdvice를 사용하여 애플리케이션 전체의 예외 처리를 통합적으로 관리할 수 있습니다.
1. 주요 오류 처리 목적
목적
설명
예외 상황 관리
네트워크 오류, 타임아웃, 서버 오류 등 다양한 예외 상황을 적절히 처리
시스템 안정성
오류가 발생해도 시스템이 계속 작동할 수 있도록 보장
사용자 경험
오류 발생 시 적절한 피드백을 제공하여 사용자 경험 향상
디버깅 용이성
상세한 오류 정보를 로깅하여 문제 해결을 용이하게 함
2. OpenFeign의 발생하는 오류 정보
💡 OpenFeign의 발생하는 오류 정보
- OpenFeign를 통해서 발생하는 예외 클래스에 대해 알아봅니다.
분류
예외 클래스
HTTP 상태
설명
클라이언트 오류
FeignException.BadRequest
400
잘못된 요청 형식이나 유효하지 않은 파라미터로 인한 에러
클라이언트 오류
FeignException.Unauthorized
401
인증 정보가 없거나 유효하지 않은 경우
클라이언트 오류
FeignException.Forbidden
403
권한이 없는 리소스에 접근 시도
클라이언트 오류
FeignException.NotFound
404
요청한 리소스를 찾을 수 없음
클라이언트 오류
FeignException.MethodNotAllowed
405
요청한 HTTP 메서드가 허용되지 않음
클라이언트 오류
FeignException.NotAcceptable
406
클라이언트가 요청한 형식으로 응답할 수 없음
클라이언트 오류
FeignException.Conflict
409
리소스 상태의 충돌로 인한 요청 실패
클라이언트 오류
FeignException.UnsupportedMediaType
415
지원하지 않는 미디어 타입
서버 오류
FeignException.ServiceUnavailable
503
서비스를 일시적으로 사용할 수 없음
서버 오류
FeignException.GatewayTimeout
504
게이트웨이 타임아웃
연결 오류
FeignException.ConnectException
-
서버 연결 시간 초과
연결 오류
FeignException.ReadTimeoutException
-
응답 읽기 시간 초과
컨텐츠 오류
FeignException.UnsupportedMediaType
-
지원하지 않는 미디어 타입
재시도 오류
RetryableException
-
재시도 가능한 오류
파싱 오류
DecodeException
-
응답 디코딩 실패
파싱 오류
EncodeException
-
요청 인코딩 실패
통합 오류
FeignClientException
4xx
Feign 클라이언트 관련 4xx 상태 코드에 대한 통합 오류
통합 오류
FeignServerException
5xx
Feign 서버 관련 관련 5xx 상태 코드에 대한 통합 오류
3. 오류 처리 방식 종류
💡 오류 처리 방식 종류
- OpenFeign를 통한 외부 통신을 하는 과정에서 발생하는 오류를 처리하는 방식들을 아래와 같습니다.
오류 처리 방식
사용 방법
적용 범위
장점
ErrorDecoder
ErrorDecoder 인터페이스 구현 클래스 작성 및 Bean 등록
전역적 적용
중앙화된 오류 처리, 일관된 예외 변환
try/catch
개별 메서드에서 FeignException 하위 클래스 catch
메서드 단위
세밀한 예외 처리, 상황별 대응 가능
@ControllerAdvice
@ExceptionHandler 메서드 구현
애플리케이션 전체
통합된 예외 처리, REST API 응답 표준화
Custom Exception
사용자 정의 예외 클래스 생성 및 변환 로직 구현
필요한 영역
비즈니스 로직에 특화된 예외 처리
3) OpenFeign 예외처리 종류 -1 : ErrorDecoder 인터페이스 활용
💡 OpenFeign 예외처리 종류 -1 : ErrorDecoder 인터페이스 활용
- OpenFeign에서 HTTP 통신 중 발생하는 오류 응답을 처리하기 위한 인터페이스입니다. - HTTP 응답이 오류 상태 코드(4xx, 5xx)를 반환할 때, 이를 적절한 Java Exception으로 변환하는 역할을 합니다.
오류 처리 방식
사용 방법
적용 범위
장점
ErrorDecoder
ErrorDecoder 인터페이스 구현 클래스 작성 및 Bean 등록
전역적 적용
중앙화된 오류 처리, 일관된 예외 변환
try/catch
개별 메서드에서 FeignException 하위 클래스 catch
메서드 단위
세밀한 예외 처리, 상황별 대응 가능
@ControllerAdvice
@ExceptionHandler 메서드 구현
애플리케이션 전체
통합된 예외 처리, REST API 응답 표준화
Custom Exception
사용자 정의 예외 클래스 생성 및 변환 로직 구현
필요한 영역
비즈니스 로직에 특화된 예외 처리
1. ErrorDecoder 인터페이스 특징
특징
설명
중앙 집중화된 예외 처리
모든 Feign 클라이언트의 에러 처리 로직을 한 곳에서 관리할 수 있습니다.
일관된 예외 변환
HTTP 응답 코드를 애플리케이션의 커스텀 예외로 일관되게 변환할 수 있습니다.
세밀한 에러 핸들링
HTTP 상태 코드별로 다른 예외 처리 로직을 구현할 수 있습니다.
로깅 통합
에러 발생 시 로깅을 중앙에서 처리할 수 있어 모니터링이 용이합니다.
유연한 확장성
새로운 에러 케이스 추가나 기존 처리 로직 수정이 용이합니다.
2. 일반적인 ErrorDecoder 구현
💡 일반적인 ErrorDecoder 구현
- ErrorDecoder 인터페이스의 구현체로 FeignClientCustomErrorDecoder라는 이름의 클래스를 구성하였습니다. - 해당 구현체로 Exception decode(String methodKey, Response response)를 오버라이딩 받아서 이를 재 구현합니다. - Response 객체 내에 status 메서드는 OpenFeign를 통해서 통신 결과 status 코드를 반환 받습니다. - 이를 통해서 exception 처리를 수행합니다.
💡 FeignClientGlobalErrorDecoder
- 아래의 반환되는 status 코드에 따라 예외 클래스를 발생시키고 메시지를 추가하여 반환하도록 구성하였습니다.
Status
코드 예외 클래스
메시지
400
BadRequestException
잘못된 요청입니다.
404
NotFoundException
리소스를 찾을 수 없습니다.
500
InternalServerException
서버 내부 오류가 발생했습니다.
기타
Exception
알 수 없는 오류가 발생했습니다.
package com.blog.springbootkeycloak.config.feign.exception;
import feign.Response;
import feign.codec.ErrorDecoder;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.NotFoundException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* OpenFeign Client Error 전역 예외처리를 관리합니다.
*
* @author : jonghoon
* @fileName : FeignClientGlobalErrorDecoder
* @since : 25. 2. 20.
*/@Slf4j@ComponentpublicclassFeignClientGlobalErrorDecoderimplementsErrorDecoder{
/**
* OpenFeign에서 발생하는 Status Code를 기반으로 오류를 커스텀 처리로 수행합니다.
*
* @param methodKey
* @param response
* @return
*/@Overridepublic Exception decode(String methodKey, Response response){
switch (response.status()) {
case400:
returnnew BadRequestException("잘못된 요청입니다.");
case404:
returnnew NotFoundException("리소스를 찾을 수 없습니다.");
case500:
returnnew InternalServerErrorException("서버 내부 오류가 발생했습니다.");
default:
returnnew Exception("알 수 없는 오류가 발생했습니다.");
}
}
}
3. Configuration 구현
💡 Configuration 구현
- OpenFeign의 에러 처리를 위한 Configuration 클래스입니다. 이 설정을 통해 OpenFeign 클라이언트에서 발생하는 HTTP 통신 오류를 일관된 방식으로 처리할 수 있습니다. - 즉, 오류가 발생하면 등록한 FeignClientCustomErrorDecoder 클래스로 전달이 되어서 해당 클래스에서 중앙 관리를 수행합니다.
- 아래와 같이 OpenFeign를 통해 통신을 수행할때, 404 에러가 발생하도록 강제하였습니다.
5. 결과확인
💡 결과확인
- 위에서 지정한 404 에러가 발생하였을때, jakarta.ws.rs.NotFoundException 오류가 발생하며, 메시지로 “리소스를 찾을 수 없습니다”라는 메시지를 출력하였습니다.
4) OpenFeign 예외처리 종류 -2 : try/catch 사용 예외처리
💡 OpenFeign 예외처리 종류 -2 : try/catch 사용
- OpenFeign을 사용할 때 발생하는 예외를 직접 try/catch 블록으로 처리하는 방법입니다. - 각 HTTP 상태 코드에 따른 FeignException의 하위 클래스를 개별적으로 캐치하여 처리할 수 있습니다. - 특정 API 호출에 대해 맞춤형 예외 처리가 필요한 경우 유용합니다.
오류 처리 방식
사용 방법
적용 범위
장점
ErrorDecoder
ErrorDecoder 인터페이스 구현 클래스 작성 및 Bean 등록
전역적 적용
중앙화된 오류 처리, 일관된 예외 변환
try/catch
개별 메서드에서 FeignException 하위 클래스 catch
메서드 단위
세밀한 예외 처리, 상황별 대응 가능
@ControllerAdvice
@ExceptionHandler 메서드 구현
애플리케이션 전체
통합된 예외 처리, REST API 응답 표준화
Custom Exception
사용자 정의 예외 클래스 생성 및 변환 로직 구현
필요한 영역
비즈니스 로직에 특화된 예외 처리
1. try/catch 사용 예외처리 특징
특징
설명
세밀한 제어
각 API 호출마다 다양한 예외 유형에 대해 개별적인 처리 로직 적용 가능
상황별 대응
HTTP 상태 코드별로 FeignException 하위 클래스를 구분하여 처리
직관적인 코드 흐름
예외 발생 시 대체 로직이나 fallback 메커니즘을 명확하게 구현 가능
유연한 오류 변환
외부 API 오류를 비즈니스 도메인에 맞는 사용자 정의 예외로 변환 가능
로컬 로깅
특정 API 호출의 실패에 대해 상세한 컨텍스트와 함께 로깅 가능
2. try/catch 사용예시 -1 : KeycloakUserFeignClient
💡 try/catch 사용예시 -1 : KeycloakUserFeignClient
- Keycloak 서버와 통신을 하는 OpenFeign입니다. 여기서 users/{id}/groups 엔드포인트를 호출하는 과정에서 임의의 무작위의 URL 경로를 추가하였습니다. - 이를 통해서, OpenFeign내에서 404 에러가 발생할 것으로 예상이 됩니다.
- 해당 서비스에서는 위의 KeycloakUserFeignClient를 호출하여 재구성하는 서비스입니다. - getUserGroups() 메서드 내에는 try/catch로 발생할 수 있는 에러들에 대해서 catch로 정의하고 로깅을 출력하도록 작성하였습니다. - 위에서 예측이 되는 404 에러가 발생할 것이고 try / catch 과정에서 “FeignException.NotFound 문제를 만나서 잘못된 경로로 호출이 되었습니다.”라는 메시지가 출력될 예정입니다.
package com.blog.springbootkeycloak.service;
import com.blog.springbootkeycloak.dto.properties.KeycloakProperties;
import com.blog.springbootkeycloak.dto.*;
import com.blog.springbootkeycloak.service.feign.KeycloakAuthFeignClient;
import com.blog.springbootkeycloak.service.feign.KeycloakUserFeignClient;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Keycloak 비즈니스 관리를 수행합니다.
*
* @author : leejonghoon
* @fileName : KeycloakUserService
* @since : 2025. 2. 10.
*/@Slf4j@Service@RequiredArgsConstructorpublicclassKeycloakUserService{
privatefinal KeycloakProperties properties; // Keycloak 설정 정보privatefinal KeycloakUserFeignClient keycloakUserFeignClient; // Keycloak User 통신privatefinal KeycloakAuthFeignClient keycloakAuthFeignClient; // Keycloak Auth 통신/**
* 사용자 그룹을 조회합니다.
*
* @param bearerToken
* @param username
* @return
*/public List<GroupRepresentation> getUserGroups(String bearerToken, String username){
// 1. [Keycloak] 토큰 유효성 체크this.validateToken(bearerToken);
// 2. [Keycloak] username 기반 ID 조회
String id = this.getKeycloakUserId(bearerToken, username);
List<GroupRepresentation> result = new ArrayList<>();
try {
// OpenFeign 클라이언트를 통한 API 호출
result = keycloakUserFeignClient.getUserGroups(bearerToken, id);
} catch (FeignException.BadRequest e) {
// 400 에러 처리
log.error("[-] 잘못된 호출이 발생하였습니다. {}", e.getMessage());
} catch (FeignException.NotFound e) {
// 404 에러 처리
log.error("[-] 잘못된 경로로 호출이 되었습니다.{}", e.getMessage());
} catch (FeignException.Unauthorized | FeignException.Forbidden e) {
// 401, 403 에러 처리
log.error("[-] 인증 및 인가 오류가 발생하였습니다. {}", id);
} catch (FeignException e) {
// 기타 Feign 예외 처리
log.error("[-] 기타 오류가 발생하였습니다. status={}, message={}", e.status(), e.getMessage());
}
return result;
}
/**
* username 기반 id 조회
*
* @param bearerToken
* @param username
* @return
*/private String getKeycloakUserId(String bearerToken, String username){
// [Validation] 빈 값 체크if (username == null) {
thrownew IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Validation] 빈 값 체크if (username.isEmpty()) {
thrownew IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Keycloak] username 기반의 사용자 조회
List<UserRepresentation> result = keycloakUserFeignClient.selectKeycloakUserDetail(
bearerToken,
null,
null,
null,
username,
null,
null,
true
);
// [Validation] 유효하지 않은 사용자if (result.isEmpty()) {
thrownew IllegalArgumentException("해당 username으로 등록된 사용자를 찾을 수 없습니다: " + username);
}
return result.get(0).getId();
}
}
4. try/catch 사용예시 -3 : 결과 화면
💡 try/catch 사용예시 -3 : 결과 화면
- 위에 구성에 따라서 try/catch 부분에서 해당 문제가 잡히고 아래와 같이 error 로그가 출력됨을 확인하였습니다.
5) OpenFeign 예외처리 종류 -3 : @ControllerAdvice 사용 오류 처리
💡 OpenFeign 예외처리 종류 -3 : @ControllerAdvice 사용 오류 처리
- OpenFeign에서 발생하는 예외를 Spring의 @ControllerAdvice를 통해 전역적으로 처리하는 방법입니다. 이 방식은 클라이언트 코드를 try-catch로 복잡하게 만들지 않고도 일관된 오류 응답을 제공할 수 있습니다. - 해당 예외처리의 범위는 애플리케이션 전체에서 발생하는 경우에 대해 오류를 처리합니다.
특징
설명
세밀한 제어
각 API 호출마다 다양한 예외 유형에 대해 개별적인 처리 로직 적용 가능
상황별 대응
HTTP 상태 코드별로 FeignException 하위 클래스를 구분하여 처리
직관적인 코드 흐름
예외 발생 시 대체 로직이나 fallback 메커니즘을 명확하게 구현 가능
유연한 오류 변환
외부 API 오류를 비즈니스 도메인에 맞는 사용자 정의 예외로 변환 가능
로컬 로깅
특정 API 호출의 실패에 대해 상세한 컨텍스트와 함께 로깅 가능
1. @ControllerAdvice 사용 오류처리
특징
설명
중앙 집중화된 예외 처리
애플리케이션 전체에서 발생하는 OpenFeign 관련 예외를 한 곳에서 일관되게 처리
비즈니스 로직과 예외 처리 분리
서비스 코드에서 try-catch 블록 없이 깔끔한 코드 유지 가능
HTTP 상태 코드 매핑
Feign 예외를 적절한 HTTP 응답 코드로 변환하여 일관된 API 응답 제공
세분화된 예외 처리
FeignException의 다양한 하위 클래스(NotFound, BadRequest 등)에 대해 개별적인 핸들러 구현 가능
상세한 오류 메시지 구성
클라이언트에게 도움이 되는 구조화된 오류 응답을 일관된 형식으로 제공
보안 강화
민감한 오류 정보를 필터링하여 외부에 노출되지 않도록 제어
로깅 표준화
모든 외부 API 호출 실패에 대한 로깅을 일관된 방식으로 구현
💡 [참고] 아래의 Global Exception / Business Exception 처리하는 과정과 비슷하게 처리가 됩니다. 이에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 이제 호출이 되는 getUserGroups() 메서드 내에서는 404 에러가 발생하지만 별도의 Exception 처리를 수행하지 않은 상태입니다.
package com.blog.springbootkeycloak.service;
import com.blog.springbootkeycloak.dto.properties.KeycloakProperties;
import com.blog.springbootkeycloak.dto.*;
import com.blog.springbootkeycloak.service.feign.KeycloakAuthFeignClient;
import com.blog.springbootkeycloak.service.feign.KeycloakUserFeignClient;
import feign.FeignException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Keycloak 비즈니스 관리를 수행합니다.
*
* @author : leejonghoon
* @fileName : KeycloakUserService
* @since : 2025. 2. 10.
*/@Slf4j@Service@RequiredArgsConstructorpublicclassKeycloakUserService{
privatefinal KeycloakProperties properties; // Keycloak 설정 정보privatefinal KeycloakUserFeignClient keycloakUserFeignClient; // Keycloak User 통신privatefinal KeycloakAuthFeignClient keycloakAuthFeignClient; // Keycloak Auth 통신/**
* 사용자 그룹을 조회합니다.
*
* @param bearerToken
* @param username
* @return
*/public List<GroupRepresentation> getUserGroups(String bearerToken, String username){
// [Keycloak] username 기반 ID 조회
String id = this.getKeycloakUserId(bearerToken, username);
// [Keycloak] 사용자 소속 그룹 조회return keycloakUserFeignClient.getUserGroups(bearerToken, id);
}
/**
* username 기반 id 조회
*
* @param bearerToken
* @param username
* @return
*/private String getKeycloakUserId(String bearerToken, String username){
// [Validation] 빈 값 체크if (username == null) {
thrownew IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Validation] 빈 값 체크if (username.isEmpty()) {
thrownew IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Keycloak] username 기반의 사용자 조회
List<UserRepresentation> result = keycloakUserFeignClient.selectKeycloakUserDetail(
bearerToken,
null,
null,
null,
username,
null,
null,
true
);
// [Validation] 유효하지 않은 사용자if (result.isEmpty()) {
thrownew IllegalArgumentException("해당 username으로 등록된 사용자를 찾을 수 없습니다: " + username);
}
return result.get(0).getId();
}
}
5. @ControllerAdvice 사용예시 -5 : 결과 확인
💡 @ControllerAdvice 사용예시 -5 : 결과 확인
- 아래와 같이 사용자 정의가 된 응답 결과를 확인할 수 있습니다.
6) OpenFeign 예외처리 종류 -3 : Custom Exception 사용 예외처리
💡 OpenFeign 예외처리 종류 -3 : Custom Exception 사용 예외처리
- 사용자 정의 예외 클래스를 생성하여서 동일한 응답으로 반환하는 예외처리를 처리하는 방식입니다. 또한, 특정 @FeignClient 별로 적용이 가능합니다. - 각 API 엔드포인트마다 예상되는 오류 유형과 처리 방식이 다를 때 세밀한 제어가 가능하며, 여러 마이크로서비스와 통신할 때 각 서비스의 특성에 맞게 예외 처리를 할 수 있다는 점입니다.
오류 처리 방식
사용 방법
적용 범위
장점
ErrorDecoder
ErrorDecoder 인터페이스 구현 클래스 작성 및 Bean 등록
전역적 적용
중앙화된 오류 처리, 일관된 예외 변환
try/catch
개별 메서드에서 FeignException 하위 클래스 catch
메서드 단위
세밀한 예외 처리, 상황별 대응 가능
@ControllerAdvice
@ExceptionHandler 메서드 구현
애플리케이션 전체
통합된 예외 처리, REST API 응답 표준화
Custom Exception
사용자 정의 예외 클래스 생성 및 변환 로직 구현
필요한 영역
비즈니스 로직에 특화된 예외 처리
💡 [참고] 이전 Global Exception / Business Exception 처리하는 과정과 비슷하게 처리가 됩니다. 이에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 이전에는 클라이언트에서 서버로 요청이 들어왔을때, 아래와 같이 처리가 되었습니다. Service Layer 내에서 비즈니스 로직 예외처리가 발생한 경우에 이에 대한 동일한 응답값으로 클라이언트에게 메시지를 전달해 주는 형태였습니다. - OpenFeign 내에서도 서버-서버간의 통신을 수행할 때, 에러가 발생했을 때 동일한 에러형태로 반환을 해준다면 서로 서버 간의 공통된 처리를 할 수 있다는 점이 있습니다.
1.2. Server To Server OpenFeign Custom Error 발생 시 처리 과정
💡 Server To Server OpenFeign Custom Error 발생 시 처리 과정
- 위에서 처리된 Service 단계에서 Exception 처리에 대해서 이를 이용하여서 OpenFeign내에서 발생하는 에러에 대해서도 현재 API Server 내에서 처리되고 있는 방식을 활용하여 동일한 응답 결과를 반환하도록 합니다. - 또한, 이러한 과정에서 반대로 B Server에서 A Server로 처리할때도 동일하게 응답 결과를 받을 수 있다는 장점이 있습니다.
2. Custom Exception 사용 예외처리 -1 : FeignClientCustomErrorDecoder
💡 Custom Exception 사용 예외처리 -1 : FeignClientCustomErrorDecoder
- 최초 OpenFeign 내에서 발생한 오류를 해당 부분에서 캐치를 하고 그대로 이를 반환하도록 구성하였습니다. - 이 과정을 통해서 발생한 FeignException은 GlobalFeignExceptionHandler 내에서 이를 처리하도록 이관합니다.
/**
* OpenFeign Client Error 지역 예외처리를 관리합니다.
*
* @author : leejonghoon
* @fileName : FeignClientCustomErrorDecoder
* @since : 2025. 3. 2.
*/@Slf4j@Component
public class FeignClientCustomErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
/*
* FeignException 발생 시 이를 그대로 반환합니다.
* 이렇게 구성하면 GlobalFeignExceptionHandler 내에서 처리하도록 이관합니다.
*/returnFeignException.errorStatus(methodKey, response);
}
}
3. Custom Exception 사용 예외처리 -2: GlobalFeignExceptionHandler
💡 Custom Exception 사용 예외처리 -2: GlobalFeignExceptionHandler
- 발생하는 Exception 대해서 Catch 하여 응답값(Response)을 보내주는 기능을 수행하는 클래스입니다. - 해당 응답값에 HttpStatus.OK를 적용한 이유는 동일하게 통신은 성공하면서 문제가 되는 코드에 대해 잘못된 부분을 반환해 주도록 구성하였습니다.
예외 유형
처리 메서드
로그 메시지
에러 코드
FeignException.BadRequest
handleBadRequestException
잘못된 요청 형식 오류
FEIGN_BAD_REQUEST_ERROR
FeignException.Unauthorized
handleUnauthorizedException
인증 오류
FEIGN_UNAUTHORIZED_ERROR
FeignException.Forbidden
handleForbiddenException
권한 없음
FEIGN_FORBIDDEN_ERROR
FeignException.NotFound
handleNotFoundException
리소스를 찾을 수 없음
FEIGN_NOT_FOUND_ERROR
FeignException.MethodNotAllowed
handleMethodNotAllowedException
허용되지 않는 메서드
FEIGN_METHOD_NOT_ALLOWED_ERROR
FeignException.ServiceUnavailable
handleServiceUnavailableException
서비스 일시 중단
FEIGN_SERIALIZATION_ERROR
RetryableException
handleRetryableException
재시도 가능한 오류 발생
FEIGN_RETRY_ERROR
FeignException
handleFeignException
Feign 클라이언트 오류
FEIGN_COMMUNICATION_ERROR
feign.codec.DecodeException
handleDecodeException
응답 디코딩 오류
FEIGN_DECODING_ERROR
package com.blog.springbootkeycloak.config.exception;
import com.blog.springbootkeycloak.dto.common.ErrorCode;
import com.blog.springbootkeycloak.dto.common.ErrorResponse;
import feign.FeignException;
import feign.RetryableException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 발생하는 Exception 대해서 Catch 하여 응답값(Response)을 보내주는 기능을 수행함.
*/@Slf4j@RestControllerAdvicepublicclassGlobalFeignExceptionHandler{
privatefinal HttpStatus httpStatusOK = HttpStatus.OK;
/**
* 클라이언트 오류: 400 Bad Request
*/@ExceptionHandler(FeignException.BadRequest.class)public ResponseEntity<ErrorResponse> handleBadRequestException(FeignException.BadRequest ex){
log.error("잘못된 요청 형식 오류: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_BAD_REQUEST_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 클라이언트 오류: 401 Unauthorized
*/@ExceptionHandler(FeignException.Unauthorized.class)public ResponseEntity<ErrorResponse> handleUnauthorizedException(FeignException.Unauthorized ex){
log.error("인증 오류: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_UNAUTHORIZED_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 클라이언트 오류: 403 Forbidden
*/@ExceptionHandler(FeignException.Forbidden.class)public ResponseEntity<ErrorResponse> handleForbiddenException(FeignException.Forbidden ex){
log.error("권한 없음: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_FORBIDDEN_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 클라이언트 오류: 404 Not Found
*/@ExceptionHandler(FeignException.NotFound.class)public ResponseEntity<ErrorResponse> handleNotFoundException(FeignException.NotFound ex){
log.error("리소스를 찾을 수 없음: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_NOT_FOUND_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 클라이언트 오류: 405 Method Not Allowed
*/@ExceptionHandler(FeignException.MethodNotAllowed.class)public ResponseEntity<ErrorResponse> handleMethodNotAllowedException(FeignException.MethodNotAllowed ex){
log.error("허용되지 않는 메서드: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_METHOD_NOT_ALLOWED_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 서버 오류: 503 Service Unavailable
*/@ExceptionHandler(FeignException.ServiceUnavailable.class)public ResponseEntity<ErrorResponse> handleServiceUnavailableException(FeignException.ServiceUnavailable ex){
log.error("서비스 일시 중단: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_SERIALIZATION_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 재시도 가능한 예외 처리
*/@ExceptionHandler(RetryableException.class)public ResponseEntity<ErrorResponse> handleRetryableException(RetryableException ex){
log.error("재시도 가능한 오류 발생: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_RETRY_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 기본 FeignException 처리 (위에서 처리되지 않은 모든 Feign 예외)
*/@ExceptionHandler(FeignException.class)public ResponseEntity<ErrorResponse> handleFeignException(FeignException ex){
log.error("Feign 클라이언트 오류: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_COMMUNICATION_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
/**
* 응답 디코딩 오류
*/@ExceptionHandler(feign.codec.DecodeException.class)public ResponseEntity<ErrorResponse> handleDecodeException(feign.codec.DecodeException ex){
log.error("응답 디코딩 오류: {}", ex.getMessage());
final ErrorResponse response = ErrorResponse.of(ErrorCode.FEIGN_DECODING_ERROR, ex.getMessage());
returnnew ResponseEntity<>(response, httpStatusOK);
}
}
4. Custom Exception 사용 예외처리-3 : ErrorCode 구성
💡 Custom Exception 사용 예외처리-3 : ErrorCode 구성
- 기존의 처리되는 로직 내에 OpenFeign에서 발생할 수 있는 에러코드를 반환부분을 추가하였습니다.
에러 코드
HTTP 상태 코드
Custom Code
설명
FEIGN_CONNECTION_TIMEOUT
503
F001
OpenFeign 통신 타임아웃 에러
FEIGN_RESPONSE_ERROR
500
F002
OpenFeign 응답 처리 중 에러
FEIGN_SERVICE_UNAVAILABLE
503
F003
OpenFeign 서비스 호출 실패
FEIGN_BAD_REQUEST
400
F004
OpenFeign 요청 형식 에러
FEIGN_UNAUTHORIZED
401
F005
OpenFeign 인증 에러
FEIGN_CLIENT_ERROR
400
F006
OpenFeign 요청 처리 중 에러
FEIGN_SERVER_ERROR
500
F007
OpenFeign 서버 에러
FEIGN_COMMUNICATION_ERROR
502
F008
OpenFeign 서비스 간 통신 에러
FEIGN_REQUEST_CANCELLED
499
F009
OpenFeign 요청 취소 에러
FEIGN_SERIALIZATION_ERROR
400
F010
OpenFeign 데이터 직렬화/역직렬화 에러
package com.adjh.springbootexternalnetwork.dto.common;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* [공통 코드] API 통신에 대한 '에러 코드'를 Enum 형태로 관리를 한다.
* Global Error CodeList : 전역으로 발생하는 에러코드를 관리한다.
* Custom Error CodeList : 업무 페이지에서 발생하는 에러코드를 관리한다
* Error Code Constructor : 에러코드를 직접적으로 사용하기 위한 생성자를 구성한다.
* OpenFeign Error CodeList : OpenFeign 통신 중 발생하는 에러코드를 관리한다.
*
* @author : jonghoon
* @fileName : ErrorCodeDto
* @since : 25. 2. 20.
*/@Getter@NoArgsConstructor(access = AccessLevel.PROTECTED)publicenumErrorCode{
/*
* ******************************* Global Error CodeList ***************************************
* IN Server HTTP Status Code
* 400 : Bad Request
* 401 : Unauthorized
* 403 : Forbidden
* 404 : Not Found
* 500 : Internal Server Error
* *********************************************************************************************
*//************************************************************************************************
********************************* HTTP Status Error *********************************************
* HTTP 통신 과정에서 발생하는 오류에 대해서 코드 기반으로 관리합니다.
*************************************************************************************************/// [400] 잘못된 서버 요청
BAD_REQUEST_ERROR(400, "S001", "Bad Request Exception"),
// [401] 인증되지 않은 요청
UNAUTHORIZED_ERROR(401, "S002", "Unauthorized request occurred Exception"),
// [403] 권한이 없음
FORBIDDEN_ERROR(403, "S003", "Forbidden Exception"),
// [404] 서버로 요청한 리소스가 존재하지 않음
NOT_FOUND_ERROR(404, "S004", "Not Found Exception"),
// [405] 지원하지 않는 HTTP 메소드 오류
METHOD_NOT_ALLOWED_ERROR(405, "S005", "The requested HTTP method is not allowed for this endpoint Exception"),
// [500] 서버 내부 오류
INTERNAL_SERVER_ERROR(500, "S006", "Internal Server Error Exception"),
/************************************************************************************************
********************************* Custom Transaction Error ************************************
* 비즈니스 로직 내에서 트랜잭션을 수행했을때 오류가 발생하는 부분에 대해 코드 기반으로 관리합니다.
*************************************************************************************************/// Business Exception Error
BUSINESS_EXCEPTION_ERROR(500, "T001", "Business Exception Error"),
// Transaction Insert Error
INSERT_ERROR(500, "T002", "Insert Transaction Error Exception"),
// Transaction Update Error
UPDATE_ERROR(500, "T003", "Update Transaction Error Exception"),
// Transaction Delete Error
DELETE_ERROR(500, "T004", "Delete Transaction Error Exception"),
/************************************************************************************************
********************************* Custom Business Error *****************************************
* 비즈니스 로직에서 발생하는 오류들을 코드기반으로 관리합니다
*************************************************************************************************/// 요청 본문 누락 오류
REQUEST_BODY_MISSING_ERROR(400, "G002", "Required request body is missing Exception"),
// 타입 변환 오류
INVALID_TYPE_VALUE(400, "G003", "Invalid Type Value Exception"),
// 요청 파라미터 누락 오류
MISSING_REQUEST_PARAMETER_ERROR(400, "G004", "Missing Servlet RequestParameter Exception"),
// 입출력 처리 오류
IO_ERROR(500, "G008", "I/O Exception"),
// JSON 파싱 오류 (Gson)
JSON_PARSE_ERROR(400, "G009", "JsonParseException"),
// Jackson 처리 오류
JACKSON_PROCESS_ERROR(500, "G010", "com.fasterxml.jackson.core Exception"),
// Null 참조 오류
NULL_POINT_ERROR(500, "G006", "Null Point Exception"),
// 요청 값 유효성 검증 실패
NOT_VALID_ERROR(400, "G007", "handle Validation Exception"),
// 헤더 값 유효성 검증 실패
NOT_VALID_HEADER_ERROR(400, "G007", "Header에 데이터가 존재하지 않는 경우"),
// 인증 정보 누락
AUTH_IS_NULL_ERROR(401, "A404", "AUTH_IS_NULL"),
// 인증 토큰 실패
AUTH_TOKEN_FAIL_ERROR(401, "A405", "AUTH_TOKEN_FAIL"),
// 유효하지 않은 인증 토큰
AUTH_TOKEN_INVALID_ERROR(401, "A406", "AUTH_TOKEN_INVALID"),
// 인증 토큰 불일치
AUTH_TOKEN_NOT_MATCH_ERROR(401, "A407", "AUTH_TOKEN_NOT_MATCH"),
// 인증 토큰 누락
AUTH_TOKEN_IS_NULL_ERROR(401, "A408", "AUTH_TOKEN_IS_NULL"),
/************************************************************************************************
********************************* OpenFeign Error CodeList **************************************
*************************************************************************************************/// [400] 잘못된 요청 형식 오류
FEIGN_BAD_REQUEST_ERROR(400, "F004", "OpenFeign bad request format Exception"),
// [401] 인증 실패 오류
FEIGN_UNAUTHORIZED_ERROR(401, "F005", "OpenFeign unauthorized access Exception"),
// [403] 권한 부족 오류
FEIGN_FORBIDDEN_ERROR(403, "F003", "OpenFeign forbidden access Exception"),
// [404] 리소스 찾을 수 없음 오류
FEIGN_NOT_FOUND_ERROR(404, "F004", "OpenFeign not found Exception"),
// [405] 지원하지 않는 HTTP 메소드 오류
FEIGN_METHOD_NOT_ALLOWED_ERROR(405, "F005", "OpenFeign method not allowed Exception"),
// [499] 요청 취소 오류
FEIGN_REQUEST_CANCELLED_ERROR(499, "F006", "OpenFeign request cancelled Exception"),
// [400] 데이터 변환 오류
FEIGN_SERIALIZATION_ERROR(400, "F007", "OpenFeign data serialization/deserialization Exception"),
// [500] 서버 내부 오류
FEIGN_SERVER_ERROR(500, "F008", "OpenFeign server error during request processing"),
// [500] 서비스 간 통신 오류
FEIGN_COMMUNICATION_ERROR(500, "F009", "OpenFeign inter-service communication Exception"),
// [500] 재시도 실패 오류
FEIGN_RETRY_ERROR(500, "F010", "OpenFeign retry Exception"),
// [500] 응답 디코딩 오류
FEIGN_DECODING_ERROR(500, "F011", "OpenFeign decoding Exception"),
// [503] 연결 타임아웃 오류
FEIGN_CONNECTION_TIMEOUT_ERROR(503, "F012", "OpenFeign connection timeout Exception"),
// [500] 응답 처리 오류
FEIGN_RESPONSE_ERROR(500, "F013", "OpenFeign response processing Exception"),
// [503] 서비스 이용 불가 오류
FEIGN_SERVICE_UNAVAILABLE_ERROR(503, "F014", "OpenFeign service unavailable Exception"),
; // End/**
* ******************************* Error Code Constructor ***************************************
*/// 에러 코드의 '코드 상태'을 반환한다.privateint status;
// 에러 코드의 '코드간 구분 값'을 반환한다.private String code;
// 에러 코드의 '코드 메시지'을 반환한다.private String message;
// 생성자 구성
ErrorCode(finalint status, final String code, final String message) {
this.status = status;
this.code = code;
this.message = message;
}
}
5. Custom Exception 사용 예외처리 -4 : ErrorResponse
💡 Custom Exception 사용 예외처리 -4 : ErrorResponse
- 이 클래스는 Spring Cloud OpenFeign에서 발생하는 다양한 예외 상황을 일관된 형식으로 클라이언트에게 전달하기 위한 응답 포맷을 정의합니다.
- API 통신 자체의 통과를 위해 200에 대한 Status 코드를 제공하면서, 발생한 문제에 대해서 status에서는 404를 반환하고, 구성한 커스텀 코드인 errorCode가 F004로 반환했습니다. - 또한, 발생한 에러를 errorMsg와 구체적인 errorDtlMsg 형태로 반환하여 최종적으로 서버간의 동일한 에러코드로 반환하도록 처리를 하였습니다.