brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 Nov 11. 2018

Spring Session Design Pattern

Eddy의 스프링 디자인 패턴 - Spring Session(1)

Session

Session, HttpSession, JSESSIONID


HTTP는 상태가 유지되지 않는 Stateless한 비접속형 프로토콜이다. 그래서 서버는 클라이언트의 보통 이전 상태를 기억하기 위해서 세션이라는 개념을 사용한다. 서버는 고유한 세션 ID(JSESSIONID)를 웹 브라우저에 쿠키로 전달하고, 클라이언트는 서버에 요청시 해당 쿠키(JSESSIONID)를 함께 전달한다. 서버는 전달받은 세션ID를 서버에 이미 저장되어있는 정보와 비교해서 클라이언트의 상태를 지속적으로 유지하게 된다. 스프링 컨트롤러단에서 HttpSession 매개변수를 전달하면, HttpSession 인터페이스를 구현한 StandardSession 클래스 객체를 가져올 수 있다. 아래와 같이, HttpSession 인터페이스의 getId() 메서드를 호출하면 JSESSIONID 쿠키의 정보를 확인할 수 있다. 물론, 사용자의 클라이언트 브라우저의 쿠키 정보에서도 동일한 값을 확인할 수 있다. 


HttpSession 인터페이스에 정의된 주요 메서드는 아래와 같다. 

setAttribute(String name, Object value) 

getAttribute(String name)

removeAttribute(String name)

getAttributeNames()

setMaxInactiveInterval(int second)

getId()


자세한 정보는 톰캣 공식 레퍼런스를 참고하자. 

https://tomcat.apache.org/tomcat-9.0-doc/servletapi/javax/servlet/http/HttpSession.html



JSESSIONID

생략 - 나중에 시간 있을 때 내용 추가!


@SessionAttributes


생략 - 나중에 시간 되면 내용 추가



Spring Boot Properties


스프링부트 환경에서 세션 설정은 아래와 같이 가능하다. 자세한 설명은 생략한다. 


server.servlet.session.cookie.comment= 

server.servlet.session.cookie.domain= 

server.servlet.session.cookie.http-only=

server.servlet.session.cookie.max-age= 

server.servlet.session.cookie.name= 

server.servlet.session.cookie.path= 

server.servlet.session.cookie.secure= 

server.servlet.session.persistent=false 

server.servlet.session.store-dir= 

server.servlet.session.timeout= 

server.servlet.session.tracking-modes= 



Spring Session Redis

스프링 세션은 사용자의 세션 정보를 관리하는 API 를 제공하는, 스프링 프로젝트이다. 


Spring Session


스프링 세션 설명. 추가

Spring Session Core

Spring Session Data Redis

Spring Session JDBC

Spring Session Hazelcast

Spring Session MongoDB


Spring Session Data Redis with 스프링 부트 Dependencies


Spring Session Data Redis 는 2018년11월4일 현재 2.1.1.RELEASE 버전이 최신 버전이다. 

Spring Session Data Redis

