1. 문제 상황
주력 서비스는 Python / Django 모놀리스로 운영 중이었고, 중장기적으로 전체 백엔드를 Java / Spring 으로 전환하는 방향이 잡혀 있었다. 한 번에 모든 도메인을 옮기는 것은 위험 부담이 컸기 때문에, 가장 기반이 되는 회원 도메인부터 단계적으로 분리하기로 했다.
이 과정에서 가장 큰 두 가지 문제가 있었다.
- DB 레벨의 도메인 간 결합: 도메인 간 호출이 API가 아닌 공유 DB를 통해 일어나, 회원 테이블을 다른 도메인이 직접 조회·조인하고 있었다. 회원 도메인만 떼어내려 해도 같은 DB를 보는 다른 도메인이 같이 영향을 받는 구조였다.
- 재가입 불가 정책: 탈퇴 시 회원 레코드를 사실상 소멸시키는 방식이라, 동일 이메일 / 휴대폰의 재가입이 받아들여지지 않아 운영 이슈가 누적되고 있었다.
2. 기존 구조

- 모든 도메인이 같은 Legacy DB를 공유했고, 회원 테이블도 도메인 간 공유 자원이었다.
users/models.py 한 파일에 User / PersonalUser / BusinessUser / AdminUser / SocialAccount / GeneralAccount 가 혼재해 책임 경계가 모호했다.
- 진짜 문제는 인터페이스(GraphQL / REST) 가 아니라 DB를 통해 도메인들이 서로 직접 묶여 있다는 점이었다.
3. 원인 분석 / 위험 요소
- 도메인 간 결합이 공유 DB 직접 참조로 이루어져 회원 스키마 변경이 곧 다른 도메인의 회귀로 이어졌다.
- 회원 / 인증은 거의 모든 트래픽이 거치는 경로라, 단 한 번의 장애가 전체 서비스 장애로 이어질 수 있었다 → 빅뱅 전환은 부적합.
- 탈퇴 시 레코드를 소멸시키는 처리 방식이라 “재가입 가능” 정책을 데이터 모델 / 코드 어느 쪽에서도 깔끔히 수용할 수 없었다.
→ 단순 리팩터링이 아닌, 도메인 경계 재정의 + 회원 라이프사이클 재설계가 동시에 필요했다.
4. 해결 방향 검토
| 대안 | 장점 | 단점 | 판단 |
|---|
| 회원 / 인증부터 단계적 분리 | 기반 도메인부터 정리, 후속 도메인이 같은 패턴 적용 가능 | 두 DB 양립 → 동기화 설계 필요 | 선택 |
| 빅뱅 전환 | 한 번에 정리 | 영향 범위가 전 서비스, 롤백 비용 매우 큼 | 제외 |
회원 도메인을 먼저 분리하는 쪽을 택한 이유는 명확했다.
- 다른 도메인 대부분이 회원에 의존하므로, 이 영역의 경계를 먼저 끊으면 후속 분리가 쉬워진다.
- “신규 서비스는 자기 DB만 쓰고, 레거시 DB로는 단방향 동기화만 한다” 라는 규칙으로 DB 결합을 끊어낼 수 있다.
- 새로 짜는 도메인 모델에 재가입 허용 정책을 자연스럽게 녹일 수 있는 시점이기도 했다.
5. 최종 설계 / 구현
5.1 전체 아키텍처

핵심 원칙은 단순하다.
- 회원 데이터의 진실은 Auth DB. Legacy DB의 회원 테이블은 레거시 / 다른 도메인이 계속 동작하기 위한 사본.
- 단방향 동기화만. 양방향 동기화는 하지 않는다.
- 회원에 대한 모든 쓰기는 auth-service만.
5.2 도메인 모델 재설계 — 소프트 딜리트로 재가입 지원
| Django (단일 파일) | Spring Boot (auth 도메인) |
|---|
User (AbstractBaseUser) | Member + MemberType + MemberStatus |
SocialAccount, GeneralAccount 분리 | MemberAuth + AuthType (다중 인증수단) |
| 권한 / 약관 / 토큰 컬럼 혼재 | MemberTermsStatus, Partner, MemberToken 분리 |
| 탈퇴 시 사실상 레코드 소멸 → 재가입 불가 | 소프트 딜리트(deletedAt + WITHDRAWN) → 재가입 허용 |
회원 라이프사이클은 다음과 같이 정리된다.

