[Java] 61. Spring boot에서 Redis 데이터베이스를 이용한 세션 클러스터링 설정하는 방법


Study/Java  2022. 3. 1. 18:19

안녕하세요. 명월입니다.


이 글은 Spring boot에서 Redis 데이터베이스를 이용한 세션 클러스터링 설정하는 방법에 대한 글입니다.


이전 글에서 Apache와 Spring boot의 Tomcat을 연결하여 로드벨런싱하는 방법에 대해서 설명했습니다.

링크 - [Java] 60. Spring boot에서 Apache 연결과 로드벨런싱을 설정하는 방법


로드벨런싱은 일단 웹 환경에서 많은 트래픽(접속)이 발생하면 웹 서버도 브라우져(클라이언트)와 소켓 통신을 하는 커넥션이 있는 리소스이기 때문에 동시에 발생하면 그 접속과 응답이 느려질 수 밖에 없습니다. 또 웹 서버 환경에 요청하는 처리가 복잡하면 복잡해 질수록 더더욱 느려집니다.

그래서 접속 캐싱은 Apache에서 관리하고, 동적 웹 처리(Html 파싱 처리)는 어플리케이션 서버(WAS:Tomcat)에서 관리하는 것으로 나누고, 또 그 부하가 많아 질 경우 Tomcat을 여러 개로 나누어서 로드벨런싱으로 관리하는 것으로 대용량 트래픽을 처리합니다.


일단 이전 글에서 간단하게 설명했습니다만 Apache에서 같은 Session이라면 같은 Tomcat을 요청하게 끔 설정을 합니다만, 상황에 따라서 1번 Tomcat에서 요청 응답하던 커넥션이 2번 Tomcat으로 넘어가는 경우도 발생할 수 있습니다.

그 상황이란 것이 갑자기 1번 Tomcat이 서버 다운이 발생하던가 갑자기 한쪽으로 대용량 트래픽이 발생하면 Apache로 이동할 수 있습니다. 그 외에도 여러 가지 상황이 있습니다만, 대표적인 것은 이 두 가지이지 않을까 싶습니다.


여기서 문제가 발생하는 것이 세션입니다.

세션이라는 것은 간단하게 설명하겠습니다.

브라우저에서 각 사이트에 접속할 때마다 로컬에서 보관하고 있는 데이터 쿠키라는 것이 있습니다.

위 헤더 정보는 제가 브라우저에서 제 블로그를 접속했을 때의 접속 정보입니다. 여기서는 쿠키 값이 알고리즘으로 암호화되어 있습니다.

암호화가 되어 있어도 이 쿠키라는 값을 브라우저에서 보여지게 됩니다. 이것을 왜 브라우저에서 보관하게 되냐고 하면 브라우저의 역사 내용까지 나오기 때문에 설명하기 어렵습니다만, 간단하게 설명하면 웹 프로토콜 사양은 소켓 비동기 형태, 즉 소켓으로 요청할 때 접속하고 응답 받으면 접속을 끊는 형태입니다.

즉, 웹 서핑을 할 때, 웹 서버와 계속 연결하는 것이 아니고 페이지 이동할 때만 서버에 요청해서 데이터를 가져오는 형태입니다. 그런데 웹 서버에 접속하는 사람이 나 혼자가 아니고 여러 명이 동시에 접속 한다고 하면 누구의 요청인지 알 수가 없습니다.

그래서 요청할 때마다 데이터를 보내 구분을 하기 위해서 쿠키라는 게 있는 것인데.. 이 쿠키가 문제가 보안이 매우 좋지 않습니다.


위 이미지에서도 브라우저 개발 모드로 가면 값이 다 보입니다. 그래서 그 값들을 클라이언트(브라우저 유저)에게 보이지 않게 사용하려고 무작위의 유니크 문자열를 생성해서 쿠키 키로 설정하고 서버의 메모리 혹은 파일로 데이터를 보관하는 형태를 세션이라고 합니다.

세션이라는 것은 Tomcat 서버는 메모리에 데이터를 저장합니다.


그래서 로드밸런싱의 상황에서 유저가 1번 Tomcat으로 접속해서 세션 정보, 로그인 정보를 가지고 있는 상황에서 2번 Tomcat으로 넘어가는 상황이 발생하면 어떻게 될까요?

