FeignClient 사용 가이드

Spring Cloud OpenFeign을 활용한 외부 API 호출 가이드

마지막 수정: 2026-05

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, 상태코드, 소요시간
HEADERSBASIC + 요청/응답 헤더
FULLHEADERS + 요청/응답 바디 전체

프로덕션 환경에서는 BASIC 또는 NONE 권장 (FULL은 개인정보 노출 위험)