Spring 7기 프로젝트/모임 플렛폼 프로젝트

도메인 경계 분리를 통한 Auth와 User 도메인 리팩토링

JuNo_12 2025. 7. 24. 17:50

들어가며

스프링 부트 기반의 모임 플랫폼을 개발하면서, 초기에는 Auth 도메인에서 인증과 사용자 관리를 함께 처리하도록 설계했습니다. 하지만 프로젝트가 성장하면서 도메인 간의 책임이 모호해지고, 특히 Auth와 User 도메인 사이의 강한 결합도가 문제가 되기 시작했습니다. 이 글에서는 도메인 경계를 명확히 분리하고, WebClient를 활용한 느슨한 결합 구조로 리팩토링한 과정을 공유하겠습니다.


기존 구조의 문제점

1. 도메인 책임의 모호함

초기 설계에서 Auth 도메인은 다음과 같은 책임을 가지고 있었습니다:

Auth 도메인이 순수한 인증/인가 책임뿐만 아니라 사용자 생성, 삭제까지 담당하면서 단일 책임 원칙을 위반하고 있었습니다.

 

2. 강한 결합도

가장 큰 문제는 UserSocial 엔티티와 User 엔티티 간의 직접적인 연관관계였습니다:

이로 인해 Auth 도메인이 User 도메인에 강하게 의존하게 되었고, 다음과 같은 문제들이 발생했습니다:

  • Auth 도메인만 독립적으로 테스트하기 어려움
  • User 도메인의 변경이 Auth 도메인에 직접적인 영향을 미침
  • 향후 마이크로서비스로 분리할 때 복잡성 증가

 

3. OAuth2 서비스의 복잡성

OAuth2UserService에서 사용자 정보를 조회하고 처리하는 로직이 복잡했습니다:


리팩토링 전략

1. 도메인 책임 재정의

먼저 각 도메인의 책임을 명확히 정의했습니다:

Auth 도메인

  • JWT 토큰 생성 및 검증
  • OAuth2 인증 처리
  • 로그인/로그아웃
  • 토큰 재발급

User 도메인

  • 사용자 정보 관리 (CRUD)
  • 회원가입/탈퇴
  • 사용자 프로필, 점수, 팔로우 등 비즈니스 로직
  • 사용자 관련 모든 데이터 조작

 

2. WebClient를 통한 느슨한 결합

도메인 간 통신을 위해 WebClient 기반의 클라이언트를 구현했습니다:

 

3. API 엔드포인트 추가

User 도메인에 이메일 기반 조회 API를 추가했습니다:


리팩토링 구현

1. UserSocial 엔티티 수정

연관관계를 제거하고 단순한 외래키 참조로 변경했습니다:

@Entity
@Table(name = "user_social")
public class UserSocial {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_id")
    private Long userId;  // User 엔티티 대신 ID만 참조
    
    private String providerId;
    private OAuth2Type type;
}

 

2. OAuth2UserService 리팩토링

UserRepository 직접 의존성을 제거하고 UserClient를 사용하도록 변경했습니다:

@Service
public class OAuth2UserService extends DefaultOAuth2UserService {
    private final UserSocialRepository userSocialRepository;
    private final UserClient userClient;  // WebClient 사용
    
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        String email = oAuth2Response.getEmail();
        
        UserSocial userSocial = userSocialRepository.findByProviderId(providerId);
        UserClientResponseDto user = userClient.getUserByEmail(email);  // WebClient 호출
        
        if (userSocial == null && user == null) {
            // 회원가입 로직을 User 도메인으로 위임
            user = userClient.registerOAuth2User(oAuth2Response);
        }
        
        return new CustomOAuth2User(user.getId(), oAuth2Response);
    }
}

 

3. 회원가입/탈퇴 로직 이동

Auth 도메인에 있던 회원 관리 로직을 User 도메인으로 이동했습니다:

// Before: AuthService에 있던 로직
// After: UserService로 이동
@Service
public class UserService {
    
    @Transactional
    public void registerUser(RegisterRequestDto request) {
        validateRegistration(request);
        
        User user = User.builder()
            .nickname(request.getNickname())
            .email(request.getEmail())
            .password(passwordEncoder.encode(request.getPassword()))
            .latitude(request.getLatitude())
            .longitude(request.getLongitude())
            .build();
            
        userRepository.save(user);
    }
}

리팩토링 결과 및 장점

1. 명확한 도메인 분리

각 도메인이 자신의 핵심 책임에만 집중할 수 있게 되었습니다:

  • Auth: 순수한 인증/인가 로직만 담당
  • User: 사용자 관련 모든 비즈니스 로직을 담당
  • 테이블 관리 책임이 명확하다는 장점

 

2. 느슨한 결합

WebClient를 통한 HTTP 통신으로 도메인 간 의존성을 크게 줄였습니다:

// Before: 직접 의존
private final UserRepository userRepository;

// After: HTTP 통신
private final UserClient userClient;

 

3. 테스트 용이성 향상

각 도메인을 독립적으로 테스트할 수 있게 되었습니다:

@ExtendWith(MockitoExtension.class)
class OAuth2UserServiceTest {
    
    @Mock
    private UserClient userClient;  // Mock 객체로 쉽게 테스트
    
    @Test
    void loadUser_신규사용자_성공() {
        // given
        given(userClient.getUserByEmail(anyString())).willReturn(null);
        
        // when & then
        // Auth 도메인만 독립적으로 테스트 가능
    }
}

 

4. 확장성 개선

향후 마이크로서비스로 분리하거나 다른 시스템과 연동할 때 변경 범위가 최소화됩니다.


성능 고려사항

HTTP 통신으로 인한 네트워크 오버헤드가 발생할 수 있지만, 다음과 같은 방법으로 최적화했습니다:

  1. 적절한 타임아웃 설정: 5초 타임아웃으로 응답성 보장
  2. 에러 핸들링: 네트워크 오류 시 적절한 fallback 처리
  3. 로깅: 디버깅을 위한 상세한 로그 기록

현재 단일 애플리케이션 내에서는 성능 영향이 미미하며, 향후 서비스 분리 시에는 더 큰 이점을 제공할 것으로 예상됩니다.


마무리

이번 리팩토링을 통해 도메인 간 경계를 명확히 하고, 각 도메인의 응집도를 높일 수 있었습니다. 특히 Auth와 User 도메인의 분리는 코드의 가독성과 유지보수성을 크게 향상시켰습니다.

물론 HTTP 통신으로 인한 약간의 성능 오버헤드는 있지만, 이는 명확한 도메인 분리와 확장성이라는 더 큰 이점을 고려할 때 충분히 감수할 만한 트레이드오프라고 생각합니다.

앞으로도 도메인 주도 설계 원칙을 바탕으로 각 도메인이 자신의 책임에만 집중할 수 있는 구조를 만들어 나가겠습니다.