로드밸런서 공부를 할 때 사용했던 코드를 가져와서 사이드 프로젝트를 만들고 있는데,
현재 로드밸런서는 요청값을 처리하는 데에 한계가 있다.
컨트롤러는 @GetMapping을 통해 오직 GET요청만 처리하도록 설계되어 있고,
RequestProcessor는 내부적으로 HTTP요청을 처리하며 POST방식만을 사용하여, POST요청만 전송 가능하다.
다양한 HTTP메서드를 처리하려면 요청에 따라 다른 로직을 처리하는 추가 설계가 필요하다.
설계 방향성 잡기: 사용자 편의성을 고려하자!
직관적으로 생각해봤을때, 클라이언트는 로드밸런서의 존재를 알지 못한다.
사용자가 api를 호출할 때 마다 request에다 method도 입력해서 보내는 것 보다, api자체를 여러 개로 나누어 관리하는 것이 유저 입장에서 편리할 것이다.
따라서 로드밸런서의 컨트롤러에서 사용자가 보낸 요청의 HTTP메서드를 확인한 뒤, requestProcessor에 적절한 메서드 타입을 전달하도록 설계하는 것이 깔끔할거라 생각했다.
대강 pseudo코드로 표현해보자.
근데 이 if문이 좀 마음에 안들었다. 만약에 "데이터 전송" 외에 다른 메서드 분리 기준이 생긴다면? 코드를 통째로 바꿔야 한다.
따라서, 각 http메서드들의 동작을 정의하는 깔끔한 방법이 필요한데..
전략패턴을 사용하기에는 http메서드의 갯수만큼 클래스가 늘어나서 오히려 유지보수가 불편해질 듯 했다.
어떻게 할지 고민하다가 문득 예~~~~~~~~~전에 계산기 과제에서 사용한 enum과 bifunction이 생각났다.
enum과 함수형 인터페이스로 여러 전략을 관리해보자
2025.01.24 - [비둘기의시크릿세션] - Blocked(프로세스의 생명주기)
거기다 최근에 프로세스의 생명주기 중 blocked상태에 대해 세션을 진행한 적이 있다.
주제에서 벗어나 좀 아쉬웠던 세션이었지만, 준비하면서 내 스스로는 스프링부트에서의 대용량 트래픽 관리 방법에 대해 많이 배우게 되었었다.
이 세션을 준비하면서 알게 된 spring webflux도 여기에다 적용해보자
비동기방식으로 요청을 처리한다면 훨씬 더 높은 성능이 나올 것이다.
동기(HttpURLConnection) vs 비동기(Mono) 부하테스트
단순히 "성능이 좋을 것 같은데?" 하고서 사용할 단계는 지났기에 실제 부하테스트를 먼저 진행해보자.
2025.01.25 - [개인 공부용 프로젝트/loadbalancer.project] - apache jmeter로 부하테스트 해보기
해당 글에서 기존방식인 httpURLconnection으로 진행했던 부하테스트 기록이 있으니 로드밸런서 프로젝트 코드를 mono와 webclient로 리팩토링해서 결과를 살펴보자.
여담인데 Mono라는 객체 이름 좀 귀여운거같다 ㅇㅅㅇ
테스트 결과다. 성공률이 50퍼센트나 올랐다. 솔직히 오를거라 예상은 했는데 50%까진..ㄷㄷ
그렇다면 왜 이렇게 차이가 날까?
동기 방식인 HttpURLConnection은 요청이 완료될 때까지 스레드를 블로킹하므로,
동시 처리량이 제한되고 스레드 풀이 고갈되면 요청이 타임아웃되거나 실패할 가능성이 높다.
반면, 비동기 방식인 webclient + mono는 논블로킹 방식으로 작동하여 대기 상태에서도 스레드를 점유하지 않아서
같은 자원으로 더 많은 요청을 병렬로 처리할 수 있다.
이는 네트워크 지연 시간이 긴 요청이 다른 요청에 영향을 주지 않도록 하며, 타임아웃을 최소화하고 성공률을 크게 높이는 데 기여한다.
또한, 비동기 방식은 로드밸런싱과 결합해 서버 부하를 고르게 분산시키고, 스레드를 효율적으로 활용해 높은 처리량과 성공률을 보장한다.
결과적으로, 비동기 방식은 CPU와 자원을 효율적으로 사용하며, 대규모 트래픽 환경에서 동기 방식 대비 탁월한 성능을 보인다.
여기서 또 한가지 의문점이 생겼다. reactor에 내장되어 있는 retry()기능을 사용한다면 어떻게 될까? retry()설정을 1에서 3정도 설정하고 각각의 성공률과 처리속도를 비교해보도록 하자.
retry(1), (3) 부하테스트
호기롭게 시작했는데 현재시간 새벽 4시 50분동안 부하테스트 창만 멍하니 보고있다가 껐다
이래서 실제 서버환경과 로컬의 차이가 나는거구나를 느꼈다 retry기능이 괜히 있는건 아닐텐데
대용량트래픽에서의 리트라이는 로컬에서 테스트하기 힘든것같다..
retry(3)으로 한건데, 1로 해도 뭐 별반 차이날거같지 않아서 gg 아직 내가 공부할 영역은 아닌 듯 하다
라고 하고 아쉬워서 retry(1)로 돌려봤는데
역시나 성능이 저하된 걸 볼 수 있었다. 아무래도 로컬이라 이런 부분은 테스트 하기 힘들다.
요청 수를 많이 줄이면 테스트가 가능하겠지만 그것또한 의미가 있겠나 싶었다ㅠ 당연히 요청이줄고, retry가 늘 수록 처리속도는 늦어지고 성공률이 늘테니..
다음에 도커에다가 cpu, 메모리를 더 할당하는 것에 대한 공부를 좀 해봐야겠다.
완성된 코드
@RequiredArgsConstructor
public enum HttpMethodStrategy {
GET((builder, url, request) ->
builder.build()
.get()
.uri(url)
.retrieve()
.bodyToMono(String.class)
.retry(1)
),
POST((builder, url, request) ->
builder.build()
.post()
.uri(url)
.bodyValue(request) //post는 데이터전송이 필요하므로
.retrieve()
.bodyToMono(String.class)
.retry(1)
),
//... 기타 등등 put, delete, patch
private final WebClientAction action;
/**
* WebClient 로 HTTP 요청 실행 (비동기 방식)
* @param builder WebClient 인스턴스
* @param url 요청 URL
* @param request 요청 데이터
* @return Mono<String> (비동기 응답데이터)
*/
public Mono<String> execute(WebClient.Builder builder, String url, String request) {
try {
return action.apply(builder, url, request)
.onErrorMap(WebClientResponseException.class, e ->
new RuntimeException("HTTP 에러: " + e.getStatusCode() + " - " + e.getResponseBodyAsString(), e)
)
.onErrorResume(e -> Mono.error(new RuntimeException("HTTP 요청을 처리하는 데 실패했습니다", e)));
} catch (Exception e) {
return Mono.error(new RuntimeException("HTTP 요청을 처리하는 데 실패했습니다", e));
}
}
@FunctionalInterface
interface WebClientAction {
Mono<String> apply(WebClient.Builder builder, String url, String request);
}
}
그래도 일단 retry(1)정도는 나름(?) 합리적인 판단같아서 내비뒀다
캠프 첫 계산기 과제 때 썼던 enum과 함수형 인터페이스 조합을 생각해낸게 되게 뿌듯하당ㅇㅅㅇ 역시 경험이 중요해
하지만 내가 또 사용하고 싶어서 사용한게 아닌가 하는 고민도 있다
저 builder에서 메서드체이닝 코드가 너무 긴게 마음에 안들긴 하는데 맘만먹으면 바로 중복제거 가능한 부분이기도 하고 일단 냅둬야겠다 ㅎ 뭐 추가할 일 생기면 제거해야지
사이드 프로젝트 acfac은 로드밸런싱과 kafka를 결합한 프로젝트입니다.
로드밸런싱을 진행하며 실시간으로 사용자 요청의 유효성을 분석합니다.
서버가 아플 때(에이 씨팍..)아팍!!!!!!!!!!!!!!!!!하고 소리지릅니다.
https://github.com/roqkfchqh/acfac