feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가 - #94
Conversation
04345a2 to
b58d07c
Compare
PR 94 — feat(S15P11A705-64): Kakao·Naver 소셜 로그인 추가
리뷰 본문
먼저 좋았던 것. PR 본문의 「리뷰 포인트」가 실제로 리뷰가 봐야 할 곳을 정확히 지목합니다 — Naver만 Spring의 식별자 검사를 우회한다는 것( 범위. 필수 하나입니다 — 설정 규약 쪽에 제안이 하나 있습니다(선택). BT-05가 "빈 값으로 두지 말고 줄 자체를 없앤다가 이 저장소의 규칙이 됐다"고 적고 있는데, BT-05가 「관련」로 링크한 설정 규약에는
줄 단위 코멘트
|
|
리뷰 감사합니다. 필수 — 지적이 맞았고, 실측으로 확인했습니다
지적하신 공백이 그대로 재현됐습니다. 확인 후 원복했습니다.
목록: Google nit 둘 — 반영했습니다
선택 둘 — 같이 처리했습니다
|
새로 들인 것은 응답 형태뿐이다. 토큰 발급·회전·쿠키·회원 확정은 공급자와 무관한
경로라 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>
c67ca38 to
f065649
Compare
요약
명세 §3.1이 요구하는 3사 소셜 로그인 중 Google만 구현돼 있어 나머지 둘을 채웠다. 프론트는 UI를 이미 붙여두고 백엔드를 대기하던 상태였다. 새로 들인 것은 응답 형태뿐이다 — 토큰 발급·회전·쿠키·회원 확정은 공급자와 무관한 경로라 BI-18이 이미 만들어 뒀다.
Jira (필수)
관련 GitHub Issue
변경 사항
정규화 — 공급자마다 사용자 정보 응답 구조가 다르다.
user-name-attributesub(최상위)email(최상위)subid(최상위, 숫자)kakao_account.emailidresponse.idresponse.emailresponseOAuthUserInfo.from에 두 공급자 분기 추가,UnsupportedSocialProviderException분기 제거application.yml에 등록정보 + 공급자 엔드포인트(Spring 내장CommonOAuth2Provider는 google·github·facebook·okta만 있다)KakaoNaverLoginCallbackTests신규 4건(파라미터화), 진입 리다이렉트 2건, 대역·지원 클래스 공급자 파라미터화리팩터(별도 커밋
c3948fa) —registrationId변환이 두 클래스에 양방향으로 흩어져 있던 것을SocialProvider로 모았다.설정 함정 수정 —
.env.example·.gitignore(아래 「미결/후속」 참고)테스트 / 검증
./gradlew clean check --no-daemonIntegrationContainerSupport(PostgreSQL + Redis)08 §3.1이 이미 세 공급자를 적고 있어 이번 변경은 구현이 명세를 따라잡은 것이다RED — 진입 2건 + 콜백 7건이 실패(kakao·naver 미등록 → 404,
UnsupportedSocialProviderException).GREEN → Regression
실제 Kakao·Naver 계정으로 수동 검증 — 스텁이 구조적으로 덮지 못하는 것을 보려고 브라우저로 한 번씩 돌렸다(인증 계약 §8).
provider_user_id5013244578— 숫자를 문자열로 저장provider_user_idresponse안의id자체(MaptoString()이 아니다)member·social_account각 2건 —(provider, provider_user_id)가 달라 별도 회원auth:refresh:{id}:{jti}+auth:refresh-index:{id}회원별, 7일 TTLlogged_in·XSRF-TOKEN이document.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_idNOT 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변환이SocialLoginController와OAuthUserInfo에 양방향으로 네 조각 흩어져 있었다 —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