Spring Session with Redis
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"
댓글남기기