Skip to content

feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가 - #94

Merged
cherry-go-round merged 5 commits into
devfrom
feat/S15P11A705-64-kakao-naver-login
Jul 29, 2026
Merged

feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가#94
cherry-go-round merged 5 commits into
devfrom
feat/S15P11A705-64-kakao-naver-login

Conversation

@cherry-go-round

Copy link
Copy Markdown
Contributor

요약

명세 §3.1이 요구하는 3사 소셜 로그인 중 Google만 구현돼 있어 나머지 둘을 채웠다. 프론트는 UI를 이미 붙여두고 백엔드를 대기하던 상태였다. 새로 들인 것은 응답 형태뿐이다 — 토큰 발급·회전·쿠키·회원 확정은 공급자와 무관한 경로라 BI-18이 이미 만들어 뒀다.

Jira (필수)

관련 GitHub Issue

변경 사항

정규화 — 공급자마다 사용자 정보 응답 구조가 다르다.

공급자 식별자 이메일 user-name-attribute
Google sub (최상위) email (최상위) sub
Kakao id (최상위, 숫자) kakao_account.email id
Naver response.id response.email response
  • OAuthUserInfo.from에 두 공급자 분기 추가, UnsupportedSocialProviderException 분기 제거
  • application.yml에 등록정보 + 공급자 엔드포인트(Spring 내장 CommonOAuth2Provider는 google·github·facebook·okta만 있다)
  • 테스트: KakaoNaverLoginCallbackTests 신규 4건(파라미터화), 진입 리다이렉트 2건, 대역·지원 클래스 공급자 파라미터화

리팩터(별도 커밋 c3948fa) — registrationId 변환이 두 클래스에 양방향으로 흩어져 있던 것을 SocialProvider로 모았다.

설정 함정 수정.env.example·.gitignore (아래 「미결/후속」 참고)

테스트 / 검증

  • ./gradlew clean check --no-daemon
  • DB 변경 시 PostgreSQL 통합 테스트 — IntegrationContainerSupport(PostgreSQL + Redis)
  • API 계약 변경 시 관련 문서 갱신 — 공용 문서 변경 없음. 08 §3.1이 이미 세 공급자를 적고 있어 이번 변경은 구현이 명세를 따라잡은 것이다

RED — 진입 2건 + 콜백 7건이 실패(kakao·naver 미등록 → 404, UnsupportedSocialProviderException).

GREEN → Regression

$ ./gradlew clean check --no-daemon
BUILD SUCCESSFUL
총 326개 · 실패 0 · 에러 0     (dev 리베이스 후. 이 티켓이 더한 것은 6개)

실제 Kakao·Naver 계정으로 수동 검증 — 스텁이 구조적으로 덮지 못하는 것을 보려고 브라우저로 한 번씩 돌렸다(인증 계약 §8).

확인 결과
Kakao provider_user_id 5013244578 — 숫자를 문자열로 저장
Naver provider_user_id 43자 식별자 — response 안의 id 자체(Map toString()이 아니다)
이메일 양쪽 정상 수신(Kakao는 비즈 앱 전환 + 동의항목 설정 후)
회원 분리 member·social_account 각 2건 — (provider, provider_user_id)가 달라 별도 회원
Redis auth:refresh:{id}:{jti} + auth:refresh-index:{id} 회원별, 7일 TTL
쿠키 logged_in·XSRF-TOKENdocument.cookie에 보이고 access_token·refresh_token은 안 보임

리뷰 포인트

1. Naver만 Spring의 식별자 검사를 우회한다. user-name-attribute: response는 감싼 Map을 지목한다. DefaultOAuth2User는 그 키의 존재만 확인하므로 response가 있고 그 안의 id가 없어도 통과한다. Google·Kakao는 최상위 스칼라를 지목해 Spring이 앞단에서 걸러 주는데 Naver만 그 보증이 없다. required(nested(attributes, "response", "id"), "response.id")가 유일한 방어선이고, 없으면 provider_user_id NOT NULL 위반이 되어 원인이 DB까지 내려간다. 수동 검증에서 43자 식별자가 저장된 것이 이 방어선이 실제로 값을 한 증거다.

