[Spring] Spring에서 Session 사용하기

오잉·2023년 8월 26일
3

Intro

HTTP는 connectionless하고 stateless하다

HTTP의 특징에는 connectionlessstateless가 있다.

  • connectionless (무연결성) : 클라이언트가 요청을 한 후 응답을 받고나면 연결을 끊어버린다.
  • stateless (무상태성) : 연결이 끝나면 상태를 유지하지 않는다.

여기서 '상태를 유지하지 않는다'란 무슨 의미일까?
이는 클라이언트와 첫 번째 통신에서 데이터를 주고받았다 해도, 두 번째 통신에서 이전 데이터를 유지하지 않는다는 뜻이다.
방금 전에 발송된 요청과 지금 발송한 요청에 대해 어떠한 연관관계를 가지지 않는다.

즉, 서버는 클라이언트의 요청을 구분하지 못한다. 그러므로 사용자별 요청 구분이 이론적으로 불가능하다.

인증 유지

그렇다면 사용자는 매번 로그인을 통해 자신이 누구인지 서버에 알려야 하는걸까? 모든 요청에 인증정보가 포함되어야 하는걸까?

이런 문제를 해결하기 위해 쿠키, 세션, 토큰등의 방법을 사용한다.
위의 방법들을 사용하면 한번 로그인 이후에 인증을 유지할 수 있다!

이 포스팅에서는 위 방법 중 세션을 스프링에서 사용하는 방법에 대해 알아보려 한다.

1. Session 사용법

	@GetMapping("/api/login/kakao")
	public ResponseEntity<Void> login(@RequestParam("code") final String code,
                                                   final HttpServletRequest request) {
        SignUserDto signUserDto = authService.loginWithKakao(code);
        Long memberId = signUserDto.getMember().getId();
        
        HttpSession session = request.getSession();
        session.setAttribute("member", memberId); // session에 'member' 속성값 저장
		...
    }

setAttribute() 메소드를 통해 session에 해당 속성값을 저장한다.

	@GetMapping("/api/members")
	public ResponseEntity<MemberProfileResponse> getMemberProfile(final HttpServletRequest request) {
        HttpSession session = request.getSession();
        String id = String.valueOf(session.getAttribute("member")); // session 속 member 속성값 반환
        
		MemberProfileResponse response = memberService.getProfile(id);
		...
    }

getAttribute() 메소드를 통해 해당 속성값을 반환한다.

그런데, session.getAttribute("member")에서
사용자 A가 접속해도 "member"를 Key로 값을 가져오고,
사용자 B가 접속해도 "member"를 Key로 값을 가져온다.
같은 Key를 쓰는데 어떻게 A와 B를 구분해서 값을 가져오는걸까?

정답이 궁금하다면 4.톰캣은 Session을 어떻게 관리할까?를 확인하자 😋

@PostMapping("/api/logout/kakao")
public ResponseEntity<Void> logout(@AuthenticationPrincipal final LoginInfo loginInfo,
                                       final HttpServletRequest request) {
   	authService.logoutWithKakao(loginInfo.getId()); 
        
    HttpSession session = request.getSession(); 
    session.invalidate(); // session 속 모든 속성값 삭제
    ...
}

session.invalidate(); : session 속 모든 속성값 일괄 삭제
session.removeAttribute("member"); : session에서 'member' 속성값 삭제

2. Session의 동작 과정

Spring Boot는 기본 서블릿 컨테이너로 Tomcat을 사용한다.
Tomcat의 경우 세션id로 JSESSIONID를 사용한다.

스프링에서 세션의 동작은 다음과 같다.

(하단 이미치 출처 : 웹 session 기반 인증)

1) 클라이언트가 Request를 보낸다.
2) 서버에서 Session을 생성하고 JSESSIONID를 발급한다.
3) Response의 Set-Cookie로 JSESSIONID를 클라이언트에게 전달한다.
4) 클라이언트는 Cookie에 JSESSIONID를 담아 요청을 보낸다.
5) 서버는 들어오는 Request의 Cookie header에서 JSESSIONID가 있는지 확인하고 해당 값을 사용하여 서버의 메모리에 저장된 Session을 가져온다.

3. Session 발급과 유지

1) Session 발급

