- Published on
Spring Boot 3 Redis 세션 병목 - Lettuce 튜닝
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 트래픽이 늘어나면 Spring Session + Redis 조합에서 가장 먼저 티가 나는 곳이 세션 I/O입니다. 로그인/권한 체크/CSRF/세션 갱신이 요청마다 Redis를 건드리기 때문에, Redis 자체가 충분히 빠르더라도 애플리케이션 측 Redis 클라이언트 설정이 병목이 되기 쉽습니다.
Spring Boot 3 기본 Redis 클라이언트인 Lettuce는 Netty 기반 비동기 클라이언트이며, 설정을 잘못 잡으면 다음과 같은 증상이 나타납니다.
- 응답 지연이 특정 구간부터 급격히 증가(꼬리 지연, tail latency)
RedisCommandTimeoutException또는 타임아웃 증가- CPU는 여유인데도 처리량이 안 올라감(대기/큐 적체)
- Tomcat 스레드가 Redis 응답을 기다리며 정체되어
503로 번지는 현상
Tomcat 레벨에서 503이 함께 보인다면 아래 글의 진단 포인트도 같이 보는 것이 좋습니다.
이 글은 “Redis는 살아있는데 세션 때문에 느리다” 상황을 전제로, Lettuce 튜닝으로 병목을 푸는 방법을 실전 순서대로 정리합니다.
1) 병목이 Lettuce인지 확인하는 빠른 체크
애플리케이션에서 보이는 전형적 징후
- 세션 관련 키 접근이 요청당 여러 번 발생
GET은 빠른데SETEX나HGETALL같은 명령에서 지연이 커짐- 동시 요청 수가 증가하면 Redis RTT가 선형이 아니라 급격히 증가
지표로 확인하기
- Redis 서버:
instantaneous_ops_per_sec,connected_clients,blocked_clients,latency doctor등 - 애플리케이션: Redis 커맨드 타이머(마이크로미터), Netty 이벤트루프 스레드 사용률, 커넥션 풀 대기 시간
Spring Boot 3에서 Micrometer 관측을 켜면 Redis 관련 메트릭을 쉽게 볼 수 있습니다.
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: my-api
그리고 metrics에서 redis로 검색해 커맨드 레이턴시/타임아웃이 튀는지 확인합니다.
2) Spring Session Redis에서 실제로 어떤 I/O가 발생하나
Spring Session은 요청 처리 중 다음과 같은 작업을 수행할 수 있습니다.
- 세션 조회:
GET또는 해시 조회 - 세션 만료 갱신: TTL 연장(쓰기)
- 세션 속성 변경: 해시 업데이트(쓰기)
- 인덱싱(옵션): principal 기반 인덱스 키 갱신
즉 “읽기 위주”라고 생각해도, TTL 갱신과 인덱스 때문에 쓰기가 섞입니다. 동시성이 커지면 쓰기 지연이 전체를 끌어내리는 패턴이 흔합니다.
3) 가장 흔한 원인 1: 커넥션 풀 미설정(또는 부족)
Lettuce는 기본적으로 단일 커넥션을 공유하며 멀티플렉싱을 지원하지만, 블로킹 방식(RedisTemplate 등)으로 사용하거나, 요청당 동시 Redis 작업이 많아지면 커맨드 큐가 길어져 지연이 커질 수 있습니다.
이때 “커넥션을 늘리면 해결”되는 경우가 많습니다. Spring Boot 3에서는 commons-pool2를 추가하고 Lettuce 풀링을 활성화할 수 있습니다.
의존성 추가
dependencies {
implementation "org.springframework.boot:spring-boot-starter-data-redis"
implementation "org.apache.commons:commons-pool2"
implementation "org.springframework.session:spring-session-data-redis"
}
풀 설정 예시
spring:
data:
redis:
host: redis.example.internal
port: 6379
timeout: 2s
lettuce:
pool:
enabled: true
max-active: 64
max-idle: 32
min-idle: 16
max-wait: 200ms
설정 해석(실무 관점)
max-active: 동시에 빌릴 수 있는 커넥션 상한. 세션 I/O가 요청당 1회가 아니라면 넉넉히 잡는 편이 안전합니다.max-wait: 풀에서 커넥션을 못 구할 때 대기 시간. 여기서 대기가 길어지면 Tomcat 스레드가 막히고, 결국 503/타임아웃으로 이어집니다.min-idle: 피크 시 커넥션 생성 비용을 줄이기 위한 예열.
주의할 점은 커넥션 수를 무작정 늘리면 Redis 서버의 connected_clients가 증가하고 컨텍스트 스위칭/네트워크 부하가 늘 수 있습니다. “풀 대기 시간”이 실제로 발생하는지(풀 고갈)부터 확인하고 늘리는 것이 좋습니다.
DB에서 HikariCP 풀 고갈을 보듯이, Redis도 풀 고갈이 병목이 되는 구조가 동일합니다. 원인 분석 프레임은 아래 글이 참고가 됩니다.
4) 가장 흔한 원인 2: 타임아웃이 짧거나(또는 너무 길거나)
Redis는 빠르지만, 네트워크/GC/이벤트루프 정체가 섞이면 순간적으로 지연이 튈 수 있습니다. 이때 타임아웃이 너무 짧으면 실패가 폭증하고, 너무 길면 Tomcat 스레드가 오래 대기하며 전체 처리량이 무너집니다.
권장 접근은 다음 순서입니다.
- Redis RTT의
p95/p99를 측정 timeout을p99보다 조금 크게(예:p99 * 2) 설정- 실패 시 재시도는 신중히(세션은 재시도가 오히려 폭주를 만들 수 있음)
Spring Boot 설정 예시는 다음과 같습니다.
spring:
data:
redis:
timeout: 2s
추가로 Lettuce 레벨에서 커맨드 타임아웃을 더 세밀하게 잡고 싶다면 LettuceClientConfigurationBuilderCustomizer로 튜닝할 수 있습니다.
import java.time.Duration;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.TimeoutOptions;
@Configuration
public class RedisLettuceTuningConfig {
@Bean
LettuceClientConfigurationBuilderCustomizer lettuceCustomizer() {
return builder -> builder
.commandTimeout(Duration.ofSeconds(2))
.clientOptions(ClientOptions.builder()
.timeoutOptions(TimeoutOptions.enabled())
.build());
}
}
5) 가장 흔한 원인 3: Netty 이벤트루프 정체(스레드 부족)
Lettuce는 Netty 이벤트루프에서 I/O와 콜백 처리를 수행합니다. 이벤트루프가 부족하거나, 애플리케이션에서 이벤트루프 스레드를 오래 점유하는 작업이 섞이면 커맨드 응답 처리가 밀리며 지연이 폭증합니다.
해결 방향
- 이벤트루프 스레드 수를 늘리거나
- 이벤트루프를 다른 작업과 분리(공유 자원 경쟁 제거)
Lettuce는 ClientResources를 통해 이벤트루프를 제어할 수 있습니다.
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
@Configuration
public class LettuceResourcesConfig {
@Bean(destroyMethod = "shutdown")
ClientResources lettuceClientResources() {
return DefaultClientResources.builder()
.ioThreadPoolSize(8)
.computationThreadPoolSize(8)
.shutdownTimeout(Duration.ofSeconds(2))
.build();
}
}
이 설정을 실제로 적용하려면 LettuceConnectionFactory를 커스터마이징하여 위 ClientResources를 사용하도록 구성해야 합니다. 운영에서는 CPU 코어 수, Redis 호출량, 동시 요청 수에 따라 ioThreadPoolSize를 실측 기반으로 조정합니다.
6) 커맨드 큐(Backpressure) 관점에서 보는 병목
Lettuce는 멀티플렉싱으로 “커넥션 1개로도 많은 요청”을 처리할 수 있지만, 이는 커맨드가 큐에 쌓여도 버틸 수 있다는 뜻이 아닙니다.
- 커맨드가 큐에 쌓이면 응답은 늦어지고
- 늦어진 응답 때문에 애플리케이션 스레드는 더 오래 대기하고
- 더 많은 요청이 쌓이며 큐가 더 길어지는 악순환이 생깁니다
이때 풀링을 켜서 커넥션을 늘리면 큐 길이를 분산시켜 tail latency를 줄일 수 있습니다. 다만 이는 “근본 원인(과도한 세션 접근)”을 숨길 수 있으니, 다음 항목도 함께 봐야 합니다.
7) 세션 접근 자체를 줄이는 설정(튜닝의 절반)
Lettuce 튜닝만으로도 개선되지만, 세션 I/O 횟수를 줄이면 효과가 훨씬 큽니다.
7-1) 불필요한 세션 저장을 줄이기
Spring Session은 세션이 변경되면 Redis에 저장합니다. 프레임워크/필터가 세션을 “건드리기만 해도” 변경으로 인식되는 케이스가 있으니, 실제로 어떤 요청이 세션을 수정하는지 먼저 확인합니다.
- 인증 정보 저장 방식 점검
- 요청마다 세션에 lastAccessedTime 갱신이 과도하게 발생하는지 확인
7-2) FlushMode / SaveMode 고려
Spring Session Redis는 저장 시점과 저장 범위를 조절할 수 있습니다. 예를 들어 “요청 끝에 한 번만 저장” 또는 “변경된 속성만 저장” 같은 모드가 병목을 크게 줄입니다.
아래는 대표적인 구성 예시입니다.
import org.springframework.context.annotation.Configuration;
import org.springframework.session.FlushMode;
import org.springframework.session.SaveMode;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@Configuration
@EnableRedisHttpSession(
redisNamespace = "myapp:session",
maxInactiveIntervalInSeconds = 1800,
flushMode = FlushMode.ON_SAVE,
saveMode = SaveMode.ON_SET_ATTRIBUTE
)
public class SessionConfig {
}
FlushMode.ON_SAVE: 요청 처리 중 매번 쓰지 않고 저장 시점에만 반영SaveMode.ON_SET_ATTRIBUTE: 실제로setAttribute로 변경된 항목만 저장
서비스 특성에 따라 보안/일관성 요구가 다르므로, 적용 전후로 로그인 유지/동시 로그인/세션 무효화 시나리오를 반드시 회귀 테스트해야 합니다.
8) Redis 서버 측도 같이 봐야 하는 포인트
애플리케이션 Lettuce를 튜닝했는데도 병목이 남는다면, Redis 서버에서 다음을 확인합니다.
maxmemory-policy로 인한 eviction이 빈번한지- AOF/RDB fsync 정책 때문에 순간 지연이 튀는지
- 네트워크(특히 cross-AZ)로 RTT가 기본적으로 높은지
slowlog에 세션 관련 명령이 남는지
세션 스토어는 “안정적인 저지연”이 핵심이라, Redis를 다른 워크로드(대용량 캐시, 배치성 키 스캔)와 섞어 쓰면 흔히 망가집니다. 가능하면 세션 전용 Redis(또는 최소한 별도 DB/클러스터)를 권장합니다.
9) 운영에서 자주 쓰는 권장 조합(출발점)
환경에 따라 다르지만, Spring Boot 3에서 Redis 세션 병목을 줄이는 출발점으로 다음 조합을 많이 사용합니다.
- Lettuce 풀링 활성화:
max-active를 트래픽에 맞게(예: 32~128) max-wait는 짧게(예: 100~300ms) 잡아 “대기 폭증”을 빨리 드러내기- Redis 타임아웃은
p99기반으로(예: 1~3s) - Spring Session은
FlushMode.ON_SAVE,SaveMode.ON_SET_ATTRIBUTE우선 검토 - 메트릭으로
pool wait,command latency,timeout count를 상시 관측
동시에, 애플리케이션 레벨에서 과도한 동시성을 제한하는 것도 중요합니다. 트래픽 급증으로 하류(세션/Redis)가 무너질 때는 Rate Limiter로 “폭주를 제어”하는 편이 전체 장애를 막는 데 효과적입니다.
10) 체크리스트: 적용 순서대로 정리
- Redis 커맨드 레이턴시/타임아웃 메트릭 확인
- Lettuce 풀링 활성화 및
max-wait로 풀 고갈 여부를 먼저 드러내기 - 타임아웃을 RTT
p99기반으로 재설정(너무 길게 잡지 않기) - 이벤트루프 정체가 의심되면
ClientResources로 스레드 풀 조정 - Spring Session 저장 모드(
FlushMode,SaveMode)로 쓰기 횟수 줄이기 - Redis 서버 측
slowlog, persistence 설정, 네트워크 RTT 점검
마무리
Spring Boot 3에서 Redis 세션 병목은 “Redis가 느려서”가 아니라, 대개 애플리케이션 측에서 커맨드 큐가 길어지거나(멀티플렉싱의 역효과), 풀/타임아웃/이벤트루프 설정이 트래픽 패턴과 맞지 않아 발생합니다.
Lettuce 튜닝은 커넥션 풀과 타임아웃 같은 기본기부터, 이벤트루프 리소스 분리, Spring Session 저장 전략 최적화까지 함께 들어가야 효과가 큽니다. 위 체크리스트 순서대로 적용하면, 대다수의 “세션 때문에 전체가 느려지는” 장애를 재현 가능하게 만들고, 수치 기반으로 안정화할 수 있습니다.