Spring Session with Redis

7 분 소요


RECOMMEND POSTS BEFORE THIS

0. 들어가면서

Spring Session with JDBC 포스트에선 데이터베이스와 Spring Session를 통해 다중 인스턴스 환경에서 세션을 공유하는 방법에 대해 다뤘습니다. 이번 포스트에선 레디스(redis)와 Spring Session을 사용해 세션을 공유하는 방법에 대해 정리하였습니다. Embedded Redis Server 포스트에서 사용한 예제 프로젝트를 확장하였으며 다음과 같은 방식으로 세션의 공유 여부를 확인하였습니다.

  • 도커 컴포즈(compose)를 사용해 백엔드 서비스 2개와 레디스 서비스 1개를 실행합니다.
    • backend-1 - exposed port number 8080
    • backend-2 - exposed port number 8081
    • redis-server - exposed port number 6379
  • 사용자 브라우저로 두 백엔드 서비스에 번갈아 접근합니다.
    • localhost:8080/session, localhost:8081/session 주소에 접근합니다.
    • 세션 정보를 식별할 때 사용하는 아이디(id)는 쿠키에 함께 전달됩니다.
    • SameSite인 경우 쿠키를 공유하므로 두 요청은 동일한 세션을 사용하게 됩니다.
    • SameSite 기준에 따라 포트 번호는 상관하지 않습니다.
    • Deep dive into cookie
  • 세션 접근 카운트가 증가하는지 확인합니다.
    • 동일한 세션 정보를 사용한다면 세션 접근 카운트는 이어지면서 증가할 것 입니다.

1. Spring Session 의존성 추가

다음과 같은 의존성들을 추가합니다.

    <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>

2. application-dev.yml

  • 레디스 접속 정보를 다음과 같이 설정합니다.
    • host - 도커 컴포즈 파일의 레디스 컨테이너의 이름
    • password - 레디스 어플리케이션 접속 비밀번호 (임의 지정)
    • port - 레디스 어플리케이션 포트 번호
  • 세션 저장소 타입을 redis로 설정합니다.
spring:
  redis:
    host: redis-server
    password: some-password
    port: 6379
  session:
    store-type: redis

3. SessionFilter 클래스

  • 세션 정보를 조회할 때 파라미터를 false인 경우 세션을 새롭게 생성하지 않고, 존재하는 세션을 반환합니다.
  • 세션 생성 URL 호출 시 해당 요청을 계속 진행합니다.
  • 세션 생성 URL이 아닌 경우 다음과 같이 수행합니다.
    • 세션이 없는 경우 세션을 생성하는 경로로 리다이렉트(redirect)합니다.
    • 세션이 있는 경우 해당 요청을 계속 진행합니다.
package action.in.blog.filter;

import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

public class SessionFilter extends OncePerRequestFilter {

    private final String sessionCreationUri = "/session/creation";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (sessionCreationUri.equals(request.getRequestURI())) {
            filterChain.doFilter(request, response);
            return;
        }
        HttpSession httpSession = request.getSession(false);
        if (httpSession == null) {
            response.sendRedirect(sessionCreationUri);
            return;
        }
        filterChain.doFilter(request, response);
    }
}

4. SessionController 클래스

  • 세션 정보를 조회할 때 파라미터가 없는 경우 세션 존재 여부에 따라 필요한 경우 새로운 세션을 생성 후 반환합니다.
  • /session/creation 경로 접근
    • 기존 세션이 존재하는 경우 존재하는 세션을 획득합니다.
    • 기존 세션이 존재하지 않다면 새로운 세션을 생성 후 획득합니다.
    • accessCount를 키(key)로 세션에 초기 값 0을 저장합니다.
  • /session 경로 접근
    • accessCount를 키로 세션 정보에 저장된 데이터를 찾습니다.
    • 저장된 데이터에 1을 더하여 세션에 다시 저장합니다.
    • 현재 조회한 데이터를 사용해 응답 문자열을 만들어 번환합니다.
package action.in.blog.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@RestController
public class SessionController {

    private final String key = "accessCount";

    @GetMapping("/session/creation")
    public void createSession(HttpServletRequest servletRequest, HttpServletResponse servletResponse) throws IOException {
        HttpSession session = servletRequest.getSession();
        session.setAttribute(key, 0);
        servletResponse.sendRedirect("/session");
    }

    @GetMapping("/session")
    public String getSession(HttpSession session) throws IOException {
        int data = (int) session.getAttribute(key);
        session.setAttribute(key, data + 1);
        return "Current Data in Session - " + data;
    }
}

5. BaseConfig 클래스

5.1. 직렬화 방법 부재 시 문제점