2. Kakao 설정 두 가지가 콘솔 상태에 묶여 있다. client-authentication-method: client_secret_post(기본 basic이면 토큰 교환 401)와 scope: account_email이다. Kakao의 client secret은 콘솔에서 활성화해야 검사되는 선택 항목이고, 동의항목을 설정하지 않으면 인가 요청 자체가 KOE205로 거절된다. 즉 콘솔 상태와 이 설정이 어긋나면 콜백 마지막 단계에서 실패하고 원인이 우리 로그에 잘 안 보인다.

3. 이메일 null 경로는 비즈 앱 전환 후에도 유효하다. 선택 동의라 사용자가 거절하거나 나중에 철회할 수 있다. DB·엔티티·팩토리·정규화 네 층이 모두 nullable이라 통과하며 callbackSucceedsWithoutEmail로 고정했다.

4. 리팩터를 같은 PR에 넣었다. registrationId 변환이 SocialLoginControllerOAuthUserInfo에 양방향으로 네 조각 흩어져 있었다 — toRegistrationId는 사실 toProvider(p).registrationId()를 손으로 펼친 것이었다. SocialProvider가 이미 자기 문자열 표현의 소유자라고 javadoc에 적고 있으면서 변환은 밖에 두 벌로 있던 상태다. 커밋을 나눠 뒀으니 따로 보실 수 있다. 예외는 호출자가 던지게 뒀다 — member 엔티티가 auth 도메인 예외에 의존할 이유가 없다.

미결 / 후속

기동을 막는 함정 하나를 고쳤다(BT-05). 등록정보를 추가하자 무관한 테스트 11개가 컨텍스트 실패했는데, 원인은 이 티켓이 아니라 .env.example이 자격증명을 빈 값(KAKAO_CLIENT_ID=)으로 정의하던 기존 함정이었다. application.yml.env를 프로퍼티로 올리므로(spring.config.import) 빈 문자열은 "없음"이 아니어서 ${VAR:unset} 기본값을 덮는다. Google만 값이 채워져 있어 드러나지 않았을 뿐, .env.example을 그대로 복사한 사람은 처음부터 앱을 띄울 수 없었다. 자격증명 6줄을 주석으로 바꿨다.

곁가지로 .gitignore.env*로 넓혔다 — 원인을 찾다 만든 .env.bak-<sha>가 추적 대상이었고 그 안에 실제 자격증명이 있었다.

BT-05의 원인 서술을 한 번 틀렸고 정정했다(04345a2). 처음에 "Spring Boot 4가 .env를 자동 로드한다"로 적었는데, 실제로는 1ed1dbe에서 우리가 명시적으로 켠 것이고 그 사실이 application.yml 주석에 이미 있었다. 증상과 해결은 유효하고 기전만 틀렸다. 정정 내역을 BT-05 본문에 남겼다.

인프라 요청이 필요하다. KAKAO_CLIENT_*·NAVER_CLIENT_*를 Sealed Secret으로 주입해야 하며, 빈 값으로 주입하면 같은 이유로 파드가 기동하지 않는다. 아직 자격증명이 없는 공급자는 주입 자체를 하지 않아야 한다.

/me/summary가 이메일을 반환하는데 null일 수 있다. 공용 계약 §1.6이 null 필드를 직렬화에서 생략하므로 응답에 email 키 자체가 없다. 프론트에 폴백 UI가 필요하다고 전달했다. 해당 엔드포인트는 아직 미구현이다.

🤖 Generated with Claude Code

@cherry-go-round cherry-go-round linked an issue Jul 29, 2026 that may be closed by this pull request
15 tasks
@cherry-go-round
cherry-go-round force-pushed the feat/S15P11A705-64-kakao-naver-login branch from 04345a2 to b58d07c Compare July 29, 2026 07:30
@minyongP

Copy link
Copy Markdown
Contributor

PR 94 — feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가

  • 작성자: cherry-go-round
  • 커밋: 04345a2
  • 첫 리뷰
  • AI 영역 포함: 아니오
  • 반복 회피로 뺀 지적: 없음 (첫 리뷰)
  • 만든 시각: 2026-07-29T07:33:32Z

리뷰 본문

base=dev, diff 703줄로 전부 봤습니다. Flyway·ai 스키마·feed_event·Feed 추천은 건드리지 않아 AI 영역과 겹치는 부분이 없습니다.

