
이번 글에서는 Spring Session을 활용한 세션 관리 과정에서 마주한 문제와 해결 과정을 공유합니다. 특히, 다중 인스턴스 환경에서 세션 중복 처리 문제를 해결한 경험을 다룹니다.
최근 제품의 요구사항은 다음과 같았습니다.
즉, Redis를 선택한 핵심 이유는 성능이 아니라, 다중 인스턴스 환경에서 일관된 세션 관리를 위함이었습니다.
먼저, Spring Session과 Redis를 사용하기 위해 Gradle에 다음 의존성을 추가해야 합니다.
dependencies {
implementation 'org.springframework.session:spring-session-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}
초기에는 maximumSessions(1) 설정을 통해 중복 로그인을 막으려고 했습니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomSessionExpiredStrategy sessionExpiredStrategy;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(sessionManagement ->
sessionManagement
.maximumSessions(1)
.expiredSessionStrategy(sessionExpiredStrategy)
)
.build();
}
}
서버가 실행 중일 때는 정상적으로 중복 로그인을 차단했지만, 서버를 재시작하거나 새 인스턴스를 추가하면 중복 로그인이 허용되는 문제가 발생했습니다.
문제의 핵심은 Spring Security의 기본 SessionRegistry가 메모리 기반으로 동작하기 때문에 .maximumSessions(1)은 서버의 메모리 내에 존재하는 세션만 확인합니다.
따라서 서버가 재시작되거나 새로운 인스턴스가 추가되면 Redis에 저장된 세션 정보를 인식하지 못하고 중복 로그인을 허용하는 것입니다.
결국, Redis에 저장된 세션을 동기화해서 관리하려면 Redis 기반의 SessionRegistry 구현이 필요했습니다.
Spring Session을 활용하면 SpringSessionBackedSessionRegistry가 존재하여 Redis 기반의 세션 관리를 간단히 적용할 수 있습니다.
하지만 여기서 중요한 설정이 있습니다.
Spring Boot는 repository-type: indexed 설정이 없으면 FindByIndexNameSessionRepository 빈을 생성하지 않습니다.
이 빈이 생성되지 않으면 결국 SpringSessionBackedSessionRegistry를 사용할 수 없습니다.
Redis는 기본적으로 Key-Value 형태로 데이터를 저장합니다. 하지만 Spring Session이 세션 관리를 할 때는 세션을 식별하는 정보(예: 사용자 이름, 세션 ID 등)를 추가로 저장하고 이를 인덱스화하여 접근할 수 있어야 합니다.
repository-type: indexed 설정을 하지 않은 경우, Redis에 단순히 세션 ID를 키로만 저장합니다.spring:session:sessions:12345678-1234-1234-1234-123456789abc
repository-type: indexed 설정을 적용하면, Redis에 세션을 조회하기 위한 추가적인 인덱스가 생성됩니다.spring:session:sessions:expires:12345678-1234-1234-1234-123456789abc
spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:user@example.com
즉, 단순히 키 하나로 세션 데이터를 접근하는 것이 아니라, 세션과 관련된 여러 키를 사용하여 복합적인 조회를 하기 위해 Redis 내 별도의 인덱스가 필요합니다.
그래서 Spring Boot는 명시적으로 다음 설정이 있어야 Redis에 인덱스를 기반으로 세션 데이터를 관리하도록 내부적으로 FindByIndexNameSessionRepository 빈을 생성합니다.
spring:
session:
redis:
repository-type: indexed
이 설정이 없으면 Redis는 단순 키-값으로만 데이터를 관리하여 세션 관련 인덱스 정보를 생성하지 않기 때문에 FindByIndexNameSessionRepository가 생성되지 않습니다.
아래와 같이 설정을 추가하면 문제는 깔끔하게 해결됩니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomSessionExpiredStrategy sessionExpiredStrategy;
private final FindByIndexNameSessionRepository<? extends Session> sessionRepository;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.sessionManagement(sessionManagement ->
sessionManagement
.maximumSessions(1)
.expiredSessionStrategy(sessionExpiredStrategy)
.sessionRegistry(sessionRegistry())
)
.build();
}
@Bean
public SessionRegistry sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(sessionRepository);
}
}
추가적으로, 아래 설정을 정확히 입력해야 Redis가 인덱스 기반 세션 저장소로 동작합니다.
spring:
data:
redis:
host: localhost
port: 6379
session:
redis:
flush-mode: on-save
namespace: spring:session
repository-type: indexed # 중요! 반드시 추가해야 함
configure-action: none # AWS ElastiCache 대응
AWS의 ElastiCache를 사용할 때 다음 에러가 발생할 수 있습니다.
RedisCommandExecutionException: ERR unknown command 'CONFIG', with args beginning with: 'GET' 'notify-keyspace-events'
이때는 위 설정에서 configure-action: none을 추가하여 해결할 수 있습니다.
이제 무중단 배포 환경에서도 일관된 세션 관리가 가능합니다. Redis 기반의 세션 관리를 사용한다면 반드시 repository-type: indexed 설정을 추가하는 것을 기억하세요!
비슷한 문제를 겪는 분들에게 이 글이 도움이 되길 바랍니다. 😊