별도로 직렬화(serialize) 방법을 정의해주지 않고, 데이터를 저장하면 다음과 같은 알아보기 힘든 데이터가 저장됩니다.

  • 기본적으로 JdkSerializationRedisSerializer를 사용합니다.
$ docker exec -it redis-server /bin/sh

# redis-cli
127.0.0.1:6379> auth some-password
OK

127.0.0.1:6379> keys *
1) "spring:session:sessions:a6b82253-4e7c-4579-82fb-9733989ba3b1"
2) "spring:session:sessions:expires:a6b82253-4e7c-4579-82fb-9733989ba3b1"
3) "spring:session:expirations:1669046940000"

127.0.0.1:6379> hgetall spring:session:sessions:a6b82253-4e7c-4579-82fb-9733989ba3b1
1) "sessionAttr:accessCount"
2) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\x01"
3) "creationTime"
4) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x84\x9a\xd7\x80\xdd"
5) "maxInactiveInterval"
6) "\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\a\b"
7) "lastAccessedTime"
8) "\xac\xed\x00\x05sr\x00\x0ejava.lang.Long;\x8b\xe4\x90\xcc\x8f#\xdf\x02\x00\x01J\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x01\x84\x9a\xd7\x81;"

5.2. 직렬화 방법 추가

  • 레디스에 저장되는 값들을 알아보기 쉬운 데이터로 직렬화하는 빈(bean)을 생성합니다.
