도메인 객체 생성을 위한 Factory 패턴 도입기
2025. 5. 9. 01:37
728x90

1. 문제의 시작: 점점 커져가는 use-case

회원가입 기능을 registerUser()라는 함수로 구현했을 때 다음과 같은 문제가 발생했다:

  • 입력값 유효성 검사
  • 이메일 중복 체크
  • 비밀번호 해시화
  • User Entity 생성
  • User DB 저장
  • 세션 생성 및 저장
  • 토큰 발급
  • RefreshToken 저장

이 모든 로직이 한 use-case에 몰려 있었다. 결국 registerUser()는 '회원가입 흐름을 처리'하는 책임 외에도 다양한 객체를 생성하고, 가공하고, 저장하는 책임까지 떠안고 있었다.

이건 단일 책임 원칙(SRP)을 위반한 대표적인 사례다.


2. 그래서 선택한 Factory 패턴

Factory란?

복잡한 객체 생성 로직을 하나의 책임으로 모은 구조 패턴

단순한 new User(...) 수준을 넘어서서 다음과 같은 작업이 필요한 경우 적합하다:

  • 입력값 가공 (예: 비밀번호 해시)
  • 기본값 설정 (예: 가입일, 유효기간 등)
  • 정책 적용 (예: 이용약관 동의 여부에 따른 처리)

언제 사용하는가?

  • Entity 생성 전에 전처리가 필요한 경우
  • 같은 생성 로직을 여러 use-case에서 재사용할 때
  • 테스트 시 더미 Entity 생성을 일관되게 만들고 싶을 때

3. 예제 코드: 회원가입 흐름 개선

기존 코드 (비추천)

const hashedPassword = await hashPassword(dto.password);
const newUser = this.userRepository.create({
  ...dto,
  password: hashedPassword,
  privacyAgreementDate: dto.agreedToPrivacyPolicy ? new Date() : null,
  privacyAgreementExpireAt: dto.agreedToPrivacyPolicy ? new Date(Date.now() + 1000 * 60 * 60 * 24 * 365) : null
});

개선된 코드

const newUser = await UserFactory.createFromRegisterDto(dto);

UserFactory.ts

export class UserFactory {
  static async createFromRegisterDto(dto: RegisterUserDto): Promise<User> {
    const hashedPassword = await hashPassword(dto.password);

    return {
      ...dto,
      password: hashedPassword,
      privacyAgreementDate: dto.agreedToPrivacyPolicy ? new Date() : null,
      privacyAgreementExpireAt: dto.agreedToPrivacyPolicy
        ? new Date(Date.now() + 1000 * 60 * 60 * 24 * 365)
        : null,
    } as User;
  }
}

이렇게 하면 회원가입 흐름은 다음처럼 간결해진다:

const user = await UserFactory.createFromRegisterDto(dto);
await this.userRepository.save(user);

4. 얻은 이점

  • 비즈니스 흐름 (use-case) 은 순차적 로직만 표현
  • 생성 책임은 factory에 분리되어 테스트 및 유지보수 용이
  • 여러 유즈케이스에서 재사용 가능한 표준 생성 로직 확보

5. 정리하며

Factory 패턴은 어렵지 않다. "객체를 만들 때 뭔가 전처리나 규칙이 들어간다면?" → 그건 Factory가 맡을 타이밍이다.

단순한 CRUD가 아닌 도메인 주도 설계의 기본적인 설계 습관으로서도,
신입 개발자일수록 반드시 익혀야 할 아키텍처적 사고이기도 하다.

반응형