[Spring] Redis Session Storage

yuKeon·2023년 11월 6일
1
post-thumbnail

개요

현재 ‘PSQ’는 세션을 메모리, 정확히 말하면 tomcat의 session 저장소를 사용한다.
사용 이유로는 서비스 규모 때문이다. 단일 서버로 처리가 가능하며, 로그인 요청이 몰리는 일도 없다. 만약 서비스 규모의 확장으로 서버의 스케일 아웃이 발생하면 어떻게 처리할 수 있을까? 이번 포스팅은 서버의 스케일 아웃을 고려한 세션 관리 방법을 다룬다.

1. Session Clustering

1.1. 개념

세션 클러스터링은 두 대 이상의 WAS에서 세션 데이터를 공유하는 기술이다. 해당 기술을 사용하면 서버 중 하나에 장애가 발생하거나 유저가 다른 서버로 리다이렉션되더라도 사용자 세션은 지속된다.

하지만, 모든 WAS에서 같은 세션 데이터를 공유하기 때문에 큰 용량의 세션 저장소를 요구하며, 트래픽이 몰리는 경우 모든 서버에 세션 정보를 저장하기 때문에 성능 저하가 발생한다.

1.2. 구현 방법

1.2.1. all-to-all Session Replication
: 하나의 세션 저장소에서 특정 세션이 변경되면 모든 저장소에 변경 사항이 반영된다. 이를 통해 정합성 문제는 해결되지만 성능이 저하되는 문제가 발생한다.

1.2.2. BackupManager

: Primary 서버의 모든 세션 정보를 Secondary 서버에 복제한다. 나머지 서버는 세션 ID와 Primary 서버의 저장 위치값만 복제한다. 이 방식은 all-to-all 방식보다 메모리 낭비가 적고 효율적이다.

2. Sticky Session

2.1. 개념

로드 밸런서를 통해 사용자의 모든 요청이 같은 WAS로 전달되도록 고정시키는 방법이다. 이 방식을 사용하면 서버는 특정 사용자의 세션만을 저장하면 되므로 메모리를 낭비하지 않는다. 하지만 특정 서버에 트래픽이 몰리는 경우 서버에 과도한 부하를 준다는 점과 서버가 다운되는 경우 해당 서버가 관리하는 모든 세션 정보를 잃는다는 단점이 있다.

3. Redis Session Storage!

3.1. 개념

Redis에 세션 정보를 저장하고 관리하는 방법이다. Redis를 세션 저장소로 사용하는 이유는 그 성능과 확장성 때문이다. Redis는 In-memory 데이터베이스로, 빠른 읽기 및 쓰기가 가능하고, 다양한 데이터 타입을 지원하여 세션 정보를 효율적으로 저장하고 관리할 수 있다.

이번 구현에서는 Redis Session Clustering을 사용하여 서버의 스케일 아웃 시 세션 불일치 문제를 해결해 본다.

4. 구현

개발 환경

Language : Java 11
Framework : Spring Boot 2.7.8


4.1. build.gradle

...

dependencies {
   ...
		// Redis
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.session:spring-session-data-redis'
}

...

4.2. application.yml

spring:
  redis:
    host: localhost
    port: 6379

	session:
	    store-type: redis

redis
: local 환경에서 Redis를 사용하므로 host는 localhost로 지정했다.
다른 서버나 Docker 환경이라면 이에 맞춰 수정한다.

session
: store-type: redis을 설정하면 설정 파일(xxxConfig.java)에서 @EnableRedisHttpSession 코드를 추가하지 않아도 된다.
(출처 : https://docs.spring.io/spring-session/reference/guides/boot-redis.html#boot-spring-configuration)

4.3. RedisConfig.java

@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        final RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        template.setDefaultSerializer(new StringRedisSerializer());
        return template;
    }
}

Java 진영의 Redis Client 라이브러리는 Jedis와 Lettuce가 있다. Spring Boot 2.0 부터 기본 클라이언트에서 Lettuce가 사용되고, 성능이 더 좋기 때문에 Lettuce로 Redis를 연결한다.
(참고 : https://jojoldu.tistory.com/418)

4.4. MemberService.java

@Service
@Transactional
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;
		...
    public void signin(MemberSignInRequest request, HttpSession session) {
				// (1)
        Member member = memberRepository.findByEmail(request.getEmail()).orElseThrow(
                () -> new NoSuchMemberException("로그인에 실패했습니다.")
        ); 

				// (2)
        if (!member.getPassword().equals( 
                encodePassword(request.getPassword(), member.getSalt()))
        )
            throw new NoSuchMemberException("로그인에 실패했습니다.");

				// (3)
        SessionInfo sessionInfo = new SessionInfo(member.getId(), member.getEmail());
        session.setAttribute("sessionInfo", sessionInfo);

				
    }
		...
}

로그인 메서드다. 해당 메서드는 크게 세 로직으로 구성된다.

(1) : DB에서 로그인을 요청한 이메일로 저장된 Member 엔티티를 조회한다. 만약 없으면 예외가 발생한다.

(2) : 로그인을 요청한 비밀번호를 암호화한 다음 조회한 Member 엔티티의 (암호화된) 비밀번호와 비교한다.

(3) : 세션 정보를 Redis에 저장한다.

세션이 성공적으로 저장되면 아래와 같이 저장된다.

1) 세션 만료키로 String 타입이다.

2) 만료 시간에 삭제될 세션 정보로 Set 타입이다.

3) 세션 정보로 Hash 타입이다.

5. 결론

서버의 스케일 아웃을 가정하고 기존의 서버 메모리에 세션을 저장하던 방식에서 Redis 저장 방식으로 변경해봤다. 추후에 Redis에 장애가 생겼을 때 SPOF를 예방하는 방법을 고민하면 좋을 것 같다.

Ref.

0개의 댓글