← 목록으로

로그인 성공의 달콤한 거짓말: "다 됐다"더니 "계정 못 쓴대"?

2026. 8. 16.

로그인 성공의 달콤한 거짓말: "다 됐다"더니 "계정 못 쓴대"?

로그인조차 못 하는 상황보다 더 최악인 게 뭘까요? 바로 "다 잘 처리됐으니 이제 계정을 쓰세요!" 라는 메시지를 보고 설레는 마음으로 사용하려는데, 정작 아무것도 안 되는 그 순간입니다.

네, 정말로 있었던 버그 이야기입니다.

그리고 어쩌다 보니 또 다른 인증 미스터리에 제가 끌려 들어가게 되었네요. 이쯤 되면 인증 버그들이 저에게 개인적인 원한이라도 있는 게 아닌가 하는 생각마저 듭니다. 😂

이전 스매시 스토리에서는 사용자가 아예 로그인조차 할 수 없었던 버그에 대해 다뤘었죠. 이번에는 문제가 훨씬 더 교묘했습니다. 흐름의 대부분이 완벽하게 정상처럼 보였거든요. 사용자 승인도 완료되고, 백그라운드 작업도 실행되었고, 이메일과 SMS도 정상적으로 발송되었으며, Cognito에도 사용자 정보가 생성되어 있었습니다.

그런데 사용자가 실제로 계정을 이용하려고 하자…

모든 것이 산산조각 났습니다.

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

저희 인증 시스템은 꽤 규모가 컸고, 시간이 지나면서 계속 진화해왔습니다. 그래서 모든 것을 처리하는 하나의 '빛나는' 사용자 풀(User Pool)이 존재하지 않았죠.

저희는 모바일 기반 가입을 포함한 기존 인증 흐름을 지원하는 오래된 Cognito 사용자 풀 하나와, 사용자에게 PIN이 포함된 이메일을 발송하는 새로운 흐름을 처리하는 또 다른 사용자 풀을 운영하고 있었습니다. 이 두 풀은 서로 다른 인증 여정의 부분을 지원했기에 의도적으로 분리해둔 상태였습니다.

이것 자체는 문제가 아니었죠.

흥미로운 부분은 애플리케이션 데이터베이스가 자체적으로 사용자를 표현하는 방식이 있었고, Cognito는 또 다른 방식으로 표현하고 있었다는 점입니다. 게다가 이 두 시스템을 연결하는 작업 중 일부는 비동기적으로 처리되었습니다.

서로가 같은 사용자를 가리키는 한, 아무도 신경 쓰지 않았죠.

하지만 그 일치가 깨지는 순간, 인증 시스템은 이 문제에 지대한 관심을 갖기 시작했습니다.

아주 미세한 타이밍의 틈

문제는 파트너 및 부양가족(dependant) 여정에서 발생했습니다.

회원은 가입 중에 파트너나 부양가족을 만들 수도 있었고, 나중에 회원 상세 정보 영역에서 추가할 수도 있었습니다. 그러면 관련 없는(non-member) 사용자가 계정을 승인하고, 이 과정에서 TaskList 데이터베이스 테이블에 SendingEmailsAfterApprovalBot이라는 비동기 작업이 예약되었습니다.

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

꽤 합리적인 흐름으로 들리죠?

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

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

이 작은 변경 사항이 중요합니다. 파트너 및 부양가족 계정의 사용자 이름이 그들의 상세 정보에서 파생되기 때문입니다. 데이터베이스는 이제 새로운 사용자 이름을 가지고 있는 반면, Cognito는 여전히 이전 사용자 이름을 기억하고 있을 수 있습니다.

아직까진 겉으로 드러난 문제는 없어 보입니다.

작업 큐에선 해당 태스크가 여전히 묵묵히 자신의 차례를 기다리고 있을 뿐이죠. 그 밑에서 신원 정보가 바뀌었으리라곤 상상조차 못 한 채 말입니다.

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

Cognito 왈, "이 이메일, 전에 본 적 있습니다"

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

하지만 Cognito는 이메일 주소가 이미 기존의 다른 사용자와 연결되어 있다는 이유로 생성을 거부합니다. 문제는 그 기존 사용자가 다른 사용자 이름을 가지고 있다는 점입니다.

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

그래서 단순히 "사용자 이름만 업데이트하면 되는" 상황이 아니었습니다. 우리는 같은 사람을 다른 사용자 이름으로 설명하는 두 개의 시스템을 가지고 있었고, Cognito는 같은 이메일로 다른 사용자를 생성하는 것을 상당히 합리적인 이유로 거부하고 있었죠. 제가 실무에서 이 부분을 테스트해 봤을 때, Cognito의 불변성(immutability)은 때로는 강력한 보안 메커니즘이지만, 이런 비동기 흐름에서는 예상치 못한 골칫거리가 될 수 있다는 걸 뼈저리게 느꼈죠.

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

