← 목록으로

로그인 성공? 그런데 왜 계정은 못 쓰죠? – 가장 교활한 인증 버그와의 사투기

2026. 8. 15.

로그인 성공? 그런데 왜 계정은 못 쓰죠? – 가장 교활한 인증 버그와의 사투기

로그인 실패보다 더 지독한 경험이 뭔지 아시나요?

모든 게 잘 처리되었다고 하는데, 막상 계정을 써보려니 아무것도 안 되는 상황 말입니다.

네, 이건 실제로 제가 겪은 버그 이야기입니다.

어쩌다 보니 또다시 이놈의 인증(Authentication) 미스터리에 발목을 잡혔네요. 솔직히 말하면, 이쯤 되니 인증 버그들이 저에게 개인적인 원한이라도 품고 있는 건 아닌가 하는 생각마저 듭니다. 😅

이전에 올렸던 또 다른 버그 스매시 이야기에서는 사용자가 아예 로그인조차 할 수 없는 버그를 다뤘었죠. 이번에는 문제가 한층 더 교활했습니다. 대부분의 플로우는 완벽하게 건강해 보였거든요. 사용자 승인도 완료되고, 백그라운드 작업도 문제없이 실행되며, 이메일과 SMS도 정상적으로 도착했습니다. 코그니토(Cognito)에도 사용자가 멀쩡히 존재했고요.

그런데 사용자가 실제로 자기 계정을 사용하려고 시도하는 순간, 모든 것이 와르르 무너졌습니다.

두 개의 사용자 풀에서 시작된 이야기

저희 인증 시스템은 꽤 규모가 크고 오랜 시간에 걸쳐 발전해왔기 때문에, 모든 것을 처리하는 '단 하나의 멋진' 사용자 풀(User Pool)은 없었습니다.

기존 인증 플로우를 지원하는 오래된 코그니토 사용자 풀이 있었는데, 모바일 기반 회원가입 등을 담당했습니다. 반면, 새로운 플로우는 사용자가 PIN이 포함된 이메일을 받는 방식이었고, 이를 위해 새로운 사용자 풀을 사용하고 있었죠. 이 두 풀은 서로 다른 인증 여정을 지원하기 위해 의도적으로 분리해둔 것이었습니다.

이게 직접적인 문제는 아니었습니다.

정말 흥미로웠던 점은 애플리케이션 데이터베이스가 자체적인 사용자 정보를 가지고 있었고, 코그니토는 또 다른 방식으로 사용자를 표현하고 있었다는 겁니다. 게다가 이 두 시스템을 연결하는 작업 중 일부는 비동기적으로(asynchronously) 이루어졌습니다.

모든 시스템이 '이 사용자가 누구인지'에 대해 합의하는 한, 아무도 신경 쓰지 않았습니다.

하지만 그 합의가 깨지는 순간, 인증 시스템은 매우 예민하게 반응하기 시작했죠.

아주 작은 타이밍 윈도우

문제는 파트너 및 부양가족(dependant) 계정 생성 과정에서 불거졌습니다.

회원은 가입 도중 또는 나중에 회원 상세 정보 영역에서 파트너나 부양가족을 생성할 수 있었습니다. 그러면 관련 없는 회원 사용자가 계정을 승인하고, 이 과정에서 TaskList라는 데이터베이스 테이블에 SendingEmailsAfterApprovalBot이라는 비동기 작업이 스케줄링됩니다.

이 작업은 15분마다 실행되었고, 작업이 완료되면 파트너 또는 부양가족은 코드(PIN)가 포함된 이메일과 SMS를 받게 됩니다. 그들은 이 코드로 계정을 확인하고, 비밀번호를 설정한 다음 로그인할 수 있었죠.

합리적으로 들리지 않나요?

이제 이런 상황을 상상해 보세요.

파트너 계정이 오전 10시 1분에 승인되어 백그라운드 작업이 스케줄링되고, 모두가 각자의 일상으로 돌아갑니다. 그런데 오전 10시 5분, 아직 백그라운드 작업이 실행되기 전에 파트너가 자신의 이름을 변경합니다.