스프링부트 1.5.X 버전에서는 스프링 세션 1.3.X 버전이 디펜던시 된다. 필자가 테스트로 1.5.15 버전으로 프로젝트를 생성해보겠다. 


 buildscript {

    ext {

        springBootVersion = '1.5.15.RELEASE'

    }


생략...

dependencies {

    compile('org.springframework.boot:spring-boot-starter-web')

    compile('org.springframework.session:spring-session-data-redis')


스프링부트 1.5.15 버전에서는 스프링 세션 1.3.3 버전이 디펜던시 추가된다.

 

그럼 Spring-Session-Data-Redis 를 Maven Repository 에서 확인해보자. Spring-Session-Data-Redis 1.3.3 버전에서는 jedis 클라이언트 라이브러리를 컴파일 디펜던시로 추가하는 것을 확인할 수 있다. 그래서 별도로 jedis 또는 lettuce driver 를 추가할 필요는 없다. 

https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis/1.3.3.RELEASE


하지만, Spring-Session-Data-Redis 2.X 버전 이후로는 jedis 또는 lettuce 등 Redis 연동하는 클라이언트 Driver 라이브러리를 땡겨오지 않는다. Maven Repository 를 확인하면 Redis Client Driver 가 없는 것을 확인할 수 있다. 

https://mvnrepository.com/artifact/org.springframework.session/spring-session-data-redis/2.1.1.RELEASE

그래서 우리는, 스프링부트 2.X 환경에서 Spring-Session-Data-Redis 를 사용하게 된다면, 기존 방식처럼 디펜던시를 추가하면 아래와 같은 오류가 발생할 것이다. 

스프링부트 2.X 버전부터 Redis Client Driver (Jedis or Lettuce) 를 가져오지 않기 때문에 발생하는 오류이다. 해결책은 간단하다. Redis Client Driver 를 명시적으로 가져오도록 설정하면 된다. 


Jedis 를 사용하겠다면 아래와 같이 설정하면 된다. 


dependencies {  

    // ...  

    compile 'redis.clients:jedis:2.9.0'  


Lettuce 를 사용하고 싶다면 아래와 같이 설정하면 된다. 


dependencies {  

    // ...

    compile 'io.lettuce:lettuce-core:5.0.5.RELEASE' 


근데, 부트2.X 버전에서의 일부 샘플 코드를 보면 아래와 같이 driver 를 선언하지 않고, spring-boot-starter-data-redis 를 디펜던시 추가한 샘플 코드도 종종 볼수 있다. 


dependencies {  

    // ...  

    compile 'org.springframework.boot:spring-boot-starter-data-redis'  


spring-boot-starter-data-redis 의 pom.xml을 확인해보면, lettuce 를 추가하는 것을 확인할 수 있다.  

spring-data-redis 와 lettuce-core 가 추가된다. 사실, 세션만 사용한다면 spring-data-redis 모듈은 딱히 필요 없다. 

https://github.com/spring-projects/spring-boot/blob/v2.1.0.RELEASE/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis/pom.xml


정리를 하면 스프링부트 2.X 버전 이후부터는 아래와 같이 2가지 방법으로 디펜던시를 추가하면 된다.


방법 1 - Driver 를 직접 추가하는 방법


dependencies {

    implementation('org.springframework.boot:spring-boot-starter-web')

    implementation('org.springframework.session:spring-session-data-redis')

    implementation('io.lettuce:lettuce-core:5.0.5.RELEASE')

    

}


방법 2 - spring-boot-starter-data-redis 를 추가하는 방법


dependencies {

    implementation('org.springframework.boot:spring-boot-starter-web')

    implementation('org.springframework.session:spring-session-data-redis')

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


방법2의 경우에는 spring-boot-starter-data-redis 에 의해서, spring-data-redis 모듈도 같이 추가가 되는데, spring-data-redis 가 필요 없는 경우라면 방법1로 작성하는 방법이 좋을 것 같다. 


해당 글에서는 스프링 부트 2.1.0.RELEASE 버전으로 공부한다.



Spring Session Data Redis with 스프링 부트 Properties


필자는 스프링부트 환경에서 테스트 중이다. 즉, RedisFactory 를 직접 작성 할 필요가 없다. 스프링 부트가 알아서 만들어준다. 대신 레디스 기본 정보는 Property 에 정의를 해줘야 한다. 


//스프링 세션 컨피그레이션

spring.session.store-type=redis

//스프링 레디스 컨피그레이션

spring.redis.host=###.###.###.###

spring.redis.password=password

spring.redis.port=6379


참고로 스프링 세션 컨피그레이션으로 spring.session.store-type=redis 를 정의하는 것은 @EnableRedisHttpSession 어노테이션을 사용하는 것과 같은 역할을 한다. 



Spring Session Data Redis with 스프링 부트 Controller


테스트를 위해서 간단한 컨트롤러를 작성해보자. 


@RestController

public class HomeController {

    @GetMapping("/")

    public String findUid(HttpSession session) {

        return session.getId();

    }

}


컨트롤러를 호출하면, Redis 에 세션 정보가 저장이 된다. 

생성된 세션의 id 는 885342dc-9ae0-4284-afb7-77e27835b54d 이다. 이 상황에서 서버를 재부팅하면 어떻게 될까? 재부팅 했음에도 클라이언트 요청이 세션 정보를 동일하게 응답해준다. 세션 정보를 Redis 에 저장하고 있기 때문에 서버의 재부팅이랑 상관없이 동일한 결과를 제공하는 것이다. 

스프링 세션을 사용하지 않는 상황이라고 가정하자. 서버를 재부팅하면 어떻게 될까? 세션 정보는 초기화 될 것이다. 물론, 클라이언트에 JSESSIONID 가 있기는 하지만, 서버에 저장된 세션 정보가 없기 때문에 세션은 초기화되고, JSESSIONID 역시 신규로 발급이 될 것이다. 

자 그럼, 클라이언트 쿠키를 확인해보자. JSESSIONID 가 동일하게 존재한다고 생각하였으나, 필자의 예상은 보기좋게 틀렸다. 역시, 초보의 삽질이 시작되었구나...

Spring Session Redis 환경에서의 클라이언트 쿠키 정보


JSESSIONID는 없다. SESSION 이라는 쿠키값이 존재할 뿐이다. 


JSESSIONID vs SESSION


JSESSIONID 는 왜 없나? 스프링 세션을 사용한다면 톰캣서버의 기본 JSESSIONID 쿠키는, 스프링에서 임의로 디폴트로 지정해주는 "SESSION" 이라는 이름의 쿠키로 replace 된다. 세션 쿠키의 이름을 변경하고 싶다면 Property 에서 설정할 수 있다. 

server.servlet.session.cookie.name=EDDYSESSION

 



스프링 세션 클러스터링


스프링 세션 클러스터링에 대해서 얘기를 하겠다. 


세션 클러스터링을 사용하지 않는 웹서비스 


한 대로 운영하는 웹서비스가 있다고 가정하자. 일반적인 클라이언트-서버 아키텍처이고, 클라이언트는 서버에서 발급한 세션 쿠키 정보를 Request 시에 같이 전달하고, 서버는 클라이언트가 전달한 세션쿠키 정보를 이미 저장하고 있는 정보와 비교해서 클라이언트의 상태를 유지한다. 

근데 웹서비스의 트래픽이 증가해서, 서버 1대로 운영하기 어려운 상황이 발생을 하였고 서버를 1대 더 증설하였다. 아래 그림을 보자. 

클라이언트(인터넷) 에서는 로드밸런스를 해주는 L4에 요청을 하고, L4는 트래픽을 분산하여 두 대의 웹서버에 분산 요청을 한다. 각 서버에서 저장하고 있는 세션 정보는 서로 공유를 하고 있지 않다. 즉, 한 서버에만 요청할 때는 발생하지 않던, 분산 환경에서의 세션이 일치하지 않는 이슈가 발생을 하게 된다. 


스프링 세션으로 클러스터링 환경 구축


해결책은 간단하다. 세션을 공유할 수 있는 저장소를 구축하면 된다. 대표적으로 많이 사용하는 솔루션은 Redis 이다. 스프링 세션은 세션 공유 저장소를 심플하게 구축할 수 있는 기능을 제공한다. 물론, 스프링 세션을 사용하지 않아도 충분히 세션 클러스터링을 구축할 있지만, 스프링 환경에서 세션 공유가 필요하다면, 스프링 세션을 검토해보면 좋을 듯 하다. 



세션 저장소를 따로 분리해야 하는 또다른 이유는?


이 글을 읽는 개발자 중에는 의문점을 제시할 수도 있다. 별도의 저장소를 구축하지 않더라도, WEB 서버 간에 세션 정보를 공유하면 되지 않을까? 즉, 세션 정보만 공유할 수 있다면 괜히 더 복잡하게 세션 저장소를 따로 구축할 필요가 있는지에 대한 의문이다. 


첫번 째 이유

그럴 일은 거의 없겠지만 모든 서버가 죽는다고 가정해보자. 서버 간에 세션 정보를 공유할수 있는 기능이 존재하더라도, 모든 서버가 죽어버리면 공유할 수 있는 세션 정보는 없게 된다. 


두번 째 이유

두번쨰 이유에 대해서는 간단한 웹서비스 환경이라면 따로 고민하지 않아도 될것같다. 필자가 경험 했던 대용량 트래픽환경에서는 웹서버가 확장되는 속도와, 저장소가 확장되는 속도는 항상 비례하지 않았다.


웹서버가 발전하고 확장되는 속도와 저장소가 확장되는 속도는 비례하지 않는다.

즉, 웹서버만 확장을 해야하는 경우가 발생할 수도 있고, 세션 저장소만 확장이 필요하는 경우도 있다. 웹서버와 세션저장소가 강하게 의존성을 맺고 있다면 효율적인 서비스 확장이 불가능하다. 분산 환경에서 안정적인 서비스 운영을 하기 위해서, 세션 저장소 레이어는 반드시 분리해야 한다. 


정리


간단하게 세션 클러스터링에 대해서 검토하였다. 스프링은 세션 클러스터링을 심플하게 지원하는 스프링세션 이라는 모듈을 제공하고, 스프링 세션과 Redis 저장소를 연동할 수 있었다. 


Spring Session MongoDB


지금까지는 레디스를 세션 저장소로 사용하였다. 이번에는 MongoDB 에 저장해보겠다. 방법은 역시 간단하다. 스프링부트 2.1.0.RELEASE 버전에서 테스트한다.


디펜던시


gradle 에 디펜던시를 추가한다.


buildscript {

    ext {

        springBootVersion = '2.1.0.RELEASE'

    }

생략..

dependencies {

    implementation('org.springframework.boot:spring-boot-starter-web')

    implementation('org.springframework.session:spring-session-data-mongodb')

    implementation('org.springframework.boot:spring-boot-starter-data-mongodb')

    testImplementation('org.springframework.boot:spring-boot-starter-test')

}



컨피그 설정 추가


@EnableMongoHttpSession

public class HttpSessionConfig {

    @Bean

    public JdkMongoSessionConverter jdkMongoSessionConverter() {

        return new JdkMongoSessionConverter(Duration.ofMinutes(30));

    }

}


프로퍼티 설정


spring.data.mongodb.host=

spring.data.mongodb.authentication-database=

spring.data.mongodb.port=

spring.data.mongodb.database=

spring.data.mongodb.username=

spring.data.mongodb.password=


결과


아래와 같이 세션이 MongoDB 에 저장되는 것을 확인할 수 있다. 


정리


간단하게 MongoDB 를 세션 저장소로 하는 테스트를 진행하였다. 스프링에서는 여러 세션 저장소를 지원한다.

Spring Session Data Redis

Spring Session JDBC

Spring Session Hazelcast

Spring Session MongoDB



스프링 세션 디자인 패턴(1)


스프링 세션 프로젝트는 어떻게 설계 되었는지 좀더 디테일하게 확인한다. 이 내용은 필자의 개인적인 의견이고, 스프링을 만든 Pivotal 에서 공식적으로 발표한 내용은 아니다. 즉, 스프링에서 추구하는 디자인 패턴, 철학과 맞는지 틀린지 검증하지 않았다. 대충 떄려맞추고 있는데 이 글을 읽는 개발자가 있다면, 잘 읽어보고 잘못된 내용이 있다면 피드백을 주길 바란다. 


SpringHttpSessionConfiguration


스프링 부트 환경의 세션 레디스에 대한 공식 레퍼런스를 확인해보자. 
https://docs.spring.io/spring-session/docs/current/reference/html5/guides/boot-redis.html#boot-servlet-configuration

Our Spring Boot Configuration created a Spring Bean named springSessionRepositoryFilter that implements Filter. The springSessionRepositoryFilter bean is responsible for replacing the HttpSession with a custom implementation that is backed by Spring Session. In order for our Filter to do its magic, Spring needs to load our Config class. Last we need to ensure that our Servlet Container (i.e. Tomcat) uses our springSessionRepositoryFilter for every request. Fortunately, Spring Boot takes care of both of these steps for us.

스프링 부트는 springSessionRepositoryFilter 라는 스프링 빈을 자동으로 생성 해준다. springSessionRepositoryFilter 를 생성은 spring-session-core에서 SpringHttpSessionConfiguration 클래스를 확인하자. 

RedisOperationSessionRepository 를 주입받는 것을 확인할 수 있다. 


이번에는 MongoDB 를 확인해보자. 마찬가지로 SpringHttpSessionConfiguration 클래스를 보자. 

MongoDB 에서는 MongoOperationsSessionRepository 를 주입받는다. 



FindByIndexNameSessionRepository


RedisOperationSessionRepository 와 MongoOperationsSessionRepository 는 spring-session-core에서 FindByIndexNameSessionRepository 인터페이스를 구현한다. 그럼 초기화는 언제 할까? 

레디스는 RedisHttpSessionConfiguration, 몽고DB는 MongoHttpSessionConfiguration 에서 초기화해준다.  그리고 RedisHttpSessionConfiguration 는 EnableRedisHttpSession 에서, MongoHttpSessionConfiguration 는 @EnableMongoHttpSession 에서 컨피그레이션을 호출한다. 좀 어렵지만 정리를 해보면, 스프링 부트 환경에서 각각의 레디스 또는 몽고DB 각각의 라이브러리에서 SessionRepository 를 만들어서, 스프링 코어단에서 Filter 를 생성해주는 프로세스이다. 


생략


필자가 이해하기에는 조금 어려워서, 일단 여기까지만 보고 마치겠다.



마무리 및 스프링 세션 디자인 패턴(2)


이번 글에서는 이정도만 이해하고 넘어갈 예정이다. 글이 길어지니깐 정리하기도 어렵고 힘든것 같다. 나중에 자세하게 공부할 기회가 생기면 그때 다시 찾아오겠다. 



주말이 이렇게 지나가버렸구나..

어쨋든 이번 글도 허접하지만 끝@

매거진의 이전글 Spring Data Design Pattern(1)
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari