← 목록으로

"다 잘됐대놓고 왜?" 로그인 불가보다 지독한 인증 버그 추적기

2026. 8. 14.

"다 잘됐대놓고 왜?" 로그인 불가보다 지독한 인증 버그 추적기

로그인 불가보다 더 최악인 게 뭘까요?

모든 과정이 완벽하게 처리됐다고 하는데, 정작 계정은 쓸 수 없을 때죠.

네, 이건 실제로 제가 맞닥뜨린 버그였습니다.

어쩌다 보니 또 다른 인증 미스터리의 수렁에 빠져들게 되더군요. 이쯤 되면 인증 버그들이 저에게 개인적인 원한이라도 있는 게 아닌가 하는 생각이 듭니다. 😅

이전 스매시 스토리에서는 사용자들이 단순히 로그인조차 할 수 없었던 버그에 대해 썼습니다. 하지만 이번에는 문제가 훨씬 더 교묘했습니다. 대부분의 흐름이 완전히 정상처럼 보였거든요. 사용자는 승인되었고, 백그라운드 작업은 실행되었으며, 이메일과 SMS도 도착했고, Cognito에는 사용자 정보도 있었습니다.

그리고 사용자가 실제로 계정을 사용하려고 시도했습니다.

그 순간, 모든 것이 산산조각 났습니다.

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

인증 시스템은 꽤 거대했고 시간이 흐르면서 진화해왔습니다. 그래서 모든 것을 처리하는 하나의 번지르르한 사용자 풀만 있는 게 아니었죠.

기존 인증 흐름, 특히 모바일 기반 회원가입을 지원하는 오래된 Cognito 사용자 풀이 있었고, 사용자들이 PIN이 포함된 이메일을 받는 새로운 흐름을 처리하는 최신 사용자 풀도 있었습니다. 두 풀 모두 인증 여정의 다른 부분을 지원하기 위해 의도적으로 분리해 둔 것이었습니다.

문제는 그게 아니었습니다.

흥미로운 부분은 애플리케이션 데이터베이스(DB)가 자체적인 사용자 표현 방식을 가졌고, Cognito 역시 마찬가지였다는 점입니다. 게다가 이 두 시스템을 연결하는 작업 중 일부는 비동기적으로(asynchronously) 일어났습니다.

사용자가 누구인지 모두가 동의하는 한, 아무도 신경 쓰지 않았습니다.

그들이 의견을 달리하는 순간, 인증 시스템은 이 문제에 아주 깊은 관심을 갖게 됩니다.

아주 작은 타이밍의 틈새

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

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

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

합리적이죠, 안 그런가요?

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

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

이 작은 변경이 중요한 이유는 파트너 및 부양가족 계정의 사용자명이 그들의 상세 정보에서 파생되기 때문입니다. 데이터베이스는 이제 새로운 사용자명을 가지고 있지만, Cognito는 여전히 오래된 사용자명을 알고 있을 수 있습니다.

아직은 눈에 띄게 망가진 것은 없었습니다.

작업은 여전히 데이터베이스에서 묵묵히 기다리고 있었고, 자신이 처리하려는 정체성이 밑바닥에서 바뀌었다는 것을 전혀 알지 못한 채 말이죠.

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

Cognito 왈, "이 이메일, 전에 봤는데?"

백그라운드 작업은 데이터베이스에 저장된 최신 사용자명으로 Cognito 사용자를 생성하려고 시도합니다.

Cognito는 해당 이메일 주소가 이미 다른 사용자명으로 존재하는 사용자와 연결되어 있다는 이유로 생성을 거부합니다.

여기서 중요한 세부 사항이 있습니다. 바로 Cognito 사용자명은 변경 불가능(immutable)하다는 것입니다.

