인증 서버를 gRPC와 WebFlux, R2DBC로 재설계해 서버 대수 줄이기

목차
시작은 서버 대수였습니다
이전 회사에서 협업 메신저 백엔드를 만들 때의 일입니다. 어느 시점에 인프라 비용을 크게 줄여야 한다는 방향이 내려왔습니다. 그런데 “서버를 줄이자”는 말만 있었지 어느 서버를 얼마나 줄일지는 정해져 있지 않았습니다. 그건 백엔드 쪽에서 직접 들여다보고 정해야 했습니다.
이럴 때 제일 쉬운 길은 전부 조금씩 줄이는 것이라고 생각합니다. 다만 그렇게 하면 서비스 전체가 조금씩 불안해지고, 정작 낭비가 큰 곳은 그대로 남습니다. 그래서 저는 “지금 단순한 일을 하면서 자원을 가장 많이 먹는 서버가 어디인가”를 먼저 찾아보기로 했습니다.
찾고 보니 인증 서버였습니다. 하는 일 자체는 단순한 서버인데, 피크 시간에는 최대 30대까지 늘어나 있었습니다. 기능의 단순함에 비해 대수가 너무 많았습니다. 이 글은 그 서버를 REST에서 gRPC + WebFlux + R2DBC 구조로 다시 만들어서 전환 후 평균 5~8대로 줄 인 과정을 정리한 것입니다.
WebFlux가 무엇인지는 예전에 쓴 글에서 다뤘기 때문에, 여기서는 “왜 이 조합이 서버 대수에 영향을 주는가”에 집중하겠습니다.
통신 패턴부터 봤습니다
처음부터 기술을 고르지는 않았습니다. 먼저 이 서버가 다른 서버들과 어떤 식으로 통신하는지를 봤습니다. 제가 확인한 것은 두 가지였습니다.
하나는 REST 동기 호출 때문에 연결(connection)이 많이 쌓인다는 점이었고, 다른 하나는 기능이 단순한데도 서버가 쓰는 자원이 과하다는 점이었습니다.
어느 쪽이 더 컸는지 수치로 갈라 보지는 못했습니다. 그래서 아래에서는 이런 구조에서 왜 서버가 많이 필요해지는지를 일반론으로 풀어 보겠습니다.
블로킹 서버가 대수를 늘리는 이유
전통적인 Spring MVC 서버는 요청 하나에 스레드 하나를 붙입니다. 그 스레드가 DB 응답을 기다리는 동안에는 아무 일도 못 하고 그냥 서 있습니다. 식당으로 치면 손님 한 분당 서버 한 명이 붙어서, 주방에서 음식이 나올 때까지 테이블 옆에 가만히 서 있는 것과 같습니다. 손님이 몰리면 서버 직원(스레드)을 더 뽑아야 하는데, 한 가게에 둘 수 있는 직원 수에는 한계가 있으니 결국 가게(인스턴스)를 늘리게 됩니다.
문제는 이때 늘어난 인스턴스들이 실제로 계산을 많이 하느라 바쁜 게 아니라는 점입니다. 대부분은 기다리는 시간입니다. CPU는 놀고 있는데 스레드 풀은 가득 찬 상태라서, 트래픽이 몰리면 서버 대수를 늘리는 수밖에 없습니다. 기능이 단순한 서버가 피크에 30대까지 가는 모습은 이런 구조에서 충분히 나올 수 있는 그림입니다.
세 가지를 같이 바꿨습니다
제가 제안한 재설계는 gRPC, R2DBC, WebFlux 세 가지였습니다. 각각이 어느 부분의 대기 시간을 줄여 주는지로 나눠 보면 이해가 쉽습니다.
gRPC, 그리고 HTTP/2 멀티플렉싱
gRPC는 HTTP/2 위에서 동작합니다. HTTP/2의 멀티플렉싱은 하나의 연결 안에서 여러 요청과 응답을 동시에 주고받는 방식입니다. 일반적인 HTTP/1.1 기반 REST 호출에서는 요청마다 연결을 따로 쓰거나, 연결을 재사용하더라도 한 번에 한 요청씩 처리하는 모양이 되기 쉽습니다.
제가 문제로 본 것이 동기 호출로 연결이 비대해지는 점이었기 때문에, 연결 수를 줄이는 쪽에서 gRPC가 맞는 선택이라고 봤습니다.
아래는 설명용으로 만든 예시입니다. 인증 서버 같은 단순한 서비스는 proto 파일로 계약을 이렇게 적습니다.
// 예시: 설명용으로 만든 가상의 proto입니다.
syntax = "proto3";
service TokenCheck {
rpc Verify (VerifyRequest) returns (VerifyReply);
}
message VerifyRequest { string token = 1; }
message VerifyReply { bool valid = 1; string subject = 2; }REST에서는 URL과 JSON 형식을 문서나 관례로 맞춰야 했다면, 여기서는 이 파일 하나가 호출하는 쪽과 받는 쪽의 약속이 됩니다.
R2DBC, DB 대기 시간을 논블로킹으로
앞에서 스레드가 DB 응답을 기다리며 서 있다고 했는데, 이걸 직접 해결하는 부분이 R2DBC입니다. 기존 JDBC는 쿼리를 던지면 응답이 올 때까지 스레드가 묶입니다. R2DBC는 쿼리를 던져 두고 스레드는 다른 요청을 처리하다가, 응답이 오면 이어서 처리합니다. 앞의 식당 비유로 하면 직원이 주문을 넣고 주방 앞에서 기다리지 않고 다른 테이블을 돌다가, 음식이 나오면 다시 가서 내는 모양입니다.
WebFlux만 쓰고 DB는 JDBC 그대로 두면 이 이점이 사라집니다. 이벤트 루프 스레드는 적은 수라서, 그 스레드가 JDBC에서 막히면 서버 전체가 멈추 는 효과가 생기기 때문입니다. 그래서 이 구조는 위에서 아래까지 논블로킹으로 맞춰야 의미가 있고, 세 가지를 같이 바꾼 이유도 거기에 있습니다.
WebFlux, 적은 스레드로 많은 요청을
WebFlux는 요청을 받는 쪽을 리액티브(필요한 시점에 이어서 처리하는) 방식으로 바꿔 줍니다. 적은 수의 스레드가 여러 요청을 번갈아 처리하기 때문에, 기다리는 시간이 길고 계산량이 적은 서버에 특히 잘 맞습니다. 인증 서버처럼 하는 일은 단순한데 호출이 많은 서버가 이 조건에 가깝다고 판단했습니다.
흐름은 대략 이런 모양입니다(예시 코드).
// 예시: 가상의 핸들러입니다. 실제 코드가 아닙니다.
public Mono<VerifyReply> verify(VerifyRequest request) {
return tokenRepository.findByToken(request.getToken()) // R2DBC, 논블로킹
.map(t -> VerifyReply.newBuilder().setValid(true).setSubject(t.getSubject()).build())
.defaultIfEmpty(VerifyReply.newBuilder().setValid(false).build());
}중간에 스레드를 막는 호출이 하나도 없다는 것이 핵심입니다.
이 설계를 다시 검증한다면
아래는 당시의 측정 로그가 아니라 재현 실험을 설계할 때의 기준입니다. 기존 구현, 통신만 바꾼 구현, DB 접근까지 바꾼 구현을 같은 요청 분포와 인스턴스 규격에서 비교해야 기여도를 나눌 수 있습니다.
| 관찰할 것 | 함께 고정하거나 기록할 조건 |
|---|---|
| 처리량과 p95·p99 | 요청률, 토큰 검증 성공·실패 비율, 워밍업 |
| 이벤트 루프 지연과 CPU | 블로킹 호출 유무, 암호 연산량, GC |
| DB 대기와 연결 수 | 풀 크기, 쿼리 계획, 캐시 적중률 |
| 장애 시 회복 | deadline, 재시도 상한, 취소 전파, 구버전 복귀 |
REST도 HTTP/2와 연결 풀을 사용할 수 있고 Node.js도 비동기 I/O를 지원합니다. 따라서 기존 서버가 Node.js였다는 사실만으로 스레드 대기를 원인으로 단정할 수 없습니다. 앞의 Spring MVC 설명은 블로킹 I/O의 일반적인 예시 입니다. 실제 원인은 연결·DB·CPU 지표를 함께 봐야 합니다.
제안을 어떻게 설득했는지
기술적으로 맞다고 해서 바로 바꿀 수 있는 것은 아니었습니다. 인증 서버는 다른 서버들이 모두 기대는 곳이라 바꾸는 쪽이 부담을 집니다. 그래서 세 가지 자료를 준비했습니다.
- 비용 시뮬레이션: 지금 구조에서 얼마를 쓰고 있고, 바뀌면 어느 정도로 줄 것으로 보는지
- 기술 위험: 팀에 익숙하지 않은 방식이니 어디서 문제가 생길 수 있는지
- 롤백 시나리오: 잘못되면 어떻게 되돌리는지
이 자료를 들고 의사결정권자와 1:1로 이야기해서 합의를 받았습니다. 각 자료의 구체적인 내용은 회사 내부 사정과 닿아 있어서 이 글에는 적지 않겠습니다.
제가 한 일은 줄이는 것 자체에 반대하는 게 아니라, 어디를 어떻게 줄일지를 조사한 데이터로 가져가서 이야기한 것입니다. 이후에 모호하게 내려오는 요구를 데이터로 구체화해서 이야기하는 방식이 팀에서 하나의 패턴으로 자리 잡았다고 기억합니다.
결과
경력 기록에 남아 있는 수치는 이전 피크 최대 30대, 전환 후 평균 5~8대입니다. 피크와 평균은 집계 기준이 달라 이 숫자로 동일 부하에서의 절감률을 계산할 수 없습니다. 이전 글의 “피크 기준 5~8대” 및 “75% 안팎” 표현을 정정합니다.
서버 대수가 줄었다는 운영 경험과 특정 기술의 성능 기여는 구분해야 합니다. gRPC·WebFlux·R2DBC를 함께 도입했으므로 gRPC 하나가 감소분을 만들었다고 분리해 말할 근거는 없습니다.
대수를 줄이는 동안 p99 지연 시간은 흔들리지 않고 유지됐습니다. 정확한 수치는 남겨 둔 자료가 없어 적지 않습니다.
한계와 주의할 점
재설계 이후 오래 운영하면서 어떤 문제가 생겼는지까지는 지켜보지 못했습니다. 그래서 이 구조가 오래 문제없이 갔다고는 말하지 않겠습니다.
이런 전환을 고려한다면 알아 둘 부담은 이렇습니다.
- 팀이 리액티브 코드에 익숙해야 합니다. 체인으로 이어진 코드는 스택 트레이스가 읽기 어렵고, 디버깅 방식도 달라집니다.
- 논블로킹 경로 중간에 블로킹 호출이 하나 섞이면 이점이 크게 줄어듭니다. 라이브러리 하나를 고를 때도 논블로킹을 지원하는지 확인해야 합니다.
- gRPC는 브라우저에서 바로 호출하기 불편하고, 로드밸런서나 프록시가 HTTP/2를 제대로 지원하는지도 확인해야 합니다.
- R2DBC는 JPA처럼 연관관계를 대신 관리해 주는 기능이 약해서, 쿼리와 매핑을 직접 챙기는 부분이 늘어납니다.
저희 인증 서버는 기능이 단순했기 때문에 이 부담을 감수할 만했다고 판단했습니다. 기능이 복잡하고 비즈니스 로직이 많은 서버라면 같은 판단이 나오지 않았을 것 같습니다.
정리하며
서버 대수가 많다고 서버가 바쁜 것은 아닐 수 있습니다. 이번 인증 서버는 바쁘다기보다 기다리는 시간이 많은 쪽이라고 판단했고, 기다리는 방식을 바꿔서 대수를 줄였습니다. 비슷한 구조를 고민하시는 분께 비교 자료 정도로 쓰이면 좋겠습니다.
