Skip to content

Feat: 외부 API 장애 격리(벌크헤드) 적용 - #135

Merged
chhezue merged 5 commits into
mainfrom
chore/134-external-api-bulkhead
Oct 10, 2026
Merged

chhezue merged 5 commits into
mainfrom
chore/134-external-api-bulkhead

Conversation

@chhezue

@chhezue chhezue commented Oct 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • FCM·카카오·Apple·Google이 멈추거나 느려도, 그 외부 API를 쓰지 않는 요청의 응답 시간이 유지된다. 로컬 부하 테스트에서 FCM 정지 중 여행방 목록 조회 p95가 28.9ms → 4.6ms, 카카오 4초 지연 중 3.57s → 7.6ms가 됐다.
  • Apple·Google 로그인이 토큰 서버 응답을 기다리지 않는다(로그인 p50 4.07s → 9.6ms, 토큰 서버 4초 지연 시).
  • API 계약은 바뀌지 않는다. 바뀐 것은 로그인 503 설명 문장 하나다(oasdiff로 확인).

⚠️ 리뷰어가 알아야 할 동작 변화

  • 카카오 로그인 503이 나오는 조건이 넓어졌다. 처리 중인 카카오 검증이 20건을 넘으면 기다리지 않고 기존 503 AUTH_SOCIAL_PROVIDER_UNAVAILABLE을 돌려준다. 새 에러 코드는 없고, 프론트 처리("잠시 후 다시 시도")도 같아 Breaking-Change-Reason 트레일러는 붙이지 않았다(스펙 B-14).
  • 알림 이력이 원래 요청의 트랜잭션에 들어간다. 참여 완료·전원 제출·정보 변경·확정·확정 취소 요청이 커밋 직전에 알림 이력을 함께 저장한다. 이력 INSERT가 실패하면 원래 요청도 실패한다(스펙 리스크 표, 수용).
  • Google 탈퇴용 토큰 저장 트레이드오프. 토큰 서버가 "느리지만 응답하는" 상태에서 로그인이 몰리면, 교환 대기열이 차서 적용 전보다 더 많이 건너뛴다(부하 테스트에서 초당 약 950건일 때 대부분 건너뜀, 초당 1건이면 전부 저장). Google은 재동의 전까지 복구되지 않아 그 사용자가 탈퇴할 때 Google 연결 해제를 못 할 수 있다. Apple은 다음 로그인 때 복구된다.

Related

주요 변경

  • FCM — Firebase 타임아웃(SDK 기본값은 무제한)과 배치당 30초 상한. 알림 이력은 BEFORE_COMMIT에서 원래 트랜잭션에 저장하고, 발송은 커밋 후 전용 실행기(스레드 4·대기열 200)가 트랜잭션 밖에서 처리한다. 대기열이 차면 푸시만 건너뛴다.
  • 잠금 순서 — 이력 INSERT 전에 Repository flush()로 여행방 UPDATE를 먼저 보낸다(빼면 동시 일정 확인 30회 중 29회 데드락). 방장 탈퇴도 여행방 행을 사용자 행보다 먼저 잠그도록 정리 뒤 flush한다.
  • Apple·Google 인가 코드 교환 — 로그인 응답 뒤 전용 실행기(스레드 4·대기열 100)에서 실행한다. 저장할 때 사용자 행을 잠가 탈퇴 여부를 확인하고, 탈퇴는 DB 정리 뒤 credential을 한 번 더 확인한다.
  • 카카오 — user/me 호출을 KakaoUserInfoClient로 빼고 @ConcurrencyLimit(20, REJECT).
  • @Async 제거 — @EnableAsync를 지우고 ArchitectureTest로 사용을 막는다. 비동기 작업은 용도별 실행기에 직접 제출한다.

Test plan

  • ./gradlew test — 541개 통과
  • ./gradlew build
  • #131과 임시로 합친 상태에서 전체 테스트 569개 통과
  • 핵심 방어 코드(flush 순서, 탈퇴 잠금 순서, 탈퇴 후 credential 재확인, 탈퇴 사용자 저장 차단, @ConcurrencyLimit)를 하나씩 빼면 해당 테스트가 실패하는지 확인
  • oasdiff breaking 0건 — 차이는 로그인 503 설명뿐
  • 부하 테스트 S1~S3 적용 전후 측정(스펙 "측정 결과")
  • 배포 변경 시: 해당 없음

Checklist

  • 요청 범위만 수정 (drive-by 리팩터링 없음)
  • docs/product/release-milestones.md Milestone(출시 이후) 확인
  • DB·인증·다파일 변경 시 docs/specs/ 링크 또는 스펙 반영
  • API 계약 변경 시 Breaking-Change-Reason: 트레일러 — N/A (필드·enum·ErrorCode·경로 변경 없음, 503 설명만 보강)

Risk / Rollback

  • 스키마 변경이 없어 DB 리셋이 필요 없다. 되돌릴 때는 이 PR의 merge commit을 revert하면 된다.

🤖 Generated with Claude Code

chhezue and others added 5 commits October 11, 2026 04:27
FCM·카카오·Google 토큰 서버가 멈추거나 느릴 때 다른 API가 받는 영향을 재는
k6·WireMock 시나리오 3개(S1~S3)를 추가한다. 이 커밋은 격리를 적용하기 전
상태라, 같은 스크립트로 적용 전 수치를 재는 기준점이다.

