프로그래밍/spring

[커리큘럼 페이지 프로젝트] 관리자 로그인 세션을 어떻게 관리할 것인가 - 로그인도 PM의 역할일까

d 0_0 b 2026. 6. 11. 14:35

 

미리보기

더보기

로그인도 PM의 역할일까

 

이번 고민을 하면서 로그인 구현도 결국 정책의 문제라는 생각이 들었다.

하지만 막상 관리자 로그인 방식을 정리하다 보니, 어떤 사용자를 허용할 것인지, 어떤 상황에서 접속을 끊을 것인지, 얼마나 오랫동안 로그인 상태를 유지할 것인지 정하는 일이었다.

 

 

생각해보면 서비스에서 로그인은 거의 기본값처럼 붙어 있다. 하지만 모든 로그인 방식이 같을 수는 없다. 쇼핑몰의 로그인, 커뮤니티의 로그인, 금융 서비스의 로그인, 관리자 페이지의 로그인은 각자 기준이 다르다. 어떤 서비스는 편의성이 더 중요하고, 어떤 서비스는 보안과 통제가 더 중요하다.

 

 

이번 프로젝트의 관리자 로그인도 그랬다.

관리자 계정은 여러 명이 동시에 접속하는 것보다 하나의 세션만 유지되는 편이 안전하다. 일정 시간 활동이 없으면 자동으로 끊기는 편이 맞다. 브라우저를 닫았을 때도 로그인 상태가 오래 남아 있는 것은 부담스럽다.

이 기준을 정하지 않고 바로 구현에 들어가면, 개발자는 각자 익숙한 방식으로 만들게 된다. 그러면 기능은 동작할 수 있지만 서비스에 맞는 로그인인지는 다시 봐야 한다.

 

 

그래서 로그인도 기획 단계에서 충분히 고민해야 하는 영역이라는 생각이 들었다. 단순히 “로그인 기능이 필요하다”가 아니라, “우리 서비스에서는 어떤 로그인 상태를 정상으로 볼 것인가”를 먼저 정해야 한다.

이번 작업을 하면서 기획과 개발의 경계가 생각보다 뚜렷하지 않다는 것도 느꼈다. PM이 모든 코드를 알아야 한다는 뜻은 아니다. 다만 서비스의 성격에 맞는 정책을 정하려면, 그 정책이 기술적으로 어떻게 구현되는지 어느 정도는 이해해야 한다.

로그인은 개발 기능처럼 보이지만, 실제로는 서비스 운영 방식의 일부다. 사용자의 편의성과 보안 사이에서 어디에 기준을 둘지 정하는 일이다.

 

그 기준을 정하는 순간부터, 로그인도 PM의 일이 된다고 생각한다.

 

 

 

 

'서비스'라 통칭 되는것들은 로그인이 필요하다 보니, 웹 백엔드를 담당한다면 서비스에 맞는 로그인 방식을 고민해야 한다.

그렇기에 나도 관성적으로 로그인을 구현하지 않으려 고민하게 되는것 같다. 

 

 

 

이번에는 관리자 로그인 세션을 어떻게 관리할지 고민했다.

먼저 JWT 만료 시간을 30분으로 두면 되는 문제로 사고하고

로그인하면 토큰을 발급하고, 30분이 지나면 만료되게 하면 끝나는 일로 처리하면 될 것이다.

 

단순히 spring security를 사용하는것에서 권한을 부여하는것에 생기는 부가적인 것들을 고민한다면

토큰 만료를 한 번 더 보아야한다고 생각한다.

 

 

예를 들어) 관리자 계정은 하나이고, 여러 사람이 동시에 접속하면 안 된다. 또 30분 동안 아무 활동이 없으면 자동으로 로그아웃되어야 한다. 다른 브라우저나 다른 기기에서 로그인하면 기존 로그인은 바로 끊겨야 한다.

그러려면 JWT만으로는 부족하다.

 

 

JWT는 한 번 발급되면 그 자체로 유효성을 가진다. 서버가 서명을 검증하고 만료 시간을 확인하면 된다.

