토큰 버전 관리로 세션 만료 정책과 강제 로그아웃 도입
목차
세션 만료 정책과 강제 로그아웃 인프라를 한 번에 묶어서 배포했다.
작업 자체는 단순해 보이지만, 이게 건드리는 게 auth 전반이라 꽤 신경 썼던 작업이다. package.json, prisma/schema.prisma, migration.sql, auth-actions.ts, 그리고 어드민 UI까지 — 파일 스펙트럼이 DB 레이어부터 UI까지 수직으로 관통한다.
왜 지금 이 작업이었나
세션 정책이 느슨한 상태로 방치되고 있다는 건 알고 있었다. 로그인 후 세션이 사실상 무기한 유지되는 구조였고, 관리자가 특정 계정을 강제로 로그아웃시킬 방법도 없었다. 보안 감사 항목에 계속 올라오던 이슈였는데, 다른 피처 작업에 밀려서 미뤄두고 있었다.
결정적으로 움직이게 된 건 팀 내부 코드리뷰에서 "지금 세션 구조면 탈취된 토큰을 서버에서 무효화할 방법이 없다"는 코멘트가 나왔을 때다. 맞는 말이었다. 그냥 더 미루기엔 리스크가 너무 컸다.
작업 내용
크게 두 가지를 묶었다.
① 세션 maxAge 8시간 고정
기존에는 세션 만료 시간이 명시적으로 설정되어 있지 않아 라이브러리 기본값에 의존하고 있었다. 이걸 8h로 명시적으로 박았다.
// src/lib/auth-actions.ts (개념 예시)
session: {
strategy: "jwt",
maxAge: 8 * 60 * 60, // 8h
},
사내 서비스 특성상 업무 시간 기준으로 하루 세션이면 충분하다는 판단이었다. 너무 짧으면 UX 불편이고, 너무 길면 보안 리스크. 8h는 그 트레이드오프에서 팀이 합의한 값이다.
② token versioning으로 강제 로그아웃 구현
이게 이번 작업의 핵심이다. 강제 로그아웃을 구현하는 방법은 여러 가지인데, 비교하면 이렇다.
| 방식 | 장점 | 단점 |
|---|---|---|
| 블랙리스트 (Redis 등) | 즉각적 무효화 | 인프라 추가, 매 요청마다 조회 비용 |
| DB token 버전 관리 | 인프라 추가 없음, 구조 단순 | DB 조회 필요 (캐시로 완화 가능) |
| 단순 세션 삭제 | 구현 쉬움 | JWT stateless면 서버에서 제어 불가 |
JWT 기반 구조에서 "서버가 클라이언트 토큰을 무효화"하려면 결국 서버 사이드에 뭔가를 남겨야 한다. 블랙리스트는 Redis 같은 별도 인프라가 필요하고, 지금 스택에서 그걸 추가하는 건 오버엔지니어링이라고 판단했다.
그래서 선택한 게 token version 컬럼을 User 테이블에 추가하는 방식이다.
-- prisma/migrations/20260523120000_add_2fa_and_token_version/migration.sql
ALTER TABLE "User" ADD COLUMN "tokenVersion" INTEGER NOT NULL DEFAULT 0;
// prisma/schema.prisma
model User {
// ...
tokenVersion Int @default(0)
}
JWT payload에 발급 시점의 tokenVersion을 심어두고, 요청마다 DB의 현재 버전과 비교한다. 관리자가 강제 로그아웃을 누르면 DB의 tokenVersion을 increment — 기존에 발급된 토큰은 전부 버전 불일치로 거부된다.
// auth-actions.ts 개념
async function validateTokenVersion(userId: string, version: number) {
const user = await prisma.user.findUnique({ where: { id: userId } });
return user?.tokenVersion === version;
}
async function forceSignout(userId: string) {
await prisma.user.update({
where: { id: userId },
data: { tokenVersion: { increment: 1 } },
});
}
③ 어드민 UI 연동
UnifiedUsersClient.tsx에 강제 로그아웃 액션을 붙였다. 운영자가 특정 유저를 선택해서 즉시 세션 만료를 트리거할 수 있는 흐름이다. 이게 없으면 버전 관리 로직을 깔아도 운영에서 쓸 수가 없으니, UI까지 한 PR에 묶는 게 맞다고 봤다.
migration 파일명에 2fa가 같이 들어가 있는 건, 이번 마이그레이션이 2FA 관련 스키마 변경도 함께 포함하고 있기 때문이다. 두 피처를 같은 마이그레이션으로 묶은 건 배포 타이밍을 맞추려는 의도였다.
회고
이런 류의 작업은 기능 자체보다 "왜 지금, 왜 이 방식"을 팀에 설명하는 게 더 중요하다. 토큰 버전 방식은 매 요청마다 DB 조회가 추가된다는 단점이 있고, 트래픽이 커지면 부담이 될 수 있다. 이 트레이드오프를 팀에 명확히 공유하고 합의한 뒤 머지했다.
또 세션 maxAge 변경은 실사용자에게 바로 영향이 가는 변경이라, 배포 전에 운영팀에 공지했다. 코드만 바꾸고 조용히 배포했다가 "갑자기 로그인이 풀린다"는 문의가 쏟아지면 신뢰 비용이 더 크다.
auth 관련 변경은 항상 이 순서로 접근하게 된다 — 보안 요구사항 → 트레이드오프 분석 → 팀 합의 → 운영 공지 → 배포. 이번도 그 순서를 지켰고, 결과적으로 무난하게 롤아웃됐다.
다음은 이 버전 검증 로직에 캐시 레이어를 얹을지 검토할 예정이다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.