시스템 디자인SYSTEM DESIGN
학습 시작하기 →
AUTHENTICATION & SESSIONS

로그아웃했는데, 세션은 아직 살아 있을까?

로그인 화면으로 돌아갔다고 끝난 것은 아닙니다. 방금 로그아웃한 세션을 다시 검사해 봤습니다.

비워진 브라우저 프레임과 서버의 두 토큰 통로 중 한 곳만 닫혀 있는 세션 폐기 모형
내용의 흐름을 표현한 개념 이미지

로그아웃을 누르니 로그인 화면으로 돌아왔습니다. 이름도 사라졌고, 쿠키도 비었습니다. 여기까지만 보면 끝난 것 같죠. 그런데 로그아웃 전에 쓰던 세션 값을 다시 보내도 서버가 받아 준다면, 화면만 로그아웃된 겁니다. 저는 이 두 가지를 따로 확인하는 편이 낫다고 봅니다. 사용자가 보게 되는 화면과, 서버가 더 이상 허용하지 않을 요청은 다른 문제니까요.

이번에는 인증 기능을 새로 붙이지 않고, 세션을 발급하고 확인하는 작은 Node.js 모델을 만들었습니다. 브라우저 쪽 값만 비운 경우와 서버 기록까지 폐기한 경우를 같은 조건으로 실행했습니다. 먼저 결과를 말하면, 값을 지우는 위치에 따라 이전 세션의 판정이 200과 401로 갈렸습니다. 왜 갈리는지부터 차근차근 보겠습니다.

1. 내 손에서 없어진 것과, 효력이 없어진 것은 다릅니다

출입증을 버렸다고 출입 시스템의 등록까지 지워지는 것은 아닙니다. 누군가 같은 출입증을 갖고 있다면 단말기는 여전히 유효하다고 판단할 수 있습니다. 세션 쿠키도 비슷합니다. 브라우저가 보관하는 값은 서버에 자신을 식별할 때 쓰는 수단이고, 그 값을 계속 받아 줄지는 서버가 결정합니다.

이 글에서 다루는 방식은 서버에 세션 기록을 두고, 브라우저에는 뜻을 해석할 수 없는 임의의 토큰을 주는 방식입니다. 서버는 토큰에 대응하는 기록이 있는지, 폐기되거나 만료되지는 않았는지 확인합니다. 쿠키 삭제는 그다음 브라우저가 토큰을 자동으로 보내지 못하게 하는 처리입니다. 둘 중 하나만 구현하면 역할 하나가 빠집니다.

실제 구현을 읽어볼 출발점도 있습니다. Coreline Auth 공개 README는 세션 확인과 로그아웃을 별도 메서드로 소개하며, 데모 로그아웃에서 토큰 폐기와 쿠키 정리를 함께 설명합니다. 이 글의 실행 예제는 그 모듈을 가져온 것이 아니라, 두 책임의 차이를 확인하려고 별도로 작성한 것입니다.

서버 쪽 세션 무효화와 만료 처리는 OWASP Session Management Cheat Sheet에서도 강조합니다. 여기서는 전체 인증 체계를 다루기보다, 로그아웃 완료를 어디에서 확인할지에 초점을 맞췄습니다.

2. 같은 세션으로 다시 확인해 봤습니다

예제에서는 임의 토큰을 발급하고 서버 Map에는 해시를 키로 저장합니다. 브라우저의 쿠키를 직접 조작하는 대신 토큰 변수 하나를 비웠습니다. 그 전에 보관한 토큰으로 검증 함수를 다시 불러, 서버의 수락 여부가 바뀌는지 확인했습니다. 실제 사용자 세션이나 운영 서버에는 접근하지 않았습니다.

2026-09-09 실행 · Node.js v24.13.1 · 메모리 모델의 판정 코드이며 실제 HTTP 응답은 아닙니다.
검사 순서판정확인한 점
로그아웃 전 토큰200기존 세션은 유효
브라우저 변수만 비운 뒤 이전 토큰200서버 기록은 그대로 남음
서버 기록 폐기 뒤 이전 토큰401다시 제출해도 거절
동일 토큰으로 로그아웃 반복401 유지폐기 상태가 되살아나지 않음
현재 세션만 종료한 뒤 다른 기기200다른 세션은 유지
모든 세션 종료 뒤 다른 기기401계정의 기존 세션도 거절

