Engineering

디자인 패턴, 팩토리 메서드(Factory Method Pattern)

소프트웨어를 개발하다 보면 객체를 생성하는 코드가 점점 복잡해지는 경우가 있습니다. 처음에는 단순히 new 키워드를 이용해 객체를 생성하면 되지만 서비스 규모가 커질수록 객체 생성 과정에 다양한 조건이 추가되기 때문입니다.

M
Mason
Software Engineer · · 5분

소프트웨어를 개발하다 보면 객체를 생성하는 코드가 점점 복잡해지는 경우가 있습니다. 처음에는 단순히 new 키워드를 이용해 객체를 생성하면 되지만 서비스 규모가 커질수록 객체 생성 과정에 다양한 조건이 추가되기 때문입니다.

예를 들어 회원 인증 시스템을 개발한다고 가정해보겠습니다. 초기에는 카카오 로그인만 지원하기 때문에 단순하게 구현할 수 있습니다.

const provider = new KakaoProvider();
const user = await provider.verify(token);

하지만 서비스가 성장하면서 구글 로그인, 애플 로그인, 네이버 로그인 등이 추가될 수 있습니다.

if (providerType === 'kakao') {
  provider = new KakaoProvider();
}
 
if (providerType === 'google') {
  provider = new GoogleProvider();
}
 
if (providerType === 'apple') {
  provider = new AppleProvider();
}

처음에는 큰 문제가 없어 보이지만 새로운 로그인 방식이 추가될 때마다 객체 생성 코드가 여러 곳에 흩어지게 됩니다. 또한 객체 생성 방식이 변경되면 이를 사용하는 모든 코드를 수정해야 하는 문제가 발생합니다.

이러한 문제를 해결하기 위해 사용하는 패턴이 팩토리 메서드(Factory Method) 패턴입니다.

팩토리 메서드 패턴은 객체를 직접 생성하지 않고 객체 생성을 전담하는 클래스를 통해 필요한 객체를 생성하는 패턴입니다. 이를 통해 객체 생성 로직과 실제 비즈니스 로직을 분리할 수 있습니다.

먼저 로그인 제공자에 대한 공통 인터페이스를 정의합니다.

export interface SocialProvider {
  verify(token: string): Promise<SocialUser>;
}

각 로그인 방식은 해당 인터페이스를 구현합니다.

export class KakaoProvider implements SocialProvider {
  async verify(token: string) {
    return this.kakaoApi.getUser(token);
  }
}
export class GoogleProvider implements SocialProvider {
  async verify(token: string) {
    return this.googleApi.verifyIdToken(token);
  }
}

이제 객체 생성을 담당하는 팩토리 클래스를 구현합니다.

export class SocialProviderFactory {
  create(type: string): SocialProvider {
    switch (type) {
      case 'kakao':
        return new KakaoProvider();
 
      case 'google':
        return new GoogleProvider();
 
      case 'apple':
        return new AppleProvider();
 
      default:
        throw new Error('지원하지 않는 로그인 방식입니다.');
    }
  }
}

실제 서비스에서는 비즈니스 로직이 객체 생성 과정을 알 필요가 없습니다.

const provider = factory.create(providerType);
 
const userInfo = await provider.verify(token);

이 구조의 가장 큰 장점은 확장성입니다.

만약 네이버 로그인을 추가해야 한다면 새로운 구현체만 작성하면 됩니다.

export class NaverProvider implements SocialProvider {
  async verify(token: string) {
    return this.naverApi.getUser(token);
  }
}

이후 팩토리에 등록만 하면 기존 인증 로직은 수정할 필요가 없습니다.

팩토리 메서드 패턴은 로그인 시스템 외에도 다양한 곳에서 활용됩니다.

결제 시스템에서는 카드 결제, 계좌이체, 간편결제 객체를 생성할 때 사용할 수 있으며 파일 저장소에서는 Local Storage, AWS S3, MinIO 등의 저장소 구현체를 선택할 때 활용할 수 있습니다.

실제로 NestJS 기반 서비스를 개발하면서도 비슷한 구조를 자주 사용하게 됩니다. 예를 들어 파일 업로드 기능을 구현할 때 개발 환경에서는 Local Storage를 사용하고 운영 환경에서는 AWS S3를 사용하는 경우가 있습니다.

const storage = storageFactory.create(process.env.STORAGE_TYPE);

애플리케이션은 어떤 저장소가 사용되는지 알 필요 없이 동일한 인터페이스만 호출하면 됩니다.

팩토리 메서드 패턴은 단순히 객체를 생성하는 패턴이 아닙니다. 객체 생성 책임을 분리하여 비즈니스 로직의 변경을 최소화하고 확장성을 높이기 위한 설계 방법입니다.

서비스 규모가 커질수록 새로운 구현체가 추가되는 경우가 많아지는데, 이때 객체 생성 로직이 비즈니스 코드 곳곳에 퍼져 있다면 유지보수가 어려워집니다. 반대로 팩토리 메서드 패턴을 적용하면 변경 지점을 한 곳으로 모을 수 있으며 새로운 기능 추가에도 유연하게 대응할 수 있습니다.

객체 생성 과정에 조건문이 많아지고 새로운 구현체가 계속 추가되고 있다면 팩토리 메서드 패턴 적용을 고려해볼 시점일 수 있습니다.

이 글이 어떠셨나요? 이모지로 반응을 남겨주세요
M
Mason
Software Engineer · masonlab

코드와 비즈니스, 그 사이에서 배운 것들을 기록하고 공유합니다.

댓글 0

아직 댓글이 없어요. 첫 댓글을 남겨보세요.

이런 글은 어때요?