그렇습니다. 2번 Tomcat에서는 세션을 가지고 있지 않기 때문에 로그인이 풀려 버립니다..


그래서 이 세션 정보를 1번 Tomcat과 2번 Tomcat, 여러 대의 경우는 모든 Tomcat의 세션을 공유해야 합니다. 그것을 세션 클러스터링이라고 합니다.

세션 클러스터링의 방법은 여러가지가 있습닌다. 공유 파일 서버를 만들어서 파일로 세션을 관리할 수도 있고 데이터베이스도 있고 여러가지 방법이 있습니다만, 로드밸런싱의 단계까지 왔으면 이미 대용량 트래픽이라는 조건까지 도달한 상황일 것입니다.

즉, 공유 세션에 값을 추가, 수정, 취득의 요청, 응답이 빨라야 하고 시간대별로 그 처리 기준이 정확한 동기화가 되어야 합니다.(즉, 데이터베이스의 자체 처리 속도로 약 0.1초 전에 요청한 처리(수정)가 이루어지지 않고 다른 서버의 취득 요청이 0.2초전의 데이터를 취득해 버리는 형태)


이러한 많은 조건을 충족해주는 데이터베이스는 Redis입니다.

Redis 데이터베이스가 만능은 아닙니다만, 개인적으로 세션 클러스터링에서는 가장 빠르고 정확하다고 생각됩니다. 개인적인 생각이니 다른 의견이 물론 있을 수도 있습니다.


Redis 데이터베이스를 설치하는 방법과 사용 방법에 대해서는 다른 글에서 소개한 적이 있습니다.

링크 - [CentOS] Redis 데이터베이스 설치와 명령어 사용법


이제 Spring boot에서 Redis를 사용하기 위해서는 먼저 Spring boot 위자드에서 Redis 라이브러리를 추가하던가 pom.xml를 추가하여야 합니다.

참고로 이게 이클립스 버그인지는 잘 모르겠으나, 위자드로 하면 기존에 선택한 것들을 전부 다시 선택해야 합니다. 이전에 선택하던 것을 선택안하고 Finish를 누르면 기존 라이브러리가 전부 빠지는 현상이 발생합니다.

pom.xml에 라이브러리가 추가되는 것을 확인할 수 있습니다.


또는 위자드가 아닌 직접 pom.xml에 추가하는 방법도 있습니다.

링크 - https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis

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

그리고 우리는 Spring boot에서 단지 Redis를 사용하는 것이 아니고 세션 클러스터링으로 사용할 것입니다. 그래서 세션 클러스러링 라이브러리도 추가합니다.

링크 - https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis

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

maven 라이브러리 추가는 끝났습니다.

이제 실제 프로젝트에서 session을 Redis 데이터베이스로 사용할 수 있겠끔 설정을 하겠습니다.

Spring boot의 main 함수가 있는 클래스에 @EnableRedisHttpSession 어노테이션을 추가하는 것으로 세션을 Redis 데이터베이스에서 사용할 수 있습니다.

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

// Spring boot의 base 패키지
@SpringBootApplication(scanBasePackages = "com.example.demo.*")
// Redis 세션 사용
@EnableRedisHttpSession
// 클래스
public class SpringBootTestApplication {
  // 메인 함수
  public static void main(String[] args) {
    // 실행
    SpringApplication.run(SpringBootTestApplication.class, args);
  }
}

소스 설정이 완료되었으면 application.properties에 Redis 데이터베이스 정보를 설정합니다.

# redis 데이터베이스가 있는 서버 ip
spring.redis.host=localhost
# redis 데이터베이스가 있는 서버 port
spring.redis.port=6379

저는 localhost에 설치되어 있는 것이 아니고 다른 서버에 있는 관계로 ip 주소를 넣었습니다.

그리고 기존 프로젝트에는 그냥 화면에 hello world 표시 밖에 없기 때문에 세션을 넣는 코드를 작성해 보도록 하겠습니다.

package com.example.demo.Controller;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

// 컨트럴러 어트리뷰트
@Controller
// Controller 클래스
public class HomeController {
  // application.properties에서 설정 값을 받아온다.
  @Value("${tomcat.ajp.port}")
  private int port;