먼저 좋았던 것. PR 본문의 「리뷰 포인트」가 실제로 리뷰가 봐야 할 곳을 정확히 지목합니다 — Naver만 Spring의 식별자 검사를 우회한다는 것(user-name-attribute: response가 감싼 Map을 지목하므로 DefaultOAuth2User는 키 존재만 확인한다)은 설정만 읽고는 알 수 없는 층이라, 이걸 미리 적어 준 덕에 리뷰가 그 지점에서 시작할 수 있었습니다. 대역만으로 덮지 못하는 것을 실제 계정으로 한 번씩 돌려 표로 남긴 것(Kakao 숫자 id의 문자열 저장, Naver 43자 식별자가 Map toString()이 아님)도, BT-05에서 원인 서술을 스스로 정정하고 그 사실을 본문에 남긴 것도 좋습니다. sameSubjectOnDifferentProvidersStaysSeparate는 공급자가 둘로 늘어나야 비로소 쓸 수 있게 된 테스트고, 막는 것이 계정 탈취라 특히 값이 큽니다.

범위. .env.example·.gitignore는 티켓 밖이지만 각각 기동 실패의 원인과 실제 자격증명이 추적되던 문제라 이 PR에서 함께 가는 것이 맞다고 봅니다. registrationId 변환 리팩터도 커밋(c3948fa)이 분리돼 있어 따로 읽혔습니다. Google 경로는 requiredStringValue(attributes, "sub")required(attributes.get("sub"), "sub")로 의미가 그대로라 회귀 위험이 없습니다.

필수 하나입니다OAuthUserInfo.java:39의 방어선을 지키는 테스트가 없습니다. 아래 줄 코멘트에 자세히 적었습니다. 정규화가 값을 꺼내는 부분은 callbackCreatesMemberForNewUser[naver]가 지켜 주지만(43자 식별자가 저장되는지 보므로 nested()를 지우면 깨집니다), 값이 없을 때 끊는 부분은 어떤 테스트도 지키지 않습니다. PR 본문이 이 줄을 "유일한 방어선"이라고 적고 있는 만큼 그 줄에 테스트가 붙어야 한다고 생각합니다.

설정 규약 쪽에 제안이 하나 있습니다(선택). BT-05가 "빈 값으로 두지 말고 줄 자체를 없앤다가 이 저장소의 규칙이 됐다"고 적고 있는데, BT-05가 「관련」로 링크한 설정 규약에는 .env라는 말이 한 번도 나오지 않습니다. 규칙의 근거와 이유는 .env.example 주석과 BT-05에 잘 남았지만, 새로 들어온 사람이 읽는 곳은 규약 문서입니다. 트러블슈팅 기록은 보존 구역이라 규칙의 상시 참조처로는 맞지 않으니, configuration.md에 한 줄(.envspring.config.import로 올라오므로 빈 값은 "없음"이 아니다 → 쓰지 않는 키는 주석으로) 추가하는 것을 제안합니다. 이 PR에서 하지 않아도 됩니다.

application.yml:55의 Naver scope는 질문으로 남겼습니다.

