Technology/Web

상태유지기술, 쿠키(Cookie)와 세션(Session)

ikjo 2022. 7. 7. 06:09

무상태(Stateless) 프로토콜, HTTP

웹 브라우저와 웹 서버는 HTTP를 통해 데이터를 주고받으며, 웹 브라우저는 데이터 요청자로서, 웹 서버는 데이터 응답자로서 클라이언트/서버 구조를 이룬다. 이때 HTTP는 무상태 프로토콜이라고도 하는데, 서버가 해당 요청에 대한 응답을 하고 나면 그 이후로 클라이언트가 이전에 무엇을 했는지에 대한 정보를 기억하지 못하는 것이다.

 

이러한 점으로 인해 서버 입장에서는 확장성이 높아지는 장점이 있다. 왜냐하면 서버가 클라이언트별 정보를 별도로 저장하지 않으므로 특정 클라이언트를 전담하는 서버가 따로 존재하지 않기 때문에 클라이언트의 요청이 급격히 늘어나도 서버 입장에서는 서버 대수를 늘리면(스케일 아웃) 그만이다.

 

하지만 클라이언트 입장에서는 서버에 이전에 보냈던 데이터를 또 전송해야하는 단점이 있다. 이러한 문제를 해결하기 위해 등장한 것이 상태 유지 기술인 '쿠키(Cookie)'와 '세션(Session)'이다. 

 

 

쿠키(Cookie)

쿠키란?

쿠키는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각(문자열)이다. 브라우저 입장에서는 이 데이터 조각들을 저장해 놓았다가 동일한 서버에 재 요청시에 저장된 데이터를 함께 전송한다. 이를 통해 서버는 여러 요청들이 동일한 브라우저에서 왔는지를 판단할 수 있게 된다. 예를 들면, 사용자의 로그인 상태를 유지할 수 있는 것이다.

 

쿠키의 용도

쿠키는 주로 세 가지 목적(세션 관리, 개인화, 트래킹)을 위해 사용된다. 우선 세션 관리이다. 서버에서 별도로 세션 저장소를 관리하고 세션 ID별 사용자 상태 정보를 저장함으로써 서버는 사용자의 로그인 여부를 판단할 수 있고 그 외, 장바구니나 게임 스코어 등의 정보를 관리할 수 있게 된다.

 

두 번째로는 개인화이다. 쿠키를 통해 사용자별로 선호하는 테마 등을 설정해줄 수 있다. 트래킹을 목적으로도 사용될 수 있다. 쿠키를 통해 사용자별로 행동을 기록하고 분석하는 용도로도 사용될 수 있는 것이다.

 

하지만 사용자가 공용 컴퓨터(PC방 등)에서 특정 웹 사이트의 쿠키를 저장하다보면 자신의 사용 정보가 타인에게 노출될 수 있는 우려도 있다. 이러한 우려 때문에 웹 브라우저는 각각의 웹 사이트당 허용하는 쿠키를 제한하기도 한다.

 

쿠키 헤더

서버는 특정 HTTP 요청에 대해 쿠키를 생성하여 이를 Set-Cookie 헤더안에 포함하여 응답할 수 있는데, 쿠키는 다음과 같이 이름(name, String)과 값(value, String) 쌍으로 데이터(텍스트)를 저장한다. (이때 하나의 쿠키는 4KByte 크기로 제한된다.)

 

Set-Cookie: <cookie-name>=<cookie-value>

 

이러한 쿠키는 보통 웹 브라우저에 의해 (로컬 등에) 저장된다. 이후 해당 쿠키는 웹 브라우저가 해당 서버에 요청을 보낼 때 다음과 같이 Cookie 헤더안에 포함되어 전송된다.

 

Cookie: <cookie-name>=<cookie-value>

 

서버가 쿠키를 생성할 때는 만료일 또는 지속시간을 설정할 수 있는데, 만료된 쿠키는 더이상 브라우저에서 서버로 전송할 수 없게 된다. 아울러, 쿠키가 유효한 도메인이나 경로 또한 설정해줄 수 있다. 이를 통해 쿠키가 원치 않는 도메인이나 리소스로 로 전송되는 것을 제한할 수 있다.

 

구분 속성명 내용 예시
유효기간 expires 해당 만료일이 되면 쿠키가 삭제된다. Set-Cookie: expires=Sat, 26-Dec-2020 04:39:21 GMT
max-age 유효기간(초, second)을 설정하고 유효기간이 경과된 후 쿠키가 삭제된다. 이때 0이나 음수를 지정하면 웹 브라우저에 저장된 쿠키가 삭제된다. Set-Cookie: max-age=3600
도메인 domain 도메인 속성값을 명시하는 것은 해당 쿠키가 유효한 도메인을 설정하는 것으로 명시한 도메인뿐만 아니라 서브 도메인에서도 유효한 쿠키가 생성된다. 만일 도메인 속성값을 생략한 경우에는 쿠키를 생성한 현재 문서 기준의 도메인에서만 쿠키가 유효하다. Set-Cookie: domain=example.org
(dev.example.org에서도 쿠키 유효)
경로 path 경로 속성값을 명시하는 경우 해당 경로를 포함한 하위 경로 페이지에서만 쿠키가 유효하다. 쿠키는 보통 한 도메인 내 전체에서 사용하기 때문 일반적으로 path=/(루트)로 지정한다. Set-Cookie: path=/

 

