06 스프링과 외부 API 호출
06 스프링과 외부 API 호출
1. Note
1. Note
- 요령있게 구현가능한 환경이나 상태에 따라서 조절 필요할듯.
- 급하게 바꿀 필요는 없는 것 같음.
- feign(페인) 같은 경우도 보면
- 의존성 버전 설정하는 Bom파일 같은 경우도 2023버전(2026.04.)
- 기술이 발달하는 속도보다
- 구축 환경이 해결할 수 있는 역량이 못 따라감
- 그것 또한 비용!
2. 스프링에서 외부 API 호출 방식 종류
1. RestTemplate (레거시)
- 호출 방식
- 의존성: spring-boot-starter-web
- 필수: Bean 등록
- 소스
1 2 3 4 5
// 동기 방식 (blocking) // 현재는 deprecated (사용 비추천) RestTemplate restTemplate = new RestTemplate(); String result = restTemplate.getForObject(url, String.class);
2. WebClient (Spring 공식 권장)
- 호출 방식
- 의존성: spring-boot-starter-webflux
- 필수: 없음
- 소스
1 2 3 4 5 6 7 8 9 10
// 비동기 / 논블로킹 (Reactive) // Spring WebFlux 기반 WebClient webClient = WebClient.create(); String result = webClient.get() .uri(url) .retrieve() .bodyToMono(String.class) .block();
3. Feign (OpenFeign)
- 호출방식
- 의존성: spring-cloud-starter-openfeign
- 필수: @EnableFeignClients
- 기본 사용: @FeignClient 인터페이스
- 소스
1 2 3 4 5 6 7 8
// 인터페이스 기반 선언형 HTTP Client // 별도로 Config 필요 @FeignClient(name = "example", url = "https://api.example.com") public interface ExampleClient { @GetMapping("/users") List<User> getUsers(); }
4. HttpClient (Apache, OkHttp 등)
- 호출방식
- 의존성: httpclient 또는 okhttp
- 필수: 없음 (직접 생성)
- 소스
1 2 3
//저수준 HTTP 클라이언트 CloseableHttpClient client = HttpClients.createDefault(); HttpGet request = new HttpGet(url);
2. 문제가 발생할 수 있는 상황들
1. 네트워크 오류
- 문제
- 네트워크 장애나 타임아웃으로 인해 API 호출이 실패하면, 데이터베이스에 이미 적용된 작업은 롤백되지 않음
- 외부 API와 데이터베이스 간 상태 불일치가 발생할 가능성이 큼
- 해결 방법
- 네트워크 장애를 예상하고, API 호출 전후의 데이터베이스 상태를 관리하는 로직을 추가함
- API 호출 실패 시 데이터베이스 작업을 취소하거나,
- API 호출 성공 시 데이터베이스 작업을 수행하도록 순서를 조정
2. 외부 시스템 장애
- 문제
- 외부 시스템(예: 결제 게이트웨이, 메시지 큐)이 다운되면, 트랜잭션 상태와 외부 작업 결과가 불일치할 수 있음
- 예: 결제 성공 후 주문 저장 중 실패하거나, 반대로 주문 저장 성공 후 결제 API 호출이 실패.
- 해결 방법:
- 보상 트랜잭션(Compensating Transaction)
- API 호출 성공 이후 데이터베이스 작업이 실패하면, API 호출을 취소하는 로직을 추가
- 예: 결제가 성공한 경우, 주문 저장 실패 시 결제를 취소하는 API 호출 수행.(기록이 남아서 위험)
- 트랜잭션 내 API 호출 순서 조정
- 데이터베이스 작업 후 외부 API를 호출하도록 설계하여, 데이터 정합성을 우선 보장
- 예: 주문 저장 후 결제 API 호출.
- 보상 트랜잭션(Compensating Transaction)
3. 타임아웃 및 예외 처리
- 문제
- API 호출이 무한 대기 상태에 빠지거나, 응답 시간이 과도하게 길어질 경우 트랜잭션이 불필요하게 오래 유지됨
- 이는 시스템 성능에 악영향을 미침
- 해결 방법
- API 호출 시 타임아웃을 설정하여, 일정 시간이 지나면 작업을 강제로 중단
- 타임아웃 발생 시 트랜잭션 롤백 또는 재시도 로직을 추가
4. 일시적인 호출 실패
- 문제
- 외부 API 호출 실패가 일시적인 네트워크 장애나 과부하로 발생하는 경우가 많음
- 즉각적인 실패 처리 대신 재시도를 통해 성공률을 높일 수 있음
- 해결 방법
- Spring의
@Retryable어노테이션이나 Resilience4j 라이브러리를 활용하여 재시도 로직을 구현 - 일정 횟수 재시도 후에도 실패하면 사용자 정의 예외를 던지고, 트랜잭션을 롤백
- Spring의
3. Retry 기법 활용 (재시도 전략)
1. 셋팅
- 의존성
1
implementation 'org.springframework.retry:spring-retry'
- Main에 어노테이션
1 2 3 4 5 6 7 8 9
@EnableRetry // 추가 필요 @EnableAsync @EnableFeignClients @SpringBootApplication public class ExamplePart3Application { public static void main(String[] args) { SpringApplication.run(ExamplePart3Application.class, args); } }
2. 적용
1
2
3
4
5
6
7
8
9
10
11
12
@Transactional
@Retryable(value = ServiceException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void save() {
// ... 비즈니스 로직
throw new ServiceException(ServiceExceptionCode.NOT_FOUND_PRODUCT); // retry 실행
}
// `value` => 재시도 대상 예외를 지정
// `maxAttempts` => 재시도를 몇 번 수행할지 설정
// `@Backoff` => 각 재시도 사이의 대기 시간을 설정
=> 1000이면 1초정도
=> 바로 바로 실행해버리면 반복 장애가 발생할 수도 있음
// 최대 재시도 횟수 내에서도 실패하면 최종적으로 `RemoteException`이 호출자에게 전달
5. 타임아웃 설정
1. 타임아웃 설정
- 외부 API 호출이 무한 대기 상태에 빠지거나, 응답 시간이 지나치게 길어질 경우 전체 시스템의 성능이 저하됨
- 이를 방지하기 위해 타임아웃 설정을 통해 일정 시간이 지나면 요청을 강제로 종료하는 방법을 사용
- OpenFeign을 활용해서 타임아웃을 구현하는 것을 권장
2. OpenFeign
- REST API를
- 직접 HttpURLConnection, RestTemplate, WebClient로 호출하는 대신
- 자바 인터페이스로 선언만 하면 내부에서 HTTP 호출을 대신 해주는 도구
- 구분
- OpenFeign → 오픈소스 프로젝트(Git)
- Spring Cloud OpenFeign → 스프링에서 통합 지원
3. FeignClient를 활용한 타임아웃 설정
- 의존성
1 2 3 4 5 6 7 8 9
dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:2023.0.1" } } dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' } - Configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@Configuration public class OpenFeignConfig { @Bean public Request.Options feignOptions() { return new Request.Options( 10000, TimeUnit.MILLISECONDS, -- 연결시도 시간 제한 10초 60000, TimeUnit.MILLISECONDS, -- 연결이후 응답 대기 시간 60초 true -- HTTP 리다이렉트(3xx)를 자동으로 따라갈지 여부 ); } @Bean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; -- 요청 실패 시 절대 재시도하지 않는다 (딱 1번만 호출) } } - Main
1 2 3 4 5 6
@EnableFeignClients public class ExamplePart3Application { public static void main(String[] args) { SpringApplication.run(ExamplePart3Application.class, args); } }
3. 서비스단
- 매핑설정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@FeignClient(
name = "external-product",
url = "${external.external-shop.url}",
configuration = OpenFeignConfig.class
)
public interface ExternalShopClient {
@GetMapping("/products?page={page}&size={size}") // url설정
ExternalProductResponse getProducts(@RequestParam("page") Integer page,
@RequestParam("size") Integer size);
// 매핑은 컨트롤러랑 동일하게 사용가능함
// 리턴타입은 요청후 받는 리스폰 되는 것이고 외부API와 맞춰야함.
// 파라미터로 넣은 것들은 URL로 잘들어감
// RequestBody도 잘들어감!
}
- 서비스단
1
2
3
4
5
6
7
8
9
10
11
12
private final ExternalShopClient externalShopClient;
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
@Transactional
@Retryable(value = DomainException.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public void save() {
try {
// 요청받는 메소드
ExternalProductResponse responses = externalShopClient.getProducts(1, 10);
~~~~~
This post is licensed under CC BY 4.0 by the author.