따라서 우리는 단순히 "사용자명만 업데이트"하는 상황이 아니었습니다. 동일한 사람을 다른 사용자명으로 설명하는 두 개의 시스템이 있었고, Cognito는 당연히 동일한 이메일로 다른 사용자를 생성하는 것을 거부하고 있었습니다.

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

"이 사람이 지금 쓰고 있는 사용자명은 이거야."

Cognito는 실질적으로 이렇게 말하고 있었죠.

"이 이메일은 이미 알고 있는데, 다른 사용자명을 가진 사람 거야."

동일한 사람.

다른 디지털 정체성.

그리고 이 불일치만으로도 전체 여정이 망가졌습니다.

모든 것이 좋아 보였지만... 결국은 아니었습니다

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

사용자는 여전히 예상했던 이메일과 SMS를 받을 수 있었습니다. 주목을 요구하는 명확한 실패는 없었습니다. 비동기 작업은 실행되었고, 알림은 전송되었으며, Cognito에는 이미 이메일과 연결된 사용자가 있었습니다.

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

하지만 Cognito에 표현된 정체성은 데이터베이스에 표현된 최신 정체성과 일치하지 않았습니다.

사용자는 데이터베이스 동기화 문제나 변경 불가능한 Cognito 사용자명, 비동기 작업에 대해 생각하고 있지 않았습니다. 그들은 완벽하게 정당한 코드를 받았고, 그저 계정 생성을 마무리하려 했을 뿐입니다.

사용자의 관점에서 보면, 시스템은 본질적으로 이렇게 말한 셈입니다.

"여기 코드입니다. 다 준비됐어요!"

그러고는:

"아... 죄송하지만, 아니네요."

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

Sentry의 등장

이 시점에 우리는 데이터베이스, 백그라운드 작업, Cognito 사이 어딘가에서 문제가 발생하고 있다는 것을 알았지만, 어디서 망가졌는지는 또 다른 이야기였습니다.

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

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

실제로 저도 복잡한 분산 시스템에서 이런 식의 비동기 오류를 추적할 때 Sentry 같은 APM 툴의 트레이싱 기능이 얼마나 강력한 무기인지 여러 번 체감했습니다. 눈에 보이는 에러 메시지 뒤에 숨겨진 진짜 맥락을 파악하는 데는 이만한 게 없죠.

우리는 실패 주변의 관련 사용자 및 작업 컨텍스트를 볼 수 있었고, Cognito 호출까지 이어진 활동을 추적했으며, 코릴레이션 ID(Correlation ID)를 사용하여 여러 서비스 및 로그에서 동일한 작업을 연결할 수 있었습니다. 덕분에 개별 로그 항목을 추적하며 동일한 요청에 속하기를 바라는 것보다 훨씬 쉽게 타임라인을 조립할 수 있었습니다.

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

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

이 순간, 버그는 더 이상 미스터리해 보이지 않았습니다.

코드는 무슨 일이 일어나야 하는지 알려주었습니다. Sentry의 에러 컨텍스트, 브레드크럼, 그리고 상관관계가 있는 로그는 실제로 무슨 일이 일어났는지 보여주었습니다.

솔직히 말해, 인증 조사 중에 훌륭한 관측 가능성(observability) 도구를 갖는 것의 가장 좋은 점 중 하나가 바로 이겁니다. 에러는 시스템이 어디서 불평했는지 알려주고, 브레드크럼은 그곳까지 이어진 이야기를 이해하는 데 도움을 줍니다.

해결책은 조사 과정보다 간단했습니다

문제를 이해하고 나니, 해결책은 놀랍도록 직관적이었습니다.

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

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

중요한 변화는 삭제-재생성 자체에 있었던 게 아닙니다. 데이터베이스가 현재 사용자명에 대한 진정한 정보원(source of truth)이며, Cognito는 동일한 사람의 오래된 표현을 여전히 가지고 있을 수 있다는 사실을 인지하는 것이었습니다.