브라우저 변수는 두 번째 검사에서 이미 비웠습니다. 그래도 서버는 토큰을 받으면 200을 반환했습니다. 반면 서버 기록을 삭제한 다음에는 같은 값이 돌아와도 401이었습니다. 화면이 어디로 이동했는지는 이 차이에 영향을 주지 않습니다.

세션 폐기 실행 예제 받기 · Node.js ↗외부 패키지·네트워크 연결 없이 실행합니다. 실제 토큰은 출력하지 않습니다.
node logout-sessions.mjs
# afterCookieOnlyReplay: 200
# afterServerRevocation: 401
# otherDeviceAfterCurrentLogout: 200
# afterAllLogout: 401
# assertions: PASS

이 예제가 증명하는 범위는 단일 프로세스의 세션 상태 변화입니다. TLS, 쿠키 전달, CSRF, 저장소 장애, 여러 서버 사이의 캐시 전파는 넣지 않았습니다. 인증 라이브러리로 배포하는 코드가 아니라, 로그아웃 테스트의 기대 결과를 분리해 볼 수 있는 예제입니다.

3. 로그인 화면으로 보내는 일은 마지막입니다

순서를 잡으면 놓치는 부분이 줄어듭니다. 세션 기반 로그아웃 요청을 받은 서버는 요청의 출처와 CSRF 보호 정책을 확인하고, 해당 세션을 폐기합니다. 그다음 쿠키를 지우는 응답을 보내고 화면을 정리합니다. 상태를 바꾸는 요청이므로 단순한 페이지 조회와도 구분해 두는 편이 좋습니다.

  1. 요청 확인세션·출처·CSRF 정책 검사
  2. 서버 기록 폐기같은 토큰의 재사용 차단
  3. 쿠키 만료 응답기존 이름·범위에 맞춰 정리
  4. 화면 정리개인 데이터 제거·로그인 안내

쿠키를 지울 때도 아무 쿠키 하나를 만료시키면 되는 것은 아닙니다. 처음 설정한 이름, Path, Domain 범위와 맞춰야 합니다. 호스트 전용 쿠키로 발급했다면 삭제할 때 불필요한 Domain을 추가하지 않습니다. HttpOnly 쿠키는 페이지 JavaScript가 지우는 대신 서버의 Set-Cookie 응답으로 정리합니다. 구체적인 속성 동작은 MDN의 쿠키 사용 문서에서 확인할 수 있습니다.

서버 기록 삭제가 실패했다면 어떨까요? 브라우저를 로그인 화면으로 보내는 것까지는 가능해도, 서버에서 종료됐다고 말할 수는 없습니다. 사용자에게 재시도할 경로를 주고, 폐기 요청의 실패를 운영에서 확인할 수 있어야 합니다. 저는 이런 경우를 정상 로그아웃 성공 수에 섞지 않겠습니다. 화면 정리 성공과 서버 폐기 성공을 같은 값으로 집계하면 실패가 숨어 버립니다.

4. ‘이 기기만’과 ‘모든 기기’는 버튼 이름부터 달라야 합니다

노트북에서 로그아웃했는데 휴대전화까지 풀리면 당황스럽습니다. 반대로 비밀번호를 바꿨는데 잃어버린 기기의 세션이 그대로 남아 있어도 곤란합니다. 둘 다 세션 종료지만 사용자의 기대가 다릅니다. 먼저 무엇을 끊을 것인지 정하고, 그 범위가 화면에 드러나게 해야 합니다.

현재 세션 종료는 세션 하나만 폐기하면 됩니다. 전체 세션 종료는 사용자에게 연결된 기존 세션을 모두 무효화해야 합니다. 세션 행들을 찾아 폐기할 수도 있고, 사용자 단위 버전 번호를 올려 이전 버전의 세션을 거절할 수도 있습니다. 첨부 예제는 후자를 사용했습니다. 새 로그인에는 새 버전을 붙여 정상 로그인까지 영구 차단되는 일이 없는지도 확인했습니다.

