해당 글에서는 외부 통신을 위해 사용하는 Spring Cloud OpenFeign를 통한 통신을 수행할 때, 발생하는 에러에 대해 예외 처리를 하는 방법에 대해 알아봅니다
![]()
💡 [참고] Java에서 외부 통신을 하는 방법들에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다
분류 | 주제 | 링크 |
RestTemplate | Spring Boot Web 활용 : RestTemplate 이해하기 | https://adjh54.tistory.com/234 |
WebClient | Spring Boot Webflux 이해하기 -1 : 흐름 및 주요 특징 이해 | https://adjh54.tistory.com/232 |
WebClient | Spring Boot Webflux 이해하기 -2 : 활용하기 | https://adjh54.tistory.com/233 |
OpenFeign | Spring Cloud OpenFeign 이해하고 활용하기 -1: 주요 개념 및 환경 구성, 활용 예시 | https://adjh54.tistory.com/616 |
OpenFeign | Spring Cloud OpenFeign 이해하고 활용하기 -2: 설정 중앙화 방법 | https://adjh54.tistory.com/667 |
OpenFeign | Spring Cloud OpenFeign 이해하고 활용하기 -3: 예외처리 방법 | https://adjh54.tistory.com/670 |
Github | 외부 통신의 활용 방법을 담은 예제 Repository | https://github.com/adjh54ir/blog-codes/tree/main/spring-boot-external-network |
1) Spring Cloud OpenFeign
💡 Spring Cloud OpenFeign
- Netflix에서 개발한 HTTP 클라이언트 라이브러리를 Spring Cloud에서 통합한 선언적 HTTP 클라이언트 라이브러리입니다.
- Spring Cloud에서는 Open Feign을 스프링 MVC 어노테이션을 사용하여 웹 서비스 클라이언트를 쉽게 작성할 수 있도록 통합했습니다.
- OpenFeign은 마이크로서비스 아키텍처에서 특히 유용하며, 코드를 더 간결하고 유지보수하기 쉽게 만듭니다.
Spring Cloud OpenFeign
Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable
docs.spring.io
1. OpenFeign 사용목적
💡 OpenFeign 사용목적
1. 선언적 REST 클라이언트
- 인터페이스와 어노테이션만으로 HTTP 클라이언트를 구현할 수 있어 코드가 더 간결해집니다.
2. Spring Web 어노테이션 지원
- @GetMapping, @PostMapping 등 개발자에게 친숙한 Spring Web 어노테이션을 그대로 사용할 수 있습니다.
3. 설정의 중앙화
- URL, 인증 정보 등의 설정을 중앙에서 관리할 수 있어 유지보수가 용이합니다.
4. 재사용성 향상
- 정의된 인터페이스를 여러 서비스에서 쉽게 재사용할 수 있습니다.
5. 테스트 용이성
- Mock 객체를 사용한 테스트가 더 쉽고, 인터페이스 기반이라 테스트 코드 작성이 간편합니다.
6. 오류 처리 통합
- ErrorDecoder를 통해 예외 처리를 중앙화하고 일관성 있게 관리할 수 있습니다.
@FeignClient(name = "user-service", url = "http://api.example.com")
public interface UserClient {
@GetMapping("/users/{id}")
User getUser(@PathVariable("id") Long id);
@PostMapping("/users")
User createUser(@RequestBody User user);
}
💡 [참고] RestTempalte 사용 예시
// RestTemplate 예시
@Service
public class UserService {
private final RestTemplate restTemplate;
private final String baseUrl = "http://api.example.com";
public User getUser(Long id) {
return restTemplate.getForObject(baseUrl + "/users/" + id, User.class);
}
}
2) OpenFeign 예외처리
💡 OpenFeign 예외처리
- 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
@Component
public class FeignClientGlobalErrorDecoder implements ErrorDecoder {
/**
* OpenFeign에서 발생하는 Status Code를 기반으로 오류를 커스텀 처리로 수행합니다.
*
* @param methodKey
* @param response
* @return
*/
@Override
public Exception decode(String methodKey, Response response) {
switch (response.status()) {
case 400:
return new BadRequestException("잘못된 요청입니다.");
case 404:
return new NotFoundException("리소스를 찾을 수 없습니다.");
case 500:
return new InternalServerErrorException("서버 내부 오류가 발생했습니다.");
default:
return new Exception("알 수 없는 오류가 발생했습니다.");
}
}
}
3. Configuration 구현
💡 Configuration 구현
- OpenFeign의 에러 처리를 위한 Configuration 클래스입니다. 이 설정을 통해 OpenFeign 클라이언트에서 발생하는 HTTP 통신 오류를 일관된 방식으로 처리할 수 있습니다.
- 즉, 오류가 발생하면 등록한 FeignClientCustomErrorDecoder 클래스로 전달이 되어서 해당 클래스에서 중앙 관리를 수행합니다.
package com.blog.springbootkeycloak.config.feign;
import com.blog.springbootkeycloak.config.feign.exception.FeignClientCustomErrorDecoder;
import feign.Feign;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 예외처리를 수행하는 클래스를 등록합니다.
*
* @author : leejonghoon
* @fileName : FeignClientDecoderConfig
* @since : 2025. 2. 26.
*/
@Configuration
public class FeignClientDecoderConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new FeignClientCustomErrorDecoder();
}
}
4. 적용 테스트
💡 적용 테스트
- 아래와 같이 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 에러가 발생할 것으로 예상이 됩니다.
package com.blog.springbootkeycloak.service.feign;
import com.blog.springbootkeycloak.config.feign.exception.FeignClientGlobalErrorDecoder;
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.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Keycloak 서버와 통신하는 OpenFeign
*
* @author : leejonghoon
* @fileName : KeycloakUserFeignClient
* @since : 2025. 2. 10.
*/
@FeignClient(
name = "keycloak-user-client",
url = "${keycloak.auth-server-url}/admin/realms/${keycloak.realm}"
)
@Service
public interface KeycloakUserFeignClient {
/**
* 사용자의 그룹 목록 조회
*/
@GetMapping("/users/{id}/groups3485903485203984203984")
List<GroupRepresentation> getUserGroups(
@RequestHeader("Authorization") String bearerToken,
@PathVariable String id
);
}
3. try/catch 사용예시 -2: KeycloakUserService
💡 try/catch 사용예시 -2: KeycloakUserService
- 해당 서비스에서는 위의 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
@RequiredArgsConstructor
public class KeycloakUserService {
private final KeycloakProperties properties; // Keycloak 설정 정보
private final KeycloakUserFeignClient keycloakUserFeignClient; // Keycloak User 통신
private final 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) {
throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Validation] 빈 값 체크
if (username.isEmpty()) {
throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Keycloak] username 기반의 사용자 조회
List<UserRepresentation> result = keycloakUserFeignClient.selectKeycloakUserDetail(
bearerToken,
null,
null,
null,
username,
null,
null,
true
);
// [Validation] 유효하지 않은 사용자
if (result.isEmpty()) {
throw new 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 처리하는 과정과 비슷하게 처리가 됩니다. 이에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 아래의 형태를 이용하여서 구성을 합니다.
[Java] Global Exception 이해하고 구성하기 : Controller Exception
해당 글에서는 Controller에서 발생하는 Exception을 Global Exception을 구성하여서 처리하는 방법에 대해서 공유합니다. 1) 개발 환경💡 Global Exception을 적용하는데 활용한 개발환경입니다.개발환경버
adjh54.tistory.com
[Java] Business Exception 이해하고 구성하기 : Service Exception
해당 글에서는 business Layer에서 발생하는 오류에 대해서 공통 처리를 위한 Business Exception 대한 구성 방법에 대해 이해하고 구성하는 방법에 대해서 공유합니다. [참고] 이전에 작성한 Global Except
adjh54.tistory.com
2. @ControllerAdvice 사용예시 -1 : @ControllerAdvice 구현
💡 @ControllerAdvice 사용예시 -1 : @ControllerAdvice 구현
- @ControllerAdvice를 사용하여 Feign 예외를 처리하는 글로벌 예외 핸들러를 구현합니다.
- 이 핸들러는 FeignException을 포함한 다양한 OpenFeign 관련 예외를 잡아내고 적절한 오류 응답을 클라이언트에게 반환합니다.
package com.blog.springbootkeycloak.config.exception;
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;
/**
* Controller 내에서 발생하는 Exception 대해서 Catch 하여 응답값(Response)을 보내주는 기능을 수행함.
*/
@Slf4j
@RestControllerAdvice
public class GlobalFeignExceptionHandler {
private final HttpStatus httpStatusOK = HttpStatus.OK;
/**
* 클라이언트 오류: 400 Bad Request
*/
@ExceptionHandler(FeignException.BadRequest.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(FeignException.BadRequest ex) {
log.error("잘못된 요청 형식 오류: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.BAD_REQUEST.value(),
"잘못된 요청",
"요청 형식이 올바르지 않거나 유효하지 않은 파라미터입니다"
);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}
/**
* 클라이언트 오류: 401 Unauthorized
*/
@ExceptionHandler(FeignException.Unauthorized.class)
public ResponseEntity<ErrorResponse> handleUnauthorizedException(FeignException.Unauthorized ex) {
log.error("인증 오류: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.UNAUTHORIZED.value(),
"인증 실패",
"유효한 인증 정보가 필요합니다"
);
return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED);
}
/**
* 클라이언트 오류: 403 Forbidden
*/
@ExceptionHandler(FeignException.Forbidden.class)
public ResponseEntity<ErrorResponse> handleForbiddenException(FeignException.Forbidden ex) {
log.error("권한 없음: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.FORBIDDEN.value(),
"접근 거부",
"요청한 리소스에 접근할 권한이 없습니다"
);
return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}
/**
* 클라이언트 오류: 404 Not Found
*/
@ExceptionHandler(FeignException.NotFound.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(FeignException.NotFound ex) {
log.error("리소스를 찾을 수 없음: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.NOT_FOUND.value(),
"리소스 없음",
"요청한 리소스가 존재하지 않습니다: " + ex.request().url()
);
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
/**
* 클라이언트 오류: 405 Method Not Allowed
*/
@ExceptionHandler(FeignException.MethodNotAllowed.class)
public ResponseEntity<ErrorResponse> handleMethodNotAllowedException(FeignException.MethodNotAllowed ex) {
log.error("허용되지 않는 메서드: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.METHOD_NOT_ALLOWED.value(),
"메서드 불허용",
"요청한 HTTP 메서드가 이 리소스에서 지원되지 않습니다"
);
return new ResponseEntity<>(errorResponse, HttpStatus.METHOD_NOT_ALLOWED);
}
/**
* 서버 오류: 503 Service Unavailable
*/
@ExceptionHandler(FeignException.ServiceUnavailable.class)
public ResponseEntity<ErrorResponse> handleServiceUnavailableException(FeignException.ServiceUnavailable ex) {
log.error("서비스 일시 중단: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.SERVICE_UNAVAILABLE.value(),
"서비스 이용 불가",
"현재 서비스를 이용할 수 없습니다. 잠시 후 다시 시도해주세요."
);
return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE);
}
/**
* 재시도 가능한 예외 처리
*/
@ExceptionHandler(RetryableException.class)
public ResponseEntity<ErrorResponse> handleRetryableException(RetryableException ex) {
log.error("재시도 가능한 오류 발생: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.SERVICE_UNAVAILABLE.value(),
"일시적 서비스 장애",
"외부 서비스와 통신 중 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."
);
return new ResponseEntity<>(errorResponse, HttpStatus.SERVICE_UNAVAILABLE);
}
/**
* 기본 FeignException 처리 (위에서 처리되지 않은 모든 Feign 예외)
*/
@ExceptionHandler(FeignException.class)
public ResponseEntity<ErrorResponse> handleFeignException(FeignException ex) {
log.error("Feign 클라이언트 오류: {}", ex.getMessage());
// HTTP 상태 코드 추출
int status = ex.status();
HttpStatus httpStatus = HttpStatus.valueOf(status != -1 ? status : 500);
ErrorResponse errorResponse = new ErrorResponse(
httpStatus.value(),
"외부 서비스 통신 오류",
ex.getMessage()
);
return new ResponseEntity<>(errorResponse, httpStatus);
}
/**
* 응답 디코딩 오류
*/
@ExceptionHandler(feign.codec.DecodeException.class)
public ResponseEntity<ErrorResponse> handleDecodeException(feign.codec.DecodeException ex) {
log.error("응답 디코딩 오류: {}", ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
"응답 처리 오류",
"외부 서비스의 응답을 처리하는 데 문제가 발생했습니다"
);
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
3. @ControllerAdvice 사용예시 -2 : ErrorResponse
💡 @ControllerAdvice 사용예시 -2 : ErrorResponse
- @ControllerAdvice 내에서 공통적으로 발생하는 에러에 대해서 클라이언트에게 응답해주는 구조입니다.
- status는 상태 코드를 전달하고, error는 발생한 에러, messages는 상세한 메시지를 전달합니다.
package com.blog.springbootkeycloak.dto.common;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* Global Feign Exception Handler 발생한 에러에 대한 응답 처리를 관리
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
private int status;
private String error;
private String message;
@Builder
public ErrorResponse(int status, String error, String message) {
this.status = status;
this.error = error;
this.message = message;
}
}
4. @ControllerAdvice 사용예시 -3 : KeycloakUserFeignClient
💡@ControllerAdvice 사용예시 -3 : KeycloakUserFeignClient
- Keycloak 서버와 통신을 하는 OpenFeign입니다.
- 여기서 users/{id}/groups 엔드포인트를 호출하는 과정에서 임의의 무작위의 URL 경로를 추가하였습니다.
- 이를 통해서, OpenFeign내에서 404 에러가 발생할 것으로 예상이 됩니다.
package com.blog.springbootkeycloak.service.feign;
import com.blog.springbootkeycloak.config.feign.exception.FeignClientGlobalErrorDecoder;
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.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Keycloak 서버와 통신하는 OpenFeign
*
* @author : leejonghoon
* @fileName : KeycloakUserFeignClient
* @since : 2025. 2. 10.
*/
@FeignClient(
name = "keycloak-user-client",
url = "${keycloak.auth-server-url}/admin/realms/${keycloak.realm}"
)
@Service
public interface KeycloakUserFeignClient {
/**
* 사용자의 그룹 목록 조회
*/
@GetMapping("/users/{id}/groups3485903485203984203984")
List<GroupRepresentation> getUserGroups(
@RequestHeader("Authorization") String bearerToken,
@PathVariable String id
);
}
5. @ControllerAdvice 사용예시 -4 : KeycloakUserService
💡 @ControllerAdvice 사용예시 -4 : KeycloakUserService
- 이제 호출이 되는 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
@RequiredArgsConstructor
public class KeycloakUserService {
private final KeycloakProperties properties; // Keycloak 설정 정보
private final KeycloakUserFeignClient keycloakUserFeignClient; // Keycloak User 통신
private final 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) {
throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Validation] 빈 값 체크
if (username.isEmpty()) {
throw new IllegalArgumentException("username이 존재하지 않습니다.: ");
}
// [Keycloak] username 기반의 사용자 조회
List<UserRepresentation> result = keycloakUserFeignClient.selectKeycloakUserDetail(
bearerToken,
null,
null,
null,
username,
null,
null,
true
);
// [Validation] 유효하지 않은 사용자
if (result.isEmpty()) {
throw new 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 처리하는 과정과 비슷하게 처리가 됩니다. 이에 대해 궁금하시면 아래의 글을 참고하시면 도움이 됩니다.
- 아래의 형태를 이용하여서 구성을 합니다.
[Java] Global Exception 이해하고 구성하기 : Controller Exception
해당 글에서는 Controller에서 발생하는 Exception을 Global Exception을 구성하여서 처리하는 방법에 대해서 공유합니다. 1) 개발 환경💡 Global Exception을 적용하는데 활용한 개발환경입니다.개발환경버
adjh54.tistory.com
[Java] Business Exception 이해하고 구성하기 : Service Exception
해당 글에서는 business Layer에서 발생하는 오류에 대해서 공통 처리를 위한 Business Exception 대한 구성 방법에 대해 이해하고 구성하는 방법에 대해서 공유합니다. [참고] 이전에 작성한 Global Except
adjh54.tistory.com
1. Exception 처리 과정 확인
1.1. API Exception 발생 시 처리 과정
💡 API 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 내에서 처리하도록 이관합니다.
*/
return FeignException.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
@RestControllerAdvice
public class GlobalFeignExceptionHandler {
private final 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());
return new 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());
return new 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());
return new 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());
return new 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());
return new 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());
return new 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());
return new 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());
return new 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());
return new 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)
public enum ErrorCode {
/*
* ******************************* 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 ***************************************
*/
// 에러 코드의 '코드 상태'을 반환한다.
private int status;
// 에러 코드의 '코드간 구분 값'을 반환한다.
private String code;
// 에러 코드의 '코드 메시지'을 반환한다.
private String message;
// 생성자 구성
ErrorCode(final int 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에서 발생하는 다양한 예외 상황을 일관된 형식으로 클라이언트에게 전달하기 위한 응답 포맷을 정의합니다.
package com.blog.springbootkeycloak.dto.common;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.validation.BindingResult;
import java.util.ArrayList;
import java.util.List;
/**
* OpenFeign 에러 응답 처리 클래스
*/
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {
private int status; // 상태 코드
private String errorCode; // 에러 구분 코드
private String errorMsg; // 에러 메시지
private String errorDtlMsg; // 에러 상세 메시지
/**
* 기본 생성자 - ErrorCode만 받는 경우
*
* @param code
*/
@Builder
protected ErrorResponse(final ErrorCode code) {
this.errorMsg = code.getMessage();
this.status = code.getStatus();
this.errorCode = code.getCode();
}
/**
* 생성자 - ErrorCode와 reason을 받는 경우
*
* @param code
* @param reason
*/
@Builder
protected ErrorResponse(final ErrorCode code, final String reason) {
this.errorMsg = code.getMessage();
this.status = code.getStatus();
this.errorCode = code.getCode();
this.errorDtlMsg = reason;
}
/**
* 생성자 - ErrorCode와 필드 에러 목록을 받는 경우
*
* @param code
* @param errors
*/
@Builder
protected ErrorResponse(final ErrorCode code, final List<FieldError> errors) {
this.errorMsg = code.getMessage();
this.status = code.getStatus();
this.errorCode = code.getCode();
}
/**
* 팩토리 메소드 - BindingResult에서 ErrorResponse 생성
*
* @param code
* @param bindingResult
* @return
*/
public static ErrorResponse of(final ErrorCode code, final BindingResult bindingResult) {
return new ErrorResponse(code, FieldError.of(bindingResult));
}
/**
* 팩토리 메소드 - BindingResult에서 ErrorResponse 생성
*
* @param code
* @return
*/
public static ErrorResponse of(final ErrorCode code) {
return new ErrorResponse(code);
}
/**
* 팩토리 메소드 - ErrorCode와 상세 이유로 ErrorResponse 생성
*
* @param code
* @param reason
* @return
*/
public static ErrorResponse of(final ErrorCode code, final String reason) {
return new ErrorResponse(code, reason);
}
/**
* 유효성 검증 오류 정보를 담는 내부 클래스
*/
@Getter
public static class FieldError {
private final String field; // 오류 필드명
private final String value; // 오류 값
private final String reason; // 오류 이유
// 단일 필드 오류 생성
public static List<FieldError> of(final String field, final String value, final String reason) {
List<FieldError> fieldErrors = new ArrayList<>();
fieldErrors.add(new FieldError(field, value, reason));
return fieldErrors;
}
// BindingResult에서 필드 오류 목록 추출
private static List<FieldError> of(final BindingResult bindingResult) {
final List<org.springframework.validation.FieldError> fieldErrors = bindingResult.getFieldErrors();
return fieldErrors.stream()
.map(error -> new FieldError(
error.getField(),
error.getRejectedValue() == null ? "" : error.getRejectedValue().toString(),
error.getDefaultMessage()))
.toList();
}
@Builder
FieldError(String field, String value, String reason) {
this.field = field;
this.value = value;
this.reason = reason;
}
}
}
6. Custom Exception 사용 예외처리 -5 : KeycloakUserFeignClient
💡 Custom Exception 사용 예외처리 -5 : KeycloakUserFeignClient
- OpenFeign를 이용하여서 Keycloak 서버로 호출을 수행합니다.
- 예외처리 발생을 위해서 @PostMapping이지만 @DeleteMapping로 오류를 발생시켰습니다.
package com.blog.springbootkeycloak.service.feign;
import com.blog.springbootkeycloak.config.feign.exception.FeignClientGlobalErrorDecoder;
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.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* Keycloak 서버와 통신하는 OpenFeign
*
* @author : leejonghoon
* @fileName : KeycloakUserFeignClient
* @since : 2025. 2. 10.
*/
@FeignClient(
name = "keycloak-user-client",
url = "${keycloak.auth-server-url}/admin/realms/${keycloak.realm}",
configuration = FeignClientGlobalErrorDecoder.class
)
@Service
public interface KeycloakUserFeignClient {
/**
* 사용자의 그룹 목록 조회
*/
@DeleteMapping("/users/{id}/groups")
List<GroupRepresentation> getUserGroups(
@RequestHeader("Authorization") String bearerToken,
@PathVariable String id
);
}
7. Custom Exception 사용 예외처리 -6 : 결과 확인
💡 Custom Exception 사용 예외처리 -6 : 결과 확인
- API 통신 자체의 통과를 위해 200에 대한 Status 코드를 제공하면서, 발생한 문제에 대해서 status에서는 404를 반환하고, 구성한 커스텀 코드인 errorCode가 F004로 반환했습니다.
- 또한, 발생한 에러를 errorMsg와 구체적인 errorDtlMsg 형태로 반환하여 최종적으로 서버간의 동일한 에러코드로 반환하도록 처리를 하였습니다.

오늘도 감사합니다 😀

'Java > 외부 통신' 카테고리의 다른 글
[Java] Spring Cloud OpenFeign 이해하고 활용하기 -2 : 설정 중앙화 방법 (0) | 2025.02.23 |
---|---|
[Java] Spring Cloud OpenFeign 이해하고 활용하기 -1 : 주요 개념 및 환경 구성, 활용 예시 (1) | 2024.11.26 |
[Java] Spring Boot Web 활용 : RestTemplate 이해하기 (2) | 2023.08.14 |