FeignClient 사용 가이드
Spring Cloud OpenFeign을 사용하면 외부 REST API를 인터페이스 선언만으로 호출할 수 있다.
1. 의존성 추가 (build.gradle)
ext {
set('springCloudVersion', '2025.0.2-SNAPSHOT')
}
repositories {
mavenCentral()
maven { url = 'https://repo.spring.io/snapshot' }
maven { url = 'https://repo.spring.io/milestone' }
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
2. 메인 클래스 설정
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. FeignClient 인터페이스 작성 규칙
| 항목 | 규칙 |
|---|---|
| 패키지 위치 | 업무 패키지 하위 client/ 별도 생성 |
| 네이밍 | [대상시스템명]Client (예: PostClient, WeatherClient) |
| URL 관리 | application.yml 환경변수로 관리 (${external.xxx.url}) |
| 어노테이션 | @FeignClient(name = "클라이언트명", url = "${...}") |
| 메서드명 | 팀 액션명 규칙 동일 (get, getList, reg, upt, del) |
4. 예제 — JSONPlaceholder API
JSONPlaceholder (
https://jsonplaceholder.typicode.com)는 테스트용 무료 공개 REST API다.
application.yml
external:
jsonplaceholder:
url: https://jsonplaceholder.typicode.com
PostVo.java
@Getter @Setter @ToString
public class PostVo {
private Long id;
private Long userId;
private String title;
private String body;
}
4-1. 외부 API 필드명이 snake_case인 경우
외부 API의 요청/응답 필드가 data_name, data_id 처럼 snake_case라면
@JsonProperty를 필드에 직접 선언해서 매핑하고, 우리 VO는 camelCase로 사용한다.
상황 예시
외부 API 응답 JSON:
{
"data_id": 1,
"data_name": "홍길동",
"created_at": "2026-05-16T09:00:00Z",
"is_active": true
}
VO 작성 방법
import com.fasterxml.jackson.annotation.JsonProperty;
@Getter @Setter @ToString
public class ExternalUserVo {
@JsonProperty("data_id")
private Long dataId;
@JsonProperty("data_name")
private String dataName;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("is_active")
private Boolean isActive;
}
- 역직렬화(JSON → VO): 외부 API 응답의
data_id값이dataId필드에 들어옴 - 직렬화(VO → JSON): 요청 시
dataId값이data_id키로 나감 @JsonProperty가 없는 필드는 camelCase 그대로 처리됨
외부 API 필드 일부만 snake_case인 경우
snake_case가 필요한 필드에만 선택적으로 적용한다:
@Getter @Setter @ToString
public class ExternalOrderVo {
// 외부 API 필드명이 snake_case → @JsonProperty 적용
@JsonProperty("order_id")
private Long orderId;
@JsonProperty("user_id")
private Long userId;
@JsonProperty("total_price")
private Integer totalPrice;
// 외부 API 필드명이 camelCase와 동일 → @JsonProperty 불필요
private String status;
private String memo;
}
FeignClient 인터페이스에서 사용
@FeignClient(name = "externalOrderClient", url = "${external.order.url}")
public interface ExternalOrderClient {
@PostMapping("/orders/get")
ExternalOrderVo get(@RequestBody ExternalOrderVo vo);
@PostMapping("/orders/reg")
ExternalOrderVo reg(@RequestBody ExternalOrderVo vo);
}
FeignClient는 Jackson을 기본 직렬화 라이브러리로 사용하므로
VO에 선언한 @JsonProperty가 요청·응답 모두에 자동 적용된다.
PostClient.java
@FeignClient(name = "postClient", url = "${external.jsonplaceholder.url}")
public interface PostClient {
@GetMapping("/posts")
List<PostVo> getList();
@GetMapping("/posts/{id}")
PostVo get(@PathVariable("id") Long id);
@PostMapping("/posts")
PostVo reg(@RequestBody PostVo vo);
@PutMapping("/posts/{id}")
PostVo upt(@PathVariable("id") Long id, @RequestBody PostVo vo);
@DeleteMapping("/posts/{id}")
void del(@PathVariable("id") Long id);
}
PostService.java
@Slf4j
@RequiredArgsConstructor
@Service
public class PostService {
private final PostClient postClient;
@Transactional(readOnly = true)
public List<PostVo> getList() {
log.info("외부 API 게시글 목록 조회");
return postClient.getList();
}
@Transactional(readOnly = true)
public PostVo get(Long id) {
log.info("외부 API 게시글 단건 조회: id={}", id);
return postClient.get(id);
}
@Transactional(rollbackFor = Exception.class)
public PostVo reg(PostVo vo) {
log.info("외부 API 게시글 등록: title={}", vo.getTitle());
return postClient.reg(vo);
}
@Transactional(rollbackFor = Exception.class)
public PostVo upt(Long id, PostVo vo) {
log.info("외부 API 게시글 수정: id={}", id);
return postClient.upt(id, vo);
}
@Transactional(rollbackFor = Exception.class)
public void del(Long id) {
log.info("외부 API 게시글 삭제: id={}", id);
postClient.del(id);
}
}
PostController.java
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/post")
public class PostController {
private final PostService postService;
@GetMapping("/getList")
public ResponseJson<List<PostVo>> getList() {
ResponseJson<List<PostVo>> response = new ResponseJson<>();
response.setResult(0);
response.setData(postService.getList());
return response;
}
@PostMapping("/get")
public ResponseJson<PostVo> get(@RequestBody PostVo vo) {
ResponseJson<PostVo> response = new ResponseJson<>();
response.setResult(0);
response.setData(postService.get(vo.getId()));
return response;
}
@PostMapping("/reg")
public ResponseJson<PostVo> reg(@RequestBody PostVo vo) {
ResponseJson<PostVo> response = new ResponseJson<>();
response.setResult(0);
response.setData(postService.reg(vo));
return response;
}
}
5. 에러 처리
FeignException 기본 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(FeignException.class)
public ResponseEntity<Map<String, Object>> handleFeignException(FeignException e) {
log.error("외부 API 호출 실패: status={}, message={}", e.status(), e.getMessage());
Map<String, Object> body = new LinkedHashMap<>();
body.put("success", false);
body.put("error", Map.of(
"code", "EXTERNAL_API_ERROR",
"message", "외부 서비스 호출 중 오류가 발생했습니다."
));
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(body);
}
}
ErrorDecoder 커스텀 구현
public class PostClientErrorDecoder implements ErrorDecoder {
private static final Logger log = LoggerFactory.getLogger(PostClientErrorDecoder.class);
@Override
public Exception decode(String methodKey, Response response) {
log.error("PostClient 오류: method={}, status={}", methodKey, response.status());
return switch (response.status()) {
case 404 -> new NoSuchElementException("게시글을 찾을 수 없습니다.");
case 400 -> new IllegalArgumentException("잘못된 요청입니다.");
default -> new RuntimeException("외부 API 오류: " + response.status());
};
}
}
ErrorDecoder를 FeignClient에 적용하려면 configuration 속성에 Config 클래스를 지정한다:
@FeignClient(
name = "postClient",
url = "${external.jsonplaceholder.url}",
configuration = PostClientConfig.class
)
public interface PostClient { ... }
public class PostClientConfig {
@Bean
public ErrorDecoder errorDecoder() {
return new PostClientErrorDecoder();
}
}
6. 타임아웃 설정
spring:
cloud:
openfeign:
client:
config:
default:
connect-timeout: 3000 # 연결 타임아웃 3초
read-timeout: 10000 # 읽기 타임아웃 10초
postClient: # 특정 클라이언트만 별도 설정
connect-timeout: 1000
read-timeout: 5000
7. 로깅 설정
logging:
level:
com.wiki.proj.example.post.client: DEBUG # FeignClient 로그 활성화
// Config 클래스에 Logger.Level 빈 등록
public class PostClientConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL; // NONE | BASIC | HEADERS | FULL
}
}
| Level | 출력 내용 |
|---|---|
NONE | 로그 없음 (기본값) |
BASIC | 메서드, URL, 상태코드, 소요시간 |
HEADERS | BASIC + 요청/응답 헤더 |
FULL | HEADERS + 요청/응답 바디 전체 |
프로덕션 환경에서는
BASIC또는NONE권장 (FULL은 개인정보 노출 위험)