이 작은 변화가 매우 중요합니다. 왜냐하면 파트너 및 부양가족 계정의 사용자 이름(username)은 그들의 상세 정보에서 파생되기 때문입니다. 이제 데이터베이스는 새로운 사용자 이름을 가지고 있지만, 코그니토는 여전히 이전 사용자 이름을 알고 있을 수 있습니다.

아직 눈에 띄게 고장 난 부분은 없습니다.

작업은 데이터베이스에서 묵묵히 기다리고 있으며, 자신이 처리할 사용자의 신원이 그새 변경되었다는 사실을 전혀 알지 못합니다.

그리고 15분 후, 작업이 깨어납니다.

코그니토의 외침: "이 이메일은 이미 봤습니다!"

백그라운드 작업은 데이터베이스에 저장된 최신 사용자 이름을 사용하여 코그니토 사용자 생성을 시도합니다.

하지만 코그니토는 생성을 거부합니다. 해당 이메일 주소가 이미 기존 사용자와 연결되어 있지만, 그 사용자는 다른 사용자 이름을 가지고 있기 때문입니다.

여기서 중요한 세부 사항이 있습니다: 코그니토 사용자 이름은 불변(immutable)입니다.

그래서 우리는 단순히 "사용자 이름만 업데이트하면 되지" 하는 상황이 아니었습니다. 같은 사람을 두 시스템이 다른 사용자 이름으로 설명하고 있었고, 코그니토는 같은 이메일로 다른 사용자를 생성하는 것을 당연히 거부하고 있었죠.

데이터베이스는 이렇게 말하고 있었습니다:

"이 사람이 지금 쓰고 있는 사용자 이름은 이거예요."

코그니토는 효과적으로 이렇게 답하고 있었습니다:

"이 이메일은 이미 알고 있어요. 그리고 이 이메일은 다른 사용자 이름을 가진 사람의 것입니다."

같은 사람. 다른 디지털 신원.

이 불일치만으로도 전체 여정이 망가져 버렸습니다.

모든 게 괜찮아 보였던 순간, 그 전까지는...

이 버그가 특히 교활했던 이유는 그다음 벌어진 일 때문입니다.

사용자는 여전히 예상했던 이메일과 SMS를 받을 수 있었습니다. 어떤 명백한 실패도 비명을 지르며 관심을 요구하지 않았죠. 비동기 작업은 실행되었고, 알림도 발송되었으며, 코그니토에는 이미 해당 이메일과 연결된 사용자가 있었습니다.

그래서 사용자는 어떤 합리적인 사람이라면 할 법한 행동을 했습니다: 안내에 따랐죠.

코드를 입력하고, 계정 확인을 시도하고, 비밀번호를 설정하려 했습니다.

하지만 코그니토에 표현된 신원과 데이터베이스에 저장된 최신 신원이 일치하지 않았습니다.

사용자는 데이터베이스 동기화 문제를 들여다보고 있지 않았습니다. 불변하는 코그니토 사용자 이름이나 비동기 작업에 대해 생각하지도 않았고요. 그들은 완벽하게 유효한 코드를 받았고, 단순히 계정 생성을 마무리하려 했을 뿐입니다.

사용자의 관점에서는 시스템이 기본적으로 이렇게 말한 것과 같았습니다:

"여기 코드가 있습니다. 모든 준비가 끝났습니다!"

그러고는:

"아... 아니네요. 죄송합니다."

그것이 바로 우리가 추적해야 했던 버그였습니다.

센트리의 등장

이 시점에는 데이터베이스, 백그라운드 작업, 코그니토 사이 어딘가에서 문제가 발생하고 있다는 건 알았지만, 정확히 어디서 문제가 터졌는지는 또 다른 이야기였습니다.

이때 센트리(Sentry)가 엄청나게 유용했습니다. 에러 컨텍스트(error context)는 코그니토 실패에 대한 상세 정보를 제공했고, 브레드크럼(breadcrumbs)은 실패로 이어진 일련의 이벤트를 재구성하는 데 도움을 주었죠. 단순히 코그니토 에러를 독립적으로 보고 단순한 중복 사용자 문제라고 가정하는 대신, 실패 이전에 무슨 일이 일어났는지 살펴보고 이를 사용자의 여정과 연결 지을 수 있었습니다.

