Spring MVC/05.기타

SpringBoot에서 Redis를 이용한 세션 관리

  • -

이번 포스트에서는 Redis를 이용해서 세션을 관리해보자. 내용은 다음의 글을 참조한다.

https://docs.spring.io/spring-session/reference/guides/boot-redis.html

 

Spring Session - Spring Boot :: Spring Session

After adding the required dependencies, we can create our Spring Boot configuration. Thanks to first-class auto-configuration support, setting up Spring Session backed by Redis is as simple as adding a single configuration property to your application.prop

docs.spring.io

 

기본 동작

 

클라이언트

일단 사용자는 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

 

[docker] 06. redis 및 redisinsight 설치

이번 포스트에서는 docker desktop을 이용해서 redis와 redisinsight를 설치해보자. redis 및 redis insight 설치 이미지 검색 하도 간만에 쓰다 보니 docker desktop의 화면이 많이 바뀐듯 하지만 사용법은 비슷했

goodteacher.tistory.com

 

의존성 작성

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에도 세션 정보가 야무지게 박혀있는 것을 볼 수 있다.

hong님 반가워요!!

 

구매처리

addtrbute 단위별로 key와 value의 쌍으로 값이 잘 저장된다.

notebook을 구매함!

 

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
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.