구글 로그인 중복 계정 생성을 멱등성 설계로 차단
목차
구글 로그인을 통한 계정 중복 생성 문제를 잡았다. 이건 생각보다 쉽게 발생하는 버그인데, 특히 모바일 환경이나 느린 네트워크에서 더 자주 터진다. 데이터베이스 유니크 제약과 애플리케이션 수준의 멱등 처리로 양쪽에서 막는 방식으로 해결했다.
OAuth 중복계정이 생기는 실제 시나리오
사용자가 구글로 한 번 로그인했는데, 자기도 모르게 두 개의 계정이 만들어지는 현상. 왜 일어날까?
- 네트워크 타임아웃 후 자동 재시도
- 사용자가 반복적으로 "로그인" 버튼을 클릭
- 로드밸런서 뒤의 여러 인스턴스에 동시 요청 도달
- OAuth 콜백 처리 중 서버 에러 후 재시도
이 모든 경우가 결국 같은 구글 ID에 대해 여러 번 "새 계정 만들어"라는 요청을 서버에 보낸다는 뜻이다. 멱등성 없이 단순히 "유저가 없으면 생성"만 했다면, 동시에 두 요청이 쿼리 후 INSERT 실행 전 사이에 끼어들어 중복 생성이 가능해진다.
변경 내용: 데이터베이스와 애플리케이션 이중화
sql/schema.sql에서는 유니크 제약을 추가해 마지막 방어선을 만들었다:
ALTER TABLE users ADD CONSTRAINT uq_google_id UNIQUE(google_id);
src/lib/user.ts에서는 멱등한 처리를 구현했다. UPSERT(INSERT OR UPDATE) 패턴으로 같은 요청이 여러 번 와도 부작용이 한 번만 일어나도록:
// 구글 ID로 기존 유저를 찾거나 새로 생성
const getOrCreateUserByGoogleId = async (googleId, email) => {
try {
// INSERT ... ON CONFLICT 패턴으로 멱등성 보장
const result = await db.query(
`INSERT INTO users (google_id, email, created_at)
VALUES (?, ?, NOW())
ON CONFLICT(google_id) DO UPDATE SET updated_at = NOW()`,
[googleId, email]
);
return result;
} catch (err) {
// 유니크 제약 위반 → 기존 유저 반환
if (err.code === 'ER_DUP_ENTRY') {
return await db.query('SELECT * FROM users WHERE google_id = ?', [googleId]);
}
throw err;
}
};
| 계층 | 역할 | 효과 |
|---|---|---|
| DB 유니크 제약 | 마지막 방어선 | 중복 삽입 원천 차단 |
| 앱 UPSERT | 멱등성 보장 | 같은 요청 여러 번 호출 → 같은 결과 반환 |
멱등성이 왜 중요한가
수학적으로는 f(f(x)) = f(x) 같은 성질인데, 웹 서비스에서는 "같은 요청을 여러 번 보내도 부작용이 한 번만 일어난다"는 뜻이다.
구글 로그인의 경우:
- 같은 구글 ID로 5번 로그인 요청 → 계정 1개 생성
- 비밀번호 재설정 링크 2번 요청 → 1개만 유효
- 구독 취소 버튼 연타 → 1번만 취소 처리
멱등성이 없으면 네트워크 재시도나 사용자 실수로 인한 중복 요청이 다중 효과를 일으킨다. 특히 OAuth 같이 외부 API를 거치는 흐름에서는 통신 불안정성이 더해져 문제가 자주 터진다.
팀 관점에서의 의사결정
이 fix가 코드리뷰에서 우선순위 높았던 이유는:
- 사용자 경험: 한 번 로그인했는데 두 계정이 생기면 로그인 시 혼란 초래
- 데이터 정합성: 중복 계정으로 인한 프로모션/결제 로직 꼬임 위험
- 운영 복잡도: 이미 생긴 중복 계정을 추후에 머지/정리하는 비용
- 다른 로그인 수단의 안정성: 카카오, 네이버 등 다른 OAuth도 같은 패턴 적용 가능하므로 패턴 정립의 기회
회고
이 작은 fix를 하면서 느낀 것들:
- 방어적 설계는 하나만으로 충분하지 않다: DB 제약 + 앱 로직 이중화. 어느 한쪽만 믿고 가면 다른 쪽이 빵꾸 났을 때 폭탄이 된다.
- 멱등성은 선택이 아닌 필수: 특히 외부 API와의 연동 부분에서는 처음부터 멱등하게 설계해야 나중에 고생이 줄어든다.
- 동시성 이슈는 테스트가 까다롭다: 단순 단위 테스트만으로는 못 잡는다. 부하 테스트나 동시 요청 시뮬레이션을 추가했고, 팀에 이 패턴을 공유해서 비슷한 상황에서 바로 적용할 수 있게 했다.
다음에 누군가가 비슷한 피처를 만들 때 이 패턴을 재사용할 수 있도록 src/lib에 제네릭 유틸 함수로 추상화하는 것도 고민 중이다.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.