다만 버전 번호도 서버가 실제로 조회해야 효력이 있습니다. 각 서버가 사용자 버전을 오래 캐시하면 그 시간만큼 이전 세션이 살아 있을 수 있습니다. ‘전체 종료’라는 문구를 붙였다면 허용 가능한 반영 지연과 캐시 무효화 방법도 같이 정해야 합니다. 중요한 요청은 최신 폐기 상태를 확인하는 편이 안전합니다.

5. JWT라면 쿠키와 서버 기록만으로 설명이 끝나지 않습니다

JWT를 쓰는 서비스에서는 먼저 무엇을 검사하는지 봐야 합니다. 리소스 서버가 토큰의 서명과 만료 시각만 확인한다면, 브라우저에서 지운 토큰도 만료 전까지는 유효하다고 판단할 수 있습니다. 중앙 세션 저장소를 조회하지 않는 장점의 반대편에, 즉시 폐기를 전달해야 하는 부담이 있는 겁니다.

짧은 access token 수명과 refresh token 폐기를 조합하면 다음 갱신을 막을 수 있습니다. 하지만 이미 발급된 access token까지 즉시 무효화되는지는 별개입니다. 정말 즉시 끊어야 한다면 폐기 목록, 세션 버전 확인, introspection 같은 상태 조회가 필요한지 검토해야 합니다. 방식마다 저장소 조회 비용과 장애 시 처리 정책이 달라집니다.

OAuth 토큰 폐기의 표준 인터페이스는 RFC 7009가 다룹니다. 우리 서비스의 세션 종료, 공급자 토큰 폐기, 공급자 웹사이트의 로그인 종료를 하나의 버튼이 모두 해결한다고 가정하면 안 됩니다. 서비스가 실제로 제공하는 범위를 구분해서 안내해야 합니다.

6. 뒤로가기보다 먼저, 이전 세션을 다시 검사합니다

뒤로가기를 눌러 개인정보가 다시 보이면 점검해야 합니다. 다만 이전 HTML이 브라우저에 남은 것인지, 서버가 새 조회를 허용한 것인지 구분해야 원인을 찾을 수 있습니다. 로그아웃 후 개인 화면의 메모리 상태를 비우고 민감한 응답에 적절한 캐시 정책을 적용하되, 서버 인증 검사를 화면 처리로 대체하지는 않습니다.

  • 로그아웃 전에 발급한 테스트 세션으로 보호된 API를 다시 호출하면 거절되는지 확인합니다.
  • 현재 기기 종료와 전체 기기 종료의 결과를 두 세션으로 비교합니다.
  • 서버를 두 대 이상 두었다면 다른 서버에서도 폐기가 반영되는지 확인합니다.
  • 유휴 만료와 절대 만료를 각각 검사합니다. 활동이 있어도 절대 만료는 연장되지 않아야 합니다.
  • 저장소 장애가 났을 때 실패를 성공으로 표시하지 않는지 봅니다.
  • 감사 로그에 세션 원문이나 인증 헤더가 남지 않는지 확인합니다.

예제에서도 유휴 만료 경계 300초와, 활동이 이어져도 절대 만료되는 20초 경계를 별도로 검사했습니다. 이것은 설명을 위한 논리 시각이지 권장 만료 설정이 아닙니다. 실제 만료 시간은 데이터 민감도와 사용 흐름에 맞춰 정해야 합니다.

결론은 ‘화면에서 나갔다’보다 ‘다시 들어오지 못한다’입니다

로그아웃 화면을 복잡하게 만들 필요는 없습니다. 오히려 버튼은 단순해야 합니다. 대신 서버에서는 종료 범위, 폐기 성공 여부, 이전 토큰의 재사용 거절을 분명히 확인해야 합니다. 이 부분이 빠지면 쿠키를 지우고도 세션은 살아 있게 됩니다.

제가 먼저 넣을 테스트는 하나입니다. “방금 로그아웃한 세션을 다시 제출하면 어떻게 되는가?” 그다음 다른 기기와 만료, 서버 간 전파를 넓혀 보겠습니다. 인증 기능의 개수보다 이런 종료 경계가 더 중요할 때가 있습니다.