로그인 상태를 어떻게 유지할까? 이 단순해 보이는 질문이 생각보다 꽤 깊은 곳까지 이어진다. 웹의 근본적인 특성인 HTTP의 무상태성(Stateless) 에서 시작해서, 서버가 여러 대로 늘어날 때 발생하는 문제, 그리고 그것을 Redis로 해결하는 방법까지 알아보자.
HTTP 통신의 핵심 특징 중 하나는 무상태(Stateless) 라는 점이다. 각 요청은 독립적으로 이루어지며, 서버는 이전 요청과 현재 요청이 같은 사용자로부터 왔다는 사실을 스스로 기억하지 못한다.
다르게 말하면, 클라이언트(브라우저) 측에서 매 요청마다 "나는 누구입니다"라고 서버에 알려줘야 한다는 뜻이다.
이 문제를 해결하는 방식이 바로 쿠키(Cookie) 와 세션(Session) 의 개념이다.
[로그인 요청]
브라우저 → 서버: "ID/PW 주겠습니다"
↓
서버 → 브라우저: "이 JSESSIONID를 쿠키로 저장해두세요"
[이후 모든 요청]
브라우저 → 서버: (쿠키에 JSESSIONID 포함해서 전송)
서버: "이 JSESSIONID로 세션 조회 → 로그인 상태 확인"

서버가 응답 시 브라우저에게 특정 값(쿠키)을 저장하도록 지시하고, 이후 요청에서 그 값을 확인해 사용자를 식별하는 흐름 — 이것이 세션 의 본질이다.
Spring Boot에는 내장 톰캣(Tomcat)이 포함되어 있으며, 이 톰캣이 처음 접근한 브라우저에게 JSESSIONID라는 쿠키를 발급한다.
개발자는 컨트롤러 메서드 파라미터에 HttpSession을 선언하면, Spring이 자동으로 현재 요청에 연결된 세션 객체를 주입해 준다.
@RestController
public class SessionController {
// GET /set?q=someValue 요청 시, 세션에 값을 저장
@GetMapping("/set")
public String set(
@RequestParam("q") String q, // URL 파라미터 "q"를 받음
HttpSession session // 현재 요청의 세션 객체를 주입받음
) {
session.setAttribute("q", q); // 세션에 키-값 쌍으로 저장
return "Saved: " + q;
}
// GET /get 요청 시, 세션에 저장된 값을 조회
@GetMapping("/get")
public String get(
HttpSession session // 세션 객체 주입
) {
return String.valueOf(session.getAttribute("q")); // 키 "q"로 세션값 조회
}
}

브라우저에서 크롬 개발자 도구 → Application → Storage → Cookies를 확인하면 JSESSIONID 값이 보인다. 이 값을 삭제한 후 /get을 호출하면, 톰캣은 연결된 세션이 없다고 판단하여 데이터를 찾지 못한다.
사용자가 늘어나면 자연스럽게 서버를 여러 대 운용하게 된다.
Scale-Out 환경에서는 로드 밸런서(Load Balancer, 트래픽을 여러 서버에 고르게 분산시켜 주는 장치)가 요청을 분산한다.
문제는 여기서 발생한다.