이런 문제가 터졌을 때 저 역시 처음엔 '어떻게 이걸 고치지?' 하며 머리를 싸맸지만, 결국 핵심은 '무엇이 진실인가'를 정의하는 데 있더군요. 한 번 기준이 명확해지자 해결책은 의외로 간단하게 보였습니다.

이상한 시나리오가 중요한 것이었다

해피 패스(정상 경로)는 이미 작동하고 있었습니다. 그래서 흥미로운 테스트는 단계 사이에 상태가 변경되는 경우였습니다.

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

그런 다음 실제로 중요한 부분을 확인했습니다. 그들이 PIN을 받고, 계정을 확인하고, 비밀번호를 설정하고, 성공적으로 로그인할 수 있는가?

우리가 얼마나 아름답게 정체성 기록을 동기화했는지는 아무도 신경 쓰지 않습니다. 그저 자기 계정에 접속하고 싶을 뿐이죠.

흥미로운 부분은 사실 Cognito가 아니었다

돌이켜보면, 이건 사실 Cognito 버그가 아니었습니다. 데이터베이스도, 스케줄링된 작업도 망가진 게 아니었죠. 각 컴포넌트는 자신의 규칙에 따라 완벽하게 작동하고 있었습니다.

문제는 그들을 연결하는 가정, 즉 비동기 프로세스가 최종적으로 실행될 때 우리가 처리하기 시작한 정체성이 여전히 필요한 정체성일 것이라는 가정이었습니다.

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

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

제가 이 버그에서 얻은 교훈은 이겁니다. 정체성이 여러 시스템에 존재할 때마다, 어떤 시스템이 진정한 정보원(source of truth)인지 명확히 하고, 이 표현들이 일치하지 않을 때 어떤 일이 벌어져야 하는지 명시해야 합니다.

결국, 그런 일은 일어날 테니까요.

누군가는 이름을 변경할 것이고, 백그라운드 작업은 나중에 실행될 것이며, 레거시 시스템에는 이미 기록이 있을 것입니다. 그리고 Cognito는 아주 합리적으로 "이 이메일, 전에 봤는데"라고 말할 것입니다.

바로 그것이 이 버그를 그토록 교묘하게 만들었습니다

승인은 작동했고, 작업은 실행되었으며, 알림은 전송되었고, Cognito에는 사용자가 있었고, 사용자는 지시를 따랐습니다. 하지만 이 모든 완벽하게 합리적인 단계들 사이 어딘가에서, 정체성은 표류하고 말았습니다.

사용자의 관점에서 보면, 어떤 근본적인 세부 사항도 중요하지 않았습니다. 그들은 두 개의 사용자 풀이 있고, 오래된 풀은 모바일 가입을 처리하고 새로운 풀은 이메일 PIN을 보낸다는 사실을 몰랐으며, 백그라운드 작업이 15분마다 실행된다는 것도 알지 못했습니다.

그들은 단지 요청받은 모든 것을 다 했음에도 불구하고 계정에 접속할 수 없다는 사실만 알았습니다.

이는 저를 첫 질문으로 돌아가게 합니다.

로그인 불가보다 더 최악인 게 뭘까요?

모든 과정이 완벽하게 처리됐다고 하는데, 정작 계정은 쓸 수 없을 때죠.

적어도 로그인 실패는 정직합니다. 이 버그는 차라리 이런 식이었죠.

"축하합니다! 모든 것이 완벽하게 작동했습니다!"

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

그리고 이것이 제가 얻은 가장 큰 교훈일 것입니다. 인증 문제를 디버깅할 때는 단순히 로그인 화면만 보지 마세요. 전체 시스템을 통해 정체성을 추적하고, 관측 가능성 도구를 사용하여 실제로 무슨 일이 일어났는지 재구성하며, 여러 조각들이 사용자 본인에 대해 동의하지 않기 시작한 순간을 찾아내세요.

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

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

인증, 또다시 치명타를 날리다. 🔐🐛


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