HttpSession

Http는 stateless한 특징을 지니기 때문에 각각의 요청이 독립적으로 이루어지며, 서버는 요청의 순서를 저장하지 않는다. 이렇게 된다면 서버는 동일한 클라이언트의 요청이라도 구분하지 못하게 되며 문제가 발생할 수 있다.

이를 해결하고자 서버는 클라이언트의 session ID를 유지하고, 클라이언트는 session ID를 쿠키에 담아 저장하며, 요청에 같이 session ID를 담아 보낸다.

즉, 이전에 요청을 보낸 클라이언트를 기억하는 상태를 유지하는 것을 세션이라고 한다.

💡 그런데 만약, 사용자가 늘어서 하나의 서버로는 사용자의 요청을 감당하기 어려워지면 어떡할까? 여러 개의 서버를 동작시켜, 각 서버들에게 요청을 분산(Load Balancing)하여 부하를 줄일 수 있다. 이런 방식의 확장을 Scale-Out이라 한다.

Session의 동작 과정을 확인하기 위해 SessionController를 작성

@RestController
public class SessionController {
// http://localhost:8080/set?q=password
@GetMapping("/set")
public String set(
@RequestParam("q") String q,
HttpSession session
) {
session.setAttribute("q", q);
return "Saved : " + q;
}
// http://localhost:8080/get
@GetMapping("/get")
public String get(
HttpSession session
) {
return String.valueOf(session.getAttribute("q"));
}
}

 

8080과 8081 서버를 두개 띄운다.(Load Balancing)

 

구성 편집 -> 복사 -> VM 옵션 추가 -> -Dserver.port=8081 추가

 

 

localhost:8080/set?q=password를 입력하고, 동일한 포트에서 /get을 호출한 결과

 

 

 

localhost:8081/get을 수행한 결과

세션이 달라졌기 때문에 8081 서버에서는 null을 반환한다.

 

로드 밸런싱으로 인해 세션이 유지가 되지 않는다.

 

이를 해결하는 한 가지 방법은 Sticky Session을 사용하는 것이다.

이는, 특정 클라이언트가 보낸 요청을 하나의 서버로 고정하는 방법이다.

 

요청을 분산하는 로드밸런서를 통해 요청을 보낸 사용자를 기록하고, 해당 사용자가 다시 요청을 할 경우 최초로 요청이 전달된 서버로 요청을 전달하는 방식이다.

 

이 또한 문제가 발생할 수 있다. 특정 서버로 보내진 사용자만 활발하게 활동한다면, 요청이 균등하게 분산되지 않게 된다.

 

9명의 사용자가 동일한 요청을 보내서 3대의 서버(A, B, C)로 요청을 분산한다고 가정해보자.

서버는 이들을 위해 세션을 발급한다. (A 서버에 3명, B 서버에 3명, C 서버에 3명)

이중 A, C 서버로 요청이 연결된 사용자들은 서비스에 관심이 떨어져 금방 떠난 반면, B 서버의 사용자(3명)들은 활발하게 서비스를 사용한다고 가정하면?

 

결과적으로는 다른 널널한 서버(A와 C는 0명 수용)들에 비해 B 서버가 더 바쁘게 동작하게 되며, 이는 과부하로 이어질 수 있다.

여기에 만약 B 서버가 다운된다면, 해당 서버에서 관리하는 세션도 다 사라지고, 사용자 입장에서 갑자기 본인 데이터가 사라지게 될 수 있다.

 

Session Clustering

그래서 Session Clustering 등장했다.

 

이는 여러 서버들(A, B, C)이 하나의 저장소를 공유하고,

해당 저장소에 세션에 대한 정보를 저장함으로서,

요청이 어느 서버로 전달이 되든 세션 정보가 유지될 수 있도록 하는 방법이다.

 

아무 사용자나 로드밸런서로 요청을 보낸다고 가정해 보자.

그러면 서버는 세션을 생성하되, 이 세션에 연결된 정보를 내부에 저장하는게 아닌 외부의 저장소에 저장(보통 Redis를 사용한다고 함)을 한다.

 

이제 해당 사용자가 다시 요청을 할 경우, 다른 서버로 요청이 보내진다고 하더라도, 세션 정보 자체는 외부 저장소에 저장되어 있기 때문에 가져와서 확인이 가능하다.

 

여기에 더해 외부 저장소를 사용하기 때문에, 사용자 요청을 처리하는 서버의 추가가 자유롭다. (확장성)

또한, 중간에 서버를 제거하더라도 세션이 사라지지 않으므로 유연하게 대처가 가능하다.

 

다만, 이렇게 될 경우 서버 외부에 세션을 저장하는 것이므로, 관리 포인트가 늘어나게 되며, 통신 과정에서 어쩔 수 없는 지연이 발생한다. (유연한 대처 및 확장성과 맞바꾼 Trade off)

 

그래서 지연이 적은 Redis와 같은 인메모리 데이터베이스가 많이 사용된다.

  Sticky Session Session Clustering
장점 애플리케이션 입장에선 구현이 쉽고, 외부와 통신할 필요가 없음 Load Balancer에서 균등하게 요청 분산 가능, 서버의 추가/제거가 비교적 자유로움
단점 요청이 분산되지 않아 과부하 가능성 존재, 서버가 다운되면 그 서버가 관리하는 세션도 삭제 외부 저장소라는 관리 포인트 추가, 외부와 통신 지연이 발생

 

Spring boot와 Redis로 Session Clustering 구현

build.gradle 추가

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'

 

해당 의존성을 추가하면 내장 Tomcat의 세션 기능을 사용하지 않고 Redis에 별도로 세션을 저장하게 된다.

 

다시 8080, 8081 포트에 Spring boot application을 실행해보고, 요청해보자.

 

8080 포트에서 /set 요청

 

SESSION 이라는 놈이 새로 생겼다.

 

JSESSIONID라는 쿠키값은, 서버 내부의 Tomcat이 처음 접근한 브라우저에게 발급하는 쿠키이다.

 

8080 포트에서 /get 요청

 

8081 포트에서 /get 요청

 

아니나 다를까, 8080 에서의 SESSION 그대로 8081에도 유지되고 있음을 확인할 수 있다.

 

그렇다면 Redis를 확인해보자.

 

 

Redis를 확인해보면, 세션이 저장된 모습을 볼 수 있다.

value가 깨지는 이유는, 자바 기반으로 직렬화 라이브러리가 사용되고 있기 때문이다. (바이트 코드가 들어감)

조원준입니다