SpringBoot에서 Redis를 이용한 세션 관리
- -
이번 포스트에서는 Redis를 이용해서 세션을 관리해보자. 내용은 다음의 글을 참조한다.
https://docs.spring.io/spring-session/reference/guides/boot-redis.html
기본 동작
클라이언트
일단 사용자는 login, logout을 처리할 수 있고 login 된 경우라면 buy 처리가 가능하다. 당연히 이 과정에는 세션이 사용된다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<input type="text" id="id" value="hong"><input type="password" id="pass" value="1234">
<button id="login">login</button> <button id="logout">logout</button>
<hr>
<input type="text" id="product">
<button id="buy">buy</button>
<hr>
<div id="response">
</div>
</body>
<script>
const target = document.querySelector("#response")
document.querySelector("#login").addEventListener("click", async () => {
const user = {
id: document.querySelector("#id").value,
pass: document.querySelector("#pass").value
}
const response = await fetch("/login", {
method: "post",
headers: {"content-type": "application/json"},
body: JSON.stringify(user)
});
const text = await response.text()
target.innerHTML = text
})
document.querySelector("#logout").addEventListener("click", async () => {
const response = await fetch("/logout", {
method: "get",
});
const text = await response.text()
target.innerHTML = text
})
document.querySelector("#buy").addEventListener("click", async () => {
const product=document.querySelector("#product").value
const response = await fetch("/buy", {
method: "post",
headers: {"content-type": "application/x-www-form-urlencoded"},
body: "product="+product
});
const text = await response.text()
target.innerHTML = text
})
</script>
</html>
controller
controller에서는 HttpSession을 이용해서 클라이언트의 요청을 처리한다. 이때 Session의 정체를 확인해보자. login 처리 과정에서 세션의 구현체를 확인해보고 있다.
@PostMapping("/login")
public ResponseEntity<String> login(HttpSession session, @RequestBody UserDto dto) {
log.debug("session type: {}", session.getClass().getName());
log.debug("dto: {}", dto);
session.setAttribute("loginuser", dto);
return ResponseEntity.ok("login ok");
}
위 코드를 실행해보면 HttpSession의 타입은 org.apache.catalina.session.StandardSessionFacade 입을 알 수 있다. 즉 HttpSession을 누가 구현해주는냐에 따라서 동작이 달라질 수 있는 것이다.(interface - class 관계이므로)
package com.quietjun.redis.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.quietjun.redis.model.UserDto;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
@RestController
@Slf4j
public class RedisController {
@PostMapping("/login")
public ResponseEntity<String> login(HttpSession session, @RequestBody UserDto dto) {
log.debug("session type: {}", session.getClass().getName());
log.debug("dto: {}", dto);
session.setAttribute("loginuser", dto);
return ResponseEntity.ok("login ok");
}
@PostMapping("/buy")
public ResponseEntity<Object> buy(HttpSession session, @RequestParam String product) {
if (session.getAttribute("loginuser") != null) {
List<String> cart = (List) session.getAttribute("cart");
if (cart == null) {
cart = new ArrayList<>();
session.setAttribute("cart", cart);
}
cart.add(product);
return ResponseEntity.ok(cart);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인해주세요");
}
}
@GetMapping("/logout")
public ResponseEntity<String> logout(HttpSession session) {
// 세션 삭제
session.invalidate();
return ResponseEntity.ok("logout");
}
}
이제 이 Session을 우리가 원하는 Redis에서 관리하는 session으로 바꿔보자.
환경 설정
Redis 준비
먼저 redis를 설치해주자.
https://goodteacher.tistory.com/711
의존성 작성
springboot에서 redis를 이용한 session 관리를 위해서는 다음의 의존성이 필요하다.
<!-- spring에서 redis를 이용해서 session을 관리하기 위한 의존성 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
의존성에 spring-session-data-redis를 추가하면 boot는 내부적으로 @EnableRedisHttpSession을 동작시킨다. 결과로 Filter를 구현한 SpringSessionRepositoryFilter라는 빈을 만들고 이 녀석이 HttpSession 구현을 대체해준다.
추가로 객체의 직렬화/역직렬화를 담당할 RedisSerialize를 사용하기 위해 다음의 의존성도 추가한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.properties
redis를 통해서 세션을 관리하기 위해서는 사실 많은 설정이 필요하지만 boot의 자랑 auto configuration이 있기 때문에 몇가지 설정만 application.properties에 추가하면 간단히 사용할 수 있다.
먼저 사용자 관리를 위해서는 spring.data.redis.xxx 를 설정한다. host와 port는 거의 필수이고 username, password 등은 db 설정에 따라 다르다.
# redis 사용자 관리
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.username=
spring.data.redis.password=
다음으로 필요에 따라서 redis에서 session을 관리하기 위한 설정을 작성할 수 있다.
# redis - session 설정
server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
spring.session.redis.flush-mode=on_save # Sessions flush mode.
spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
너무도 허망하면서 좋게도 이게 끝이다. 정말 부트는 사랑이다.
필요한 빈 설정
@Configuration에 필요한 빈을 추가해보자.
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
접속 자체는 application.properties에 설정된 host, port 등을 이용해서 자동으로 설정되므로 신경 쓸 필요가 없어진다.
이제 HttpSession의 구현체 교체가 끝났으니 다시 한번 테스트 해보자.
Redis 동작 확인
초기 상태
처음에는 당연히 아무것도 없다. (화면은 redisinsight에 대한 캡쳐이다.)
로그인
로그인 시도를 해보자. 일단 서버의 로그를 살펴보면. 초면인 분으로 변경되어있다. SessionRepositoryFilter에 inner class로 작성된 녀석인가보다.
org.springframework.session.web.http.SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
redis에도 세션 정보가 야무지게 박혀있는 것을 볼 수 있다.
구매처리
addtrbute 단위별로 key와 value의 쌍으로 값이 잘 저장된다.
logout
logout을 시도하면 session을 invalidate 시킨다.
이제 여러 개의 spring web application이 여러 서버에서 동작하더라도 session store의 정보를 이용한다면 동일한 세션 정보를 사용할 수 있게 되었다.
앗!!
그런데 테스트 과정에서 요상한 일이 발생했다.
@PostMapping("/buy")\
public ResponseEntity<Object> buy(HttpSession session, @RequestParam String product) {
if (session.getAttribute("loginuser") != null) {
List<String> cart = (List) session.getAttribute("cart");
if (cart == null) {
cart = new ArrayList<>();
session.setAttribute("cart", cart);
}
cart.add(product);
//session.setAttribute("cart", cart); // 이게 왜 필요하지?
return ResponseEntity.ok(cart);
} else {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("로그인해주세요");
}
}
원래는 세션에서 cart 객체를 가져온 후 추가할 product만 추가하면 되는 코드였는데 Redis를 이용하니 마지막 녀석만 바뀌는 현상이 발생했다. cart의 hashcode로 찍어보니 변경이 일어날 때마다 다른 값이 출력되는 것이 새로운 객체를 만들고 반영을 안하는 모양이다.
명시적으로 session에 다시 설정해주면 되긴 하는데 내가 뭘 덜 알고 있는건가ㅜㅜ 아시는 분들 조언 부탁합니다.~
'Spring MVC > 05.기타' 카테고리의 다른 글
[Servlet] Spring Legacy에서 web.xml 파일 제거 (0) | 2024.10.17 |
---|
소중한 공감 감사합니다