저번 포스팅에서는 세션 기반 인증 방식의 문제점에 대해서 알아봤습니다.
https://doreree.tistory.com/27
서버 요청에 따라 Scale Out 했을 경우 세션 불일치 문제가 발생합니다.
이번 포스팅에서는 Spring Boot를 통한 코드 작성과 직접 서버에 배포하는 과정을 통해 어떻게 세션 불일치 문제가 발생하는지 알아보도록 하겠습니다.
세션관리 테스트 환경
- 서비스는 Spring Boot로 작성하였습니다.
Spring Boot Service Code
[ 회원가입 API ]
@PostMapping("/join")
public ResponseEntity<?> join(HttpServletRequest request, @RequestBody MemberJoinDTO memberJoinDTO){
memberService.save(memberJoinDTO);
return new ResponseEntity<String>("Join Success", HttpStatus.OK);
}
[ 로그인 API ]
@PostMapping("/login")
public ResponseEntity<?> login(HttpServletRequest request, @RequestBody MemberLoginDTO memberLoginDTO){
// Session 로그인
HttpSession session = request.getSession(true);
if(session == null){
log.debug(">> login 정보가 없습니다. 로그인 진행");
}
MemberLoginDTO login = memberService.login(memberLoginDTO);
log.debug("Login User Info : {}", login.toString());
session.setAttribute("userId", login.getId());
session.setMaxInactiveInterval(1800);
return new ResponseEntity<String>("login Success", HttpStatus.OK);
}
[ 로그인정보 확인 API ]
@GetMapping("/user")
public ResponseEntity<String> findUsername(HttpServletRequest request){
HttpSession session = request.getSession(false);
if(session.getAttribute("userId") != null){
log.warn(">> Login Session 정보가 없습니다.");
}
String userId = (String) session.getAttribute("userId");
log.debug(">> Session에 저장된 UserId : {}", userId);
MemberLoginDTO memberLoginDTO = memberService.findUsername(userId);
return new ResponseEntity<String>(memberLoginDTO.getName(), HttpStatus.OK);
}
클라이언트는 로그인 정보를 가지고 있지 않네요??
토큰 기반 인증 방식에서는 AccessToken을 파싱해서 그 안에 담겨있는 내용으로 데이터 조회를 수행하지만 세션 기반 인증 방식에서는 서버가 그 정보를 가지고 있기 때문에 다릅니다.
그러면 어떤 사용자인지 어떻게 판별할까요??
HttpSession은 자바 서블릿 API에 의해 제공됩니다.
톰캣 내부에 구현이 있으며 톰캣 내부적으로 동작하기 때문에 개발자는 이를 코드로 구현할 필요가 없습니다.
> HttpSession API
- HttpServletRequest.getSession()
> 세션 ID (Session ID)
- 세션이 생성되면 고유한 세션 ID가 할당됩니다.
- 보통 쿠키에 JSESSIONID 라는 이름으로 저장되게 됩니다.
- JSESSIONID는 클라이언트 브라우저에 저장되고 서버로의 요청 시마다 같이 전달됩니다.
- 서버는 JSESSIONID를 통해 클라이언트를 식별하고 이에 대응되는 HTTPSession 가져오게 되는 것입니다.
서버 운영 환경
서버 운영 환경은 다음과 같습니다.
- 개발 : Java 11, Spring Boot 2.7.1
- DB : AWS RDS Freetier, Spring JPA
- 서버 : AWS EC2 ubuntu 22.01, Docker, Nginx
- 테스트 : PostMan, Google Chrome, MS Edge
우선 테스트 하기 전에 간단하게 회원가입을 진행한 후 진행하겠습니다.
AWS EC2 환경에서 Docker를 사용하여 두 개의 서비스를 배포합니다.
Nginx에서 Upstream을 통해 2개의 서버로 Reverse Proxy 설정을 합니다.
PostMan과 웹 브라우저 테스트
PostMan 테스트
- 간단하게 두 회원에 대해 회원가입을 진행합니다.
- 두 회원이 각각 로그인을 진행합니다.
- 서버에 getName 요청을 보냈을 때 정상적으로 회원 이름이 반환되는지 테스트합니다.
두 회원이 각각 로그인 진행합니다.
[ 회원1 로그인 ]
[ 회원2 로그인 ]
사용자 이름을 반환하는 요청을 보내 테스트합니다.
[ 첫 번째 요청 ]
[ 두 번째 요청 ]
똑같은 요청에 대해서 요청을 보냈을 때 500 Error (서버 에러)가 발생하는 것을 확인하였습니다.
하나의 서버에는 로그인 세션 정보가 저장되어 있고, 또 다른 서버에는 로그인 세션 정보가 없기 때문에 발생하는 문제임을 확인할 수 있습니다.
@PostMapping("/")
public ResponseEntity<String> findUsername(HttpServletRequest request){
HttpSession session = request.getSession(true);
String userId = (String) session.getAttribute("userId");
log.debug(">> Session에 저장된 UserId : {}", userId);
MemberLoginDTO memberLoginDTO = memberService.findUsername(userId);
}
[ NullPointerException 발생 ]
Web 브라우저 테스트
Chrome, Edge에서 동시에 테스트를 진행해보도록 하겠습니다.
원활한 테스트를 위해 POST 방식에서 GET 방식으로 HTTP Method를 변경한 후 진행합니다.
Chrome
사용자1 로그인 정보
- id : testId1
- password : password1
Edge
사용자2 로그인 정보
- id : testId2
- password : password2
각각의 브라우저에서 /user 요청을 통해 회원의 닉네임을 불러오도록 해보겠습니다.
Chrome
Edge
잘 불러와지는 것을 확인할 수 있습니다.
요청이 잘 처리되는데요???
다시 요청을 보내보겠습니다.
어떤 요청에 대해서는 로직이 정상적으로 처리되고, 어떤 요청에 대해서는 로직이 정상적으로 처리되지 않습니다.
이처럼 Nginx를 통해 세션 정보가 저장되어 있는 서버로 요청이 보내졌을 경우에만 세션 정보가 있기 때문에 응답을 반환할 수 있었습니다.
즉, 세션 불일치 문제가 발생하는 것입니다.
매 요청에 대해 이런 불일치 문제가 발생한다면 수 많은 User들은 불편함을 겪게 될 수 있습니다.
'BackEnd' 카테고리의 다른 글
세션 인증 방식과 토큰 인증 방식 (1) | 2023.07.13 |
---|---|
Interceptor 를 통해 회원 API 리팩토링하기 (1) | 2023.07.03 |
[Spring] Interceptor 와 Filter의 개념 및 차이점 (0) | 2023.06.20 |
Message Queue (3) | 2023.04.27 |
티켓 예약 서비스의 대규모 트래픽 처리에 대한 고민 (1) | 2023.04.22 |