중요한 단서는 코그니토가 기존 사용자에게 이미 이메일이 연결되어 있다는 이유로 사용자 생성을 거부했다는 점이었습니다. 이는 코그니토가 무엇을 싫어했는지 알려주었지만, 주변 컨텍스트는 우리가 애초에 그 지점에 왜 도달했는지 설명하는 데 도움이 되었습니다.

실패 주변의 관련 사용자 및 작업 컨텍스트를 확인하고, 코그니토 호출로 이어진 활동을 따라가며, 상관 관계 ID(correlation ID)를 사용하여 여러 서비스 및 로그에서 동일한 작업을 연결할 수 있었습니다. 덕분에 개별 로그 엔트리를 쫓아가며 '이게 같은 요청의 일부일까?' 하고 희망하는 대신, 타임라인을 훨씬 쉽게 조립할 수 있었죠.

그리고 타임라인이 눈에 보이자, 문제는 갑자기 명확해졌습니다:

승인 → 작업 지연 → 이름 변경 → 사용자 이름 변경 → 코그니토에 이미 이메일 존재 → 생성 거부 → 신원 불일치 → 확인 실패

이때 버그는 더 이상 미스터리로 보이지 않았습니다.

코드는 무엇이 일어나야 하는지를 알려주었고, 센트리의 에러 컨텍스트, 브레드크럼, 상관 관계 로그는 실제로 무엇이 일어났는지를 파악하는 데 결정적인 도움을 주었습니다. 제가 실무에서 이런 문제들을 겪으면서 느낀 건, 관측 가능성(observability) 도구가 얼마나 중요한지 새삼 깨닫게 된다는 겁니다. 에러는 시스템이 불평하는 지점을 알려주고, 브레드크럼은 그곳으로 이끈 이야기를 이해하도록 돕는 마법과도 같죠.

해결책은 조사보다 간단했다

문제를 이해하고 나니, 해결책은 놀랍도록 간단했습니다.

우리는 이미 해당 이메일에 대한 코그니토 사용자가 존재하는지 확인하고 있었지만, 그걸로 충분하다고 생각했습니다. 대신, 로직을 변경하여 이렇게 물었습니다: "현재 데이터베이스 레코드에 대한 '올바른' 코그니토 사용자가 존재하는가?"

만약 이메일은 코그니토에 존재하지만, 구식 사용자 이름에 속해 있다면, 우리는 해당 사용자를 삭제하고 데이터베이스의 최신 사용자 이름으로 다시 생성했습니다. 코그니토 사용자 이름이 불변이기 때문에, 두 시스템을 다시 정렬하려면 사용자 재생성이 필수적이었습니다.

중요한 변화는 삭제 후 재생성 자체가 아니었습니다. 핵심은 **데이터베이스가 현재 사용자 이름에 대한 진실의 원천(source of truth)**이며, 코그니토는 같은 사람의 오래된 정보를 여전히 가지고 있을 수 있다는 점을 인식하는 것이었습니다.

이상한 시나리오가 핵심이었다

정상 경로(happy path)는 이미 잘 작동하고 있었으니, 흥미로운 테스트는 단계 사이에 상태가 변경되는 시나리오들이었습니다.

우리는 일반적인 회원가입 플로우, 파트너 및 부양가족 생성, 기존 코그니토 사용자, 누락된 코그니토 사용자 등을 테스트했습니다. 그리고 가장 중요하게, 이 버그를 드러낸 시나리오를 테스트했죠: 파트너 또는 부양가족이 승인된 후 SendingEmailsAfterApprovalBot이 실행되기 전에 이름을 변경하고, 그 후에 백그라운드 프로세스가 데이터베이스의 최신 상태를 사용하여 코그니토 사용자 생성을 시도하는 경우였습니다.

그리고 정말 중요했던 부분을 검증했습니다: 과연 그들이 PIN을 받고, 계정을 확인하고, 비밀번호를 설정한 다음 성공적으로 로그인할 수 있는가?

