트러블슈팅과 고민

소셜 로그인/회원가입 반환 프로토콜 재설계

정재익 2025. 10. 3. 03:02

문제 발견

시작은 단순했다. “기존 회원과 신규 회원의 로그인 반환값을 어떻게 구분할까?”
당시 구조에서는 MemberDetail이라는 DTO가 모든 책임을 떠안고 있었다. 로그인 과정에서 Member 도메인이 MemberDetail을 생성해 Auth 도메인으로 넘기고, Auth는 그 DTO를 기반으로 JWT와 쿠키를 발급했다.

하지만 곧 두 가지 문제가 눈에 들어왔다.
첫째, 기존 회원과 신규 회원의 흐름이 모두 하나의 DTO에 묶여 있어 분기 로직이 어색했다. 기존 회원의 경우 실제 Member 엔티티가 필요한데, DTO로 변환되는 순간 더티 체킹이나 추가 갱신이 불가능했다. 반대로 신규 회원은 DTO만 넘겨둔 채 Redis에 임시 저장을 맡기다 보니 “회원가입을 완료하지 않은 사용자”의 상태를 추적하기가 어려웠다.

둘째, MemberDetail이 사실상 모든 도메인의 공용 DTO처럼 사용되면서, Auth 도메인이 Member 도메인의 내부 구조에 과도하게 얽히는 문제가 생겼다. “기존 회원일 때 바로 쿠키를 발급해야 한다”는 요구 때문에 Member 도메인에서 AuthToken까지 생성하는 상황도 발생했다. 결국 반환값 하나가 전체 구조를 비틀어 놓고 있었던 셈이다.


고민

자연스럽게 떠오른 첫 질문은 “그럼 반환 타입을 DTO 대신 엔티티로 바꾸면 되지 않을까?”였다.
하지만 엔티티를 곧장 Auth 도메인으로 넘기면, 트랜잭션 경계가 어긋나서 지연 로딩이나 더티 체킹이 동작하지 않거나 Auth가 Member 내부를 직접 다루는 문제가 생긴다. 그렇다고 MemberDetail을 완전히 없애버리자니 JWT, SSE, 필터 로직까지 전부 개편해야 했다.

결국 방향은 이렇게 정리됐다.

  • 반환값을 최소화하면서도 단일 책임 원칙을 지킬 것.
  • 기존 회원이면 Member, AuthToken, FCM이 어떻게 연결되는지 정리할 것.
  • 신규 회원이면 Redis에 어떤 데이터를 남기고, 가입 단계에서 어떻게 복원할지 정의할 것.
  • 로그인 단계와 가입 단계 사이에서 어떤 정보가 전달돼야 하는지 구체화할 것.

핵심은 ‘책임 분리’였다. 기존 MemberSaveService는 “기존 회원 처리 + 신규 회원 처리 + FCM 등록 + AuthToken 생성”을 모두 떠안고 있었지만, 이제는 각 단계가 자신이 맡은 역할만 하도록 쪼개야 했다.


해결 방법

가장 먼저 AuthToMemberPort를 새로 설계했다.
기존에는 delegateUserData 한 메서드로 모든 흐름을 처리했지만, 새 구조에서는 checkMember, handleExistingMember, handleNewUser 세 가지 메서드로 나눴다.

  • SocialLoginService는 checkMember로 회원 존재 여부를 확인한다.
  • 기존 회원이라면 handleExistingMember를 호출해 Member 엔티티 자체를 돌려받는다.
    • 이때 MemberSaveService는 “주어진 엔티티에 소셜 닉네임/프로필/카카오 토큰을 반영하고 반환”만 담당한다.
    • 더 이상 DTO를 건네주지 않아도 트랜잭션이 유지된다.
  • 새 카카오 토큰, AuthToken, FCM 토큰 생성은 SocialLoginService가 글로벌 포트를 통해 처리한다.
  • 마지막에 JWT에 필요한 필드만 담은 MemberDetail을 새로 생성한다.

이제 기존 회원 흐름은 Member와 MemberDetail 두 축으로 분리되었고, 도메인 간 결합도 최소화됐다.

신규 회원 흐름은 훨씬 단순해졌다.

  • handleNewUser는 SocialMemberProfile과 UUID를 Redis에 저장한다.
  • SocialLoginService는 이 UUID로 임시 쿠키를 발급한다.
  • 회원가입 단계(SignUpService)가 호출되면 Redis에서 프로필을 꺼내 Member 엔티티를 만들고, SaveMemberPort가 이를 영속화한다.
  • 이후 AuthToken, FCM, JWT 발급은 글로벌 서비스에서 이어진다.

즉, 로그인 단계에서는 UUID를 통해, 가입 단계에서는 Member와 MemberDetail을 통해 신규 회원 반환값을 처리하는 구조로 정리됐다.


결과

이 설계를 적용하면서 가장 크게 얻은 성과는 도메인 경계가 명확해졌다는 점이다.
기존에는 MemberSaveService가 기존 회원/신규 회원/FCM/AuthToken까지 모두 책임졌지만, 이제는 각 포트와 서비스가 명확히 분리되었다.

  • 기존 회원 경로: 카카오 토큰 저장 → Member 갱신 → AuthToken 저장 → FCM 등록 → JWT 생성
  • 신규 회원 경로: Redis 저장 → UUID 발급
  • 회원가입 경로: Redis 임시 데이터 → Member 생성 → AuthToken/FCM → JWT 발급

테스트도 자연스럽게 단순해졌다. 경로마다 검증 포인트가 뚜렷해졌고, 한눈에 흐름을 따라갈 수 있다.

무엇보다도 처음 문제였던 “기존 회원과 신규 회원의 반환값을 어떻게 구분할까?”는 더 이상 걸림돌이 아니다.

  • 기존 회원은 Member 엔티티
  • 신규 회원은 UUID + Redis 프로필
  • MemberDetail은 JWT, SSE, 필터에서 필요한 최소 데이터만 담는 인증 전용 DTO로 자리잡았다.

한 번의 로그인/회원가입 요청이 어떤 경로로 흘러가고, 각 단계가 무엇을 책임지는지 명확히 보이는 것. 이것이 이번 리팩터링의 가장 큰 수확이었다.