Spring Session Redis 관리

Spring Session이 필요한 이유

단일 서버에서는 HttpSession이 메모리에 저장되어 문제가 없지만, 다중 인스턴스 환경에서는 사용자가 다른 인스턴스로 라우팅되면 세션이 사라집니다. Spring Session은 세션 저장소를 외부(Redis, JDBC 등)로 분리하여 이 문제를 해결합니다.

Spring Session Redis 설정

의존성 추가

// build.gradle.kts
dependencies {
    implementation("org.springframework.session:spring-session-data-redis")
    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

application.yml

spring:
  session:
    store-type: redis
    timeout: 30m                    # 세션 만료 시간
    redis:
      namespace: myapp:session      # Redis 키 접두사
      flush-mode: on-save           # 변경 시에만 저장 (immediate는 매 요청)
      save-mode: on-set-attribute   # 속성 변경 시에만 저장
  data:
    redis:
      host: redis.internal
      port: 6379
      password: ${REDIS_PASSWORD}
      timeout: 3s
      lettuce:
        pool:
          max-active: 20
          max-idle: 10
          min-idle: 5

이 설정만으로 HttpSession이 자동으로 Redis에 저장됩니다. 코드 변경 없이 기존 session.setAttribute()가 Redis로 투명하게 전환됩니다.

세션 직렬화 최적화

기본 Java 직렬화는 느리고 크기가 큽니다. JSON 직렬화로 전환하면 성능과 디버깅 편의성이 향상됩니다.

@Configuration
@EnableRedisHttpSession
public class SessionConfig {

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        mapper.activateDefaultTyping(
            mapper.getPolymorphicTypeValidator(),
            ObjectMapper.DefaultTyping.NON_FINAL,
            JsonTypeInfo.As.PROPERTY
        );
        return new GenericJackson2JsonRedisSerializer(mapper);
    }
}
직렬화 방식 크기 속도 가독성
JdkSerializationRedisSerializer 느림
GenericJackson2JsonRedisSerializer 중간 빠름
Kryo/Protobuf 작음 매우 빠름

동시 세션 제어

같은 계정으로 동시 로그인 수를 제한하는 설정입니다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .sessionManagement(session -> session
                .maximumSessions(1)                    // 최대 1개 세션
                .maxSessionsPreventsLogin(false)       // false: 이전 세션 만료 (기본)
                                                       // true: 새 로그인 차단
                .expiredUrl("/login?expired=true")
                .sessionRegistry(sessionRegistry())
            )
            .sessionManagement(session -> session
                .sessionFixation().changeSessionId()   // 세션 고정 공격 방지
                .invalidSessionUrl("/login?invalid=true")
            )
            .build();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    // Spring Session Redis 환경에서는 아래 사용
    @Bean
    public FindByIndexNameSessionRepository<?> sessionRepository(
            RedisIndexedSessionRepository repository) {
        return repository;
    }

    @Bean
    public SpringSessionBackedSessionRegistry<?> springSessionRegistry(
            FindByIndexNameSessionRepository<?> sessionRepository) {
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }
}

특정 사용자의 세션 강제 만료

@Service
@RequiredArgsConstructor
public class SessionManagementService {

    private final FindByIndexNameSessionRepository<?> sessionRepository;

    // 특정 사용자의 모든 세션 조회
    public Map<String, ?> getUserSessions(String username) {
        return sessionRepository
            .findByPrincipalName(username);
    }

    // 특정 사용자의 모든 세션 만료
    public void expireUserSessions(String username) {
        Map<String, ?> sessions =
            sessionRepository.findByPrincipalName(username);

        sessions.keySet().forEach(sessionRepository::deleteById);
    }

    // 비밀번호 변경 시 다른 세션 만료
    public void expireOtherSessions(String username, String currentSessionId) {
        sessionRepository.findByPrincipalName(username)
            .entrySet().stream()
            .filter(e -> !e.getKey().equals(currentSessionId))
            .forEach(e -> sessionRepository.deleteById(e.getKey()));
    }
}

세션 보안 강화

@Configuration
public class SessionSecurityConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSIONID");
        serializer.setDomainName("example.com");
        serializer.setCookiePath("/");
        serializer.setUseHttpOnlyCookie(true);     // JS 접근 차단
        serializer.setUseSecureCookie(true);        // HTTPS만
        serializer.setSameSite("Lax");              // CSRF 방어
        serializer.setCookieMaxAge(1800);           // 30분
        return serializer;
    }
}

Spring Security CORS·CSRF 설정에서 다룬 CSRF 보호와 함께 사용하면 세션 기반 인증의 보안을 완성할 수 있습니다.

설정 기능 공격 방어
HttpOnly JS에서 쿠키 접근 차단 XSS
Secure HTTPS에서만 쿠키 전송 MITM
SameSite=Lax 크로스 사이트 요청 시 쿠키 제한 CSRF
changeSessionId() 로그인 시 세션 ID 변경 Session Fixation

Redis 세션 모니터링

# Redis에서 세션 키 확인
redis-cli KEYS "myapp:session:sessions:*"

# 특정 세션 조회
redis-cli HGETALL "myapp:session:sessions:abc123-def456"

# 세션 수 카운트
redis-cli DBSIZE

# TTL 확인
redis-cli TTL "myapp:session:sessions:abc123-def456"

Spring Data Redis Repository와 연동하면 세션 외 캐시 데이터도 동일한 Redis 인스턴스에서 관리할 수 있습니다.

정리

Spring Session Redis는 다중 인스턴스 환경에서 세션 클러스터링을 투명하게 해결합니다. JSON 직렬화로 성능과 디버깅 편의성을 확보하고, 동시 세션 제어로 보안을 강화하며, CookieSerializer로 세션 쿠키 보안을 완성하세요. 비밀번호 변경 시 다른 세션을 강제 만료하는 패턴은 실무에서 반드시 구현해야 할 보안 요구사항입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux