앱이 마지막으로 선택한 계좌 순번을 저장하고 있었다. 계좌가 추가되거나 비활성화된 뒤 다시 로그인하면 저장된 순번이 현재 목록 범위를 벗어나거나 전혀 다른 계좌를 가리켰다. 배열 위치를 계좌의 정체성처럼 사용한 것이 문제였다.
목록은 시간에 따라 바뀌지만 저장된 상태는 과거 목록의 모양을 기억하고 있었다.
순번을 대체한 안정적 계좌 ID
선택 상태에는 목록 인덱스가 아니라 서버가 제공하는 불변 계좌 ID를 저장했다.
Account SelectSaved(
IReadOnlyList<Account> current,
string savedId)
{
return current.FirstOrDefault(x => x.Id == savedId)
?? current.FirstOrDefault(x => x.IsAvailable);
}
저장 ID가 없으면 제품 규칙에 따른 기본 계좌를 선택하고 새 상태를 저장했다.
이전 저장 형식 마이그레이션
기존 사용자의 설정에는 인덱스만 있었다. 파일 버전을 확인해 당시 목록 정보를 안전하게 복원할 수 있으면 ID로 바꾸고, 확실하지 않으면 기본 계좌로 이동했다.
version 1: selectedIndex
version 2: selectedAccountId
잘못된 인덱스를 억지로 현재 목록에 적용해 다른 계좌를 선택하는 것보다 선택을 초기화하는 편이 안전했다.
계좌 수 변경 시나리오
계좌 추가, 첫 계좌 비활성화, 목록 순서 변경, 계좌 0개, 권한에 따라 일부만 보이는 경우를 확인했다. 로컬 캐시와 서버 응답의 계좌 수가 다를 때는 서버의 현재 목록을 기준으로 선택 상태를 재검증했다.
동일한 계좌가 중복으로 들어오거나 ID는 같지만 표시 속성이 바뀐 경우도 확인했다. 중복은 ID 기준으로 정리하되 어떤 값을 우선할지는 서버 응답 순서가 아니라 명시적인 갱신 규칙을 따랐다. 선택 복원과 목록 정규화를 한 단계에서 뒤섞지 않았다.
화면에는 로컬 선택이 유효한지 확인되기 전 민감한 계좌 데이터를 먼저 표시하지 않았다.
저장 형식 변경 뒤에는 구버전 파일, 이미 새 형식인 파일, 일부만 기록된 파일을 각각 읽었다. 마이그레이션이 여러 번 실행돼도 같은 결과가 나와야 재실행과 앱 강제 종료에 안전했다.
목록 위치와 계좌 식별자의 차이
배열 인덱스는 화면을 그리는 순간에는 편리하지만 시간이 지나도 같은 대상을 보장하지 않는다. 저장과 API 경계를 넘는 값에는 안정적인 식별자가 필요했다.
이 문제 뒤 목록의 선택 상태를 저장할 때 순서가 바뀔 가능성을 먼저 본다. 과거 상태는 현재 데이터에 그대로 적용하는 값이 아니라 유효성을 다시 확인해야 하는 힌트였다.
추가 구현 기준
작업 당시에는 눈앞의 오류를 먼저 고쳤지만, 이후 기술 문서와 맞춰 보며 확인 순서를 조금 더 구체화했다.
OWASP ASVS와 API Security 기준을 따라 다시 보면 화면에서 숨겼다는 사실은 권한 검사가 아니다. 계좌나 사용자 식별자를 클라이언트가 보내더라도 서버는 인증 주체가 그 객체에 접근할 수 있는지 매 요청에서 확인해야 한다.
클라이언트의 역할은 권한을 결정하는 것이 아니라 잘못된 조작을 줄이고, 거부 응답을 이전 데이터와 섞지 않으며, 민감한 정보를 로그나 저장소에 남기지 않는 데 가깝다.
다음에 저장 계좌와 현재 계좌의 수량 불일치 같은 문제를 보면 아래 순서부터 확인하려고 한다.
- 입력 조건과 현재 상태를 한 줄로 기록
- 성공·빈 값·실패·취소를 구분
- 늦은 응답과 중복 이벤트 처리
- 실제 대상 환경에서 회귀 확인
식별자 형식을 어렵게 만들거나 메뉴를 감추는 방법은 서버의 객체 수준 권한 검사를 대체하지 못한다.
공식 참고 자료
- OWASP ASVS 4.0.3 — 인증·세션·접근제어의 서버 측 검증 기준
- OWASP API Security Top 10 2023 — 객체 수준 권한과 인증·자원 소비 경계