Session

서버의 Session Storage를 만들어 Key-Value 형태로 저장되는 자료구조를 뜻한다.

쿠키는 브라우저에 저장되므로 Value가 String Type으로 정해져 있는데 반해, 세션은 Object Type을 이용해 자바의 모든 객체를 담을 수 있도록 구현된다.

이 세션 Value에 무엇을 넣냐에 따라 역할이 달라지는데, 쿠키를 넣어 클라이언트 간의 구분을 위해 사용할 수 도 있고, 별도의 데이터를 넣어 Cache Memory처럼 활용할 수도 있다.

여러 사용자가 동시 접근할 수 있기 때문에 Thread-Safe한 자료구조를 사용해야 한다. 톰캣의 세션의 경우에도 ConcurrentMap을 사용하고 있다.

서버의 세션과 클라이언트의 쿠키를 혼합하여 사용하면 다음의 장점이 있다.

  1. 클라이언트가 아닌 서버 세션에 쿠키를 저장하고 관리하므로 클라이언트에게는 세션 Key만 쿠키에 담아서 보내면 된다.
  2. 더이상 쿠키에 민감한 정보를 저장하지 않으므로 탈취 당해도 큰 위험이 없고, ( 단 해커가 다른 사용자의 권한을 탈취하는 것은 여전히 위험하다. ) 랜덤하게 생성된 Key 값을 보내기 때문에 위변조의 걱정이 줄어들게 된다.
  3. 해당 쿠키에서 이상 동작이 포착되는 경우, 서버에서 해당 키를 삭제하는 것만으로도 대처가 가능해진다.

Spring Session

HttpSession 인터페이스

package jakarta.servlet.http;

public interface HttpSession {
    // 세션이 생성된 시간
    public long getCreationTime();

    // 세션 ID - JSESSIONID 값
    public String getId();

    // 세션과 연결된 사용자가 최근 서버에 접근한 시간
    public long getLastAccessedTime();

    // 세션의 유효 시간 (s)
    public int getMaxInactiveInterval();
    public void setMaxInactiveInterval(int interval);

    // 새로 생성된 세션인지 확인
    public boolean isNew();

    // 세션에 저장된 값을 가져온다.
    public Object getAttribute(String name);

    // 세션에 저장된 모든 값들의 이름(key)를 가져온다.
    public Enumeration<String> getAttributeNames();

    // 세션에 값을 저장한다.
    public void setAttribute(String name, Object value);

    // 특정 세션을 삭제한다.
    public void removeAttribute(String name);

    // 모든 세션을 삭제한다.
    public void invalidate();
}

Spring은 HttpSession 인터페이스를 통해 Session을 관리한다.

HttpServletRequest.getSession(boolean create)

HttpSession session = request.getSession(false);
if (session == null) {
    throw new Exception();
}

HttpServletRequest의 getSession 메서드를 통해 세션 스토리지를 접근할 수 있다.

인자로 true, false를 넘길 수 있는데 이는 세션을 새로 생성할지 여부를 정하는데 사용된다. (기본값 true)

  • request.getSession(true)
    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 새로운 세션을 생성하여 반환한다.
  • request.getSession(false)
    • 세션이 있으면 기존 세션을 반환한다.
    • 세션이 없으면 null을 반환한다.

ArgumentResolver

@GetMapping
public String index(
        HttpSession session
) {
    Member loginMember = (Member) session.getAttribute("Session Key");
    return "ok";
}

디스페처 서블릿에서는 컨트롤러에게 넘겨주기 전에 HttpSession을 주입해서 전달하는 기능을 제공한다.

때문에, request.getSession을 통해 가져오지 않고 바로 세션에 접근할 수 있다.

클라이언트가 해당 컨트롤러를 호출하게 되면 자동적으로 세션을 생성하게 된다. 그래서 생성을 원하지 않는다면 위의 방법을 사용할 수 없다. (request.getSession(true)와 동일하다.)

@SessionAttribute

@GetMapping
public String index(
        @SessionAttribute(
                value = "Session Key",
                required = false
        ) Object sessionValue
) {
    log.info("sessionValue: {}", sessionValue);
    return "ok";
}

쿠키의 @CookieValue와 마찬가지로 세션도 편리하게 사용할 수 있도록 @SessionAttribute 애노테이션을 제공한다.

위의 ArgumentResolver와 달리 애노테이션을 사용할 경우 세션이 생성되지 않는다. 그래서 불필요한 세션의 생성이 일어나지 않는다. (request.getSession(false)와 동일하다.)

  • name, value
    • 브라우저에선 보낸 쿠키의 Key 이름
    • 따로 입력하지 않으면 해당 변수의 이름(cookieValue)을 찾는다.
  • required
    • 세션에 해당 값이 필수적으로 존재해야 하는지 여부를 설정
    • true로 설정했는데 값이 없는 경우, ServletRequestBindingException을 발생시킨다.
    • false로 설정했는데 값이 없는 경우, sessionValue는 null이 할당된다.
    • 기본값: true

