로그인 서버가 멈추면 위키도 멈춰야 할까?
매 요청마다 Auth에 세션을 묻던 구조를 짧은 서명 토큰과 Region의 로컬 보안 정보로 바꾼 과정.
Sevenwiki — The Knowledge InfrastructureAugust 31, 2026
처음에는 매번 Auth에 물어봤습니다
Auth를 별도 서비스로 나눈 뒤 가장 먼저 만든 인증 흐름은 평범했습니다.
Browser → Region → Auth → Redis
↓
session Region이 요청을 받을 때마다 Auth에 세션이 유효한지 물어봅니다. Auth는 Redis에서 세션을 찾고 사용자 정보를 돌려줍니다.
이 방식은 이해하기 쉬웠습니다. 관리자가 세션을 끊으면 바로 다음 요청부터 거부할 수 있고, 세션의 기준도 Auth 한곳에만 있었습니다. 처음에는 저도 이 구조가 가장 안전하다고 생각했습니다.
그런데 Region을 여러 곳에 두고 보니 이상했습니다.
공개 문서를 읽는 요청도 브라우저에 로그인 쿠키가 있다는 이유로 중앙 Auth를 기다렸습니다. Auth 애플리케이션이나 Redis가 느려지거나, 중간 Tunnel의 응답이 늦으면 모든 Region의 문서 응답도 함께 느려졌습니다.
Region을 사용자와 가까운 곳에 둬도 인증 한 번 때문에 중앙 서버를 왕복한다면 지연을 줄인 의미가 많이 사라집니다. 무엇보다 Auth 한곳의 장애가 모든 위키의 읽기와 편집으로 번졌습니다.
로그인과 장기 세션은 중앙에서 관리하되, 평소 문서 요청까지 매번 Auth를 기다리게 하고 싶지는 않았습니다.
오래 유지할 세션과 짧게 쓸 증명을 나눴습니다
V7에서는 로그인 상태를 오래 유지하는 인증 정보와, 각 요청에 사용하는 짧은 접근 토큰을 분리했습니다.
장기 세션을 갱신하는 값은 Auth만 해석할 수 있는 opaque credential입니다. 토큰 갱신과 로그아웃, 기기 관리에 사용하며 Region에는 전달하지 않습니다.
Region으로 보내는 것은 Auth가 EdDSA로 서명한 짧은 access assertion입니다.
user_id
session_id
security_generation
recovery_generation
issued_at / expires_at
assertion_id Region은 이 토큰을 받으면 다음을 직접 확인합니다.
kid와 서명 알고리즘이 허용된 값인가- 발급자와 대상이 정확한가
- V7 접근 토큰이라는 타입이 맞는가
- 서명이 유효한가
- 만료 시각이 지나지 않았는가
- 허용한 최대 수명보다 지나치게 길지 않은가
검증용 공개키는 배포 설정으로 Region에 전달합니다. 요청마다 Auth에 공개키를 조회하지 않습니다.
키를 교체할 때는 잠시 이전 키와 새 키를 함께 허용한 뒤, 충분한 시간이 지나면 이전 키를 제거합니다. 중앙 호출을 없애는 대신 키 배포와 교체 절차까지 직접 책임져야 했습니다.
서명이 맞다고 지금도 유효한 사용자는 아닙니다
토큰 서명이 올바르다는 것은 Auth가 한때 이 토큰을 발급했다는 뜻입니다.
발급 이후 사용자가 로그아웃했을 수도 있고, 관리자가 계정을 정지했을 수도 있습니다. Auth 데이터베이스가 PITR로 과거로 돌아가 토큰이 다른 복구 이력에서 만들어졌을 가능성도 있습니다.
그래서 Region은 서명만 보지 않고 로컬 PostgreSQL에 저장한 보안 정보와 함께 확인합니다.
서명과 만료 확인
+
Region의 로컬 보안 상태
↓
현재 요청의 사용자 확정 구체적으로는 다음을 봅니다.
- 토큰의
recovery_generation이 현재 Auth 복구 세대와 같은가 - 해당 사용자가 Region에 존재하며 삭제 상태가 아닌가
- 토큰의
security_generation이 로컬 값보다 오래되지 않았는가 session_id가 폐기된 세션 목록에 없는가- 토큰 자체가 아직 유효한가
이 과정에는 Auth HTTP 요청도, Auth Redis 조회도 없습니다.
새로 발급된 토큰의 사용자별 보안 세대가 Region보다 높다면, 서명된 값을 이용해 그 사용자의 로컬 값을 앞으로 갱신할 수 있습니다. 다만 전체 보안 이벤트의 마지막 처리 버전까지 올리지는 않습니다.
한 사용자의 최신 토큰을 봤다는 사실이, 그 사이 다른 사용자의 세션 폐기 이벤트도 모두 받았다는 뜻은 아니기 때문입니다.
사용자 한 명의 상태와 전체 이벤트 흐름을 같은 숫자로 뭉치지 않은 이유입니다.
로그아웃했는데 5분 더 로그인되는 건 싫었습니다
접근 토큰을 짧게 만들었다면 로그아웃 뒤에는 만료만 기다리는 방법도 있습니다.
구현은 훨씬 단순합니다. Auth에서 장기 세션만 지우고, 이미 발급된 접근 토큰은 몇 분 뒤 자연스럽게 끝나게 두면 됩니다.
하지만 이 경우 사용자가 로그아웃 버튼을 눌러도 기존 토큰은 남은 수명 동안 계속 동작합니다. 토큰을 탈취당해 세션을 강제로 끊었을 때도 같은 문제가 생깁니다.
V7은 일반 로그아웃도 세션 폐기 이벤트로 각 Region에 전달합니다.
Auth는 세션을 폐기할 때 변경 기록과 outbox를 같은 트랜잭션에 남깁니다. 이벤트를 받은 Region은 해당 session_id를 로컬 폐기 목록에 저장하고, 이후 요청에서 거부합니다.
비동기 전달이므로 모든 Region이 정확히 같은 순간에 바뀐다고 약속할 수는 없습니다. 대신 두 겹으로 제한합니다.
- 접근 토큰 자체의 수명을 짧게 둡니다.
- 세션 폐기 이벤트를 빠르게 전달하고, 놓친 Region은 변경 내역이나 스냅샷으로 복구합니다.
중앙 Auth를 매 요청마다 호출하지 않으면서도 로그아웃을 단순히 “다음 토큰 갱신부터 막기”로 약화시키지 않기 위한 절충이었습니다.
SSE는 연결할 때 한 번만 보면 안 됐습니다
일반 HTTP 요청은 인증을 확인한 뒤 금방 끝납니다. SSE 이벤트 스트림은 훨씬 오래 열려 있습니다.
연결을 시작할 때만 토큰을 확인하면, 그 뒤 세션이 폐기되거나 토큰이 만료돼도 이미 버퍼에 들어간 이벤트가 계속 사용자에게 나갈 수 있습니다.
처음에는 producer task만 중단하면 될 것 같았습니다. 하지만 producer가 멈춰도 bounded channel 안에는 이미 만들어진 이벤트가 남을 수 있었습니다.
그래서 V7의 활동 스트림은 세 단계에서 다시 봅니다.
연결 시작
→ 토큰과 로컬 상태 확인
연결 유지 중
→ 15초마다 세션 상태 재확인
이벤트를 실제 응답에 쓰기 직전
→ 만료와 무효화 상태 마지막 확인 세션이 무효화되면 버퍼의 일반 이벤트를 더 보내지 않습니다. 대신 auth_invalidated를 전달하고 스트림을 닫습니다.
권한 검사를 handler 입구에만 놓지 않고, 정보가 실제로 외부로 나가는 마지막 지점까지 가져간 셈입니다.
이 부분은 구현하면서 특히 인상적이었습니다. 요청을 인증했다는 사실과, 몇 분 뒤의 응답을 계속 보여줘도 된다는 사실은 같지 않았습니다.
로컬 보안 정보가 오래되면 어떻게 할까?
Auth에 매번 묻지 않는 순간 Region이 가진 보안 정보의 최신 상태를 직접 판단해야 합니다.
최근 이벤트를 하나 받았다고 무조건 최신인 것은 아닙니다. 중간 이벤트를 빠뜨린 채 가장 새 번호만 봤을 수도 있습니다.
V7의 security_projection_is_fresh는 다음을 함께 확인합니다.
- 현재
recovery_generation - 마지막으로 빠짐없이 적용한 보안 버전
- 최근 Auth에서 확인한 최신 보안 버전
- 마지막 확인 시각
Auth가 알려준 최신 버전까지 연속해서 반영했는지를 봅니다.
그렇다고 보안 정보가 조금 오래됐다는 이유로 모든 요청을 닫으면 온라인 세션 조회를 없앤 의미가 없습니다.
공개 문서를 읽는 것과 ACL을 바꾸거나 사용자를 차단하는 일은 위험도가 다릅니다. 그래서 V7은 오래된 보안 상태에서 모든 기능을 똑같이 처리하지 않습니다.
최신 전역 상태가 꼭 필요한 ACL 변경과 역할 부여, 파괴적인 모더레이션 같은 작업은 중단합니다. 일반 문서 요청은 짧은 토큰 수명과 사용자별 보안 세대, 로컬 세션 폐기 정보를 기준으로 처리합니다.
가용성과 보안을 하나의 true/false로 결정하지 않고, 어떤 작업인지에 따라 허용 범위를 나눴습니다.
네트워크 호출 하나를 없애고 꽤 많은 것을 떠안았습니다
이 구조로 평소 Region 요청에서 Auth와 Redis로 향하는 네트워크 왕복은 사라졌습니다.
Auth가 잠시 중단돼도 이미 발급된 접근 토큰과 로컬 보안 정보로 문서를 읽고 편집할 수 있습니다. 멀티 리전 구조에서 중앙 Auth의 지연이 모든 요청에 더해지는 문제도 줄었습니다.
대신 직접 운영해야 할 것이 꽤 많아졌습니다.
- 서명 키 배포와 교체
- 세션 폐기와 보안 이벤트 전달
- 이벤트 순서와 중복 처리
- Region별 반영 상태 확인
- 메시지 보존 기간을 넘긴 누락 복구
- PITR 전후를 구분할 복구 세대
- 오래 열린 SSE 연결의 재검증
온라인 세션 조회를 없앴다고 인증이 단순해진 것은 아닙니다.
매 요청마다 중앙에 물어보던 구조를, 짧게 유효한 서명 증명과 복구 가능한 로컬 상태로 바꾼 것입니다. 지연과 장애 범위는 줄었지만, 이제 Region이 자신이 가진 보안 데이터의 의미를 설명할 수 있어야 했습니다.
그리고 바로 그 지점에서 다음 문제가 나왔습니다.
이벤트를 하나 놓친 Region은 어떻게 다시 현재 상태를 따라잡을까?
다음 글에서는 JetStream 수신 워커를 다시 켜는 것만으로 해결되지 않았던 이벤트 누락과 PITR 복구 이야기를 다뤄보겠습니다.