개요
HTTP Client에 대해서는 인턴십 때 익히 알고 있었다. 당시 회사 코드는 단순한 MVC 구조가 아니라 모두 HTTP Client 요청을 날렸다. 이때 Webclient를 사용했는데, Asynchronous & Non-blocking으로 리소스 자원의 효율성을 극대화했다.
보통 front → back
회사 코드는 front → back → platform
아무튼 그렇게 첫 HTTP Client를 사용했었고.. 이후 KOALA 프로젝트에서 Feign을 접했다. 작년 10월부터 올해 7월, HTTP Client를 사용하며 노션에 정리해둔 내용을 오픈한다...🤗 당시에 feign의 동시성 관련해서 헷갈리는 점이 너무 많았으나, 공식문서와 사례를 읽으면서 모두 정리해봤다!!
1. HTTP Request Client
HTTP 프로토콜을 사용하여 외부 API나 서비스와 통신하기 위한 기능을 제공하는 라이브러리다.
- HTTP 메서드를 사용하여 서버에 요청을 보내는 기능
- 요청 헤더 및 본문 설정
- 응답 처리
- 오류 처리
Spring에서 HTTP Request Client를 구현하는 방법은 대표적으로 3가지가 있다.
① WebClient
② FeignClient
③ RestClient ④ RestTemplate
가 있다.
이 중, 마지막 RestTemplate은 deprecated된다는 말이 나올 만큼 선호도가 낮은 방식이므로 이 글에서는 생략한다.
궁금한 점이 있다면 이 문서를 추천한다.
2. Http Request Client를 왜 사용했는가?
- KOALA 서비스 API 구현을 위해서 백준 사이트 문제를 가져와야 했었다.
- 문제를 가져올 수 있는 방법은 두 가지 ① 백준 사이트 크롤링 ② 직접 JSON 파싱해서 사용
- 크롤링은 법적 문제와 부하 문제가 항상 동반한다. ➡️ 실제로 백준 사이트에서도 크롤링을 법적으로 금지하고 있다.
- 백준 문제 크롤링은 응답값 자체가 복잡하고, JSON 배열에 있는 값을 일일이 파싱해서 원하는 값만 도출하는 로직이 필요하다.
방법을 리서치 하다가 비공식 API 문서를 발견했다! 이 비공식 문서 API를 HTTP Client로 호출하면 개발시간 단축과 효율적인 구현 가능하다. 따라서solved.ac API를 FeignClient를 통해 사용하기로 결정했다.
3. Http Request Client의 여러 구현 방식을 이해하기 위한 상식
1) Reactive Streams API
- asynchronous processing with non-blokcking
- 비동기 스트림 처리를 위한 표준 인터페이스를 정의
- Publisher, Subscriber, Subscription, Processor 등의 인터페이스를 제공하여 데이터 스트림을 효과적으로 처리
- JDK 9에 포함되어 다양한 Reactive 라이브러리에서 사용
⇒ 기존의 요청-응답 방식과는 차이가 나는 스트림 형태의 데이터 처리 방식이다.
1. 데이터가 생산되면 Publisher → Subscriber에게 전달
2. Subscriber는 데이터를 비동기적으로 처리
2) Spring MVC와 Spring Webflux
Spring Webflux: Reactive Streams 기반의 웹 프레임워크로, Servlet 기반의 웹 애플리케이션 모델인 Spring MVC와는 다르다.
종류 Feign Client Web Client
종류 | Feign Client | Web Client |
방식 | Spring MVC | Spring Webflux |
모델 | 요청-응답 모델 | Reactive Stream 기반 |
동작 | synchronous & blocking - 클라이언트는 응답을 기다리는 동안 블로킹되어 다른 작업 불가능 (완전 불가능은 아니다. 간단한 방법이 있는데 프로젝트에 어떻게 적용했는지 뒤에 설명해보겠다.) |
asynchronous & non-blocking |
데이터 처리 |
단일 객체 또는 컬렉션 데이터 ex) 사용자가 특정 상품 정보를 요청하면 서버는 해당 상품 정보 객체 반환 | Reactive Streams API를 사용하여 데이터 스트림을 처리 ex1) 실시간 주식 시세 정보 요청이 들어오면 서버는 주식 시세 데이터를 실시간으로 스트리밍 형태로 전송 ex2) 대용량 파일 다운로드 요청이 들어왔을 때, 서버는 파일 데이터를 청크 단위로 나누어 스트리밍 형태로 전송 |
스레드 관리 |
- thread pool 사용 - 각 요청마다 스레드 생성 |
- 적은 수의 스레드 - 논블로킹 방식의 I/O 작업 |
4. WebClient
Spring webflux 기반의 라이브러리, HTTP 요청을 수행하는 클라이언트가 포함되어있다. 스레드나 동시성 처리를 할 필요없이 비동기 논리의 선언적 구성이 가능하다.
“It is fully non-blocking, it supports streaming“ - docs.spring.io
→ 비동기성과 논블로킹 처리에 초점을 둔 Http Client
WebClient Configuration
@Configuration
public class WebClientConfig {
@Bean
public WebClient webClient(WebClient.Builder webClientBuilder) {
return webClientBuilder
.baseUrl("${feign.svc1.url}")
.build();
}
}
Webclient Interface
@Component
public class SolvedAcProblemSearchClient {
private final WebClient webClient;
public SolvedAcProblemSearchClient(WebClient webClient) {
this.webClient = webClient;
}
// 빌더 패턴
public Mono<ProblemResponse> searchProblems(int page, String query, String sort, String direction) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.queryParam("page", page)
.queryParam("query", query)
.queryParam("sort", sort)
.queryParam("direction", direction)
.build())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(ProblemResponse.class);
}
}
5. FeignClient
Spring MVC 기반의 HTTP 클라이언트 구현체다. 선언적 방식의 웹 서비스 클라이언트로, 쉽게 웹 클라이언트를 생성할 수 있다. → 동기식 처리/블로킹 방식
- Netflix에서 개발된 Http Client Binder → Spring Cloud 진영에 통합되면서 spring-cloud-starter-openfeign로 사용
- HTTP 요청을 추상화/표준화하는 과정의 복잡성을 줄이기 위해 만들어졌다.
- 웹 서비스 클라이언트 구현이 쉽고 직관적이다. 이건 써보면 안다.. 사용성이 월등히 높은게 느껴진다! 익숙한 Spring MVC 구조를 가지고 있으며 어노테이션으로 개발이 가능하다. 참고로 WebClient는 빌더 패턴으로 구현하고, 각각의 메서드 파라미터를 유의해야 한다. (실제로 인턴십 중에 platform으로 요청하는 client를 짜면서 URL 관련 문제로 허둥댄 적이 있다..) 아래 코드 예시를 보자.
- API의 Restfulness에 관계없이 일관된 방식으로 API 호출 가능하다. (비표준적인 URL 구조나 HTTP 메서드 등을 사용하더라도 표준화된 방식으로 API 호출이 가능하다.)
FeignClient와 동시성 처리
Feign과 동시성을 이해하는 것에 많은 어려움이 있었다. 여기에 작성하면 너무 길어져 노션 링크로 대체한다.
Feign을 사용할 사람이라면 한 번 읽어보는 것을 추천한다!
결론만 공유하면 다음과 같다.
1. feign의 내부 구현이 자바 11 HttpClient, 자바 17에서의 synchronized → reentrantLock로 변경되면서 많은 부분이 해결됨
2. 블로그 구현 방식도 괜찮은 부분이 있다. Apache Client 등을 사용 → 안정적
3. 동시성 문제를 일으킬 만큼 다량의 스레드를 쏟아내는 로직 자체를 feign과 함께 만나기가 어려움 → 동시성이 걱정되는 로직이 있다면 webclient를 쓰면 됨
Feign을 구현하기 위해 공부하면서 "동시성 문제가 있을 수 있다고....?"에서부터 출발하였고, 결론은 위와 같다.
우리 서비스는 단 하나의 호출만 했고, 확장 가능성을 고려해도 동시성 문제가 우려될 만큼의 외부 API 호출 기능이 도입되지 않을 것이다. Feign을 통해 구현하면 사용 편의성뿐만 아니라 Spring의 Scheduler와 @Async를 활용한 비동기 요청이 가능한 구조로 설계할 수 있다. 따라서 사용 편의성과 비동기 처리를 모두 갖춘 Feign을 선택했다!
6. FeignClient 사용방법
6-1. FeignClient 활성화
- Application 단에 @EnableFeignClients 적용 → Feign 클라이언트를 찾아서 자동으로 등록
6-2. 의존성 추가
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.0.0'
6-3. Feign용 Conf Class
@Configuration
public class FeignClientConfig {
@Bean
Request.Options requestOptions() {
return new Request.Options(5, TimeUnit.SECONDS, 3, TimeUnit.SECONDS, false);
}
@Bean
Retryer retry() {
return new Retryer.Default(500, 1000, 2);
}
@Bean
Logger.Level feignLoggerLevel() {
return Level.ALL;
}
@Bean
public RequestInterceptor requestInterceptor() {
return requestTemplate -> {
requestTemplate.header("Content-Type", "application/json");
requestTemplate.header("Accept-Encoding", "gzip");
};
}
}
✶ RequestInterceptor
6-4. @FeignClient 인터페이스
@FeignClient(name = "solvedAc", url = "${feign.solved-ac.url}", configuration = FeignClientConfig.class)
public interface SolvedAcClient {
@GetMapping("/search/problem")
ProblemResponse searchProblems(
@RequestParam("page") int page,
@RequestParam("query") String query,
@RequestParam("sort") String sort,
@RequestParam("direction") String direction
);
}
- 개별 Feign Client에 적용할 인터페이스
- 사용할 이름과 요청 url은 하드코딩하지 않고 YAML 파일에 정의 → 유연성/재사용성/관리 용이/보안
6-5. 호출
이렇게 요청할 API와 관련한 인터페이스를 정의하고,
서비스 함수에서 사용할 특정 FeignClient interface에 대한 의존성 주입 + 호출만 하면 구현 끝이다.
간단하죠
public class CreateAlgorithmService {
private final SolvedAcClient solvedAcClient;
private final AlgorithmRepository algorithmRepository;
private static final int NUMBER_PER_PAGE = 30;
public void createAlgorithm() {
AlgorithmResponse initResponse = solvedAcClient.searchAlgorithm(1);
int pageCount = initResponse.getCount() / NUMBER_PER_PAGE + 1;
for (int page = 1; page <= pageCount; page++) {
log.info("{}번 페이지 알고리즘 저장", page);
AlgorithmResponse algorithmResponse = solvedAcClient.searchAlgorithm(page);
List<AlgorithmDto> algorithmDtoList = algorithmResponse.getAlgorithmList();
List<Algorithm> algorithmList = new ArrayList<>();
for (AlgorithmDto algorithmDto : algorithmDtoList) {
algorithmList.add(algorithmDto.toEntity());
}
algorithmRepository.saveAll(algorithmList);
}
}
}
6-7. 결과
7. 느낀점
- webclient가 web flux로부터 나와서, fully asynchronous & non-blocking에 유의하자.
인턴십 사례처럼...
요청은 synch로 받더라도 이 요청을 실제로 처리하는 platform 서버로는 비동기식으로 요청 및 처리를 하게 되는 구조
⇒ 이런식의 전략이 필요할 때 webclient를 사용하는 것이지 간편하다고만 해서 사용하면 안 되는 것을 알았다.
- 블로그는…. 내용의 질도 중요하지만 날짜도 중요하다.
계속해서 개선이 되고 있는 기술은 이전에 작성된 블로그와 맞지 않는 부분이 많기에 주의해야 한다. 또, 한 번 오개념이 작성된 블로그가 뜨면 그 오개념을 여러 사람이 따라가고, 이게 AI 답변에도 영향을 준다고 느꼈다. 최대한 공식적으로 작성된 기술 문서를 참고하고, 그 기술이 나온 기반과 배경을 따라가면 유추할 수 있는 부분이 있다. 그걸 해석하는 능력 또한 개발에 있어서 중요하며, 이를 함양하기 위해 노력해야 한다.
'Dev > Spring & JPA' 카테고리의 다른 글
[Spring] 처리율 제한(Rate Limit)으로 악의적인 공격 차단하기 (0) | 2024.11.23 |
---|---|
[spring] null 처리를 위한 spring의 Stringutils (2) | 2023.12.21 |
[spring] @Async와 SimpleAsyncTaskExecutor, TaskExecutor 그리고 thread pool (0) | 2023.10.11 |
[Backend] 객체 지향 특징 | 다형성 | 좋은 객체 지향 설계 5 가지 원칙(SOLID) | EJB (0) | 2023.07.19 |