줄 단위 코멘트

  • src/main/java/com/pinlog/pinlogback/domain/auth/dto/OAuthUserInfo.java:39 — 이 줄이 유일한 방어선인데 이 줄을 지키는 테스트가 없습니다. 새로 추가된 4건은 모두 값이 있는 경로만 지납니다 — callbackCreatesMemberForNewUser[naver]가 43자 식별자의 저장을 보므로 nested(...)를 지우면 깨지지만, required(...)만 걷어내고 nested(...)를 그대로 두면 4건 다 그대로 통과합니다. 그 상태에서 response.id가 빠진 응답이 오면 provider_user_idnull로 내려가 NOT NULL 위반이 되고, 원인이 DB까지 내려가는 것이 정확히 이 줄이 막으려던 상황입니다. Google·Kakao는 Spring이 앞단에서 걸러 주므로 이 공백은 Naver에만 있고 이 PR에서 새로 생긴 것입니다(코드 리뷰 가이드 「리뷰어 — 무엇을 보는가」 2번의 "오류 경로 테스트가 있는가"). 대역을 태울 필요 없이 순수 단위 테스트로 충분합니다 — 테스트 규약도 순수 단위 테스트에는 Context를 올리지 않게 하고 있고, OAuthUserInfo.from은 아직 단위 테스트가 하나도 없습니다. 예: assertThatThrownBy(() -> OAuthUserInfo.from("naver", Map.of("resultcode", "00", "response", Map.of())))IllegalStateException이고 메시지에 response.id가 담기는지. 같은 클래스에 Kakao의 kakao_account 없음(이메일 null 허용)과 Google의 sub 없음까지 세 줄로 넣어 두면 정규화 세 분기가 전부 고정됩니다.

  • src/main/resources/application.yml:55 — 질문입니다. Naver는 인가 요청의 scope를 보지 않고 콘솔에 설정한 「제공 정보 선택」으로 항목이 정해지는 것으로 알고 있는데, 여기 scope: email이 실제로 무엇을 바꾸나요? 확인하고 여쭙는 것이 아니라 바로 위 Kakao의 scope: account_email은 (PR 본문대로) 없으면 KOE205가 나는 실동작 항목인데 두 줄이 같은 모양으로 나란히 있어서, 나중에 읽는 사람이 Naver 쪽도 같은 무게로 읽을 것 같다는 뜻입니다. 콘솔이 실제 결정자라면 Kakao 쪽처럼 한 줄 주석을 달아 두는 편이 좋겠습니다(값 자체는 그대로 둬도 무해해 보입니다).

  • src/test/java/com/pinlog/pinlogback/domain/auth/KakaoNaverLoginCallbackTests.java:73 — nit: 이 파라미터 이름 provider가 상위 클래스의 StubOAuthProvider provider 필드를 가리고, 그래서 115행의 provider() 접근자가 필요해졌습니다. 즉 접근자는 이 이름 하나 때문에 생긴 것입니다. 파라미터를 expected 같은 이름으로 바꾸면 접근자와 그 javadoc이 함께 사라지고, provider().useEmail(null)도 다른 두 테스트와 같은 provider.useEmail(null)로 통일됩니다. 같은 클래스 안에서 provider라는 이름이 타입이 다른 두 대상을 가리키는 상태가 읽는 쪽에 부담이라 제안합니다.

  • src/test/java/com/pinlog/pinlogback/domain/auth/KakaoNaverLoginCallbackTests.java:119 — nit: countMembers()@Value("${local.server.port}")·JdbcTemplate 필드가 GoogleLoginCallbackTests와 글자 그대로 같아졌습니다. SocialLoginTestSupport가 스스로 javadoc에 "이 절차가 테스트마다 복제되면 콜백 경로나 state 전달 방식이 바뀔 때 고쳐야 할 곳이 늘어난다"고 적고 있으니, 이 셋도 그 클래스로 올리는 것이 그 의도에 맞다고 봅니다(countMembersdeleted_at IS NULL 조건까지 복제돼 있어 소프트 삭제 규칙이 바뀌면 두 곳을 고쳐야 합니다). 이 PR에서 안 하셔도 됩니다.

@cherry-go-round

Copy link
Copy Markdown
Contributor Author

리뷰 감사합니다. c67ca38로 반영했습니다.

필수 — 지적이 맞았고, 실측으로 확인했습니다

OAuthUserInfo.java:39를 PR 본문에 *"유일한 방어선"*이라고 적어 두고 정작 그 줄을 지키는 테스트가 없었습니다. 말씀하신 뮤테이션을 그대로 적용해 봤습니다 — required(...)만 걷어내고 nested(...)는 그대로 두는 것입니다.

결과
OAuthUserInfoTest(신규) 2건 실패response 안에 id가 없으면 끊는다, response 자체가 없으면 끊는다
KakaoNaverLoginCallbackTests 4건 전부 통과

지적하신 공백이 그대로 재현됐습니다. 확인 후 원복했습니다.

OAuthUserInfoTest는 Context를 올리지 않는 순수 단위 테스트입니다. 제안대로 세 분기를 함께 고정했고, 성공 경로도 같이 넣었습니다 — Kakao의 숫자 id"5013244578"로 변환되는 것과 Naver의 감싼 구조에서 값이 나오는 것은 지금까지 HTTP 경계에서만 확인되던 것이라, 단위 층에도 두는 편이 낫다고 봤습니다.

목록: Google sub 없음 / Kakao id 없음 · kakao_account 없음(이메일 null 허용) / Naver response.id 없음 · response 자체 없음 / 지원하지 않는 공급자.

nit 둘 — 반영했습니다