이 방식은 간단하고 빠르지만, 반대로 말하면 이미 발급된 토큰을 중간에 끊어내기가 어렵다.

그렇기에 보통 JWT 안에 세션 정보를 넣고, DB에도 현재 유효한 세션 정보를 저장하는 방식으로 정리한다.

 

 

왜 세션 정보를 DB에 저장해야 했나

관리자 계정은 일반 사용자 회원 테이블이 따로 있는 구조가 아니었다. admin_settings에 관리자 설정 정보가 있고, 이 값을 기준으로 관리자 로그인 여부를 판단하는 구조였다.

그래서 별도의 회원 테이블을 만들기보다는 admin_settings에 현재 로그인 세션 정보를 추가하는 방식이 더 자연스럽다고 봤다.

추가할 값은 다음과 같다.

current_session_id
session_expires_at
last_activity_at

여기서 핵심은 current_session_id다.

로그인할 때마다 새로운 sessionId를 만들고, 이 값을 DB에 저장한다. 그리고 JWT 안에도 같은 sessionId를 넣는다.

그러면 이후 API 요청이 들어올 때마다 JWT 안의 sessionId와 DB에 저장된 current_session_id를 비교할 수 있다.

둘이 같으면 현재 유효한 로그인이다.
둘이 다르면 이미 다른 곳에서 새로 로그인한 것이다.

이 구조를 쓰면 기존 토큰이 아직 만료되지 않았더라도 바로 거부할 수 있다. 이 점이 중요했다.

 

 

로그인 성공 시 처리

로그인에 성공하면 서버는 새 세션을 만든다.

새 sessionId 생성

admin_settings.current_session_id = 새 sessionId
admin_settings.session_expires_at = now + 30분
admin_settings.last_activity_at = now

JWT 안에도 sessionId 포함

이렇게 하면 서버와 클라이언트가 같은 세션 값을 공유하게 된다.

다만 세션의 최종 기준은 클라이언트가 아니라 서버다. JWT 안에 sessionId가 들어 있어도, DB에 저장된 current_session_id와 맞지 않으면 유효하지 않은 요청으로 본다.

처음에는 JWT 하나만 믿어도 되지 않을까 싶었다. 그런데 동시 로그인을 차단하려면 서버가 “지금 살아 있는 세션은 이것 하나다”라고 기억하고 있어야 한다.

그래서 현재 세션의 기준을 DB에 두는 쪽이 맞다고 판단했다.

 

 

API 요청마다 확인할 것

이제 API 요청이 들어올 때마다 JwtAuthenticationFilter에서 몇 가지를 확인한다.

JWT 서명이 유효한가?

JWT가 만료되지 않았는가?

JWT의 sessionId가 DB의 current_session_id와 같은가?

last_activity_at이 30분 이내인가?

 

 

이 조건을 모두 통과하면 정상 요청으로 본다.

그리고 요청이 정상적으로 통과할 때마다 last_activity_at을 현재 시간으로 갱신한다.

이렇게 하면 단순히 로그인 시점부터 30분이 아니라, 마지막 활동 시점부터 30분을 기준으로 만료를 처리할 수 있다.

이 차이도 생각보다 중요하다.

관리자가 계속 사용 중인데 로그인 후 30분이 지났다는 이유만으로 끊기면 불편하다. 반대로 아무 활동이 없는데 토큰 만료 시간만 길게 남아 있으면 보안상 애매하다.

그래서 기준은 로그인 시간이 아니라 마지막 활동 시간이어야 한다.

 

 

다른 곳에서 로그인하면 기존 로그인은 끊긴다

이 방식의 장점은 동시 로그인 차단이 비교적 단순하게 처리된다는 점이다.

예를 들어 A 브라우저에서 먼저 로그인했다고 하자. 이때 DB에는 A의 sessionId가 저장되어 있다.

그 뒤 B 브라우저에서 다시 로그인하면 새 sessionId가 생성되고, DB의 current_session_id가 B의 값으로 바뀐다.

기존 current_session_id = A 세션