참고로 쿠키에는 영속 쿠키세션 쿠키가 있는데, 영속 쿠키란 만료 날짜를 입력함으로써 해당 날짜까지 유지되는 쿠키를 뜻하며, 세션 쿠키는 만료 날짜를 생략함으로써 브라우저가 종료될 때까지만 유지되는 쿠키를 뜻한다.

 

쿠키의 Secure 및 HttpOnly 설정

Secure 설정 시 해당 쿠키는 https 프로토콜 상에서 암호화된 요청일 경우에만 전송되게 된다. (그럼에도 불구하고 민감 정보는 쿠키에 절대 저장하면 안 된다.) http에 대해서는 쿠키에 Secure 설정을 할 수 없다.

 

HttpOnly 설정 시 자바스크립트 단에서 document.cookie API를 통해 쿠키에 접근할 수 없게 된다. 해당 쿠키는 서버에 전송만 할 수 있는 것이다. 이는 XSS 공격을 방지하는 효과가 있다. 

 

쿠키의 SameSite 설정

SameSite 설정을 통해 Cross Stie인 경우 쿠키를 서버에 전송하지 못하도록 할 수 있다. 이를 통해 CSRF 공격을 방지할 수 있다.

 

쿠키 생성 및 처리(스프링)

스프링 웹 앱의 경우 자바의 javax.servlet.http 패키지의 Cookie 객체를 통해 다음과 같이 쿠키를 생성할 수 있다.

 

    @GetMapping("/cookies")
    public void cookies(HttpServletResponse response) {
        // 서버에서 쿠키 생성
        Cookie cookie = new Cookie("Name", "Value");

        // 쿠키의 유효기간 설정 : setMaxAge()는 초 단위의 정수형 인자를 둔다. 10 * 60 = 600초 = 10분
        cookie.setMaxAge(60 * 10);

        // 쿠키를 클라이언트에 전송
        response.addCookie(cookie);
    }

 

또한 브라우저에서 보낸 쿠키 정보를 읽을 수도 있다. 이때 하나의 서버가 클라이언트에 쿠키를 여러 개 보낼 수 있으므로 클라이언트 역시 쿠키를 서버에 여러개 보낼 수 있기 때문에 배열을 사용한다. 이때 쿠키 값이 없으면 null이 반환된다. (브라우저마다 다르지만 보통 웹 사이트당 최대 20개의 쿠키만 허용한다.)

 

    @GetMapping("/cookies")
    public void cookies(HttpServletRequest request) {
        // 클라이언트에서 보낸 쿠키 정보 읽기
        Cookie[] cookies = request.getCookies();
    }

 

쿠키를 삭제하는 명령은 별도로 없다. 쿠키의 관리는 웹 클라이언트가 하기 때문이다. 대신 서버 측에서 클라이언트의 쿠키를 삭제하기 위해서는 seMmaxAge의 인자를 0으로 두어 쿠키를 전송하는 방법이 있다.

 

    @GetMapping("/cookies")
    public void cookies(HttpServletResponse response) {
        // 클라이언트에게 쿠키 삭제 요청
        Cookie cookie = new Cookie("이름", null);
        cookie.setMaxAge(0);
        response.addCookie(cookie);
    }

 

이외에도 org.springframework.http 패키지의 ResponseCookie 객체를 통해서도 쿠키를 생성할 수 있다.

 

    @GetMapping("/cookies")
    public void cookies(HttpServletResponse response) {
        // 서버에서 쿠키 생성
        response.setHeader("Set-Cookie", ResponseCookie
            .from("Name", "Value")
            .path("/")
            .build()
            .toString());
    }

 

 

세션(Session)

세션이란?

세션은 쿠키와 함께 무상태 특성의 HTTP를 보완하고자 나온 상태 유지 기술로서 클라이언트 별로 서버에 저장되는 정보(Key-Value 자료구조)라고 할 수 있다.

 

https://docs.spring.io/spring-boot-data-geode-build/1.2.x/reference/html5/guides/caching-http-session.html

 

이때 WAS에 세션(Session)을 저장하게 되면 기본적으로 서버 메모리에 저장되는데, 어떤 서버(WAS)가 shutdown되면 해당 서버 메모리에 저장된 세션 정보가 모두 사라질 수 있다. 따라서 세션 정보가 영속성을 가지기 위해서는 WAS간 세션 동기화(2개 이상 WAS 운용 시) 내지 DB 같은 별도 저장소에 저장해야한다.

 