:73 파라미터 이름. expected로 바꿨습니다. 지적하신 대로 provider() 접근자가 제가 고른 이름 하나 때문에 생긴 것이 맞습니다. 접근자와 javadoc이 함께 사라지고 세 테스트가 같은 provider.useEmail(...) 형태로 통일됐습니다.

:119 중복 헬퍼. countMembers()·port·JdbcTemplateSocialLoginTestSupport로 올렸습니다. deleted_at IS NULL이 한 곳에만 남습니다. 확인하다 하나 더 찾았는데, AuthTokenContractTests도 상속받은 port를 가리고 있었습니다 — 함께 정리했습니다. SocialLoginRedirectTestsIntegrationContainerSupport를 직접 상속하므로 자기 port를 유지합니다.

선택 둘 — 같이 처리했습니다

application.yml:55 Naver scope. 질문에 정직하게 답하면 — 제 검증으로는 갈라낼 수 없습니다. 콘솔에서 이메일을 켠 상태로만 성공했으니 scope가 기여했는지 콘솔만으로 된 것인지 구분이 안 됩니다. 말씀대로 콘솔이 결정자로 보이고, Kakao와 무게가 다르다는 점을 주석으로 표시했습니다 — 확인하지 못한 범위까지 적어 뒀습니다. 값 자체는 의도(이메일을 받는다)를 남기는 것으로 두었습니다.

configuration.md. 근거가 옳습니다 — BT-05는 보존 구역이라 규칙의 상시 참조처가 아니고, BT-05가 「관련」로 링크한 그 문서에 .env라는 말이 한 번도 없었습니다. 빈 값 규칙 한 단락과 Kakao·Naver 변수 4개를 표에 추가했습니다. 로컬 .env와 운영 Secret에 같이 적용되는 규칙이라는 점을 명시했습니다 — 인프라에 전달할 때 필요한 문장이기도 합니다.

clean check 337개 통과, 실패 0입니다.

cherry-go-round and others added 5 commits July 29, 2026 17:29
새로 들인 것은 응답 형태뿐이다. 토큰 발급·회전·쿠키·회원 확정은 공급자와 무관한
경로라 BI-18이 이미 만들어 뒀고, 이 티켓이 더한 것은 공급자마다 다른 사용자 정보를
하나로 옮기는 일이다.

  Google  sub (최상위)          / email (최상위)
  Kakao   id  (최상위, 숫자)     / kakao_account.email
  Naver   response.id           / response.email

RED — 진입 2건 + 콜백 7건이 실패(kakao/naver 미등록, UnsupportedSocialProvider)

드러난 것

- Naver만 Spring의 식별자 검사를 우회한다. user-name-attribute: response가 감싼
  Map을 지목하므로 Spring은 그 키의 존재만 확인하고 안의 id는 보지 않는다.
  Google·Kakao는 최상위 스칼라라 앞단에서 걸러 주는데 Naver만 그 보증이 없어,
  required(nested(attributes,"response","id"))가 유일한 방어선이다
- Kakao는 client secret을 본문으로 받는다. 기본값 basic이면 토큰 교환이 401이라
  client_secret_post를 명시했다. 콘솔에서 활성화해야 검사되는 선택 항목이라
  콘솔 상태와 설정이 어긋나면 콜백 마지막 단계에서 실패한다
- 이메일 없는 가입이 기본 경로일 수 있었다. Kakao는 동의항목을 콘솔에 설정하지
  않으면 인가 요청 자체가 KOE205로 거절되고, 이메일 수집은 비즈 앱 전환을 요구한다.
  네 층(DB·엔티티·팩토리·정규화)이 모두 nullable이라 통과하며 테스트로 고정했다 —
  선택 동의라 동의한 사용자도 철회할 수 있어 비즈 앱 전환 후에도 유효한 경로다

실제 Kakao·Naver 계정으로 수동 검증

  Kakao provider_user_id=5013244578  숫자를 문자열로 저장
  Naver 43자 식별자로 저장 — Map의 toString()이 아니라 response 안의 id 자체.
        위 방어선이 실제로 값을 한 지점이다
  회원 2건 분리, auth:refresh-index 회원별 생성·7일 TTL, 쿠키 4종 확인

