다수의 서버에 애플리케이션을 배포하고 Nginx를 통해 요청을 전달하는 환경에서, 사용자가 한 서버에 로그인하면 다른 서버에서도 재로그인할 필요 없이 세션이 유지되도록 하는 기술이 필요합니다. 본 문서에서는 Spring Session과 Redis를 사용하여 이러한 분산 세션 환경을 구축하는 방법을 설명합니다.
1. Maven 의존성 추가
먼저, 프로젝트의 pom.xml 파일에 다음과 같은 Maven 의존성을 추가합니다.
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>2.7.0</version> <!-- 최신 버전 사용 권장 -->
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
<version>2.7.0</version> <!-- 최신 버전 사용 권장 -->
</dependency>
2. Spring 설정 파일 구성 (redis-config.xml)
Spring 설정 파일에 Redis를 세션 저장소로 사용하도록 다음과 같이 빈(Bean)을 등록합니다. 사용자 정의 Redis 직렬화 방식을 적용할 수 있으며, 세션 만료 시간 및 Redis 네임스페이스를 설정할 수 있습니다. 쿠키 이름과 경로도 필요에 맞게 설정합니다.
<!-- Spring Session Configuration -->
<!-- RedisSerializer 설정 (필요에 따라 사용자 정의) -->
<bean id="redisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<bean id="genericRedisSerializer" class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"/>
<!-- Spring Session Redis Configuration -->
<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<!-- 세션 비활성 최대 유지 시간 (초 단위, 예: 8시간) -->
<property name="maxInactiveIntervalInSeconds" value="28800"/>
<!-- Redis에 저장될 세션 키의 네임스페이스 (프로젝트별로 고유하게 설정) -->
<property name="redisNamespace" value="my_app:sessions"/>
<!-- RedisTemplate Bean 참조 (RedisTemplate 빈이 정의되어 있어야 함) -->
<property name="redisTemplate" ref="sessionRedisTemplate"/>
</bean>
<!-- 사용자 정의 RedisTemplate Bean (Jackson 직렬화 사용) -->
<bean id="sessionRedisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnectionFactory"/> <!-- RedisConnectionFactory 빈 참조 -->
<property name="keySerializer" ref="redisSerializer"/>
<property name="valueSerializer" ref="genericRedisSerializer"/>
<property name="hashKeySerializer" ref="redisSerializer"/>
<property name="hashValueSerializer" ref="genericRedisSerializer"/>
</bean>
<!-- Cookie Serializer Configuration -->
<bean id="cookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
<!-- 클라이언트에서 사용될 세션 쿠키의 이름 (고유하게 설정) -->
<property name="cookieName" value="MYAPP_SESSION_ID"/>
<!-- 쿠키의 유효 경로 -->
<property name="cookiePath" value="/"/>
<!-- 쿠키 도메인 설정 (필요시) -->
<!-- <property name="domainName" value=".example.com"/> -->
<!-- 쿠키의 Max-Age 설정 (초 단위, 세션 만료 시간과 일치시키는 것이 좋음) -->
<property name="cookieMaxAge" value="28800"/>
</bean>
<!-- HttpSession Strategy Configuration -->
<bean id="httpSessionStrategy" class="org.springframework.session.web.http.CookieHttpSessionStrategy">
<property name="cookieSerializer" ref="cookieSerializer"/>
</bean>
3. 세션 데이터 관리
로그인 성공 후 세션에 데이터를 저장하고 조회하는 과정은 일반적인 HttpSession 사용 방식과 동일합니다. Spring Session은 내부적으로 이 과정을 Redis에 연결하여 처리합니다. 다음은 세션 데이터를 관리하는 예시 유틸리티 클래스입니다.
import org.springframework.session.ExpiringSession;
import org.springframework.session.SessionRepository;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import java.io.Serializable;
// LoginInfo 클래스는 Serializable을 구현해야 합니다.
class LoginInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String userId;
private String username;
public LoginInfo(String userId, String username) {
this.userId = userId;
this.username = username;
}
// Getters and Setters
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
@Override
public String toString() {
return "LoginInfo{" +
"userId='" + userId + '\'' +
", username='" + username + '\'' +
'}';
}
}
public class SessionUtil {
// Spring Session의 SessionRepository를 주입받습니다.
@Resource(name = "sessionRepository") // Spring Data Redis 설정을 통해 자동으로 빈이 생성됩니다.
private SessionRepository<? extends ExpiringSession> sessionRepository;
private static final String LOGIN_INFO_ATTRIBUTE_NAME = "USER_LOGIN_DETAILS";
/**
* 현재 요청의 HttpSession 객체를 가져옵니다.
* RequestContextHolder를 사용하여 현재 스레드의 요청 컨텍스트에 접근합니다.
*/
private HttpServletRequest getCurrentRequest() {
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
return attr.getRequest();
}
/**
* 현재 세션 ID를 반환합니다.
* @return 현재 세션 ID
*/
public String getCurrentSessionId() {
return getCurrentRequest().getSession().getId();
}
/**
* 세션에서 로그인 정보를 가져옵니다.
* @return LoginInfo 객체 또는 null
*/
public LoginInfo getLoginInfo() {
HttpServletRequest request = getCurrentRequest();
// Spring Session은 HttpSession 인터페이스를 구현하므로 getAttribute를 그대로 사용합니다.
return (LoginInfo) request.getSession().getAttribute(LOGIN_INFO_ATTRIBUTE_NAME);
}
/**
* 세션에 로그인 정보를 설정합니다.
* @param loginInfo 설정할 LoginInfo 객체
* @return 설정된 세션 ID
*/
public String setLoginInfo(LoginInfo loginInfo) {
HttpServletRequest request = getCurrentRequest();
request.getSession().setAttribute(LOGIN_INFO_ATTRIBUTE_NAME, loginInfo);
return request.getSession().getId();
}
/**
* 세션에서 로그인 정보를 제거하여 로그아웃 처리합니다.
*/
public void logout() {
HttpServletRequest request = getCurrentRequest();
request.getSession().removeAttribute(LOGIN_INFO_ATTRIBUTE_NAME);
// 세션을 만료시키려면 invalidate()를 호출할 수 있습니다.
// request.getSession().invalidate();
}
/**
* 특정 세션 ID를 사용하여 로그인 정보를 조회합니다.
* @param sessionId 조회할 세션 ID
* @return LoginInfo 객체 또는 null
*/
public LoginInfo getLoginInfoBySessionId(String sessionId) {
// SessionRepository를 사용하여 직접 세션에 접근합니다.
ExpiringSession session = sessionRepository.getSession(sessionId);
if (session != null) {
return (LoginInfo) session.getAttribute(LOGIN_INFO_ATTRIBUTE_NAME);
}
return null;
}
}
위 SessionUtil 클래스는 현재 요청의 HttpSession을 사용하여 사용자 정보를 저장하고 조회합니다. 또한 SessionRepository를 직접 사용하여 특정 세션 ID로도 사용자 정보를 조회할 수 있는 기능을 제공합니다. LoginInfo 클래스는 직렬화 가능하도록 Serializable 인터페이스를 구현해야 합니다.