쿠팡이나 네이버 같은 서비스를 이용하다 보면 로그인 이후에 여러 인증 절차를 거치는 경우가 있다.
예를 들면 2FA를 해야 하기도 하고, 추가 인증이 필요하기도 하고, 어떤 경우에는 로그인 자체가 제한되어 안내 화면으로 이동하기도 한다.
나도 회사에서 로그인 관련 기능을 개발하면서, 처음에는 단순한 추가 인증 정도만 생각했었다. 물론, 좀 더 나이스하게 설계할 수 있었지만 회원통합 프로젝트에서 그만한 일정은 없었다.
하지만 서비스가 커지고 정책이 늘어나면서 로그인은 더 이상 아이디와 비밀번호만 검증하면 끝나는 기능이 아니었다.
사용자의 상태에 따라 여러 절차를 거쳐야 했고, 그 순서와 경우의 수도 점점 많아졌다.
이번 글에서는 기존 로그인 구조에서 어떤 한계가 있었는지, 그리고 그 문제를 해결하기 위해 왜 로그인 플로우를 State Machine 기반으로 다시 설계하게 되었는지 정리해보려 한다.
기존 로그인 구조
기존 로그인은 비교적 단순했다.
단순한 이메일 로그인 기준으로 보면 아래와 같은 흐름이었다.
- 아이디/비밀번호 입력
- 아이디/비밀번호 검증
- 사용자 식별
- 추가 인증(SMS or 본인인증) 필요 여부 확인
- 추가 인증이 필요 없으면 로그인 성공 처리
- 추가 인증이 필요하면 추가 인증이 필요하다는 응답과 함께 OTT(One-Time Token) 반환
- 클라이언트는 OTT 를 이용해 추가 인증 수행
- 추가 인증 성공 후 OTT 기반으로 최종 로그인 성공 처리
이 구조는 추가 인증이 한 단계만 존재할 때는 충분히 괜찮았다.
아이디/비밀번호 검증 이후, “추가 인증이 필요한가 아닌가”만 판단하면 됐기 때문이다.
문제는 인증 절차가 늘어나면서 시작됐다
이후 요구사항이 바뀌면서 로그인 과정에서 고려해야 하는 절차가 늘어났다.
예를 들면 아래와 같은 것들이다.
- 블랙리스트 해제
- 2FA
- 추가 인증 (SMS 또는 본인인증)
처음에는 단순히 “추가 인증 단계가 하나 더 늘어났구나” 정도로 생각할 수 있다.
하지만 실제로는 그렇지 않았다.
각 인증 단계가 독립적으로 존재하는 것이 아니라, 사용자 상태와 이전에 수행한 인증 수단에 따라 다음 단계가 달라졌기 때문이다.
예를 들면 이런 식이다.
- 블랙리스트 해제 -> 2FA -> 추가 인증 -> 로그인 성공
- 블랙리스트 해제 -> 2FA -> 로그인 성공
- 블랙리스트 해제 -> 로그인 성공
- 2FA -> 추가 인증 -> 로그인 성공
- 2FA -> 로그인 성공
- 추가인증 -> 로그인 성공
이렇게 되면 기존처럼 “추가 인증이 필요한가?”만 판단하는 방식으로는 처리하기 어려워진다.
왜냐하면 각 단계에서 다음 단계를 직접 결정하기 시작하면 흐름 제어가 분산되기 때문이다.
예를 들어 블랙리스트 해제 응답에서 “다음에는 2FA를 하세요” 같은 식으로 다음 로그인 처리 코드를 함께 넘기기 시작하면, 각 단계가 다음 단계를 알아야 하는 구조가 된다.
이렇게 되면 인증 단계가 늘어날수록 분기문도 많아지고, 유지보수도 빠르게 어려워진다.
그래서 로그인 플로우를 State Machine 으로 바꾸게 되었다
이 문제를 해결하기 위해 로그인 플로우를 State Machine 기반으로 다시 설계했다.
핵심 아이디어는 단순하다.
아이디/비밀번호 검증이 성공한 뒤, 곧바로 로그인 성공 여부를 결정하는 것이 아니라 로그인에 필요한 인증 step들을 순서대로 평가하는 것이다.
내가 정의한 step은 크게 아래와 같았다.
- 블랙리스트 step
- 2FA step
- 추가 인증 step (SMS 또는 본인인증)
로그인 시도는 이 step들을 한 바퀴 순회한다.
그리고 현재 사용자에게 필요한 step이 존재하면, 그 시점에서 바로 해당 step을 수행해야 한다는 응답을 반환한다.
반대로 모든 step을 통과했다면, 그때 최종적으로 로그인 성공 처리와 함께 access token, refresh token 을 발급한다.
즉, 로그인의 핵심이 “한 번의 분기 처리”에서 “현재 상태를 기준으로 다음 step을 결정하는 상태 전이”로 바뀐 것이다.
Step 을 두 가지 타입으로 나누었다
여기서 한 가지 더 정리한 것이 있다.
모든 step이 같은 성격은 아니라는 점이다.
어떤 step은 사용자가 추가 인증을 수행해야 하고, 어떤 step은 더 이상 인증을 진행할 수 없어서 안내 화면으로 보내야 한다.
그래서 로그인 step의 타입을 아래 두 가지로 나누었다.
1. Challenge Step
Challenge Step 은 사용자의 추가 액션이 필요한 step이다.
예를 들면:
- 2FA 인증
- SMS 인증
- 본인인증
클라이언트는 Challenge Step 응답을 받으면, step 종류에 맞는 인증 API를 호출하면 된다.
2. Exit Step
Exit Step 은 로그인 플로우를 더 진행하지 않고 종료해야 하는 step이다.
예를 들면:
- 블랙리스트 해제가 불가능한 상태
- 더 이상 로그인 진행이 불가능한 정책 위반 상태
클라이언트는 Exit Step 응답을 받으면, 추가 인증을 시도하는 것이 아니라 해당 타입에 맞는 안내 화면으로 이동하면 된다.
이렇게 나누니 클라이언트 입장에서도 흐름이 단순해졌다.
Challenge Step 이면 “후속 API 호출”, Exit Step 이면 “안내 화면 이동”으로 명확하게 처리할 수 있었기 때문이다.
정리
기존 로그인 구조는 “아이디/비밀번호 검증 이후, 추가 인증 한 단계만 처리하는 구조”였다.
이 방식은 단순한 로그인에는 잘 맞았지만, 블랙리스트, 2FA, 추가 인증처럼 여러 인증 절차가 조합되기 시작하자 한계가 분명해졌다.
그래서 로그인 플로우를 State Machine 기반으로 재설계했고,
각 인증 단계를 순차적으로 평가하면서 필요한 step만 수행하도록 만들었다.
또한 각 step을 Challenge Step, Exit Step 으로 나누어 클라이언트와 서버 모두가 일관된 방식으로 로그인 흐름을 처리할 수 있도록 했다.
완벽하게 거창한 인증 프레임워크를 만들었다기보다는,
늘어나는 로그인 정책을 감당할 수 있도록 흐름을 조금 더 견고하게 정리한 과정에 가까웠다.
실제로 이런 종류의 기능은 처음부터 복잡해 보이지 않지만, 정책이 하나씩 붙기 시작하면 금방 분기 지옥으로 들어가기 때문에 초기에 흐름을 어떻게 잡는지가 꽤 중요하다고 느꼈다. 물론, 처음에 이런 내용을 알았어도 일정 때문에 개발을 못했을 듯..