곁가지로 기동을 막는 함정 하나를 고쳤다(BT-05). 등록정보를 추가하자 무관한 테스트
11개가 컨텍스트 실패했는데, 원인은 .env.example이 자격증명을 빈 값으로 정의하던
기존 함정이었다 — Spring Boot 4가 .env를 자동 로드하고, 빈 문자열은 "없음"이
아니어서 application.yml의 기본값(unset)을 덮는다. Google만 값이 채워져 있어
드러나지 않았다. 자격증명 6줄을 주석으로 바꿨고, .gitignore도 .env* 로 넓혔다 —
원인을 찾다 만든 .env.bak-<sha>가 추적 대상이었다.

08 §3.1은 이미 세 공급자를 적고 있어 구현이 명세를 따라잡은 것이라 공용 문서
변경은 없다.

clean check 309개 통과, 실패 0

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 지식이 네 조각으로 흩어져 있었다. "SocialProvider와 wire form(registrationId)
사이를 어떻게 오가는가" 하나인데, 두 클래스에 각각 양방향으로 펼쳐져 있었다.

  SocialLoginController  equalsIgnoreCase 순회  +  toLowerCase(ROOT)
  OAuthUserInfo          equalsIgnoreCase 순회  +  toLowerCase(ROOT)

toRegistrationId는 사실 toProvider(p).registrationId()를 손으로 펼친 것이었다.

SocialProvider가 자기 문자열 표현의 소유자다 — javadoc이 이미 "이름은 그대로
core.social_account.provider에 저장되므로 변경하면 기존 행과 어긋난다"고 적고 있다.
그런데 그 문자열을 만들고 되읽는 코드는 밖에, 두 벌로 있었다. 소유권과 구현이
갈린 상태였다.

무엇이 깨지는가: registrationId가 소문자 규칙을 벗어나는 공급자를 추가하는 순간이다
(apple-signin 같은 것). 만드는 쪽과 되읽는 쪽을 각각 고쳐야 하는데 한쪽만 고쳐도
컴파일이 통과하고, 증상은 진입 404와 콜백 실패로 서로 다른 층에서 나와 같은
원인으로 보이지 않는다. Kakao·Naver를 더하면서 이 중복의 대가가 2배가 됐다.

- SocialProvider.from(String) → Optional<SocialProvider>, registrationId()
- 예외는 호출자가 던진다. 지원하지 않는 공급자를 어떤 예외로 알릴지는 auth 도메인의
  결정이고, member 엔티티가 그 예외에 의존할 이유가 없다. AuthTokenService의
  parse(...).orElseThrow(...)와 같은 관용구다
- OAuthUserInfo.registrationId()는 아무도 쓰지 않는 죽은 메서드였다 — 함께 지웠다

동작 변경 없음. 기존 테스트(진입 404, Kakao·Naver·Google 콜백 정규화)가 그대로
회귀 검증이 된다. clean check 309개 통과, 실패 0

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
두 가지를 고쳤다.

1) BT-05의 원인이 틀렸다

"Spring Boot 4가 작업 디렉터리의 .env를 자동으로 읽는다"고 적었는데, 실제로는
application.yml이 명시적으로 올린다.

  config:
    import: optional:file:.env[.properties]

이 줄은 1ed1dbe(S15P11A705-63)에서 들어왔고, 바로 위 주석이 "Spring Boot는 .env를
자동으로 읽지 않아서"라고 적고 있었다. 그 주석을 읽지 않고 프레임워크 기본 동작으로
단정한 결과다.

증상(빈 문자열이 ${VAR:기본값}을 덮어 기동 실패)과 해결(.env.example의 자격증명
6줄을 주석 처리)은 그대로 유효하다. 기전만 틀렸다. troubleshooting/README의
"잘못 작성된 문서도 정정으로 처리" 규칙대로 지우지 않고 고쳤고, 무엇을 어떻게
틀렸는지 BT-05 본문에 남겼다 — 같은 종류의 단정을 다시 하지 않으려면 그 기록이
필요하다.

.env.example의 같은 서술도 함께 고쳤다. README 색인 행은 기전 대신 증상의 핵심
("빈 문자열은 '없음'이 아니다")을 담도록 바꿨다.

2) 테스트 수가 리베이스로 달라졌다

BI-24와 WORKLOG가 309개를 들고 있었는데 dev 리베이스로 326개가 됐다(#82 등이
가져온 테스트). 기록이 서로 다른 수를 말하는 것은 직전 PR 리뷰에서 지적받은
항목이라 같이 맞췄다. 이 티켓이 더한 것은 6개다.