결국, 누구도 우리의 아름답게 동기화된 신원 기록에 신경 쓰지 않습니다. 그들은 그저 자신의 계정에 접속하고 싶어 할 뿐이니까요.

흥미로웠던 점은 사실 코그니토 문제가 아니었다

돌이켜보면, 이건 엄밀히 말해 코그니토 버그가 아니었습니다. 데이터베이스도 고장 나지 않았고, 스케줄된 작업도 아니었죠. 각 구성 요소는 자체 규칙에 따라 행동하고 있었습니다.

문제는 이들을 연결하는 가정에 있었습니다. 비동기 프로세스가 최종적으로 실행될 때, 우리가 처리를 시작했던 그 신원이 여전히 우리가 필요로 하는 신원일 것이라는 가정 말입니다.

그러다가 누군가가 이름을 변경했습니다.

그 작은 변화가 데이터베이스의 사용자 이름과 코그니토가 이미 가지고 있던 불변하는 사용자 이름을 다르게 만들기에 충분했습니다. 이메일이 이미 사용 중이었기 때문에 코그니토는 새로운 사용자 생성을 거부했고, 두 시스템은 서로 멀어졌습니다.

제가 이 사건에서 얻은 교훈은 이겁니다: 신원이 여러 시스템에 존재할 때마다, 어떤 시스템이 진실의 원천인지를 명확히 하고, 이 표현들이 일치하지 않을 때 어떻게 처리할지 명시해야 합니다.

왜냐하면 결국, 언젠가는 불일치가 발생할 것이기 때문입니다.

누군가 이름을 바꿀 수도 있고, 백그라운드 작업이 나중에 실행될 수도 있으며, 레거시 시스템에 이미 레코드가 있을 수도 있습니다. 그리고 코그니토는 당연히 "그 이메일은 이미 봤습니다"라고 말할 것입니다.

그래서 이 버그가 더 교활했다

승인은 잘 되었고, 작업도 실행되었으며, 알림도 발송되었고, 코그니토에도 사용자가 있었고, 사용자는 지시에 따랐습니다. 하지만 이 완벽하게 합리적인 단계들 사이 어딘가에서 신원이 미묘하게 어긋나버린 것입니다.

사용자의 관점에서는 이러한 복잡한 내부 사정은 전혀 중요하지 않았습니다. 그들은 두 개의 사용자 풀이 있고, 오래된 풀은 모바일 가입을 처리하고 새로운 풀은 이메일 PIN을 보낸다는 사실을 몰랐으며, 백그라운드 작업이 15분마다 실행된다는 것도 알 리가 없었죠.

그들은 단지 요청받은 모든 것을 다 했는데도 계정에 접속할 수 없다는 사실만 알 뿐이었습니다.

결국, 처음의 질문으로 돌아옵니다.

로그인 실패보다 더 최악인 상황이 뭐냐고요?

모든 게 잘 처리되었다고 하는데, 막상 계정을 써보려니 아무것도 안 되는 상황입니다.

적어도 로그인 실패는 솔직하기라도 합니다. 이 버그는 마치 이런 식이었죠:

"축하합니다! 모든 게 잘 처리되었습니다!"

"정말요? 전 아직도 접속이 안 되는데요."

그리고 이것이 아마도 제가 얻은 가장 큰 교훈일 겁니다: 인증 디버깅을 할 때는 단순히 로그인 화면만 보지 마세요. 시스템 전체를 통해 신원이 어떻게 흐르는지 추적하고, 관측 도구를 활용해 실제로 무슨 일이 일어났는지 재구성하며, 여러 조각들이 그 사용자가 누구인지에 대해 합의하지 못하게 된 순간을 찾아내야 합니다.

때로는 인증 버그가 로그인 화면에 숨어 있지 않습니다.

때로는 백그라운드 작업 어딘가에 조용히 숨어 있다가, 누군가가 이름을 바꾸기를 기다리고 있을 수도 있습니다.

인증이 또 한 건 했네요. 🔐🐛


원문: https://dev.to/ujja/you-know-whats-worse-than-not-being-able-to-log-in-5379 수집일: 2026-08-15 00:30:26