Skip to content

[TMT-271] feat: 카카오 로그인 - #75

Open
creatived-toychip wants to merge 5 commits into
mainfrom
feat/TMT-271
Open

[TMT-271] feat: 카카오 로그인#75
creatived-toychip wants to merge 5 commits into
mainfrom
feat/TMT-271

Conversation

@creatived-toychip

@creatived-toychip creatived-toychip commented Aug 31, 2026

Copy link
Copy Markdown

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) 분기 기준
    • 과도기 계약: 토큰 발급(TMT-272) 전까지 FE는 응답의 userId를 기존 X-User-Id 헤더에 그대로 사용
  • 새 모듈 tmt-output-oauth — 카카오 토큰 교환 + /v2/user/me 프로필 조회 어댑터 (KakaoAuthPort 뒤)
  • users kakao_id 조회·없으면 생성 (UserRepository·UserAccountAdapter) — 스키마 변경 없음, V1의 kakao_id(U1) 사용
  • ErrorCode 추가: AUTH_KAKAO_CODE_INVALID(401) · AUTH_KAKAO_UNAVAILABLE(502)
  • 설정 tmt.auth.kakao.rest-api-key / client-secret — env KAKAO_REST_API_KEY / KAKAO_CLIENT_SECRET, 운영은 SSM /tmt-prod/auth/* 예정
  • 테스트 19개 (서비스 7 · 어댑터 8 · 컨트롤러 4)

계약: 로그인 엔드포인트는 mock에 없던 신규라 Additive. 계약 변경 이력 기록과 명세 X 대조는 머지 전에 반영한다.

How

  • 헥사고날 준수: AuthControllerLoginWithKakaoUseCase(KakaoLoginService) → KakaoAuthPort(oauth 모듈) + UserAccountPort(persistence)
  • 어댑터는 juso(TMT-187) 패턴 그대로 — 최소 HTTP 인터페이스(KakaoHttpClient) 뒤에 RestClient 구현, 테스트는 Fake로 실카카오 호출 없음. 키 미설정이면 즉시 AUTH_KAKAO_UNAVAILABLE
  • 실패 분류: 사용자가 재로그인하면 되는 invalid_grant(코드 만료·재사용, KOE320)만 401로,
    그 외(리다이렉트 URI 불일치·앱 설정 오류·카카오 장애)는 502 + error 로그 — 설정 오류가 사용자 실패로 묻히지 않게
  • 닉네임은 U3(2~10자)로 정규화: 카카오 닉네임이 없거나 2자 미만이면 기본값 "또맛또 미식가", 10자 초과는 절삭.
    확정 닉네임은 온보딩(TMT-273)에서 받는다
  • 동시 로그인 경쟁: kakao_id UNIQUE 충돌 시 생성 대신 재조회로 수습 (isNewUser=false)

Prompt Log

  • 할당 이슈(TMT-270~274) 덤프를 주고 착수 순서 분석 요청 → 의존 관계상 271 선행, 274 병렬 가능 결론
  • 카카오 개발자 콘솔 설정을 단계별로 문답 (앱 생성 → 플랫폼 키/리다이렉트 URI 등록 → 클라이언트 시크릿 —
    콘솔 개편으로 위치가 문서와 달라 공식 문서 본문을 넘겨 재확인)
  • "개발부터 진행" 지시 → 기존 패턴(juso 어댑터·포트 구조·ExceptionAdvice·컨트롤러 테스트) 탐색 후
    같은 구조로 구현 + ./gradlew build 통과 확인
  • 커밋을 레이어별 5개로 분리 요청 (common → application → oauth 모듈 → persistence → api)

nickname = normalizeNickname(profile.nickname),
profileImageUrl = profile.profileImageUrl,
)
if (created != null) return created.toResult(isNewUser = true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[must] 가입 보상 티켓 1장이 빠졌습니다.

명세 X §2 마지막 줄입니다 — "가입이 성립하면 기본 티켓 1장을 지급한다 (T2)". 지금은 users 행만 만들고 RewardGrant·GroupJoinTicket이 안 생깁니다.

명세 §4-2에 "T2를 유지하는가"가 미확정으로 올라와 있어 헷갈릴 수 있는데, 확인해보니 유지가 맞습니다. 그리고 이건 명세 한 줄이 아니라 이미 여러 곳이 전제하고 있습니다.

  • V1__init.sqlsource_type VARCHAR(20) 주석이 'SIGNUP' | 'REVIEW', reward_grant_uq UNIQUE (source_type, source_id, reward_type)가입당 1장·재발급 불가를 위한 제약(T8)
  • RewardSourceType enum에 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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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는 문자열을 받게 됩니다.

과도기 계약이 "응답 userIdX-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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[q] 닉네임 상한이 명세와 다릅니다 — 다만 이건 준형님 잘못이 아니라 제가 안 해둔 일입니다.

명세 X §2·§3-1이 U3를 2~20자로 올려놨는데(Figma 명세 기준), V1__init.sqlVARCHAR(10)·CHECK BETWEEN 2 AND 10UserEntity@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) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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:}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 mingdodev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

구조는 깔끔합니다. 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)와 그 분류 기준
  • 닉네임 정규화 — 기본값 또맛또 미식가, 길이 초과 절삭
  • 과도기 계약 — 응답 userIdX-User-Id 헤더에 쓴다는 것과 그게 언제 끝나는지(TMT-272)

특히 실패 분류 기준은 명세에 꼭 남겨주세요. 코드 주석에만 있으면 FE가 502를 만났을 때 재시도해도 되는지 판단할 수 없습니다. juso 502처럼 "자동 재시도 금지"인지 아닌지가 화면 동작을 가릅니다.

계약 변경 이력에는 Additive로 한 줄이면 됩니다. mock에 없던 신규 엔드포인트라 기존 FE 코드는 안 깨집니다. 다만 TMT-272(X-User-Id 제거)는 Breaking이라 전환 시점과 묶어야 합니다.

@wnsvy607 wnsvy607 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

헥사고날 경계(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_SECRETdocker/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-56trimmed.take(NICKNAME_MAX)는 UTF-16 코드유닛 기준이라 이모지 닉네임을 10번째 유닛에서 자르면 lone surrogate가 생겨 DB 인코딩 오류(500)가 날 수 있습니다. codePoints().limit(10) 기준 절단을 권합니다. (U3 CHECK 자체는 코드포인트 기준이라 절단만 고치면 됩니다.)

[want] UserAccountAdapter.kt:25-28catch (DataIntegrityViolationException)이 U1 UNIQUE 경쟁 외의 무결성 위반(CHECK 등)까지 삼키고 예외를 로그 없이 버립니다. 경쟁이 아니었을 때 재조회 실패 → INTERNAL_ERROR인데 원인이 어디에도 안 남아요. 최소 logger.warn(e) 부탁드립니다.

[q] KakaoLoginService.kt:40 — 에러 로그에 카카오 회원번호(kakaoId)가 남는데, PII 로깅 정책을 따로 두고 있나요? 에러 경로 한정이라 실익이 커서 질문만 남깁니다.

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.

3 participants