동작 변경 없음. clean check 326개 통과, 실패 0

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
필수 지적이 맞았다. OAuthUserInfo.java:39의 required(...)가 "유일한 방어선"이라고
PR 본문에 적어 두고 정작 그 줄을 지키는 테스트가 없었다. 새로 추가한 콜백 테스트
4건은 전부 값이 있는 경로만 지난다.

실측으로 확인했다. required(...)만 걷어내고 nested(...)를 그대로 두는 뮤테이션에서

  OAuthUserInfoTest(신규)            2건 실패   ← 잡는다
  KakaoNaverLoginCallbackTests 4건   전부 통과  ← 못 잡는다

리뷰어가 지적한 공백이 그대로 재현됐다. 그 상태에서 response.id가 빠진 응답이 오면
provider_user_id가 null로 내려가 NOT NULL 위반이 되고, 원인이 DB까지 내려가는 것이
정확히 그 줄이 막으려던 상황이다.

OAuthUserInfoTest는 Context를 올리지 않는 순수 단위 테스트다(테스트 규약). 정규화
세 분기의 성공·오류 경로를 함께 고정했다 — Google sub 없음, Kakao id 없음과
kakao_account 없음(이메일 null 허용), Naver response.id 없음과 response 자체 없음,
그리고 지원하지 않는 공급자.

nit 둘도 반영했다.

- 파라미터 provider가 상위 클래스의 StubOAuthProvider provider 필드를 가려서
  provider() 접근자가 생겼던 것을 expected로 바꿨다. 접근자와 javadoc이 함께
  사라지고, 세 테스트가 같은 provider.useEmail(...) 형태로 통일됐다
- countMembers()·port·JdbcTemplate이 Google/Kakao·Naver 콜백 테스트에 글자 그대로
  복제돼 있던 것을 SocialLoginTestSupport로 올렸다. deleted_at IS NULL 조건이
  한 곳에만 남아 소프트 삭제 규칙이 바뀔 때 고칠 곳이 하나다. AuthTokenContractTests가
  상속받은 port를 가리고 있던 것도 함께 정리했다

선택 제안 둘도 함께 처리했다.

- Naver scope에 주석을 달았다. Kakao의 scope는 콘솔 동의항목과 어긋나면 KOE205가
  나는 실동작 항목이지만 Naver는 콘솔의 「제공 정보 선택」이 결정자로 보인다. 다만
  콘솔에서 이메일을 켠 상태로만 검증했으므로 둘을 갈라 확인하지는 못했다 — 그
  한계까지 주석에 적었다
- configuration.md에 .env 빈 값 규칙과 Kakao·Naver 변수 4개를 추가했다. BT-05는
  보존 구역이라 규칙의 상시 참조처로 맞지 않다는 지적이 옳다

clean check 337개 통과, 실패 0

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
같은 내용의 줄이 테스트 수만 다르게 두 벌 남아 있었다(309개 / 326개).

원인은 WORKLOG.md의 merge=union이다. 이 티켓의 줄을 처음 309개로 적고 dev
리베이스 후 326개로 고쳤는데, union 병합은 "기존 줄 수정"을 삭제+추가로 보지 않고
양쪽 버전을 모두 남긴다. WORKLOG 헤더가 적어 둔 그 함정이다.

  기존 줄을 수정하면 중복이 생길 수 있습니다. 두 브랜치가 같은 줄을 각각 고치면
  두 버전이 나란히 남습니다.

두 줄은 테스트 수를 빼면 1501자까지 완전히 동일했다. 낡은 309개 쪽을 지웠다.

전체를 숫자 정규화해 다시 훑었고 다른 중복은 없다(표 72줄, 고유 72줄). S15P11A705-131
줄도 같은 방식으로 수정했지만 그쪽은 리베이스 전에 고쳐 union 병합을 타지 않았다.

동작 변경 없음.

Refs #33

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cherry-go-round
cherry-go-round force-pushed the feat/S15P11A705-64-kakao-naver-login branch from c67ca38 to f065649 Compare July 29, 2026 08:33
@cherry-go-round
cherry-go-round merged commit c75151c into dev Jul 29, 2026
2 checks passed
@cherry-go-round
cherry-go-round deleted the feat/S15P11A705-64-kakao-naver-login branch July 29, 2026 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가

2 participants