Spring Boot 환경에서 Redis Session Cluster 구성

Jieun Yang·2024년 9월 18일

1. 분산된 서버 환경에서의 세션 관리 문제점(세션 불일치 등)

로드밸런서(L4)를 이용한 Scale-out된 분산 환경에서는 세션 불일치의 문제가 발생함

  • 서버 A, B가 존재할 때 사용자는 로그인 요청을 통해 세션이 서버 A에 저장됨
  • 이후 사용자가 저장된 SessionID로 요청 시, 서버 A에 트래픽이 몰려 서버 B에 요청함
  • 서버 B의 인메모리 저장소에는 사용자의 Session 정보가 없기 때문에 인가 처리가 되지 않음
  • Request-Response Headers의 JSESSIONID 의 value가 맞지 않아 세션 불일치 문제 발생

1.1 Sticky Session

  • 로드 밸런서의 설정을 통해 사용자의 요청이 처음 세션을 저장한 서버로만 가도록 설정하는 방법
  • 특정 세션의 요청(첫 요청 이후의 모든 요청)을 처음 세션을 처리한 서버로만 전송하는 것

그러나 특정 서버로만 요청을 하기 때문에 문제점이 존재한다.

  • 특정 서버에 요청이 몰릴 수 있어 서버 과부하가 발생
  • 다른 서버에는 특정 세션이 없어 사용할 수 없다.
  • 특정 서버에 장애 발생 시, 해당 서버의 다른 세션들의 데이터가 소실될 수 있다.

1.2 Session Clustering

  • 여러 WAS 서버가 세션을 공유하여 클라이언트 요청이 어느 서버로 라우팅되더라도 동일한 세션 데이터를 사용할 수 있음
  • 어느 서버에 접속해도 세션 데이터가 일관되게 유지됨
  • 특정 서버가 다운되더라도 다른 서버에서 세션 데이터를 사용할 수 있음
  • 그러나 세션 데이터가 생성될 때마다 모든 서버에 세션 정보를 추가해야 함
  • 서버 메모리에 오버헤드 발생



2. Spring Session

  • Spring Session을 이용해 세션 데이터를 외부 저장소에 저장
  • 애플리케이션과 세션 저장소 간 추상화 계층을 제공하여, 세션을 다양한 저장소에 저장할 수 있게 해줌
  • 또한 외부 저장소(Redis, JDBC, Hazelcast..) 변경 시 애플리케이션 코드 변경 없이 쉽게 변경이 가능



3. Redis Session Clustering

  • 모든 서버의 세션 데이터가 Redis 저장소에 저장되어 각 서버 인스턴스가 동일한 세션 데이터 공유 (세션 저장소를 외부로 분리)
  • 하나의 세션이 생성될 때 모든 서버에 세션을 업데이트해야 하는 Session Clustering과 달리, Redis 인스턴스에 한 번만 저장이 되어 메모리 부하를 줄일 수 있음


3.1 세션 저장소 RDB vs Redis 비교

  • 세션은 만료시간이 존재하여 영속성이 필요하지 않음
  • 사용자가 로그인 후 요청할 때마다 확인이 필요하기 때문에 I/O가 많이 발생
  • RDB 사용 시 디스크 I/O로 인해 서버 메모리 부하가 커져 성능이 저하될 가능성이 있음
  • Redis는 In-Memory 기반으로 디스크 I/O 작업을 하지 않기 때문에 메모리 낭비를 줄일 수 있음



4. 의존성 설정


4.1 의존성 설정 코드 - pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

4.2 spring-boot-starter-data-redis

  • Spring Boot에서 제공하는 Data Redis
  • spring-boot-starter-data-redis는 Spring Data Redis 프레임워크 및 Redis Client 라이브러리인 Lettuce 를 함께 의존성에 추가함

4.2.1 Redis Client - Jedis VS Lettuce

  • Jedis는 멀티스레드 환경에서 안전하지 않다.
  • Lettuce는 Netty 기반 환경으로 비동기를 지원한다.
  • Lettuce는 거의 모든 측면에서 성능이 Jedis보다 우수하다.

4.3 Spring Data Redis

  • Spring에서 Redis 사용 시 불필요한 중복 코드 제거 및 Redis와 상호작용하는 추상화된 인터페이스를 제공하여 Spring에서 Redis를 간편하게 사용할 수 있는 프레임워크



5. Redis Session Clustering 구성

5.1 application.yml

spring:
  session:
    store-type: redis 
    redis:
      namespace: spring:session # 기본값
  data:
    redis:
      host: localhost 
      password: 1234
      port: 6379

server.servlet.session.cookie.name

  • Spring Sesion 사용 시 세션 쿠키의 기본 name : SESSION
  • 일반적으로 세션 쿠키 이름이 JSESSIONID 이므로 변경함

spring.session.store-type:redis

  • Session 저장소를 Redis로 구성
  • spring-session-data-redis의 @EnableRedisHttpSession 어노테이션을 활성화
  • @EnableRedisHttpSession 활성화함으로써 springSessionRepositoryFilter 인터페이스를 구현한 Bean 생성
  • SpringSessionRepositoryFilter의 SessionRepository의 구현체로 RedisSessionRepository가 지정됨

spring.session.redis.namespace

  • 세션 저장 시 prefix 지정
  • 기본값 : spring:session

spring.data.redis.host : 서버 주소

spring.data.redis.password : 서버 주소의 pw

spring.data.redis.port : 서버 port 번호


5.2 SessionConfig.java

Spring Session을 활성화하고 Redis를 세션 저장소로 설정하는 클래스

@Configuration
public class SessionConfig {

  @Bean
  public CookieSerializer cookieSerializer() {
            DefaultCookieSerializer serializer = new DefaultCookieSerializer();
            serializer.setCookieName("JSESSIONID"); // 세션 쿠키 이름 
            serializer.setCookiePath("/"); // 세션 쿠키 경로 
            serializer.setDomainNamePattern("^.+?(\\w+\\.[a-z]+)$"); // 서브 도메인에 세션 쿠키 공유
            serializer.setUseBase64Encoding(true); // Base64 인코딩 사용 유무 (false 시 세션ID를 단순 문자열로 처리)
            return serializer;
  }
}

5.3 AuthInterceptor.java

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Base64;

@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {

    private static final String SESSION_KEY = "SESSION";
    private static final String REDIS_SESSION_KEY = ":sessions:";

    @Value("${spring.session.redis.namespace}")
    private String namespace;

    private final StringRedisTemplate redisTemplate;

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        final String sessionIdByCookie = getSessionIdByCookie(request);
        if (sessionIdByCookie != null) {
            final String decodedSessionId = new String(Base64.getDecoder().decode(sessionIdByCookie)); // Base64 인코딩 false 시 코드 제거 

            // Redis에서 세션 키 확인. 존재하지 않으면 예외 발생 
            if (!redisTemplate.hasKey(namespace + REDIS_SESSION_KEY + decodedSessionId)) {
                log.warn("Session Cookie exists, but Session in Storage does not exist");
                throw new AuthException.FailAuthenticationMemberException();
            }
        }
        return true;
    }

    private String getSessionIdByCookie(HttpServletRequest request) {
        if (request.getCookies() != null) {
            for (var cookie : request.getCookies()) {
                if (cookie.getName().equals("JSESSIONID")) {
                    return cookie.getValue();
                }
            }
        }
        return null;
    }
}

5.4 Redis 세션 데이터 형식

  • 세션 데이터 키: namespace:sessions:{sessionId}
  • 세션 인덱스 키: namespace:sessions:expires:{sessionId}



6. reference

0개의 댓글