[TMT-271] feat: 카카오 로그인 - #75
Conversation
| nickname = normalizeNickname(profile.nickname), | ||
| profileImageUrl = profile.profileImageUrl, | ||
| ) | ||
| if (created != null) return created.toResult(isNewUser = true) |
There was a problem hiding this comment.
[must] 가입 보상 티켓 1장이 빠졌습니다.
명세 X §2 마지막 줄입니다 — "가입이 성립하면 기본 티켓 1장을 지급한다 (T2)". 지금은 users 행만 만들고 RewardGrant·GroupJoinTicket이 안 생깁니다.
명세 §4-2에 "T2를 유지하는가"가 미확정으로 올라와 있어 헷갈릴 수 있는데, 확인해보니 유지가 맞습니다. 그리고 이건 명세 한 줄이 아니라 이미 여러 곳이 전제하고 있습니다.
V1__init.sql—source_type VARCHAR(20)주석이'SIGNUP' | 'REVIEW',reward_grant_uq UNIQUE (source_type, source_id, reward_type)가 가입당 1장·재발급 불가를 위한 제약(T8)RewardSourceTypeenum에SIGNUP존재- mock이 이미 모든 사용자에게 1장을 주고 있습니다 (
MockStoreConfig.signupReward,SIGNUP_BONUS = 1) - 마이페이지 티켓 이력에
SIGNUP_REWARD타입으로 내려가고 FE 테스트가 검증합니다 (UserMockControllerTest:291)
isNewUser = true인 경로에서 RewardGrant(sourceType = SIGNUP, sourceId = userId) + GroupJoinTicket 1장을 만들어주세요. reward_grant_uq가 있어 동시 로그인 경쟁에서도 두 번 나가지 않습니다.
| ) | ||
|
|
||
| data class KakaoLoginResponse( | ||
| val userId: Long, |
There was a problem hiding this comment.
[q] userId가 raw Long인데, 레포의 다른 응답은 전부 user_1 문자열입니다.
PublicIds.user(id) = "user_$id"가 있고 F·G·I §6-3 예시, mock 마이페이지, ReviewCardAssembler가 다 그 형태입니다. mock의 타인 프로필은 경로 파라미터까지 user_7을 받습니다. 그래서 지금 상태면 FE가 같은 화면에서 로그인 응답만 숫자, GET /v1/users/me는 문자열을 받게 됩니다.
과도기 계약이 "응답 userId를 X-User-Id 헤더에 그대로 쓴다"라 raw가 편한 건 이해합니다. 다만 접두를 떼는 건 FE 한 줄이고, TMT-272에서 헤더가 사라지면 raw로 둘 이유가 없어집니다 — 그때 바꾸면 그게 Breaking이고요.
@wnsvy607 관련해서 하나 여쭙고 싶습니다. 이 접두 표기가 1c3cab7 (mock 32종, TMT-152)에서 처음 들어왔는데, 어떤 의도로 넣으신 건지 기억하시나요? 공통 API 규약에 표기 규칙이 없어서 근거가 코드에만 남아 있습니다.
자원 오식별 방지나 로그 가독성 같은 이유가 있었을 것 같은데, 그렇다면 규약 문서에 의도를 적어두는 게 좋겠습니다. 근거가 없다 보니 이번처럼 새 엔드포인트에서 갈리고, 예전에 54f0b66(타인 프로필 경로가 user_7을 받으면 500)처럼 mock 자신도 한 번 밟았습니다. 지금은 응답은 접두, X-User-Id 헤더는 raw로 두 표기가 공존하는 상태라 어디까지 붙는지도 정해두면 좋겠습니다.
| companion object { | ||
| /** users_nickname_len CHECK (U3) */ | ||
| private const val NICKNAME_MIN = 2 | ||
| private const val NICKNAME_MAX = 10 |
There was a problem hiding this comment.
[q] 닉네임 상한이 명세와 다릅니다 — 다만 이건 준형님 잘못이 아니라 제가 안 해둔 일입니다.
명세 X §2·§3-1이 U3를 2~20자로 올려놨는데(Figma 명세 기준), V1__init.sql의 VARCHAR(10)·CHECK BETWEEN 2 AND 10과 UserEntity의 @Column(length = 10)이 아직 그대로입니다. 명세 §3-1에 "새 V{n} 마이그레이션이 필요하다"고 할 일로 적어뒀는데 아무도 안 했고, 티켓도 없습니다.
그래서 지금 10자 절삭은 DB 현재 상태로는 맞는 구현입니다. 카카오 닉네임이 11자만 돼도 조용히 잘리는 게 문제일 뿐입니다.
혹시 이 PR에서 마이그레이션까지 같이 해주실 수 있을까요? 컬럼 폭·CHECK 제약을 늘리는 새 V{n}__*.sql + UserEntity + 여기 NICKNAME_MAX까지입니다. 부담되시면 별도 티켓으로 빼주셔도 됩니다 — 그 경우 이 상수 옆에 "명세는 20자, 마이그레이션 대기 (TMT-___)" 정도로 남겨두면 나중에 놓치지 않습니다. J 명세 §2·§6-2의 2~10자 서술도 함께 고쳐야 합니다.
| userRepository | ||
| .save(UserEntity(kakaoId = kakaoId, nickname = nickname, profileImageUrl = profileImageUrl)) | ||
| .toAccount() | ||
| } catch (e: DataIntegrityViolationException) { |
There was a problem hiding this comment.
[want] 이 복구가 성립하는 건 login에 트랜잭션이 없기 때문입니다.
save가 자기 트랜잭션 안에서 INSERT를 날리고 거기서 제약 위반이 나므로, 롤백되는 건 그 트랜잭션뿐이고 호출자의 재조회는 새 트랜잭션에서 정상 동작합니다.
나중에 누가 KakaoLoginService.login에 @Transactional을 붙이면 제약 위반이 트랜잭션을 rollback-only로 만들어 재조회가 조용히 실패합니다. 지금 코드로는 그 위험이 안 보여서, 주석 한 줄로 못 박아두면 좋겠습니다.
| # 카카오 로그인 (TMT-271). 정본은 SSM /tmt-prod/auth/* -> 배포 .env. | ||
| # 키가 없으면 AUTH_KAKAO_UNAVAILABLE로 끊는다 — juso 키와 같은 방식 | ||
| rest-api-key: ${KAKAO_REST_API_KEY:} | ||
| client-secret: ${KAKAO_CLIENT_SECRET:} |
There was a problem hiding this comment.
[must] 배포 경로가 아직 안 열려 있어서, 이대로 릴리즈 태그를 끊으면 카카오 로그인이 전부 502입니다.
세 곳이 다 빠져 있습니다.
| 곳 | 현재 |
|---|---|
cicd-release.yml |
/tmt-prod/auth/*를 읽는 코드 없음 |
| 같은 파일의 필수값 검사 루프 | auth 키가 없어 빈 값으로 조용히 배포됨 |
infra/terraform/iam.tf |
db/*·media/*·address/*·ai/*만 열려 있음 |
${KAKAO_REST_API_KEY:}가 빈 기본값이라 기동은 되고 로그인만 죽습니다. TMT-252에서 같은 걸 겪어서 iam.tf에 "배포 스크립트가 읽는 경로를 전부 열어야 한다"는 주석까지 달려 있고, juso 키(TMT-187) 때 필수값 검사 루프에 넣은 선례가 있습니다.
배포가 릴리즈 태그 기준이라 지금 당장 터지지는 않습니다. 릴리즈 전에는 반드시 필요한데, cicd-release.yml·iam.tf가 준표님 영역이라 이 PR에 넣기 애매하면 후속 티켓으로 빼고 에픽 TMT-270에 릴리즈 차단 항목으로 달아두는 것이 어떨까요.
mingdodev
left a comment
There was a problem hiding this comment.
구조는 깔끔합니다. juso 어댑터(TMT-187) 패턴을 그대로 따라가서 읽기 편했고, 특히 세 가지가 좋았습니다.
- 실패 분류 —
invalid_grant(코드 만료·재사용)만 401이고 나머지는 502 + error 로그. 리다이렉트 URI 불일치나 앱 설정 오류가 "사용자가 뭔가 잘못했나" 로 묻히지 않습니다 - 로그에
code·access token을 안 남기고error_code만 찍는 것 - 커밋을 레이어별 5개로 쪼갠 것 — 리뷰가 훨씬 수월했습니다
라인 코멘트 5개 남겼습니다. [must]는 가입 보상 티켓 누락과 배포 키 주입 둘이고, 나머지는 질문·제안입니다.
머지 전에 명세 X를 채워주세요
명세 X는 엔드포인트를 의도적으로 안 정해둔 문서입니다.
엔드포인트를 확정하지 않는다. 카카오 로그인은 UT2 이후이고 화면도 없다.
그래서 "명세 X 대조"는 대조가 아니라 명세를 새로 쓰는 일입니다. 이 PR이 정한 것들이 명세의 유일한 근거가 됩니다.
POST /v1/auth/login/kakao요청·응답isNewUser— 명세에 없던 필드이고 FE 온보딩 분기 기준AUTH_KAKAO_CODE_INVALID(401) ·AUTH_KAKAO_UNAVAILABLE(502)와 그 분류 기준- 닉네임 정규화 — 기본값
또맛또 미식가, 길이 초과 절삭 - 과도기 계약 — 응답
userId를X-User-Id헤더에 쓴다는 것과 그게 언제 끝나는지(TMT-272)
특히 실패 분류 기준은 명세에 꼭 남겨주세요. 코드 주석에만 있으면 FE가 502를 만났을 때 재시도해도 되는지 판단할 수 없습니다. juso 502처럼 "자동 재시도 금지"인지 아닌지가 화면 동작을 가릅니다.
계약 변경 이력에는 Additive로 한 줄이면 됩니다. mock에 없던 신규 엔드포인트라 기존 FE 코드는 안 깨집니다. 다만 TMT-272(X-User-Id 제거)는 Breaking이라 전환 시점과 묶어야 합니다.
wnsvy607
left a comment
There was a problem hiding this comment.
헥사고날 경계(oauth 어댑터 → KakaoAuthPort), juso 어댑터 패턴 재사용, 2s/5s 타임아웃, invalid_grant(401)/그 외(502) 분류, 동시 로그인 경쟁 수습까지 설계가 좋고 테스트 19개도 실질적입니다. 코드 자체는 approve 수준이고, 아래 [must] 하나만 채워지면 됩니다.
[must] 계약 변경 이력 미기재 — 신규 POST /v1/auth/login/kakao + ErrorCode 2종은 Additive 계약 변경인데 [계약] API 변경 이력 문서에 아직 항목이 없습니다 (현재 마지막 행 8/30, X 명세 상태도 "계획"). 본문에서 "머지 전에 반영한다"고 하셨으니 그 기록 완료를 머지 조건으로 걸어둡니다.
[want] 운영 키 주입 경로 부재 — KAKAO_REST_API_KEY/KAKAO_CLIENT_SECRET이 docker/docker-compose.prod.yml environment, cicd-release.yml의 SSM prefix(db/media/ai/address뿐), infra/terraform/iam.tf 읽기 범위 어디에도 없습니다. fail-soft라 기동은 되지만 이대로 릴리즈하면 운영 로그인이 항상 502(AUTH_KAKAO_UNAVAILABLE)입니다. SSM /tmt-prod/auth/* 등록 + 3파일 갱신이 어느 티켓에서 가는지만 명시해 주세요 (#76 리뷰의 JWT_SECRET 건과 같이 가면 딱 맞습니다).
[want] README.md 모듈 트리에 새 모듈 tmt-output-oauth가 없습니다. CLAUDE.md가 모듈 구조를 README에 위임하므로 같은 PR에서 한 줄 추가 부탁드려요. (참고: tmt-output-storage:s3도 이미 빠져 있는 기존 드리프트라 같이 잡으면 좋습니다.)
[want] KakaoLoginService.kt:52-56 — trimmed.take(NICKNAME_MAX)는 UTF-16 코드유닛 기준이라 이모지 닉네임을 10번째 유닛에서 자르면 lone surrogate가 생겨 DB 인코딩 오류(500)가 날 수 있습니다. codePoints().limit(10) 기준 절단을 권합니다. (U3 CHECK 자체는 코드포인트 기준이라 절단만 고치면 됩니다.)
[want] UserAccountAdapter.kt:25-28 — catch (DataIntegrityViolationException)이 U1 UNIQUE 경쟁 외의 무결성 위반(CHECK 등)까지 삼키고 예외를 로그 없이 버립니다. 경쟁이 아니었을 때 재조회 실패 → INTERNAL_ERROR인데 원인이 어디에도 안 남아요. 최소 logger.warn(e) 부탁드립니다.
[q] KakaoLoginService.kt:40 — 에러 로그에 카카오 회원번호(kakaoId)가 남는데, PII 로깅 정책을 따로 두고 있나요? 에러 경로 한정이라 실익이 커서 질문만 남깁니다.
Related Issue
Why
인증이 X-User-Id 헤더 스텁(TMT-150)이라 실제 사용자 식별이 없다. 9/12 런칭 범위(에픽 TMT-270)의
첫 단계로, 카카오 인가코드를 토큰으로 교환하고 카카오 회원번호로 users 행을 생성·조회하는
실구현을 넣는다. 세션·토큰 발급과 스텁 제거는 TMT-272에서 이어진다.
What
POST /v1/auth/login/kakao추가 — 요청{code, redirectUri}→ 응답{userId, nickname, profileImageUrl, isNewUser}isNewUser: 이번 로그인으로 users 행이 만들어졌는지. FE 온보딩(TMT-273) 분기 기준userId를 기존X-User-Id헤더에 그대로 사용tmt-output-oauth— 카카오 토큰 교환 +/v2/user/me프로필 조회 어댑터 (KakaoAuthPort뒤)userskakao_id 조회·없으면 생성 (UserRepository·UserAccountAdapter) — 스키마 변경 없음, V1의kakao_id(U1) 사용AUTH_KAKAO_CODE_INVALID(401) ·AUTH_KAKAO_UNAVAILABLE(502)tmt.auth.kakao.rest-api-key/client-secret— envKAKAO_REST_API_KEY/KAKAO_CLIENT_SECRET, 운영은 SSM/tmt-prod/auth/*예정계약: 로그인 엔드포인트는 mock에 없던 신규라 Additive. 계약 변경 이력 기록과 명세 X 대조는 머지 전에 반영한다.
How
AuthController→LoginWithKakaoUseCase(KakaoLoginService) →KakaoAuthPort(oauth 모듈) +UserAccountPort(persistence)KakaoHttpClient) 뒤에 RestClient 구현, 테스트는 Fake로 실카카오 호출 없음. 키 미설정이면 즉시AUTH_KAKAO_UNAVAILABLEinvalid_grant(코드 만료·재사용, KOE320)만 401로,그 외(리다이렉트 URI 불일치·앱 설정 오류·카카오 장애)는 502 + error 로그 — 설정 오류가 사용자 실패로 묻히지 않게
확정 닉네임은 온보딩(TMT-273)에서 받는다
kakao_idUNIQUE 충돌 시 생성 대신 재조회로 수습 (isNewUser=false)Prompt Log
콘솔 개편으로 위치가 문서와 달라 공식 문서 본문을 넘겨 재확인)
같은 구조로 구현 +
./gradlew build통과 확인