그리하여 웹 서버에 급격한 트래픽이 몰리게 되면 웹 서버에 저장되는 세션 또한 급격히 늘어나므로 서버에 부담이 되므로 세션이 종료되는 시점을 정해주는 게 중요하다. 세션의 유지 시간(세션 타임아웃)은 클라이언트가 서버에 마지막으로 요청한 시간으로부터 지정된 시간까지의 시간으로서, 특별히 지정하지 않으면 세션의 유지 시간은 기본적으로 30분으로 설정된다.

 

※ 스프링 부트에서는 server.servlet.session.timeout 라는 글로벌 설정을 통해 세션 타임아웃을 설정할 수 있다. 또는 특정 세션 단위로 설정 시 setMaxInactiveInterval 메서드를 사용할 수 있다.

 

이와 더불어 세션에는 최소한의 데이터만 보관하는 것이 좋다. 데이터 용량 * 사용자 수로 서버 메모리를 점유하는 세션 데이터가 급격히 늘어나기 때문이다.

 

 

세션 동작원리

 

 

우선 클라이언트가 서버측에 로그인을 요청하면 서버는 DB를 통해 해당 사용자에 대한 인증 절차를 거친다. 이때 유효한 회원이라면 서버는 해당 클라이언트를 식별할 수 있는 id 값인 session id를 생성(브라우저당 1개씩 생성)하고 이를 세션 저장소에 저장하고 세션 객체를 발급받는다. 이 세션 객체에는 해당 사용자에 대한 상태 정보가 저장된다.

 

이때 스프링 웹 앱에서는 이 session id를 jsessionid라고 하며, 세션 객체를 HttpSession이라고 한다.

 

https://docstore.mik.ua/orelly/xml/jxslt/ch08_02.htm

 

이후 서버는 HTTP 응답과 함께 session id를 저장하고 있는 쿠키를 생성하여 클라이언트에 응답해주고 클라이언트는 서버에 재요청을 할 때 서버로부터 받았던 session id를 저장하고 있는 쿠키를 함께 전송해준다.

 

서버는 이 쿠키에 있는 session id를 통해 앞서 요청에서 생성했던 HttpSession을 찾고 해당 세션을 검증한 후 클라이언트의 요청을 처리하고 클라이언트에 응답해준다.

 

서버는 각각의 브라우저별로 session id를 가지고 있으므로 특정 계정을 공유하는 것을 허용 및 제한하거나 특정 사용자를 강제로 로그아웃 시킬 수 있는 등 사용자 관리 기능을 구현할 수 있다.

 

세션 생성 및 처리(스프링)

스프링을 통한 웹 앱을 개발하는 경우 자바의 javax.servlet.http 패키지 HttpSession 인터페이스를 제공하며 다음과 같이 세션을 생성 및 처리할 수 있다.

 

    @GetMapping("/sessions")
    public void sessions(HttpServletRequest request) {
        // 세션 생성
        // HttpSession session = request.getSession(true); 와 동일
        HttpSession session = request.getSession();
        
        Member member = new Member("ikjo");
        
        // 세션에 사용자 상태값 저장
        session.setAttribute("user", member)

        // 세션 값 조회
        Member user = (Member) session.getAttribute("user");

        // 세션 값 삭제
        session.removeAttribute("user"); // name 값에 해당하는 세션 정보를 삭제
        session.invalidate(); // 모든 세션 정보를 삭제
    }

 

사용자가 로그인을 한 후 로그아웃 요청이 들어올 때 서버에서는 HttpSession 메서드 removeAttribute 또는 invalidate를 사용하여 이 요청을 처리할 수 있다. 이때 이 둘의 차이점은 무엇일까?

 

우선 removeAttribute는 세션의 특정 name 값의 데이터(객체)를 제거하는 것이다. 반면 invalidate는 해당 세션을 비활성화 시키고 해당 세션과 연결된 모든 데이터(객체)들의 결속을 해제(HttpSession에 의해 참조 X -> GC 대상)시킨다. 둘 다 사용자의 로그아웃 요청을 처리할 수는 있지만 운영 상에서 차이가 있다.

 

만일 수많은 사용자들이 로그인을 했다가 로그아웃을 했는데 서버에서 removeAttribute 처리를 해주어 특정 세션 데이터만 삭제되고 나머지 데이터는 그대로 있게 된다면 이 또한 서버 메모리를 점유하게 되는 것이므로, 서버 입장에서는 가용성에 나쁜 영향, 혹여나 보안에도 문제가 생길 수 있다. 이에 로그아웃을 처리 할 때는 removeAttribute 보다는 invalidate를 많이 사용한다고 한다.

 

 

참고자료

  • 인프런 모든 개발자를 위한 HTTP 웹 기본 지식
  • https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies
  • https://sowon-dev.github.io/2020/06/28/200629jspi/