Tomcat

Spring Boot의 기본 서블릿 도구인 톰캣은 내부적으로 JSESSIONID 쿠키를 사용하여 세션을 관리한다.

그래서 서버에서 세션을 생성하면 톰캣은 자동적으로 JSESSIONID 라는 쿠키를 클라이언트에게 보내게 된다.

이는 각 클라이언트 마다 개별적인 HttpSession 스토리지를 생성하고 관리할 수 있도록 제공되는 기능이다.

컨트롤러로 넘어오는 Session 데이터는 내부적으로 JSESSIONID를 통해 먼저 특정지어지고 넘어오기 때문에 어떤 클라이언트인지 확인하는 과정을 생각하지 않아도 된다.

정말 간단하게 코드로 구현하면 아래와 같다고 볼 수 있다.

// Map<JSESSIONID, Map<Attribute Key, Attribute Value>>
Map<String, Map<String, Object>> sessionStorage;

이에 대한 자세한 설명은 이 블로그에서 볼 수 있다.

application.properties

# 쿠키의 도메인을 설정한다.
server.servlet.session.cookie.domain = 

# 쿠키의 자바스크립트 접근 여부를 설정한다.
# 기본값: false
server.servlet.session.cookie.http-only = 

# 쿠키의 최대 사용기간을 설정한다.
# 기간 접미사를 지정할 수 있다. (h, m, s)
# 기간 접미사를 지정하지 않으면 초(s)로 설정된다.
# 0으로 설정하면 쿠키의 생성 즉시 만료된다.
# -1로 설정하면 최대 수명이 없음을 의미한다.
server.servlet.session.cookie.max-age =

# 쿠키의 이름을 정의한다.
# 기본값: JSESSIONID
server.servlet.session.cookie.name = 

# 쿠키의 유효 경로를 설정한다.
# 기본값: /
server.servlet.session.cookie.path = 

# 쿠키의 허용 범위 정책을 설정한다.
# Same-Site: 도메인 + 접미사 ( github.io, naver.com )
# strict, lax, none
# - strict: Same-Site 만 허용
# - lax: strict + HTTP GET, <a>, <link> 허용
# - none: 모든 사이트에 허용
# 기본값: lax
server.servlet.session.cookie.same-site = 

# HTTPS에서 통신할지 여부를 설정한다.
# true:  HTTPS 통신에 쿠키를 보낼 수 있다.
# false: HTTPS 통신에 쿠키를 보낼 수 없다.
# 기본값: false
server.servlet.session.cookie.secure = 

# 쿠키를 브라우저가 아닌 디스크에 저장할지 여부
# true:  브라우저를 닫거나 컴퓨터를 재시작해도 남아있다.
# false: 브라우저를 닫으면 삭제된다.
# 기본값: false
server.servlet.session.persistent = 

# 세션 데이터를 저장할 디렉토리를 지정한다.
# 기본값: 없음
server.servlet.session.store-dir = 

# 세션의 만료 시간을 설정한다.
# 클라이언트가 세션에 접근할 때마다 초기화되며, 
# 접근하지 않은 상태로 아래에서 설정한 값이 지나면 삭제된다.
# 기본값: 30m
server.servlet.session.timeout = 30m

# 쿠키를 지원하지 않는 웹브라우저를 위해 만들어진 방법이다.
# 설정하지 않는다면 쿠키 값도 전달하고 URL에 JSESSIONID도 함께 전달하게된다.
# - http://localhost:8080/;jsessionid=...
# URL에 JSESSIONID를 노출시키고 싶지 않다면 cookie로 설정하면 된다.
server.servlet.session.tracking-modes = cookie

톰캣의 세션의 설정은 application.properties를 통해서 할 수 있으며, 그에 해당하는 설정은 위와 같다.

정리

세션과 쿠키가 무엇이고 Spring에서는 어떻게 사용하는지에 대해 알아보았다.

세션과 쿠키를 적절히 이용하면 Statefull한 서비스를 만들 수 있지만, 세션의 특성상 디스크가 아닌 메모리 영역에 저장이 되기 때문에 자칫하다가는 메모리 부족으로 장애가 일어날 수 있다.

그렇기 때문에 세션에 저장되는 데이터는 신중히 생각하여 결정해야하고, 세션의 유지시간을 적절하게 설정해야 한다.

또한, 세션은 하나의 서버에서 따로따로 관리되기 때문에 해당 서버가 아닌 다른 서버로 접속하면 클라이언트 입장에서는 세션이 만료된 것과 같은 경험을 겪게 된다. 이는 로드밸런서를 통해 여러 WAS를 관리하거나 서버의 장애가 나는 경우에 볼 수 있는데, 세션만을 저장하는 별도의 서버를 만들어 관리하거나 세션 클러스팅이라는 기술을 이용해서 해결해야 한다.

참고

profile
백엔드 개발자 지망생

0개의 댓글