HttpSession 기반 세션 저장소를 Redis로 전환하는 작업을 진행했습니다.
HttpSession은 서버가 사용자의 로그인 상태 및 인증 정보를 메모리에 저장하여 관리하는 방식입니다.
사용자가 로그인하면 서버는 세션 객체를 생성하고, 이 객체를 통해 이후 요청에서도 사용자의 인증 상태를 유지할 수 있습니다.
Spring Boot에서는 기본적으로 HttpSession을 지원하므로, 별도의 추가 설정 없이 바로 사용할 수 있다는 장점이 있습니다.
하지만 HttpSession 방식은 단일 서버 환경에서는 간편하고 효율적이지만, 멀티 서버 환경에서는 다음과 같은 단점이 존재합니다.
📌 HttpSession의 한계
HttpSession의 한계를 이해하기 위해 단일 서버 환경과 멀티 서버 환경에서의 세션 관리 흐름을 비교해보겠습니다.

단일 서버 환경에서는 클라이언트의 모든 요청이 항상 동일한 서버로 전달됩니다. 서버는 메모리에 저장된 세션 정보를 이용하여 로그인 상태를 유지합니다.
항상 동일한 서버로 요청하기 세션 관리에 문제가 발생하지 않습니다
멀티 서버 환경은 여러 서버에서 동시에 클라이언트 요청을 처리합니다. 이때 로드 밸런서 등을 이용하여 요청을 분산합니다. 이로 인해 요청이 항상 같은 서버로 전달되지 않습니다.

예를 들어 로그인은 서버 A로 전달되어 세션을 생성했지만, 이후 요청은 서버 B로 전달 될 수 있습니다.
이런 경우 사용자 정보는 서버 A의 메모리에만 저장되기 때문에, 서버 B로 요청이 전달되면 세션 정보를 찾을 수 없어 인증 오류가 발생하게 됩니다.
멀티 서버 환경에서 발생하는 세션 공유 문제를 해결하기 위해,
HttpSession 대신 세션 정보를 외부 저장소인 Redis에 저장하는 방식을 이용합니다.

클라이언트가 보낸 요청을 서버는 Redis에서 세션 정보를 조회합니다. 모든 인스턴스가 공통된 Redis에서 조회하므로, 서버가 달라져도 일관된 세션 관리가 가능합니다.
📌 Redis 세션 관리의 장점
멀티 서버 환경에서 Redis를 통한 세션 저장 구조는 다음과 같이 동작합니다.
멀티 서버 환경에서 Redis를 통한 세션 저장 구조를 간략히 정리하면 다음과 같습니다.