재가입 정책의 핵심 결정 사항:
- 탈퇴 회원은 소프트 딜리트로 보존 (
deletedAt, MemberStatus = WITHDRAWN).
- 재가입 시 기존 레코드를 복원하지 않고 신규
Member 생성 → 운영 도구에서 “복귀”와 “신규”가 구분 가능.
- 식별자(이메일 / 휴대폰) 유니크 제약은 활성 회원만 대상으로 적용 → 탈퇴 회원의 식별자는 새 가입을 막지 않는다.
Member.join(), Member.withdraw() 등 행위 중심 모델로 라이프사이클 규칙을 도메인에 응집.
5.3 Outbox 기반 단방향 동기화
두 DB에 동기적으로 쓰면 분산 트랜잭션 문제를 피할 수 없으므로 Transactional Outbox 패턴을 도입했다.

핵심 설계 포인트만 추리면 다음과 같다.
FOR UPDATE SKIP LOCKED 로 워커 다중화에도 이벤트 중복 처리 방지.
REQUIRES_NEW 로 이벤트 단위 트랜잭션 격리 → 한 이벤트 실패가 다른 이벤트에 전파되지 않음.
authTransactionManager / legacyTransactionManager 를 명시적으로 분리 → 트랜잭션이 어느 DB를 향하는지 코드에 드러남.
- 모든 이벤트는 멱등하게 설계되어 재처리해도 Legacy DB 상태가 1회 처리와 같다.
- 탈퇴 / 재가입 같은 라이프사이클 변경도 동일한 이벤트 흐름으로 동기화되어 두 DB의 회원 상태가 항상 일치한다.
@Transactional("authTransactionManager")
public void joinByOAuth(OAuthJoinCommand cmd) {
Member member = Member.join(cmd.toMemberProfile());
memberRepository.save(member);
outboxEventPublisher.publish(
OutboxEvent.of(EventType.OAUTH_JOIN, member.toSyncPayload())
);
}
6. 검증
- 가입 / 로그인 / OAuth / 비밀번호 변경 / 휴대폰 변경 / 탈퇴 / 재가입 등 12종 이벤트 시나리오 자동 테스트.
- “탈퇴 → 재가입 → 정상 로그인” 시나리오에서 다음을 모두 확인:
- 동일 이메일 / 휴대폰으로 재가입이 정상 수락되는가
- 탈퇴 회원은
WITHDRAWN 상태로 보존되어 운영 도구에서 신규와 구분되는가
- Legacy DB가 동일한 라이프사이클을 따라가는가
- 동일 이벤트 강제 재발행 시 Legacy DB 상태가 1회 처리와 같은지(멱등성), 워커 강제 종료 후 재기동 시 누락 없이 재처리되는지 확인.
- Auth DB / Legacy DB 양쪽을 같은 사용자 식별자로 조회해 핵심 필드 정합성 검증 스크립트를 운영 데이터 기준 실행.
- 배포 후 Outbox FAILED 건수를 Actuator / 알림 채널로 상시 모니터링.
기준은 단 하나였다 — “회원이 두 DB에서 다르게 보이는 순간이 단 한 번도 없어야 한다.”
7. 결과
- 마이그레이션 이후 6개월간 회원 / 인증 관련 장애 0건.
- 회원 / 인증 도메인이 독립 서비스로 분리되어, 인증 정책 / OAuth / 토큰 흐름을 다른 도메인 영향 없이 배포 가능.
- 운영 이슈로 누적되던 “재가입 불가” 문제를 해소, 탈퇴 / 재가입 라이프사이클이 도메인 모델에 명시적으로 표현됨.
- 도메인 간 결합 방식이 “공유 DB 직접 참조 → 단방향 동기화”로 바뀌어, 후속 도메인 분리 시 같은 패턴 재사용 가능.
- Java / Spring 전환 로드맵의 첫 도메인 전환을 장애 없이 끝냈다는 사실 자체가 이후 작업의 가장 큰 자산이 되었다.