B 브라우저 로그인

current_session_id = B 세션으로 교체

이후 A 브라우저가 API를 요청하면 JWT 자체는 아직 살아 있을 수 있다. 하지만 JWT 안의 sessionId는 A 세션이고, DB의 current_session_id는 B 세션이다.

둘이 다르다.

그래서 A 브라우저의 요청은 거부된다.

이렇게 하면 “다른 세션에서 접속 시 기존 로그인 끊김”을 구현할 수 있다. 굳이 기존 토큰을 직접 찾아서 삭제하지 않아도 된다. 서버가 현재 세션 기준을 하나만 들고 있으면 된다.

 

 

로그아웃 API도 필요하다

자동 만료만으로는 부족하다. 사용자가 직접 로그아웃할 수 있어야 한다.

그래서 로그아웃 API를 추가한다.

POST /api/auth/logout

로그아웃 요청이 들어오면 서버에서는 현재 세션 정보를 비운다.

current_session_id = null
session_expires_at = null
last_activity_at = null

프론트에서는 저장해둔 토큰을 삭제한다.

이렇게 하면 서버 기준에서도, 클라이언트 기준에서도 로그아웃 상태가 된다.

여기서도 중요한 것은 서버 값을 비우는 것이다. 프론트에서 토큰만 삭제하면 현재 브라우저에서는 로그아웃처럼 보일 수 있다. 하지만 서버에는 세션이 남아 있다.

관리자 로그인처럼 보안을 조금 더 신경 써야 하는 구조에서는 서버 기준의 로그아웃 처리가 필요하다.

 

 

브라우저 종료는 서버가 완벽히 알 수 없다

이번에 고민했던 지점이 하나 더 있었다.

브라우저를 닫았을 때 바로 로그아웃 처리를 할 수 있을까.

처음에는 브라우저 종료 시 서버에 로그아웃 요청을 보내면 되지 않을까 생각했다. 하지만 브라우저 종료는 서버가 100% 감지하기 어렵다. 사용자가 탭을 닫을 수도 있고, 브라우저를 강제 종료할 수도 있고, 네트워크가 끊길 수도 있다.

그래서 브라우저 종료를 서버가 완벽히 감지하는 방식으로 설계하는 것은 무리가 있다고 봤다.

대신 프론트에서 토큰 저장 위치를 localStorage가 아니라 sessionStorage로 두는 방식을 선택할 수 있다.

localStorage
-> 브라우저를 껐다 켜도 값이 남아 있음

sessionStorage
-> 브라우저 또는 탭 세션이 종료되면 값이 사라짐

이렇게 하면 브라우저나 탭 세션이 종료될 때 클라이언트에 저장된 토큰도 같이 사라진다.

물론 이 방식도 완벽한 해결책은 아니다.

sessionStorage는 탭 단위로 동작하기 때문에 여러 탭에서 로그인 상태를 공유하는 데 불편함이 생길 수 있다. 새 탭을 열었을 때 다시 로그인이 필요할 수도 있다.

또 보안 관점에서 sessionStorage가 모든 문제를 해결하는 것도 아니다. 스크립트에서 접근할 수 있기 때문에 XSS에 취약한 구조라면 토큰 탈취 위험은 여전히 있다.

그럼에도 이번 구조에서는 관리자 단일 계정이고, 브라우저 종료 시 로그인 상태가 오래 남는 것을 피하는 것이 더 중요하다고 봤다. 그래서 accessToken은 sessionStorage에 저장하는 쪽이 더 적절하다고 판단했다.

 

 

정리한 구현 방향

결국 백엔드와 프론트에서 해야 할 일은 다음과 같다.

백엔드에서는 admin_settings에 세션 컬럼을 추가한다.

current_session_id
session_expires_at
last_activity_at

로그인 성공 시에는 새 sessionId를 만들고, DB와 JWT에 함께 반영한다.

인증 필터에서는 매 요청마다 JWT 유효성뿐 아니라 DB에 저장된 현재 세션과 일치하는지도 확인한다.