클라이언트가 로그인 요청을 보내면, 서버는 인증에 성공한 뒤 Redis에 세션 정보를 저장합니다.
서버는 클라이언트에게 세션 ID를 담은 쿠키를 전달합니다.
서버는 쿠키에서 세션 ID를 추출하여 Redis에서 해당 세션 정보를 조회하고, 세션 데이터가 존재하면 사용자가 로그인된 상태로 요청을 정상 처리합니다.
이제 Redis를 세션 저장소로 사용하는 방법을 살펴보겠습니다.
// Redis 기반 Spring Session 기능을 활성화하기 위한 의존성 추가
implementation 'org.springframework.session:spring-session-data-redis'
Redis에 세션을 저장하려면, Redis와 연동할 수 있도록 도와주는 모듈이 필요합니다.
📌 Spring Session이란?
HttpSession 구현을 추상화한 것으로, Redis와 같은 외부 저장소를 통해 세션을 관리할 수 있도록 지원합니다.
이를 통해 서버 메모리에 의존하지 않고, 세션 정보를 안정적으로 관리할 수 있습니다.
spring:
session:
store-type: redis # 세션 저장소 지정
session:
redis:
namespace: spring:session # 키 설정
timeout: 60m # 세션 유효 시간
Redis를 세션 저장소로 사용하려면, application.yml 파일에서 위와 같이 설정을 추가해야 합니다.
store-type을 redis로 지정하면, Spring Boot는 기본 HttpSession 저장소 대신 Redis를 사용하여 세션을 관리하게 됩니다.
Redis 세션 저장소 사용을 위한 설정 값은 다음과 같습니다.
| 항목 | 설명 |
|---|---|
| store-type | 세션 저장소를 Redis로 지정 |
| namespace | Redis 키 앞에 붙는 네임스페이스 |
| timeout | 세션 유효 시간 설정 |
세션 저장 구조를 실제로 Redis에 연동하여 검증하기 위해 단위 테스트 대신 통합 테스트 코드를 작성했습니다.
Redis 세션 동작을 검증하기 위해 테스트용 사용자를 생성하고, 테스트 종료 후 Redis를 초기화합니다.
@BeforeEach
void setUp() throws Exception {
// 테스트 계정 생성
User user = User.create(
"test@example.com",
passwordEncoder.encode("Test123!!"),
UserRole.USER,
null,
"테스터",
LocalDate.of(2000, 12, 12),
Gender.MALE
);
userRepository.save(user);
// 로그인 API 호출 후 세션 생성
MvcResult result = mockMvc.perform(post("/api/v2/auth/signin")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"email\":\"test@example.com\",\"password\":\"Test123!!\"}"))
.andExpect(status().isOk())
.andReturn();
cookies = result.getResponse().getCookies();
}
@AfterEach
void tearDown() {
// 테스트 종료 후 Redis 데이터 초기화
RedisConnection connection = redisConnectionFactory.getConnection();
connection.flushAll();
}
로그인 후 Redis에 세션 정보가 정상적으로 저장되는지 확인합니다.
@Test
void Redis에_세션이_저장() {
RedisConnection connection = redisConnectionFactory.getConnection();
Set<byte[]> keys = connection.keys("spring:session:sessions:*".getBytes());
assertThat(keys).isNotEmpty();
}
요청이 들어올 때마다 Redis에 저장된 세션 TTL이 자동으로 갱신되는지 확인합니다.
@Test
void TTL이_요청_시마다_자동_갱신_체크() throws Exception {
RedisConnection connection = redisConnectionFactory.getConnection();
String sessionId = extractSessionIdFromCookie(cookies);
Thread.sleep(5000); // TTL 줄어들게 대기
Long ttlBefore = connection.ttl(("spring:session:sessions:" + sessionId).getBytes());
System.out.println("TTL Before = " + ttlBefore);
mockMvc.perform(get("/api/v2/users").cookie(cookies))
.andExpect(status().isOk());
Long ttlAfter = connection.ttl(("spring:session:sessions:" + sessionId).getBytes());
System.out.println("TTL After = " + ttlAfter);
assertThat(ttlAfter).isGreaterThan(ttlBefore);
}
세션이 만료된 경우, 요청 시 401 Unauthorized 응답을 받는지 확인합니다.
@Test
void 세션_만료_시_재로그인() throws Exception {
String sessionId = extractSessionIdFromCookie(cookies);
RedisConnection connection = redisConnectionFactory.getConnection();
connection.del(("spring:session:sessions:" + sessionId).getBytes());
mockMvc.perform(get("/api/v2/auth/users").cookie(cookies))
.andExpect(status().isUnauthorized());
}
로그아웃 API를 호출하면 Redis에 저장된 세션이 정상적으로 삭제되는지 검증합니다.
@Test
void 로그아웃_호출시_Redis_세션_삭제() throws Exception {
mockMvc.perform(post("/api/v2/auth/signout").cookie(cookies))
.andExpect(status().isOk());
String sessionId = extractSessionIdFromCookie(cookies);
RedisConnection connection = redisConnectionFactory.getConnection();
Boolean exists = connection.exists(("spring:session:sessions:" + sessionId).getBytes());
assertThat(exists).isFalse();
}
Redis 세션 저장을 위해 Jackson 기반 JSON 직렬화를 적용했습니다.
Redis 세션 데이터를 JSON 형식으로 저장하고 복원할 수 있도록 Serializer를 등록합니다.
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
@Bean
public GenericJackson2JsonRedisSerializer springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
역직렬화 지원 설정
@JsonCreator
private AuthUser(@JsonProperty("id") Long id, @JsonProperty("userRole")UserRole userRole) {
this.id = id;
his.userRole = userRole;
}
세션에 저장할 객체가 정상적으로 역직렬화될 수 있도록,
생성자에 @JsonCreator와 @JsonProperty 어노테이션을 이용합니다.