  // 매핑 주소
  @RequestMapping(value = { "/", "/index.html" })
  public String index(Model model, HttpSession session) {
    // 데이터를 템플릿에 전달한다.
    model.addAttribute("data", session.getAttribute("session"));
    // 템플릿 파일명
    return "Home/index";
  }
  // 매핑 주소
  @RequestMapping(value = { "/setSessionData.json" })
  // String 데이터를 리턴한다.
  @ResponseBody
  public String setSessionData(@RequestParam("data") String data, Model model, HttpSession session) {
    // 세션에 값을 등록
    session.setAttribute("session", "(" + port + ")" + data);
    // 결과 json 값
    return "{\"result\":\"OK\"}";
  }
}

메인 화면에서는 세션 값이 있으면 session의 키로 값을 취득해서 화면에 출력합니다.

그리고 setSessionData.json의 주소로 비동기 형태의 data값을 받습니다. 그리고 session의 키로 데이터를 세션에 저장합니다.

위 사양에 맞추어서 html 파일도 수정합니다.

<!DOCTYPE html>
<html>
<head>
<title>Insert title here</title>
</head>
<body>
  Session result :
  <span th:text="${data}">message</span>
  <br />
  <br />
  <input type="text" id="sessionText">
  <button id="updateBtn">Update Session Data</button>
  <script>
  document.addEventListener('DOMContentLoaded',() => {
    document.getElementById("updateBtn").addEventListener("click", ()=>
    {
      // ajax를 하기 위한 XmlHttpRequest 객체
      let xhttp = new XMLHttpRequest();
      // XmlHttpRequest의 요청
      xhttp.onreadystatechange = (e)=>{
        // XMLHttpRequest를 이벤트 파라미터에서 취득
        let req = e.target;
        // 통신 상태가 완료가 되면.
        if(req.readyState === XMLHttpRequest.DONE) {
          // Http response 응답코드가 200(정상)이면
          if(req.status === 200) {
            // json 타입이므로 object 형식으로 변환
            console.log(JSON.parse(req.responseText));
          }
        }
      }
      // http 요청 타입과 주소, 동기식 여부
      xhttp.open("POST", "setSessionData.json", false);
      // form 형식
      xhttp.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
      // http 요청
      xhttp.send("data=" + document.getElementById("sessionText").value);
    });
  });
  </script>
</body>
</html>

최초 span에는 session에서 취득해 온 값을 출력하고 그 다음 줄에는 TextBox와 Button로 setSessionData.json 비동기 주소로 세션을 등록합니다.

그런 후에 페이지 재요청하면 화면에 세션 값이 출력되는 형태로 만들어 집니다.

여기에서 텍스트에 값을 넣고 버튼을 누릅니다.

콘솔에 ok가 된 값이 나오는 것을 확인할 수 있습니다. 다시 페이지를 재요청합니다.

그럼 화면에 (9091)hello world의 결과가 나온 것을 확인할 수 있습니다.

9091인 이유는 AJP 예제에서 설정한 값입니다.

실제 Redis 데이터베이스를 확인해 보니 Session이 등록되어 있는 것을 확인할 수 있습니다.

여기까지 Spring boot에서 Redis 데이터베이스를 이용한 세션 클러스터링 설정된 것을 확인할 수 있습니다.

그럼 로드벨런싱 환경에서도 제대로 사용되는 지 확인해 보겠습니다.

링크 - [Java] 60. Spring boot에서 Apache 연결과 로드벨런싱을 설정하는 방법


위 예제에서 설정한 것과 같이 Apache를 실행하고 Spring boot 프로젝트를 두 곳으로 복사하여 실행합니다.

실행하여 텍스트에 데이터를 넣고 버튼을 누른 다음에 세션을 확인해 보니 위 예제는 9092로 두번째 Tomcat에 연결되어 있는 것을 확인할 수 있습니다.

그럼 여기서 제가 두번째 Tomcat을 실행 중지 해보겠습니다.

종료되었습니다.

2번 서버가 확실히 서버 다운된 것을 확인할 수 있습니다.

그럼 다시 웹 페이지를 재요청해 보겠습니다.

AJP 포트가 9092 서버는 확실이 다운 되었는데 AJP 포트가 9091인 1번 서버는 그대로 세션을 유지한 채로 값을 가져오는 것을 확인 할 수 있습니다.

서버가 하나 셧 다운되더라도 웹 서버가 그대로 운영되는 것도 확인이 되네요.


여기까지 Spring boot에서 Redis 데이터베이스를 이용한 세션 클러스터링 설정하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.