JWT 서명 검증
JWT 만료 검증
sessionId 일치 여부 검증
30분 미활동 여부 검증
last_activity_at 갱신

로그아웃 API도 추가한다.

POST /api/auth/logout

로그아웃 시에는 DB의 세션 정보를 비우고, 프론트에서는 저장된 토큰을 삭제한다.

프론트에서는 accessToken을 sessionStorage에 저장한다.

로그인 성공 시 sessionStorage에 저장
로그아웃 시 sessionStorage에서 삭제
브라우저 또는 탭 세션 종료 시 토큰 제거

 

 

로그인도 PM의 역할?

 

이번 고민을 하면서 로그인 구현도 결국 정책의 문제라는 생각이 들었다.

처음에는 로그인이라고 하면 단순히 아이디와 비밀번호를 확인하고, 토큰을 발급하는 기능 정도로 생각했다. 하지만 막상 관리자 로그인 방식을 정리하다 보니, 이건 단순한 개발 기능이 아니었다. 어떤 사용자를 허용할 것인지, 어떤 상황에서 접속을 끊을 것인지, 얼마나 오랫동안 로그인 상태를 유지할 것인지 정하는 일이었다.

생각해보면 서비스에서 로그인은 거의 기본값처럼 붙어 있다. 하지만 모든 로그인 방식이 같을 수는 없다. 쇼핑몰의 로그인, 커뮤니티의 로그인, 금융 서비스의 로그인, 관리자 페이지의 로그인은 각자 기준이 다르다. 어떤 서비스는 편의성이 더 중요하고, 어떤 서비스는 보안과 통제가 더 중요하다.

이번 프로젝트의 관리자 로그인도 그랬다.

관리자 계정은 여러 명이 동시에 접속하는 것보다 하나의 세션만 유지되는 편이 안전하다. 일정 시간 활동이 없으면 자동으로 끊기는 편이 맞다. 브라우저를 닫았을 때도 로그인 상태가 오래 남아 있는 것은 부담스럽다.

이 기준을 정하지 않고 바로 구현에 들어가면, 개발자는 각자 익숙한 방식으로 만들게 된다. 그러면 기능은 동작할 수 있지만 서비스에 맞는 로그인인지는 다시 봐야 한다.

그래서 로그인도 기획 단계에서 충분히 고민해야 하는 영역이라는 생각이 들었다. 단순히 “로그인 기능이 필요하다”가 아니라, “우리 서비스에서는 어떤 로그인 상태를 정상으로 볼 것인가”를 먼저 정해야 한다.

이번 작업을 하면서 기획과 개발의 경계가 생각보다 뚜렷하지 않다는 것도 느꼈다. PM이 모든 코드를 알아야 한다는 뜻은 아니다. 다만 서비스의 성격에 맞는 정책을 정하려면, 그 정책이 기술적으로 어떻게 구현되는지 어느 정도는 이해해야 한다.

로그인은 개발 기능처럼 보이지만, 실제로는 서비스 운영 방식의 일부다. 사용자의 편의성과 보안 사이에서 어디에 기준을 둘지 정하는 일이다.

그 기준을 정하는 순간부터, 로그인도 PM의 일이 된다.

 

 

이번 구조에서는 DB 회원 테이블 없이도 관리자 단일 계정에 대해 두 가지를 처리할 수 있다.

30분 미활동 로그아웃
동시 로그인 차단

핵심은 JWT만 믿지 않고, 서버가 현재 유효한 세션을 하나 기억하게 만드는 것이다.

관리자 계정은 편의성보다 통제가 더 중요하다. 그래서 이번에는 조금 더 엄격한 쪽으로 설계하는 것이 맞다고 봤다.

결국 이번 작업은 로그인 기능을 만드는 일이기도 했지만, 관리자 페이지의 접근 기준을 정하는 일이기도 했다. 기능은 코드로 구현되지만, 그 전에 어떤 상태를 허용하고 어떤 상태를 막을지 먼저 정해야 한다.

그 기준을 세우는 것이 이번 작업에서 가장 중요했던 부분이었다.