OpenID Connect 2026.09.04 7 min read

Google 로그인과 Redis 실시간 모듈에서 정한 신뢰 경계

Google ID Token 검증과 Redis Pub/Sub 공통 모듈을 구현했다. 계정 연결, 복제본 간 이벤트 전달, Presence·세션에 남은 문제와 확인한 범위를 정리했다.

Google 로그인과 Redis 실시간 공통 모듈을 직접 구현했다. 로그인에서는 브라우저가 보낸 정보를 어디서 검증할지 정해야 했고, 실시간 기능에서는 한 서버의 이벤트를 다른 서버까지 어떻게 전달할지 정해야 했다.

두 기능 모두 로컬에서 한 번 동작하는 것만으로는 확인이 부족했다. 토큰 검증과 계정 연결 조건을 정리하고, 복제본 사이에서 공유되는 정보와 JVM 안에 남는 상태를 구분했다.

Google 로그인에서 브라우저의 이메일을 믿지 않았다

브라우저가 보낸 이메일과 이름만으로 계정을 만들면 요청을 조작해 다른 사람의 이메일을 주장할 수 있다. 서버에서 Google이 발급한 정보인지 확인하는 과정이 필요했다.

Google Identity Services가 발급한 OpenID Connect ID Token 원문을 백엔드로 보냈다. 서버는 서명, issuer, audience, 만료를 확인하고 검증된 claim만 가입과 로그인에 사용하도록 구현했다.

MeetBack에는 Google ID Token을 백엔드에서 직접 검증하는 방식을 적용했다. Authorization Code 교환이나 Access Token, Google UserInfo API는 사용하지 않았다.

Google 로그인 버튼
  → 브라우저가 ID Token 획득
  → 백엔드로 ID Token 전달
  → 백엔드가 서명·issuer·audience·만료 검증
  → 검증된 sub와 email claim 사용
  → MeetBack 자체 인증 Token 또는 가입 단계 진행

소셜 계정 식별자는 provider + sub로 정했다. 이메일은 바뀌거나 다른 공급자의 계정과 겹칠 수 있어 영구 식별자로 쓰지 않았다. sub는 Google이 사용자를 식별하기 위해 발급한 값이다.

같은 이메일로 가입한 일반 계정이 있어도 자동 병합하지 않았다. 이메일 문자열이 같다는 사실만으로 기존 계정의 소유권까지 확인할 수는 없었다.

운영에서 만난 OAuth 오류는 Secret 하나로 설명되지 않았다

운영에서는 Kakao 로그인의 KOE006과 KOE320 오류를 차례로 확인했다. 화면만 보면 Client Secret이나 Docker Secret이 잘못된 것으로 생각하기 쉬웠지만 각각 점검할 항목이 달랐다.

오류별로 아래 내용을 먼저 확인했다.

  • KOE006: 등록한 리디렉션 URI와 앱 설정이 실제 요청과 일치하는지 확인
  • KOE320 invalid_grant: 이미 사용했거나 만료된 인가 코드를 다시 교환했는지 확인

공급자 콘솔에 등록한 URI와 실제 요청의 redirect_uri를 대조했다. 프록시가 전달한 스킴·호스트와 콜백의 중복 요청도 따로 확인했다. 운영 Secret이 존재한다는 것만으로 설정값이 맞다고 판단하지 않았다.

HTTPS 요청에 http://.../home 리디렉션이 만들어지고 JSESSIONID에 Secure가 빠지는 현상도 있었다. OAuth 설정 외에 전달 헤더 처리와 Spring의 리디렉션·쿠키 설정을 함께 봐야 했다.

Swarm VIP는 JVM 상태를 공유하지 않는다

App을 두 복제본으로 늘리면 Swarm VIP가 HTTP 요청을 분산한다. 하지만 Spring SimpleBroker와 Presence 맵은 각 JVM의 메모리에 따로 남는다.

사용자 A가 복제본 1에, B가 복제본 2에 연결되면 1번 JVM에서 보낸 STOMP 메시지는 2번 JVM으로 자동 전달되지 않는다. 복제본 사이 이벤트를 전달하려고 Redis Pub/Sub 공통 모듈을 만들었다.