"이것이 이 사람의 현재 사용자 이름입니다."

Cognito는 사실상 이렇게 말하고 있었죠:

"저는 이미 이 이메일을 알고 있으며, 다른 사용자 이름을 가진 사람에게 속해 있습니다."

같은 사람.

다른 디지털 신원.

그리고 이 불일치가 여정 전체를 망가뜨리기에 충분했습니다.

모든 것이 괜찮아 보였는데… 괜찮지 않았다

이 버그를 특히 교묘하게 만든 것은 다음에 일어난 일이었습니다.

사용자는 예상했던 이메일과 SMS를 여전히 받을 수 있었습니다. '실패'라고 대놓고 소리 지르는 명백한 오류는 없었죠. 비동기 작업은 실행되었고, 알림은 발송되었으며, Cognito는 이미 해당 이메일과 관련된 사용자를 가지고 있었습니다.

그래서 사용자는 이성적인 사람이면 누구나 할 법한 행동을 했습니다. 지시에 따라 코드를 입력하고, 계정 확인을 시도하고, 비밀번호를 설정하려고 했습니다.

문제는 Cognito에 표현된 신원 정보가 데이터베이스에 표현된 최신 신원 정보와 일치하지 않는다는 것이었습니다.

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

그들 입장에서 시스템은 기본적으로 이렇게 말한 것이나 다름없었습니다:

"여기 코드가 있습니다. 모든 준비가 완료되었습니다!"

그리고는:

"음... 사실은 아니에요."

우리가 쫓아야 했던 버그는 바로 이것이었습니다.

Sentry 등장

이 시점에는 데이터베이스, 백그라운드 작업, Cognito 사이 어딘가에서 문제가 발생했다는 건 분명했지만, 정확히 '어디서' 터졌는지를 아는 건 또 다른 이야기였죠.

바로 여기서 Sentry가 엄청나게 유용했습니다. Sentry의 에러 컨텍스트(Error Context)는 Cognito 오류에 대한 상세 정보를 제공했고, 브레드크럼(Breadcrumbs)은 오류 발생으로 이어진 일련의 사건들을 재구성하는 데 큰 도움을 주었습니다. 단순히 Cognito 오류를 개별적으로 보고 '중복 사용자 문제'라고 가정하는 대신, 오류 발생 전에 무슨 일이 있었는지 살펴보고 사용자 여정과 연결할 수 있었습니다.

결정적인 단서는 Cognito가 '이미 이메일이 기존의 다른 사용자와 연결되어 있어서' 사용자 생성을 거부했다는 점이었어요. 이는 Cognito가 무엇을 싫어하는지 알려줬지만, 주변 맥락이 '왜 우리가 이 지경에 이르렀는지'를 설명해 줬죠.

우리는 오류 주변의 관련 사용자 및 작업 컨텍스트를 확인할 수 있었고, Cognito 호출까지 이어진 활동을 추적할 수 있었으며, 상관관계 ID(Correlation ID)를 사용하여 여러 서비스와 로그에서 동일한 작업을 연결할 수 있었습니다. 덕분에 개별 로그 엔트리를 쫓아가며 같은 요청에 속하기를 바라는 대신, 타임라인을 훨씬 쉽게 조합할 수 있었습니다.

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

승인 → 지연된 작업 → 이름 변경 → 사용자 이름 변경 → Cognito에 이미 이메일 있음 → 생성 거부 → 신원 불일치 → 확인 실패

이 순간, 버그는 더 이상 미스터리로 보이지 않게 되었습니다.

코드는 '무엇이 일어나야 하는지'를 알려주었죠. Sentry의 에러 컨텍스트, 브레드크럼, 그리고 상관관계가 있는 로그는 '실제로 무엇이 일어났는지'를 볼 수 있도록 도와주었습니다.

솔직히 말해, 인증 조사를 할 때 좋은 관측 가능성(observability)을 갖추는 것의 가장 좋은 점 중 하나가 바로 이겁니다. 오류는 시스템이 불평하는 지점을 알려주고, 브레드크럼은 그곳까지 이르게 된 이야기를 이해하도록 돕습니다.

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

문제의 근원을 파악하고 나니, 해결책은 놀랍도록 간단했습니다.

우리는 이미 이메일에 대한 Cognito 사용자가 존재하는지 확인하고 있었지만, 그것만으로 충분하다고 간주했습니다. 대신, 로직을 변경하여 "현재 데이터베이스 레코드에 대한 '올바른' Cognito 사용자가 존재하는가?" 라고 묻도록 했습니다.

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

