개발 slecs

구글 로그인 중복 계정 생성을 멱등성 설계로 차단

목차

구글 로그인을 통한 계정 중복 생성 문제를 잡았다. 이건 생각보다 쉽게 발생하는 버그인데, 특히 모바일 환경이나 느린 네트워크에서 더 자주 터진다. 데이터베이스 유니크 제약과 애플리케이션 수준의 멱등 처리로 양쪽에서 막는 방식으로 해결했다.

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

첫 댓글 달아줘.