업무 기능
  → RealtimeEventPublisher
  → 현재 JVM의 SimpleBroker
  → Redis Pub/Sub
  → 다른 JVM의 subscriber
  → 해당 JVM의 SimpleBroker
  → 연결된 사용자

브라우저의 WebSocket 연결과 STOMP 목적지는 기존 Spring 구성이 담당한다. Redis는 JVM 사이에 이벤트를 전달하는 역할로 사용했다.

local-first로 실패 범위를 나눴다

Publisher는 현재 JVM에 먼저 이벤트를 전달한 뒤 Redis로 발행한다. Redis 연결이 잠깐 끊겨도 현재 복제본에 연결된 사용자에게는 전달을 시도할 수 있도록 순서를 정했다.

이때 다른 복제본의 사용자는 이벤트를 놓칠 수 있다. Pub/Sub에는 누락 메시지를 나중에 재생하는 기능이 없으므로 결과 객체에서도 로컬 전달 결과와 복제본 간 전파 결과를 나눠 반환했다.

발행한 Redis 메시지를 다시 받아 중복 전달하는 경우는 eventId와 sourceInstanceId로 줄였다. 다만 전체 분산 환경에서 exactly-once나 전역 순서를 보장하는 구성은 아니다.

Presence 지연을 프록시 문제로 단정하지 않았다

접속 후 Presence가 늦게 나타난다는 보고가 있었다. 먼저 /ws 업그레이드를 확인했는데 Cloudflare Tunnel과 NPM 경로 모두 HTTP 101이었다. WebSocket 연결 자체가 차단된 상황은 아니었다.

이어서 로그의 STOMP CONNECT와 CONNECTED를 비교했다. 누적 성공 비율은 약 1~2%였다. 업그레이드 이후의 인증, 반복 재접속, 클라이언트 상태와 Presence 처리를 우선 조사할 근거로 삼았다.

Presence의 현재 상태도 여전히 JVM 메모리에 남아 있었다. 접속·종료 이벤트를 Redis로 전달하더라도 비정상 종료, 다중 탭, 누락된 heartbeat를 정리하지 않으면 서버별 상태가 달라질 수 있다.

HTTP 세션은 각 JVM의 HttpSession을 사용했다. 코드에서 Spring Session Redis가 활성화된 근거는 찾지 못했다. 태스크 재배치나 프록시 경로 변경 때 로그인 상태가 사라질 가능성은 별도 과제로 남겼다.

후속 설계에는 Redis set 또는 hash를 기준 상태로 두는 방법을 검토해야 했다. 세션 TTL, heartbeat, 인스턴스별 연결 수와 비정상 종료 정리 정책도 함께 필요했다.

현재 적용 범위와 다음 작업

Google 로그인은 토큰 검증과 계정 식별 기준을 구현했다. Redis 공통 모듈은 이벤트 규약, JSON 변환, 로컬 전달, 자기 메시지 중복 억제와 실패 경로를 테스트했다.

현재 적용 범위를 기준으로 다음 작업이 남아 있다.

  • 모든 실시간 업무 코드가 공통 publisher로 전환되지는 않았다.
  • Redis Pub/Sub은 메시지 영속성과 exactly-once를 제공하지 않는다.
  • WebSocket 세션을 실제로 종료하는 공통 SPI는 구현체 연동이 필요하다.
  • Presence의 전역 상태 설계와 부하 시험이 남아 있다.
  • OAuth 콜백 중복과 전달 헤더 문제는 애플리케이션 수정 후 회귀 테스트가 필요하다.

인증은 어떤 입력을 신뢰할 수 있는지, 실시간 기능은 어느 상태까지 공유되는지를 정리해야 구현 범위도 설명할 수 있었다. 애플리케이션을 배포한 정적 IP와 Swarm 환경은 별도 글에서 다뤘다.

실제 검증 화면

  1. 모바일에서 확인한 KOE006 오류다. Kakao 콘솔에 등록한 리디렉션 URI와 운영 요청의 값을 대조할 때 이 화면을 기준으로 삼았다.
    MeetBack 카카오 로그인에서 앱 관리자 설정 오류 KOE006이 표시된 모바일 화면

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동