중요한 변화는 삭제 후 재 생성이 아니었습니다. 핵심은 데이터베이스가 현재 사용자 이름의 진실의 원천(Source of Truth)이며, Cognito는 여전히 같은 사람의 오래된 표현을 가지고 있을 수 있다는 점을 인식하는 것이었습니다. 사실 이런 종류의 문제는 종종 복잡한 시스템 아키텍처에서 비롯되지만, 핵심은 '어디를 진실의 원천으로 삼을 것인가?'에 대한 명확한 정의에 있다는 걸 여러 번 경험했어요. 이번에도 마찬가지였죠.

'이상한 시나리오'가 중요했다

일반적인 성공 경로는 이미 잘 작동하고 있었으므로, 흥미로운 테스트는 단계 사이에 상태가 변경되는 경우였습니다.

우리는 일반적인 가입 흐름, 파트너 및 부양가족 생성, 기존 Cognito 사용자, 없는 Cognito 사용자 등 다양한 시나리오를 테스트했습니다. 그리고 가장 중요하게는 버그를 노출시킨 시나리오를 검증했습니다. 즉, 파트너나 부양가족이 승인된 후, SendingEmailsAfterApprovalBot이 실행되기 전에 이름을 변경하고, 그 후에 백그라운드 프로세스가 데이터베이스의 최신 상태를 사용하여 Cognito 사용자 생성을 시도하는 경우였죠.

그리고 나서 실제로 중요한 부분을 검증했습니다. 그들이 PIN을 받고, 계정을 확인하고, 비밀번호를 설정하고, 성공적으로 로그인할 수 있었을까요?

왜냐하면 그 누구도 우리 시스템이 얼마나 완벽하게 신원 정보를 동기화하는지에는 관심이 없으니까요. 그저 자기 계정에 로그인하고 싶을 뿐이죠.

정작 흥미로운 부분은 Cognito가 아니었다

돌이켜보면, 이건 사실 Cognito 버그가 아니었습니다. 데이터베이스도 고장 나지 않았고, 예약된 작업도 마찬가지였습니다. 각 구성 요소는 자신만의 규칙에 따라 작동하고 있었습니다.

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

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

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

제가 이 사건에서 얻은 교훈은 이것입니다. 신원 정보가 여러 시스템에 존재할 때는 어떤 시스템이 진실의 원천인지 명확히 하고, 이들 표현이 서로 다를 때 어떻게 처리할 것인지 명시적으로 정의해야 합니다.

왜냐하면 언젠가는, 분명히 어긋나게 되어 있거든요.

누군가는 이름을 바꿀 테고, 백그라운드 작업은 나중에 실행될 테고, 레거시 시스템은 이미 레코드를 가지고 있을 테고, 그리고 Cognito는 "그 이메일은 전에 본 적 있습니다"라고 아주 합리적으로 말할 것입니다.

그리고 그것이 이 버그를 그토록 교묘하게 만들었다

승인은 완료되었고, 작업은 실행되었고, 알림은 발송되었고, Cognito에는 사용자가 있었고, 사용자는 지시를 따랐습니다. 하지만 이 완벽하게 합리적인 단계들 사이 어딘가에서 신원 정보가 미묘하게 어긋나 버린 것이죠.

사용자의 관점에서는 어떤 내부적인 세부 사항도 중요하지 않았습니다. 두 개의 사용자 풀이 있고, 오래된 풀은 모바일 가입을 처리하고 새로운 풀은 이메일 PIN을 보낸다는 것, 혹은 백그라운드 작업이 15분마다 실행된다는 것 따위는 알 필요가 없었습니다.

그들은 그저 자신이 요구받은 모든 것을 했지만, 여전히 계정에 로그인할 수 없다는 사실만 알고 있었습니다.

이것이 제가 서두에 던졌던 질문으로 다시 돌아오게 만드는 지점입니다.

로그인조차 못 하는 상황보다 더 최악인 게 뭘까요?

바로 "다 잘 처리됐으니 이제 계정을 쓰세요!" 라는 메시지를 보고 설레는 마음으로 사용하려는데, 정작 아무것도 안 되는 그 순간입니다.

차라리 로그인 실패는 솔직하기라도 하죠. 이 버그는 마치 이런 식이었으니까요:

"축하합니다! 모든 것이 정상 처리되었습니다!"

"정말요? 전 아직도 계정에 갇혀 있는데요?"

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

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

때로는 백그라운드 작업에 숨어, 누군가 이름을 변경하기를 조용히 기다리고 있을지도 모릅니다.

인증, 또 한 건 터뜨리다. 🔐🐛


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