어라? 분명 로그인을 했는데 . . . 왜 세션이 해제되어있지 ???
서론
현재 배달과 관련 된 토이프로젝트를 진행하고 있다.
기존에 필자가 Session 로그인을 개발했던 방식은 아래 코드와 같았다.
@PostMapping("/user/login")
public String login(@RequestBody @Valid UserLoginDto userDto,
HttpServletRequest request) {
User user = userService.login(userDto);
if ( user == null ) return ResponseCode.LOGIN_ERROR_CODE;
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
session.setMaxInactiveInterval(60 * 30);
return ResponseCode.SUCCESS_CODE;
}
@GetMapping("/userInfoPage")
public String getUserInfoPage(HttpSession session, Model model) {
// Session 끊킬 시 redirect
User user = (User) session.getAttribute("loginUser");
boolean loginSession = user != null;
if ( !loginSession ) return "redirect:/loginPage";
return ResponseCode.SUCCESS_CODE;
}
이런 방식으로, 항상 Session 을 가지고 다니면서, 컨트롤러를 부를 때마다 그 세션 정보를 활용하였다.
물론 이 방법이 틀린 방법은 아니다. 레거시한 코드들을 보면 대부분 이렇게 세션을 가지고 다닌다.
그러나, 분산 서버를 사용하게 되면서 이제 이런 방식은 잘 사용하지 않게되었다.
본론
1. 왜 세션을 가지고 다니면 안될까?
이 이유는 간단하다. 분산 서버이므로, A 서버에서 로그인을 하여 세션 정보를 가지고 다녀도, B 서버 응답이 오면 그 세션 정보를 가지고 올 수가 없다.
즉, 메인페이지에서 로그인을 하였는데, 게시판에 가보니 로그인이 풀려있는 상태가 될 수 있다는 것이다.
2. 어떻게 해결할 수 있을까?
해결 방법은 여러가지가 있지만, 필자는 크게 2가지를 생각하였다.
먼저 하나는 스티키 세션 이라는 로드 밸런싱 방식을 사용하는 것이다. 이는 처음 접속한 서버로만 계속 전송하는 방법이다. 즉, 처음에 로그인한 서버가 A 서버라면, 그 유저에게는 계속 A서버만 연결해주는 것이다.
그러나 이런 방법은 자칫, 하나의 서버에 유저가 몰릴 수 있기 때문에, 서버를 분산시킨 의미가 퇴색 될 수 있으므로 과부하가 생길 수 있으며, 해당 서버에 장애가 생길 시 모든 세션들이 소실 될 수 있는 문제가 있다.
그래서 긴급한 상황에만 사용하는 것이 좋다.
다른 방법은 바로 Redis Session 을 이용하는 방법이다.
이는 세션 클러스터링 방식 중 하나이며, Redis 를 이용하는 방법이다.
3. Redis Session
레디스 세션을 사용하려면 먼저 gradle 에 아래 내용을 추가해주어야 한다.
implementation 'org.springframework.session:spring-session-data-redis'
그 다음은 properties 에 아래 내용을 추가해준다.
spring.data.session.store-type = redis
spring.data.seesion.redis.flush-mode = on_save
이후 기본적인 RedisConfig 를 만들어준다.
@Configuration
public class RedisConfig {
@Value("${spring.redis.host}")
public String host;
@Value("${spring.redis.port}")
public int port;
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(connectionFactory);
return redisTemplate;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(host);
configuration.setPort(port);
return new LettuceConnectionFactory(configuration);
}
}
그리고 로그인 관련 서비스를 만들어서, 해당 서비스에 private final HttpSession session; 으로 공통 된 session 을 만들고, 해당 서비스에서 로그인 관련 로직을 만들면 된다.
마지막으로, main 메서드가 위치하는 클래스 위에 @EnableRedisHttpSession 어노테이션을 추가해주면 된다.
@EnableRedisHttpSession
@SpringBootApplication
public class RedisSessionStorageTest {
public static void main(String[] args) {
//...
}
}
이후 테스트를 진행해보았다.
Redis 에 정상적으로 Session 정보가 저장되며,
로그아웃시에도 정상적으로 로그아웃이 된다.
이로써 분산된 서버에서도 로그인을 유지할 수 있게 클러스터링이 되었다.
결론
성능, 트래픽 등을 고려하였을 때 분산 서버는 빠질 수 없다.
이전에 분산 서버를 고려하여 AWS S3 를 활용하여 이미지를 저장할 수 있게 하였는데, Session 을 이용한 로그인 기능도 마찬가지이다.
물론 스프링 시큐리티를 활용하여 JWT 를 하는 방법도 있다고는 하는데, 필자는 세션 밖에 안써봐서 이부분은 아직 잘 모르겠다. 다음에는 JWT 를 꼭 공부해보아야 겠다.
'개발 회고록' 카테고리의 다른 글
[ 개발 회고록 ] Redis Session, Cache 저장소 분리하기 (0) | 2024.01.24 |
---|---|
[ 개발 회고록 ] @RestAdviceController 와 @ExceptionHandler 로 예외 처리하기 (1) | 2024.01.24 |
[ 개발 회고록 ] Self - Invocation 에 대한 간단한 이야기 (0) | 2024.01.23 |
[ 개발 회고록 ] Redis INCR 를 활용하여 선착순 쿠폰 발급 기능 만들기 (0) | 2024.01.23 |
[ 개발 회고록 ] 낙관적 Lock 에서 @Transacional 을 사용하면 안되는 이유 + Lock 을 통한 라이더 배차 서비스 개발 (2) | 2024.01.23 |