자동 로그인은 “저장된 정보가 있으면 로그인한다” 정도로 이해하고 있었다. 실제 코드를 보니 앱 시작, 저장 데이터 읽기, 네트워크 연결, 세션 확인, 로그인 화면 표시가 각자 다른 시점에 움직였다. 연결이 느린 기기에서는 로그인 요청이 두 번 나가거나, 자동 로그인이 진행 중인데 로그인 화면이 잠깐 나타났다.
조건문을 하나씩 추가할수록 어떤 조합이 가능한지 알기 어려워졌다. isAutoLogin, isConnecting, hasSession, isLoginViewOpen 같은 bool이 서로 모순된 값이 되기도 했다.
다중 bool을 대체한 단일 상태
먼저 앱 시작 후 상태를 순서대로 적었다.
Booting
-> LoadingCredentials
-> WaitingForNetwork
-> Authenticating
-> SignedIn | NeedManualLogin | Failed
모든 상태를 거창한 프레임워크로 바꾸진 않았다. 최소한 현재 단계가 하나만 존재하도록 enum을 두고, 화면과 요청은 그 상태를 기준으로 움직이게 했다.
enum LoginState
{
Booting, LoadingCredentials, WaitingForNetwork,
Authenticating, SignedIn, NeedManualLogin, Failed
}
Authenticating 상태에서는 새 로그인 요청을 막으니 중복 호출도 자연스럽게 줄었다.
이벤트와 상태 전이의 분리
네트워크 연결 이벤트가 오면 무조건 로그인하고, 저장 데이터 로드가 끝나면 또 로그인하던 구조가 문제였다. 이벤트는 조건 하나가 준비됐음을 알릴 뿐이었다. 실제 다음 행동은 현재 상태와 필요한 조건을 함께 확인한 뒤 한 곳에서 결정했다.
void TryAdvance()
{
if (state == LoginState.WaitingForNetwork && networkReady)
StartAuthentication();
}
이렇게 하니 어떤 이벤트가 먼저 와도 최종 전이는 한 번만 일어났다.
실패 상태의 세분화
저장 정보가 없어서 수동 로그인이 필요한 경우, 네트워크가 끊긴 경우, 저장된 세션이 만료된 경우는 사용자에게 보여줄 다음 행동이 달랐다. 전부 Failed로 보내면 재시도와 입력 화면 전환이 뒤섞였다.
세션 만료는 저장 정보를 지우고 수동 로그인으로, 일시적 연결 실패는 정보를 유지한 채 재연결로 보냈다. 실패 원인을 UI 메시지와 다음 전이까지 연결해 생각하게 됐다.
상태 흐름도에서 발견한 누락 경로
자동 로그인 작업을 하며 상태 머신이라는 말을 실감했다. 복잡한 기능이라서 상태 머신을 쓰는 게 아니라, 이미 존재하는 상태를 코드에서 숨기지 않는 방법에 가까웠다.
이후 여러 bool이 서로를 막고 있는 코드를 만나면 먼저 가능한 상태와 전이를 적는다. 체크박스 하나처럼 보이던 자동 로그인이 이벤트 순서와 실패 복구를 다루는 작은 시스템이었다.
기술 문서 기반의 책임 경계
이 문제를 고친 뒤 관련 공식 자료를 찾아 읽으면서, 당시 코드에서 우연히 맞았던 부분과 규칙으로 남겨야 할 부분을 나눠 봤다.
OWASP ASVS와 API Security 기준을 따라 다시 보면 화면에서 숨겼다는 사실은 권한 검사가 아니다. 계좌나 사용자 식별자를 클라이언트가 보내더라도 서버는 인증 주체가 그 객체에 접근할 수 있는지 매 요청에서 확인해야 한다.
클라이언트의 역할은 권한을 결정하는 것이 아니라 잘못된 조작을 줄이고, 거부 응답을 이전 데이터와 섞지 않으며, 민감한 정보를 로그나 저장소에 남기지 않는 데 가깝다.
다음에 자동 로그인 상태 머신 같은 문제를 보면 아래 순서부터 확인하려고 한다.
- 익명·인증 중·인증됨·만료 상태
- 재시도 가능한 오류와 사용자 입력 필요 오류
- 토큰 폐기와 화면 초기화 순서
- 동시에 도착한 만료 응답의 단일 처리
식별자 형식을 어렵게 만들거나 메뉴를 감추는 방법은 서버의 객체 수준 권한 검사를 대체하지 못한다.
공식 참고 자료
- OWASP ASVS 4.0.3 — 인증·세션·접근제어의 서버 측 검증 기준
- OWASP API Security Top 10 2023 — 객체 수준 권한과 인증·자원 소비 경계