static create 패턴이 유용한 상황

대부분의 경우 생성자를 사용하는 것이 더 직관적이고 단순하지만, 다음과 같은 상황에서는 static create가 더 적합

  • 생성 로직이 복잡하거나 조건부 처리가 필요할 때
  • 팩토리 패턴을 간소화하고 싶을 때
  • 생성 과정에서의 불변성을 강화하고 싶을 때
  • 다른 타입을 반환하거나, 실패 가능성을 명시적으로 처리하고 싶을 때

1. 생성 로직 커스터마이징

  • 생성자가 단순히 필드 초기화만 수행하는 경우는 일반 생성자로 충분하지만, 생성 과정에서 유효성 검사, 값 변환, 혹은 복잡한 비즈니스 로직이 필요하다면 static create 패턴이 더 적합
  • 예를 들어, 유효성 검사를 통과하지 못하면 객체를 반환하지 않거나, 에러를 던질 수 있음
  • 생성 과정에서의 제약 조건을 명시적으로 처리할 수 있음
class User {
  private constructor(private readonly name: string) {}

  static create(name: string): User | null {
    if (!name || name.length < 3) {
      console.error('Invalid name!')

      return null
    }

    return new User(name)
  }
}

const validUser = User.create('John') // 성공
const invalidUser = User.create('Jo') // 실패, null 반환

2. 팩토리 패턴의 간소화

  • static create를 간단한 팩토리 메서드로 활용해서 객체 생성의 복잡성을 숨기기
  • 객체의 생성 방식이 호출자의 관점에서는 중요하지 않다면 내부 로직을 캡슐화할 수 있다
  • 이런 방식은 타입에 따라 다른 구현체를 생성해야 하는 경우
class Shape {
  static create(type: 'circle' | 'rectangle', size: number): Shape {
    if (type === 'circle') {
      return new Circle(size)
    } else {
      return new Rectangle(size)
    }
  }
}

3. 불변성 및 제약 강화

  • 생성자를 private로 설정하고, static 메서드만을 통해 객체를 생성하도록 제한하면 객체의 불변성을 더 강하게 유지할 수 있다
  • 특정 조건에 따라 객체의 생성을 제한하거나 실패하도록 설계할 수 있다
class CardNumber {
  private constructor(private readonly number: string) {}

  static create(number: string): CardNumber | null {
    if (!CardNumber.isValid(number)) {
      return null
    }

    return new CardNumber(number)
  }

  private static isValid(number: string): boolean {
    return number.length === 16
  }
}

const card = CardNumber.create('1234567812345678') // 유효한 카드
const invalidCard = CardNumber.create('123') // null

4. 다른 타입 반환

생성자는 항상 해당 클래스의 인스턴스만 반환할 수 있다. 반면, static create는 경우에 따라 다른 타입의 값을 반환할 수 있다.

class Result<T> {
  private constructor(
    public readonly value: T | null,
    public readonly error: string | null
  ) {}

  static success<T>(value: T): Result<T> {
    return new Result(value, null)
  }

  static failure<T>(error: string): Result<T> {
    return new Result(null, error)
  }
}

class User {
  private constructor(private readonly name: string) {}

  static create(name: string): Result<User> {
    if (!name || name.length < 3) {
      return Result.failure('Name must be at least 3 characters long')
    }

    return Result.success(new User(name))
  }
}

const result = User.create('Jo') // 실패 결과 반환

5. 객체 재사용

동일한 인스턴스를 재사용하고 싶을 때도 static create 패턴이 적합하다. 예를 들어, 싱글톤(Singleton) 패턴처럼 동작하거나 캐싱된 객체를 반환할 수 있다.

class Configuration {
  private static instance: Configuration

  private constructor(public readonly settings: Record<string, string>) {}

  static create(): Configuration {
    if (!Configuration.instance) {
      Configuration.instance = new Configuration({ mode: 'production' })
    }

    return Configuration.instance
  }
}

const config1 = Configuration.create()
const config2 = Configuration.create()

console.log(config1 === config2) // true
#381

Footnotes

  1. 클래스에 대한 구문, 메서드, 속성, 상속 등에 대한 포괄적인 설명

  2. 클래스, 프로토타입 및 팩토리 함수 사용과 같이 객체 지향 프로그래밍을 구현하는 다양한 접근 방식을 설명과 각 접근 방식의 예와 장단점들

#5