계약이 끝난 점포의 계정을 전부 "사용 안 함"으로 바꿨는데, 이미 로그인해 있는 사람은 그대로 쓸 수 있었다. 세션 만료(12시간)까지 정상 사용자다. 버그가 아니라 세션 인증의 성질이다. 로그인할 때 한 번 검증하고 그 뒤로는 세션이 있다는 것만으로 인증된 요청이 된다. 발급한 것을 회수하는 경로는 따로 만들어야 한다. 그리고 세션을 지우려다 엉뚱한 걸 지울 뻔했다. 삭제 기준으로 잡은 속성이 "그 사람이 누구인지"가 아니었다.
세션 인증에는 "무효화"가 없다
로그인 시점에 계정을 검증하고, 통과하면 세션에 사용자 정보를 담는다. 그 다음부터는 세션이 있다는 것만으로 인증된 요청이 된다.
요청마다 하는 검사는 이 정도다.
// 요청한 점포와 세션의 점포가 같은지
if (!requestOffice.equals(sessionOffice)) { ... }
계정이 아직 유효한지는 안 본다. 볼 이유가 없었다. 로그인할 때 확인했으니까.
그래서 계정을 차단해도 이미 발급된 세션은 그대로 산다.
이건 세션 인증의 성질이다
토큰이든 세션이든 한 번 발급하면 회수 경로가 따로 필요하다. JWT를 쓰면 더 심하다. 서버에 상태가 없으니 만료 전에는 손을 못 댄다.
세션은 그나마 낫다. 저장소에 실물이 있으니 지우면 즉시 끊긴다. 우리는 Redis에 담고 있었다.
지우려다 엉뚱한 걸 지울 뻔했다
점포 단위로 끊으려고 세션 속성 중 storeCode로 검색했다. 33개 계정이 있는 점포인데 딱 하나 잡혔다.
그게 내 세션이었다.
이유가 있었다. 내부 운영 계정은 점포를 전환하며 여러 점포를 볼 수 있고, 그때마다 인터셉터가 세션의 storeCode를 조회 중인 점포로 덮어쓴다. 그 점포를 마지막으로 본 사람이 나였다.
그대로 지웠으면 나만 로그아웃되고 정작 대상은 그대로였다.
교훈은 단순하다. 세션 속성은 "그 사람이 누구인지"가 아니라 "지금 무엇을 보고 있는지"일 수 있다. 지우는 기준으로 쓸 값인지 먼저 확인해야 한다.
계정 아이디로 다시 훑었다. 활성 세션 99개를 전부 대조했더니 그 점포 계정은 0건, 차단된 계정도 0건이었다. 차단한 지 오래돼서 이미 다 만료된 것이다.
결국 지울 게 없었다. 하지만 "없다"를 확인한 것과 "있는데 못 찾은 것"은 다르다. 잘못된 기준으로 조회했으면 후자였다.
네임스페이스를 확인하지 않고 조회했다
여기서 한 번 더 틀렸다. 처음 조회했을 때 키가 하나도 안 나왔다. "세션이 없나 보다" 하고 넘어갈 뻔했다.
실제로는 키 접두어가 달랐다. 앱별로 네임스페이스를 나누는 커밋이 있었는데 그게 운영 브랜치에는 안 들어가 있었다. 코드 기준으로 접두어를 넘겨짚은 것이다.
admin:session:sessions:* → 0건
spring:session:sessions:* → 8,428건
0건은 "없다"가 아니라 "못 찾았다"일 수 있다. 조회 조건이 맞는지부터 확인해야 한다. 확실히 존재하는 것으로 한 번 돌려 보면 검사 자체가 동작하는지 알 수 있다.
같은 명령이 다른 데서 뭔가를 잡아낼 때만 0이 0이다 — 인프라 쪽에서도 같은 함정을 겪었다. 대조군 없는 0건은 근거가 못 된다.
제대로 고치려면
세션을 지우는 건 사후 처리다. 근본은 요청마다 계정 상태를 다시 보는 것이다.
인터셉터에 한 줄 넣으면 된다.
// 세션의 계정이 아직 유효한지
if (!userService.isActive(sessionUserId)) {
session.invalidate();
response.sendRedirect("/login");
return false;
}
비용이 걸리면 짧은 캐시를 둔다. 1분 캐시여도 12시간보다는 720배 낫다.
우리는 아직 안 넣었다. 이번 건은 오래전에 차단한 계정이라 급하지 않았고, 전체 요청에 조회를 하나 더 다는 판단이 필요해서다. 다만 즉시 차단이 필요한 상황이 오면 지금은 세션을 직접 지우는 수밖에 없다는 걸 알고 있는 것과 모르는 건 다르다.
급할 때 쓰는 방법
계정 기준으로 세션을 찾아 지운다. 점포나 다른 속성이 아니라 계정 아이디로.
KEYS 로 세션 키를 훑고
각 키의 사용자 아이디 속성을 읽어
대상이면 세션 키와 만료 키를 함께 지운다
만료 키를 같이 지우는 걸 잊으면 찌꺼기가 남는다.
운영에서 KEYS는 조심해야 한다. 키가 많으면 블로킹된다. 세션 수가 만 단위면 SCAN으로 나눠 도는 게 맞다.
정리
- 세션·토큰 인증은 발급 후 회수 경로가 따로 없으면 차단이 즉시 반영되지 않는다.
- 우리 경우 최대 12시간(세션 만료)까지 차단된 계정이 그대로 쓴다.
- 세션 속성은 "누구인지"가 아니라 "지금 무엇을 보고 있는지"일 수 있다. 삭제 기준으로 쓰기 전에 확인한다.
- 조회 결과 0건은 "없다"가 아니라 "못 찾았다"일 수 있다. 네임스페이스·접두어부터 확인한다.
- 근본 해결은 요청마다 계정 상태를 재검증하는 것. 짧은 캐시를 둬도 12시간보다는 훨씬 낫다.
'웹개발' 카테고리의 다른 글
| 메서드 이름이 트랜잭션을 결정한다 — Spring AOP 일괄 트랜잭션의 함정 (0) | 2026.09.17 |
|---|---|
| npm ci와 npm install 차이 — 속도가 아니라 락 파일을 대하는 태도다 (0) | 2026.09.14 |
| Spring readOnly 트랜잭션을 리더로 보내면 생기는 read-after-write 문제 (0) | 2026.09.13 |
댓글