0. 들어가기 전
이전 포스팅에서 간략하게 Redis를 살펴봤었습니다.
이전 포스팅에서는 Redis의 장점으로 인메모리 저장소의 특성으로 인한 빠른 성능을 중점적으로 소개했습니다.
이번에는 Spring을 사용한 개인 프로젝트에서 Redis Session Clustering을 사용한 과정을 포스팅해보도록 하겠습니다!
https://ksh-coding.tistory.com/127
1. Session Clustering
앞에서 Session Clustering을 구현하는 저장소로 Redis를 사용한다고 했습니다.
여기서, Session Clustering이란 무엇일까요?
Session Clustering이란, 여러 WAS의 Session이 모두 공유되도록 하는 방법을 말합니다.
일반적으로 1대의 서버에 인메모리로 세션 저장소가 존재하는 상황을 가정해봅시다.
이때는 단일 서버 환경에서는 문제가 없겠지만,
로드밸런서를 사용한 Scale-out된 분산 환경에서는 문제가 발생합니다.
서버 A, B가 존재할 때 사용자는 로그인 요청을 통해 세션이 서버 A에 저장됩니다.
이후에 사용자가 저장된 SessionID로 요청을 할 때, 서버 A에 트래픽이 몰려서 서버 B에 요청을 하게 된다면
서버 B의 인메모리 저장소에는 사용자의 Session 정보가 없기 때문에 인가 처리가 되지 않을 것입니다.
이러한 문제는 간단하게 포트를 8080, 8081로 다르게 서버 2개를 띄워서 재현할 수 있었습니다.
8080 포트의 서버에서 로그인 후에 세션이 정상적으로 저장되어 /boards의 인가 처리가 되는 것을 볼 수 있지만
저장된 세션으로 8081 포트로 /boards로 요청 시 세션이 불일치하여 인가 처리가 정상적으로 되지 않는 것을 볼 수 있습니다.
이러한 문제를 해결하는 방법으로는 크게 다음과 같은 2가지 방법이 존재합니다.
- Sticky Session
- Session Clustering
첫 번째 방법인 Sticky Session을 간략하게 설명하면,
로드 밸런서의 설정을 통해 사용자의 요청이 처음 세션을 저장한 서버로만 가도록 설정하는 방법입니다.
이 방법은 다음과 같은 문제점이 존재합니다.
- 실시간 트래픽과 상관없이 세션이 저장된 서버에 요청이 가기 때문에 한 서버에 트래픽이 몰릴 수 있어서 로드밸런싱을 하는 의미가 퇴색될 수 있습니다.
- 특정 서버에 장애가 발생한다면 해당 서버에 존재하는 세션이 모두 사라질 수 있습니다.
두 번째 방법인 Session Clustering이 이번에 사용한 세션 불일치 문제의 해결 방법입니다.
앞서 말했듯이 Session Clustering은 여러 WAS의 Session이 모두 공유되도록 하는 방법을 말합니다.
일반적으로 위와 같이 하나의 서버에 세션이 생성되면 나머지 모든 서버의 세션 저장소에 같은 Session을 생성하는 방식을 이용합니다.
하지만 이러한 방식을 이용할 때 서버가 많아진다면 하나의 세션이 생성될 때 모든 서버에 세션을 생성해야하기 때문에 서버의 메모리 부하가 상당히 커지게 되는 문제가 있습니다.
이러한 문제를 해결하는 것이 바로 Redis Session Clustering입니다.
Redis Session Clustering은 모든 서버가 같은 Redis Session 저장소를 바라보게 하는 방법입니다.
간략하게 말하면, 외부로 Session 저장소를 분리한 것입니다.
이렇게 하면 하나의 서버에서 세션이 생성될 때 한 번만 저장소에 저장이 되기 때문에 메모리 부하를 줄일 수 있습니다.
※ 왜 Redis일까?
그렇다면, 위의 방법을 이용할 때 Session 저장소로 RDB를 이용할 수도 있을 텐데 왜 Redis를 사용할까요?
이는 이전 포스팅의 Redis의 장점 부분을 보면 알 수 있습니다.
Session 저장소 같은 경우는 거의 모든 인가 요청 시에 사용되기 때문에 저장소에 접근이 잦습니다.
이때, 저장소가 RDB라면 디스크 I/O 작업이 많아지기 때문에 서버의 메모리 부하가 상당히 커질 것입니다.
하지만, Redis는 인메모리 기반으로 디스크 I/O 작업을 하지 않기 때문에 메모리 낭비를 줄일 수 있습니다.
이러한 이유 때문에 Redis를 외부 Session 저장소로 사용합니다.
2. 의존성 설정
앞서 이론 설명이 좀 길었는데, 실전으로 어떻게 Redis Session Clustering을 적용하는지 살펴봅시다!
2-1. Redis 의존성 설정
Spring에서 Redis를 사용하기 위해서는 다음과 같은 2가지 방법이 존재합니다.
- 순수 Redis Client 라이브러리(Jedis, Lettuce 등) 사용
- Spring Boot의 Starter 중 ‘spring-boot-starter-data-redis’ 사용
※ spring-boot-starter-data-redis
두 가지 방법 중에서 순수한 Redis Client 라이브러리를 사용하기 보다는,
Spring을 사용하는 만큼 Spring Boot에서 제공하는 스프링 부트의 Data Redis를 사용해보도록 하겠습니다.
Spring Boot의 Starter 공식 문서를 살펴보면, 다음과 같이 나와 있습니다.
보시는 것처럼 Spring Data Redis 프레임워크와 순수 Redis Client 라이브러리인
Lettuce를 함께 의존성에 추가하는 것을 알 수 있습니다.
※ Spring Data Redis Framework
Spring Data Redis의 공식문서를 봐보면, Spring Data Redis를 다음과 같이 소개하고 있습니다.
The Spring Data Redis (SDR) framework makes it easy to write Spring applications that use the Redis key-value store by eliminating the redundant tasks and boilerplate code required for interacting with the store through Spring’s excellent infrastructure support.
Spring Data Redis provides easy configuration and access to Redis from Spring applications. It offers both low-level and high-level abstractions for interacting with the store, freeing the user from infrastructural concerns.
간략히 요약하면 Spring Data Redis는 Spring에서 Redis를 사용할 때 불필요하고 귀찮은 중복 코드를 제거하고, Redis와 상호작용하는 추상화된 인터페이스를 제공하여 Spring에서 Redis를 간편하게 사용할 수 있게 하는 프레임워크입니다.
※ 순수 Redis Client 라이브러리 중 Jedis VS Lettuce
위의 spring-boot-starter-data-redis에서는 Redis Client 라이브러리 중에서 Lettuce를 사용하는 것을 알 수 있습니다.
Lettuce와 가장 많이 비교되는 라이브러리로 Jedis가 존재하는데, Spring boot는 왜 Lettuce를 선택한 걸까요?
간략하게 이유를 요약하면 다음과 같습니다.
- Jedis는 멀티스레드 환경에서 안전하지 않다.
- Lettuce는 Netty 기반 환경으로 비동기를 지원한다.
- Lettuce는 거의 모든 측면에서 성능이 Jedis보다 우수하다.
Jedis와 Lettuce의 성능 비교는 잘 포스팅된 블로그가 있어서 해당 글을 소개하면서 넘어가겠습니다.
(성능 비교글이 2019년에 작성되었지만 현재 최신 버전의 Spring Boot에서 Lettuce를 사용하는 것으로 보아 최근에도 비슷할 것이라고 예상합니다.)
https://jojoldu.tistory.com/418
‘spring-boot-starter-data-redis’를 사용하기 위해서는 다음과 같이 build.gradle에 의존성을 추가해주면 됩니다.
(프로젝트에서 빌드 도구로 Gradle을 사용했기 때문에 Gradle 기준으로 작성하겠습니다.)
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
2-2. Spring Session 의존성 설정
Spring Data Redis 외에도 추가적으로 Spring에서 제공하는 Spring Session이 존재합니다.
※ Spring Session
Spring Session은 어떤 기능을 제공할까요?
Spring Session의 공식문서를 보면, 다음과 같이 소개하고 있습니다.
Spring Session provides a layer of abstraction between the application and the session management. It allows the session data to be stored in various persistent stores, such as relational databases, NoSQL databases, and others.
With Spring Session, you can use the same API to manage sessions, regardless of the persistent store used. This makes it easier to switch between stores without changing the application code. Spring Session also provides features such as session expiry and cross-context communication between different web applications.
간략히 요약하면 Spring Session은 애플리케이션과 세션 저장소 간의 추상화 계층을 제공하여,
세션을 다양한 저장소에 저장할 수 있게 해줍니다.
따라서, 저장소 구현체(JDBC, MongoDB, Redis 등)를 변경할 때도
애플리케이션 코드의 변경없이 쉽게 구현체를 변경할 수 있다고 합니다.
결론적으로, Spring Session은 다음과 같은 이유로 사용합니다.
- Session을 인메모리가 아닌 외부 저장소에 쉽게 저장하기 위함
- 외부 저장소의 구현체를 애플리케이션의 코드 변경없이 쉽게 변경할 수 있다.
Spring Session을 사용하기 위해서는 다음과 같이 build.gradle에 의존성을 추가해주면 됩니다.
implementation 'org.springframework.session:spring-session-data-redis'
3. Redis Session Clustering 구성
본격적으로 Redis Session Clustering을 구성해보겠습니다.
Redis Session Clustering을 하기 위해서 application.yml 설정 파일에 추가 설정이 필요합니다.
먼저 설정을 완료한 application.yml을 보여드리고, 이후에 설명하도록 하겠습니다.
※ application.yml
// 3-1
server:
servlet:
session:
cookie:
name: JSESSIONID
spring:
// 3-2
session:
store-type: redis
redis:
namespace: shboard:session
// 3-3
data:
redis:
host: localhost
password: 1234
port: 6379
3-1. Session Cookie Configuration
server:
servlet:
session:
cookie:
name: JSESSIONID
Spring Session 사용 시 세션 쿠키의 기본 Name은 SESSION이지만,
일반적으로 세션 쿠키의 이름은 JSESSIONID이기 때문에 JESSIONID로 세션 쿠키 Name을 변경해주었습니다.
3-2. Spring Boot Configuration
session:
store-type: redis
redis:
namespace: shboard:session
store-type: redis의 설정은 spring-session-data-redis의 @EnableRedisHttpSession 어노테이션을 활성화하는 설정입니다.
해당 어노테이션을 활성화함으로써, springSessionRepositoryFilter라는 이름의 Filter 인터페이스를 구현한 빈을 생성합니다.
그리고 SpringSessionRepositoryFilter의 SessionRepository의 구현체로 RedisSessionRepository가 지정됩니다.
이를 통해 Redis로 Session 저장소를 간편하게 구성할 수 있습니다.
또, redis.namespace의 설정은 세션 저장 시 prefix를 지정하는 설정입니다.
기본값은 spring:session인데, 서비스 이름으로 변경해주었습니다.
3-3. Configuring the Redis Connection
3-1의 설정을 통해 RedisSessionRepository를 구현체로 가진 SpringSessionRepositoryFilter 빈이 생성되었습니다.
이때, Redis 연결과 관련한 설정 클래스인 RedisConnectionFactory를 설정해줘야 설정이 마무리됩니다.
Spring Boot에서 다음과 같이 간편하게 설정할 수 있습니다.
spring:
data:
redis:
host: localhost
password: 1234
port: 6379
Redis의 연결 정보를 작성하면 됩니다.
host에는 Scale-out된 분산 프로덕션 환경에서는 외부 Redis Session 저장소 서버가 따로 존재하여 해당 서버의 주소를 작성해야겠지만
현재는 학습하는 입장이기 때문에 로컬에 Redis 저장소가 존재하는 환경으로 설정하였습니다.
이렇게 간단하게 Redis Session Clustering 설정이 가능합니다.
4. 실제 애플리케이션 적용
앞서 Redis Session Clustering 설정을 마쳤으니 개인 프로젝트에 적용해보도록 하겠습니다.
관련 코드들을 살펴보도록 하겠습니다.
※ MemberController
@PostMapping("/login")
private ResponseEntity<Void> login(@RequestBody final MemberLoginRequest request, final HttpServletRequest httpRequest) {
final String memberLoginId = memberService.login(request);
final HttpSession session = httpRequest.getSession();
session.setAttribute("memberId", memberLoginId);
session.setMaxInactiveInterval(3600);
return ResponseEntity.ok().build();
}
MemberController에서 로그인 관련 로직을 처리하고 있습니다.
로그인 시 Session을 생성하고, 유저 정보를 Attribute로 저장하는 로직입니다.
이때 우리는 Spring Session을 사용하기 전처럼 HttpSession을 사용하면 됩니다.
Spring Session을 사용하면 HttpSession에 우리가 설정한 RedisSession이 구현체로 지정되기 때문에 그대로 사용하면 됩니다.
세션 쿠키의 기본 Name은 SESSION이고, Value로는 SessionId를 Base64로 인코딩한 값이 설정됩니다.
따라서, 인증/인가 비즈니스 로직으로 세션 쿠키가 사용된다면 추출의 Key를 SESSION으로 바꿔주고
Value를 Base64로 인코딩한 값을 지정해주면 됩니다.
Base64로 인코딩하는 과정이 귀찮다면 다음과 같이 구성 클래스로
DefaultCookieSerializer의 useBase64Encoding를 false로 빈을 등록하면 됩니다.
@Configuration
public class SessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?(\\w+\\.[a-z]+)$");
serializer.setUseBase64Encoding(false);
return serializer;
}
}
※ AuthInterceptor
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthInterceptor implements HandlerInterceptor {
private static final String SESSION_KEY = "SESSION";
private static final String REDIS_SESSION_KEY = ":sessions:";
@Value("${spring.session.redis.namespace}")
private String namespace;
private final StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
final String sessionIdByCookie = getSessionIdByCookie(request);
final String decodedSessionId = new String(Base64.getDecoder().decode(sessionIdByCookie.getBytes()));
if (!redisTemplate.hasKey(namespace + REDIS_SESSION_KEY + decodedSessionId)) {
log.warn("Session Cookie exist, but Session in Storage is not exist");
throw new AuthException.FailAuthenticationMemberException();
}
return true;
}
...
}
인가 처리를 담당하는 AuthInterceptor입니다.
먼저 Cookie에 세션 값이 존재하는지 확인 후 없으면 예외를 발생시킵니다. (아래 메소드를 추출했지만 코드 길이상 생략했습니다.)
그 후에 쿠키의 세션 값이 Redis 저장소의 Key에 존재하지 않는다면 RedisTemplate의 hasKey를 통해 예외를 발생시켰습니다.
이러한 과정을 통해 인가 처리를 수행했습니다.
5. Redis Session Clustering을 통한 Session 불일치 문제 해결
마지막으로 앞서 살펴 본 세션 불일치 문제를 해결한 영상을 보고 마무리하도록 하겠습니다.
환경은 이전과 똑같이 8080 포트, 8081 포트로 서버를 2개 띄우고
8080에서 로그인 후 8081 포트로 다른 요청을 보내도 세션이 일치하여 인가 처리가 되는 것을 확인할 수 있습니다.
'Spring' 카테고리의 다른 글
[Spring] 스프링 동시성 처리 방법(feat. 비관적 락, 낙관적 락, 네임드 락) (4) | 2023.11.21 |
---|---|
[Spring] SpringBoot의 Tomcat 설정 알아보기 (feat. Thread, Thread Pool) (4) | 2023.09.14 |
[Spring] 스프링 이벤트를 사용하여 도메인 의존성 분리하기 (2) | 2023.07.30 |
[Spring] 테스트 시 DB 데이터 초기화 Trouble Shooting (0) | 2023.05.28 |
[Spring] 스프링 HTTP API 요청 & 응답 시 역직렬화 직렬화 원리 (4) | 2023.04.18 |