A 서버에서 세션을 생성한 사용자가, 다음 요청에서 B 서버로 연결되면 어떻게 될까?
B 서버는 A 서버의 세션 정보를 알 수 없다. 결과적으로 /get을 호출해도 데이터를 찾지 못하는 오류가 발생한다. 세션은 각 서버의 메모리에 독립적으로 저장되기 때문이다.
IntelliJ에서 두 개의 Run Configuration을 만들어 포트를 8080/8081로 분리해 직접 실험해 볼 수 있다. 8080에서 /set으로 저장 후 8081에서 /get을 호출하면, Spring Boot 기본 에러 페이지를 만나게 된다.
한 가지 방법은 특정 사용자의 요청을 항상 같은 서버로 고정하는 것이다.
Sticky Session : 로드 밸런서가 최초 요청이 전달된 서버를 기억하고, 이후 같은 사용자 요청을 동일 서버로 라우팅하는 방식
언뜻 보면 깔끔한 해결책이지만, 두 가지 치명적인 단점이 있다.
1. 트래픽 불균형
특정 서버로 고정된 사용자들만 활발히 활동한다면, 그 서버에만 부하가 집중된다. 로드 밸런서를 쓰는 이유 자체가 무색해진다.
2. 서버 장애 시 세션 소멸
특정 서버가 다운되면, 그 서버의 메모리에 저장된 세션 데이터도 함께 사라진다. 사용자는 갑자기 로그인이 풀려버리는 경험을 하게 된다.
더 근본적인 해결책은 세션 저장소를 서버 외부로 분리 하는 것이다.
Session Clustering : 여러 서버가 하나의 외부 저장소를 공유하고, 세션 데이터를 그곳에 저장하는 방식. 어느 서버로 요청이 들어오든 동일한 세션 정보를 조회할 수 있다.
[Load Balancer] / \ 서버 A 서버 B \ / [외부 세션 저장소] (Redis)
이 구조의 장점은 명확하다.
물론 단점도 있다.
이 지연을 최소화하기 위해 디스크 기반 DB 대신, Redis 와 같은 인메모리 데이터베이스가 선택된다.
| Sticky Session | Session Clustering | |
|---|---|---|
| 장점 | 구현이 단순, 외부 통신 없음 | 균등한 요청 분산, 서버 추가/제거 자유로움 |
| 단점 | 트래픽 불균형, 서버 다운 시 세션 소멸 | 외부 저장소 관리 필요, 통신 지연 발생 |
Spring Boot와 Redis를 함께 사용한다면, build.gradle에 의존성 하나만 추가해도 Session Clustering이 동작한다.
dependencies {
// Redis 연결을 위한 기본 의존성
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// Spring Session Data Redis: Tomcat의 세션 대신 Redis에 세션 저장
implementation 'org.springframework.session:spring-session-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
// ... (기타 의존성)
}
Spring Session 은 Spring의 하위 프로젝트로, 세션 관리 로직을 서블릿 컨테이너(Tomcat 등)로부터 분리해주는 추상화 계층이다.
spring-session-data-redis를 추가하면, 내장 Tomcat의 세션 기능을 사용하지 않고 Redis에 세션을 저장한다.
이 의존성을 추가하면 주목할 변화가 생긴다.
JSESSIONID 쿠키 대신 SESSION 이라는 이름의 쿠키가 사용된다.Redis CLI로 확인하면 세션이 아래와 같은 키 구조로 저장된 것을 볼 수 있다.
spring:session:sessions:{세션ID}
spring:session:expirations:{만료시각}
기본 설정에서는 Java 기본 직렬화(Java Serialization)가 적용된다. 이는 Redis에 바이너리 형태로 저장되어 내용을 직접 읽기 어렵다.
가독성 있는 JSON 형태로 저장하고 싶다면 springSessionDefaultRedisSerializer 빈(Bean, Spring이 관리하는 객체)을 등록하면 된다.
@Configuration
public class RedisConfig {
// 이 빈의 이름이 "springSessionDefaultRedisSerializer"여야 Spring Session이 자동으로 인식한다
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return RedisSerializer.json(); // Jackson 기반 JSON 직렬화 사용
}
}
단, 주의할 점이 있다. Spring Security의 SecurityContext와 같은 일부 객체는 기본 생성자(no-args constructor)가 없어 JSON 역직렬화 시 오류가 발생할 수 있다.
이 문제는 직접
RedisSerializer를 커스텀하거나, 직렬화가 가능한 형태의SecurityContext를 구성하는 방식으로 해결해야 한다. 지금 단계에서는 이런 문제가 존재한다는 점만 인지해두면 충분하다.
Spring Boot 3.x부터 application.properties/application.yml 설정 키가 일부 변경되었다.
# Spring Boot 3.x 기준 (spring.data.redis.* 로 변경)
spring:
data:
redis:
host: localhost
port: 6379
session:
store-type: redis # 세션 저장소를 Redis로 명시
timeout: 1800 # 세션 타임아웃 (초 단위, 30분)
redis:
namespace: myapp:sessions # Redis 키 네임스페이스 (선택 사항)
Spring Boot 2.x에서는
spring.redis.*를 사용했지만, 3.x부터는spring.data.redis.*로 변경되었다. 레거시(기존에 구축된) 프로젝트에서 2.x를 사용하는 경우라면 기존 키를 유지하면 된다.
실제 서비스 환경에서 Redis를 세션 저장소로 사용할 때 몇 가지 더 고려해야 한다.
HttpSession.invalidate()를 호출해야 Redis에서도 세션이 정리된다.많은 자료에서 JWT(JSON Web Token)가 "현대적인 방식"처럼 소개되는 경향이 있다. 그러나 2025~2026년 현재, 이 논쟁은 여전히 진행 중이며 단순히 "JWT = 최신, 세션 = 구식"이라는 도식은 지나친 단순화다.
| 구분 | Session 기반 | JWT 기반 |
|---|---|---|
| 즉각 로그아웃 | 가능 (서버 측 세션 삭제) | 어려움 (토큰 만료 전까지 유효) |
| 서버 부하 | 매 요청마다 저장소 조회 | 서명 검증만으로 처리 가능 |
| 확장성 | Redis 공유로 해결 가능 | 서버 간 상태 공유 불필요 |
| 보안 제어 | 서버 측에서 강력한 제어 | 토큰 탈취 시 만료 전까지 악용 가능 |
| 적합한 상황 | 웹 애플리케이션, 보안이 중요한 서비스 | 마이크로서비스, 모바일 앱, API 서버 |
실제 개발 환경에서는 둘 중 하나만 쓰는 것이 아니라, 아키텍처 특성에 따라 적절히 선택하거나 혼용하기도 한다.
특히, 기존에 레거시로 구성된 환경의 경우 세션 기반 인증이 여전히 주류인 경우가 많다. 신규 서비스라 해도 "로그인 직후 모든 기기에서 즉시 로그아웃" 같은 강력한 세션 제어가 필요하다면, 세션 기반이 오히려 적합한 선택일 수 있다.
이번 챕터에서 다룬 흐름을 요약하면 다음과 같다.
JSESSIONID 쿠키를 발급하고 HttpSession으로 세션 관리spring-session-data-redis 의존성 하나로 간단하게 Redis 기반 Session Clustering 적용 가능SecurityContext 등 일부 객체의 역직렬화 문제에 주의