- 카카오 user/me와 Google 토큰 엔드포인트 주소를 설정 키로 뺀다. 기본값은
  지금 주소이고 부하 테스트에서만 WireMock으로 바꾼다
- bootTestRun용 실행 설정은 테스트 소스에만 둔다. WireMock으로 돌릴 수 없는
  FCM과 Google ID 토큰 검증은 대역으로 바꾸고, 시드 데이터를 만든다
- loadtest/README.md에 실행 방법과 결과를 읽을 때 주의할 점을 적는다

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FCM이 응답하지 않으면 알림 작업이 DB 커넥션을 쥔 채 무한히 기다려, 커넥션
10개 중 8개가 묶이고 다른 API까지 느려지던 문제를 없앤다(실험으로 재현).

- Firebase 연결·읽기·쓰기 타임아웃(3초·5초·5초)과 배치당 대기 상한(30초)을
  건다. SDK 기본값은 무제한이다
- 알림 이력은 이벤트를 발행한 트랜잭션의 커밋 직전에 저장한다. 원래 작업과
  함께 저장·취소되므로 푸시를 못 보내도 알림센터는 실제 상태와 맞다
- 이력 INSERT 전에 Repository flush로 여행방 UPDATE를 먼저 보낸다. 빼면
  같은 방의 동시 일정 확인 30회 중 29회가 데드락이었다
- FCM 발송은 커밋 후 전용 실행기(스레드 4개, 대기열 200)가 트랜잭션 밖에서
  처리하고, 대기열이 차면 푸시만 건너뛴다. 무효 토큰 삭제만 짧은 트랜잭션
- 정기 리마인드는 500명마다 쓰기 트랜잭션을 따로 열고, 한 묶음이 실패해도
  나머지는 계속 보낸다
- 방장 탈퇴가 여행방 행을 사용자 행보다 먼저 잠그도록 정리 뒤 flush한다.
  알림 이력 저장과 잠금 순서를 맞추기 위해서다
- @async와 @EnableAsync를 없애고, ArchitectureTest로 @async 사용을 막는다

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
탈퇴 때 연결 해제에만 쓰는 refresh token을 받으려고 로그인 응답이 토큰 서버를
최대 8초 기다리던 것을, 응답을 먼저 보내고 전용 실행기에서 교환하도록 바꾼다.

- 교환은 전용 실행기(스레드 4개, 대기열 100)에서 실행한다. 대기열이 차면
  기존 "교환 실패 시 저장만 생략" 정책대로 건너뛴다
- 저장 트랜잭션은 사용자 행을 잠금 조회해 탈퇴 여부를 확인하고, 탈퇴했으면
  저장하지 않고 받은 토큰을 바로 revoke한다
- 탈퇴는 DB 정리가 커밋된 뒤 Apple·Google 로그인 credential을 한 번 더
  revoke·삭제한다. 로그인 직후 탈퇴해 교환이 늦게 끝난 경우를 잡는다

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
카카오가 느려지면 로그인 요청이 요청 처리 스레드 200개를 다 차지해 다른 API까지
응답하지 못하던 것을 막는다.

- 카카오 user/me 호출을 KakaoUserInfoClient로 빼고 @ConcurrencyLimit(20,
  REJECT)를 건다. 같은 빈 안에서 부르면 어노테이션이 적용되지 않기 때문이다
- 상한을 넘으면 기다리지 않고 기존 503 AUTH_SOCIAL_PROVIDER_UNAVAILABLE을
  돌려준다. 새 ErrorCode는 없고 프론트 처리(잠시 후 재시도)도 같다
- ResilienceConfig(@EnableResilientMethods)는 #131과 같은 파일을 같은 내용으로
  추가해 두 PR의 머지 순서와 무관하게 충돌하지 않는다
- 503 Swagger 설명과 AUTH_SOCIAL_PROVIDER_UNAVAILABLE 설명에 상한 초과를 더한다

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#134 스펙(external-api-bulkhead.md)에 결정 B-1~B-16, 부하 테스트 적용 전후
측정 결과, 검증 시나리오와 테스트를 기록하고 바뀐 동작을 관련 문서에 반영한다.

- how-it-works: 알림 이력·푸시 분리, 소셜 로그인 서버가 느릴 때의 동작
- notification·auth-social-login·google-login-revoke·apple-oauth-multi-audience
  ·user-account-withdrawal·auth-error-code-granularity: 변경 이력과 인라인 주석
- spring-boot-java 규칙: BEFORE_COMMIT 저장 전 flush, @async 대신 전용 실행기
- package-layout·specs README 갱신, loadtest README에 적용 전 기준 커밋 명시

Refs #134

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chhezue chhezue added this to the 출시 이후 milestone Oct 10, 2026
@chhezue chhezue added kind: chore CI·리팩터·설정 priority: could 성능 개선·폴더/패키지 구조 정리·리팩터·최적화 (MoSCoW Could have) labels Oct 10, 2026
@chhezue
chhezue merged commit c2d7180 into main Oct 10, 2026
4 checks passed
@chhezue
chhezue deleted the chore/134-external-api-bulkhead branch October 10, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind: chore CI·리팩터·설정 priority: could 성능 개선·폴더/패키지 구조 정리·리팩터·최적화 (MoSCoW Could have)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Chore] 외부 API 장애 격리 벌크헤드 적용

1 participant