클라이언트(웹 브라우저의 사용자)가 처음으로 웹 어플리케이션을 방문하거나 request.getSession()을 통해 HttpSession을 처음으로 가져 오면 서블릿 컨테이너는 새로운 HttpSession 객체를 생성하고 길고 unique한 ID를 생성 후, 서버의 메모리에 저장한다.

Request에 세션id가 포함되어 있다면, 서블릿 컨테이너는 새로운 세션을 발급하지 않고 해당 세션id에 해당하는 Session을 전달한다.

Request에 세션id가 포함되어 있지 않다면, 서블릿 컨테이너는 HttpSession을 요구하는 모든 요청에 대해 새로운 Session을 발급한다.
(참고로 새로운 Session을 발급했을 경우 JSESSIONID란 이름을 key로, 생성한 session ID를 value로 하여 HTTP 응답의 Set-Cookie header에 cookie로 알아서 설정해준다!)

	@GetMapping("/test")
	public ResponseEntity<Void> test(final HttpServletRequest request) {
        HttpSession session = request.getSession();
        ...
	}

즉, 위의 request.getSession()에서 세션id가 있으면 기존 세션을 반환하고, 없으면 신규 세션을 생성한다.

2) Timeout

HttpSession은 web.xml의 설정인 session-timeout에 지정된 값 까지만 살아있다. 기본값은 30분이다.

따라서 클라이언트가 time out보다 오랫동안 웹 어플리케이션을 방문하지 않으면 서블릿 컨테이너가 해당 Session을 삭제한다.

그러므로 삭제 이후에는 JSESSIONID를 Cookie에 담아 request보내더라도 더 이상 동일한 Session에 액세스 할 수 없으며, 서블릿 컨테이너는 새로운 Session을 생성 할 것이다.

3) 브라우저 닫기

클라이언트 측에서는 웹브라우저 인스턴스가 실행되는 동안 Session Cookie가 활성화된다.

따라서 클라이언트가 웹 브라우저 인스턴스(모든 탭 / 창)를 닫으면 클라이언트 Session이 삭제된다.

이후 새롭게 브라우저를 켜서 요청을 보내더라도 Cookie에 Session관련 정보가 존재하지 않으므로 더 이상 JSESSIONID는 전송되지 않는다. 따라서 새로운 Session이 생성된다.

4. 톰캣은 Session을 어떻게 관리할까?

1) 세션의 구조

HttpSession 인터페이스의 구현체인 StandardSession에 들어가보면
해당 Session에 저장되어 있는 속성값들이 map 형태로 관리되고 있는 것을 확인할 수 있다.

session에 속성값을 저장하기 위해 사용한 setAttribute 메소드를 살펴보면 해당 map에 put을 해주고 있다!

즉, session.setAttribute("member", memberId); 을 하게되면,
"member"가 key, memberId가 value로 map에 저장되게 된다.

2) 모든 세션이 관리되는 곳

HttpServletRequest 인터페이스의 구현체인 ApplicationHttpRequest의 getSession 메소드 속 findSession을 타고가면 Manager 인터페이스에 도달할 수 있다.
(Manager는 session의 pool을 관리해주는 역할을 한다)

Manager 인터페이스의 구현체인 ManagerBase에 들어가보면

현재 살아있는 모든 Session들이 map형태로 관리되고 있는 것을 확인할 수 있다.

findSession 메소드를 살펴보면 세션id(JSESSIONID)를 통해 해당하는 Session을 반환하고 있다.

3) 정리

정리하자면 스프링(톰캣의 경우)은 세션을
Map< JSESSIONID, Map< 속성key, 속성value>> 이런식으로 관리된다고 보면 될 것 같다.

좀 더 직관적으로 말하자면
Map< 세션id, 세션저장소>라고 할 수 있다.

결국, 톰캣의 JSESSIONID는 서블릿 컨테이너가 있는 웹서버에 접속한 여러 사용자 각각의 세션 공간을 관리하기 위한 값이라고 볼 수 있다.


참고

HttpSession은 어떻게 만들어지고 어떻게 유지될까
톰캣에서는 어떻게 JSESSIONID를 만드는 것일까
JSESSIONID
MVC HttpSession, Interceptor, Cookie 정리하기
서블릿의 인스턴스화, session, 멀티스레딩에 관련된 질문과 답변에 대한 번역

profile
오잉이라네 오잉이라네 오잉이라네 ~

0개의 댓글