package action.in.blog.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class BaseConfig {

    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

6. 테스트

도커 컴포즈를 사용해 서비스를 실행 후 다음과 같은 내용들을 확인합니다.

  • 브라우저를 통해 세션이 공유되는지 확인합니다.
  • redis-server 컨테이너에 접근 후 redis-cli 커맨드를 통해 저장된 데이터를 확인합니다.
Dockerfile
  • 실행 환경을 dev로 주입 받습니다.
FROM maven:3.8.6-jdk-11 as MAVEN_BUILD

WORKDIR /build

COPY pom.xml .

RUN --mount=type=cache,target=/root/.m2 mvn dependency:go-offline

COPY src ./src

RUN --mount=type=cache,target=/root/.m2 mvn package -Dmaven.test.skip=true

FROM openjdk:11-jdk-slim-buster

WORKDIR /app

ARG JAR_FILE=*.jar

COPY --from=MAVEN_BUILD /build/target/${JAR_FILE} ./app.jar

EXPOSE 8080

ENV RUN_ENV dev

CMD ["java", "-Dspring.profiles.active=${RUN_ENV}", "-jar", "app.jar"]
docker-compose.yml
  • 테스트를 위해 다음과 같은 yml 파일을 실행합니다.
version: "3.9"
services:
  redis:
    image: redis
    command: redis-server --requirepass some-password --port 6379
    container_name: redis-server
    ports:
      - '6379:6379'
  backend-1:
    build: .
    ports:
      - '8080:8080'
    depends_on:
      - redis
    restart: on-failure
  backend-2:
    build: .
    ports:
      - '8081:8080'
    depends_on:
      - redis
    restart: on-failure
실행 로그
$ docker-compose up     

WARN[0000] Found orphan containers ([action-in-blog-backend-1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. 
[+] Running 4/4
 ⠿ Network action-in-blog_default        Created                                                                                                                                                                                                                                           0.0s
 ⠿ Container redis-server                Created                                                                                                                                                                                                                                           0.0s
 ⠿ Container action-in-blog-backend-2-1  Created                                                                                                                                                                                                                                           0.0s
 ⠿ Container action-in-blog-backend-1-1  Created                                                                                                                                                                                                                                           0.0s
Attaching to action-in-blog-backend-1-1, action-in-blog-backend-2-1, redis-server
redis-server                | 1:C 21 Nov 2022 15:49:29.329 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
redis-server                | 1:C 21 Nov 2022 15:49:29.329 # Redis version=7.0.5, bits=64, commit=00000000, modified=0, pid=1, just started
redis-server                | 1:C 21 Nov 2022 15:49:29.329 # Configuration loaded
redis-server                | 1:M 21 Nov 2022 15:49:29.330 * monotonic clock: POSIX clock_gettime
redis-server                | 1:M 21 Nov 2022 15:49:29.330 * Running mode=standalone, port=6379.
redis-server                | 1:M 21 Nov 2022 15:49:29.330 # Server initialized
redis-server                | 1:M 21 Nov 2022 15:49:29.331 * Ready to accept connections
action-in-blog-backend-1-1  | 
action-in-blog-backend-1-1  |   .   ____          _            __ _ _
action-in-blog-backend-1-1  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
action-in-blog-backend-1-1  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
action-in-blog-backend-1-1  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
action-in-blog-backend-1-1  |   '  |____| .__|_| |_|_| |_\__, | / / / /
action-in-blog-backend-1-1  |  =========|_|==============|___/=/_/_/_/
action-in-blog-backend-1-1  |  :: Spring Boot ::                (v2.7.5)
action-in-blog-backend-1-1  | 
action-in-blog-backend-2-1  | 
action-in-blog-backend-2-1  |   .   ____          _            __ _ _
action-in-blog-backend-2-1  |  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
action-in-blog-backend-2-1  | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
action-in-blog-backend-2-1  |  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
action-in-blog-backend-2-1  |   '  |____| .__|_| |_|_| |_\__, | / / / /
action-in-blog-backend-2-1  |  =========|_|==============|___/=/_/_/_/
action-in-blog-backend-2-1  |  :: Spring Boot ::                (v2.7.5)
action-in-blog-backend-2-1  | 
action-in-blog-backend-1-1  | 2022-11-21 15:49:30.669  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : Starting ActionInBlogApplication v0.0.1-SNAPSHOT using Java 11.0.16 on d98a6e0d04fe with PID 1 (/app/app.jar started by root in /app)
action-in-blog-backend-1-1  | 2022-11-21 15:49:30.672  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : The following 1 profile is active: "dev"
action-in-blog-backend-2-1  | 2022-11-21 15:49:30.754  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : Starting ActionInBlogApplication v0.0.1-SNAPSHOT using Java 11.0.16 on 71d5f16faf75 with PID 1 (/app/app.jar started by root in /app)
action-in-blog-backend-2-1  | 2022-11-21 15:49:30.757  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : The following 1 profile is active: "dev"
action-in-blog-backend-1-1  | 2022-11-21 15:49:31.423  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
action-in-blog-backend-1-1  | 2022-11-21 15:49:31.426  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
action-in-blog-backend-1-1  | 2022-11-21 15:49:31.468  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 7 ms. Found 0 Redis repository interfaces.
action-in-blog-backend-2-1  | 2022-11-21 15:49:31.510  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode
action-in-blog-backend-2-1  | 2022-11-21 15:49:31.513  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Redis repositories in DEFAULT mode.
action-in-blog-backend-2-1  | 2022-11-21 15:49:31.562  INFO 1 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 6 ms. Found 0 Redis repository interfaces.
action-in-blog-backend-1-1  | 2022-11-21 15:49:31.991  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
action-in-blog-backend-1-1  | 2022-11-21 15:49:32.004  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
action-in-blog-backend-1-1  | 2022-11-21 15:49:32.005  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.68]
action-in-blog-backend-2-1  | 2022-11-21 15:49:32.096  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
action-in-blog-backend-1-1  | 2022-11-21 15:49:32.099  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
action-in-blog-backend-1-1  | 2022-11-21 15:49:32.099  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1366 ms
action-in-blog-backend-2-1  | 2022-11-21 15:49:32.111  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
action-in-blog-backend-2-1  | 2022-11-21 15:49:32.112  INFO 1 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.68]
action-in-blog-backend-2-1  | 2022-11-21 15:49:32.198  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
action-in-blog-backend-2-1  | 2022-11-21 15:49:32.198  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1356 ms
action-in-blog-backend-1-1  | 2022-11-21 15:49:33.640  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
action-in-blog-backend-1-1  | 2022-11-21 15:49:33.744  INFO 1 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
action-in-blog-backend-1-1  | 2022-11-21 15:49:33.753  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : Started ActionInBlogApplication in 3.686 seconds (JVM running for 4.125)
action-in-blog-backend-2-1  | 2022-11-21 15:49:33.755  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
action-in-blog-backend-2-1  | 2022-11-21 15:49:33.843  INFO 1 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
action-in-blog-backend-2-1  | 2022-11-21 15:49:33.852  INFO 1 --- [           main] action.in.blog.ActionInBlogApplication   : Started ActionInBlogApplication in 3.761 seconds (JVM running for 4.187)
브라우저 테스트

redis-cli 데이터 확인
$ docker exec -it redis-server /bin/sh

# redis-cli

127.0.0.1:6379> auth some-password
OK

127.0.0.1:6379> keys *
1) "spring:session:expirations:1669048200000"
2) "spring:session:sessions:008dbd11-cf0d-4333-b63c-a649a3acd605"
3) "spring:session:sessions:expires:008dbd11-cf0d-4333-b63c-a649a3acd605"

127.0.0.1:6379> hgetall spring:session:sessions:008dbd11-cf0d-4333-b63c-a649a3acd605
1) "lastAccessedTime"
2) "1669046346018"
3) "maxInactiveInterval"
4) "1800"
5) "creationTime"
6) "1669046318008"
7) "sessionAttr:accessCount"
8) "23"

TEST CODE REPOSITORY

REFERENCE

댓글남기기