본문 바로가기
우테코 자유테크 스터디/자유주제

[스터디] 정적 팩토리 메서드(Static Factory Method)

by ♡˖GYURI˖♡ 2023. 12. 8.

정적 팩토리 메서드란?

정적(Static), 팩토리(Factory), 메서드(Method)


이 중 팩토리는 GoF 디자인 패턴 중 팩토리 패턴에서 유래한 것으로 객체를 생성하는 역할을 분리하겠다는 의미가 담겨있다.

다시 말해, 정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드라는 의미로 요약할 수 있다.

 

java.time 패키지 내의 LocalTime 클래스의 정적 팩토리 메서드를 같이 살펴보자.

// LocalTime.class
...
public static LocalTime of(int hour, int minute) {
  ChronoField.HOUR_OF_DAY.checkValidValue((long)hour);
  if (minute == 0) {
    return HOURS[hour];
  } else {
    ChronoField.MINUTE_OF_HOUR.checkValidValue((long)minute);
    return new LocalTime(hour, minute, 0, 0);
  }
}
...

// hour, minutes을 인자로 받아서 9시 30분을 의미하는 LocalTime 객체를 반환한다.
LocalTime openTime = LocalTime.of(9, 30);

 

위 예시 코드에서 본 LocalTime 클래스의 of 메서드처럼 직접적으로 생성자를 통해 객체를 생성하는 것이 아닌, 메서드를 통해서 객체를 생성하는 것을 정적 팩토리 메서드라고 한다.

 

또 다른 예시로 enum의 요소를 조회할 때 사용하는 valueOf도 정적 팩토리 메서드의 일종이라고 할 수 있다. 미리 생성된 객체를 "조회"하는 메서드이기 때문에 팩토리의 역할을 한다고 볼 수는 없지만, 외부에서 원하는 객체를 반환해주고 있으므로 결과적으로는 정적 팩토리 메서드라고 간주할 수 있다.

public enum Color {
  RED,
  BLUE;
}
...
Color redColor = Color.valueOf("RED");
Color blueColor = Color.valueOf("BLUE");

 

예시를 하나 더 봐보자.

class Book {
    private String title;
    
    // 생성자를 private화 하여 외부에서 생성자 호출 차단
    private Book(String title) { this.title = title; }
    
    // 정적 팩토리 메서드
    public static Book titleOf(String title) {
        return new Book(title); // 메서드에서 생성자를 호출하고 리턴함
    }
}
public static void main(String[] args) {
    // 정적 메서드 호출을 통해 인스턴스화된 객체를 얻음
    Book book1 = Book.titleOf("어린왕자"); 
}

 

정적 팩토리 메서드가 무엇인지는 알겠는데...

왜 생성자가 하는 역할을 정적 팩토리 메서드를 따로 만들어서 해야하는걸까?

 

 

생성자와의 차이점

생성자 대신 정적 팩토리 메서드를 고려하라.
- 이펙티브 자바 中

 

1. 이름을 가질 수 있다.

객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있다. new라는 키워드를 통해 객체를 생성하는 생성자는 내부 구조를 잘 알고 있어야 목적에 맞게 객체를 생성할 수 있다. 하지만 정적 팩토리 메서드를 사용하면 메서드 이름에 객체의 생성 목적을 담아낼 수 있다.

 

다음의 자동로또와 수동로또를 생성하는 팩토리 클래스의 일부 코드를 살펴보자.

public class LottoFactory() {
  private static final int LOTTO_SIZE = 6;

  private static List<LottoNumber> allLottoNumbers = ...; // 1~45까지의 로또 넘버

  public static Lotto createAutoLotto() {
    Collections.shuffle(allLottoNumbers);
    return new Lotto(allLottoNumbers.stream()
            .limit(LOTTO_SIZE)
            .collect(Collectors.toList()));
  }

  public static Lotto createManualLotto(List<LottoNumber> lottoNumbers) {
    return new Lotto(lottoNumbers);
  }
  ...
}

 

createAutoLotto와 createManualLotto 모두 로또 객체를 생성하고 반환하는 정적 팩토리 메서드이다. 이름만 보아도 자동인지 수동인지를 알 수 있다.

 

예시를 하나 더 봐보자.

class Car {
    private String brand;
    private String color = "black";

    public Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    public Car(String brand) {
        this.brand = brand;
    }
}

 

Car 클래스는 브랜드명과 자동차 색깔을 정의하는 멤버를 가지고 있다. 브랜드명은 반드시 생성자를 통해 외부로부터 입력받아야 하지만, 자동차 색깔은 기본값이 '검정'이며 선택적으로 입력받을 수 있다. 즉, 객체 생성에 있어 필수 속성과 선택 속성이 나뉘게 되는데, 이를 생성자를 통해 구현하면 위와 같이 두 가지 형식의 생성자 오버로딩으로 처리해야하고, 이를 호출하는 쪽에서 생성자의 인자 갯수를 다르게 할당함으로써 구현해야 한다.

public static void main(String[] args) {
    // 검정색 테슬라 자동차 
    Car teslaCar = new Car("Tesla");

    // 빨간색 BMW 자동차
    Car bmwRedCar = new Car("BMW", "Red");
}

 

위의 방식은 문제점이 있다.

 

프로그래밍할 때 중요한 요소 중 하나가 코드를 읽기 쉽도록 작성해야 한다는 점이다. 그런 의미에서 new 생성자 방법은 단지 매개변수의 유형과 개수를 제안할 뿐이지 어떠한 역할 표현이나 편의성을 제공하지 않는다. 즉, 생성자로 넘기는 매개 변수만으로는 반환될 객체의 특성을 제대로 표현하기가 어렵다는 것이다.

 

이런 경우 정적 팩토리 메서드를 통해 적절한 네이밍을 해줄 수 있다.

class Car {
    private String brand;
    private String color;

    // private 생성자
    private Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    // 정적 팩토리 메서드 (매개변수 하나는 from 네이밍)
    public static Car brandBlackFrom(String brand) {
        return new Car(brand, "black");
    }

    // 정적 팩토리 메서드 (매개변수 여러개는 of 네이밍)
    public static Car brandColorOf(String brand, String color) {
        return new Car(brand, color);
    }
}
public static void main(String[] args) {
    // 검정색 테슬라 자동차 
    Car teslaCar = Car.brandBlackFrom("Tesla");

    // 빨간색 BMW 자동차
    Car bmwRedCar = Car.brandColorOf("BMW", "Red");
}

 

이처럼 정적 팩토리 메서드를 사용하면 해당 생성의 목적을 이름에 표현할 수 있어 가독성이 좋아지는 효과가 있다.

 

 

2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.

enum과 같이 자주 사용되는 요소의 개수가 정해져 있다면 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만들 수 있다. 정적 팩토리 메서드와 캐싱 구조를 함께 사용하면 매번 새로운 객체를 생성할 필요가 없어진다.

 

이번에도 로또 번호를 생성하는 메서드를 살펴보자. 1~45의 로또 번호를 enum으로 만들 수 있지만, LottoNumber 클래스 안에서 반복문을 통해 쉽게 45개의 인스턴스를 만들 수 있으므로 후자의 방법을 사용했다.

public class LottoNumber {
  private static final int MIN_LOTTO_NUMBER = 1;
  private static final int MAX_LOTTO_NUMBER = 45;

  private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();

  static {
    IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
                .forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
  }

  private int number;

  private LottoNumber(int number) {
    this.number = number;
  }

  public LottoNumber of(int number) {  // LottoNumber를 반환하는 정적 팩토리 메서드
    return lottoNumberCache.get(number);
  }

  ...
}

 

여기서 두 가지 장점을 얻을 수 있다.

 

1. 미리 생성된 로또 번호 객체의 캐싱을 통해서 새로운 객체 생성의 부담을 덜 수 있다.

2. 생성자의 접근제한자를 private로 설정함으로써 객체 생성을 정적 팩토리 메서드만 가능하도록 제한할 수 있다.

 

이를 통해 정해진 범위를 벗어나는 로또 번호의 생성을 막을 수 있다는 장점을 확보할 수 있다.

 

 

예시를 하나 더 봐보자.

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    // 정적 팩토리 메서드
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public static void main(String[] args) {
    Singleton s1 = Singleton.getInstance();
    Singleton s2 = Singleton.getInstance();
    Singleton s3 = Singleton.getInstance();

    System.out.println(s1);
    System.out.println(s2);
    System.out.println(s3);

    System.out.println(s1 == s2);
    System.out.println(s1 == s3);
}

 

 

3. 하위 자료형 객체를 반환할 수 있다.

하위 자료형 객체를 반환하는 정적 팩토리 메서드의 특징은 상속을 사용할 때 확인할 수 있다. 이는 생성자의 역할을 하는 정적 팩토리 메서드가 반환값을 가지고 있기에 가능한 특징이다.

 

Basic, Intermediate, Advanced 클래스가 Level이라는 상위 타입을 상속 받고 있는 구조를 생각해보자. 시험 점수에 따라 결정되는 하위 등급 타입을 반환하는 정적 팩토리 메서드를 만들면, 다음과 같이 분기 처리를 통해 하위 타입의 객체를 반환할 수 있다.

public class Level {
  ...
  public static Level of(int score) {
    if (score < 50) {
      return new Basic();
    } else if (score < 80) {
      return new Intermediate();
    } else {
      return new Advanced();
    }
  }
  ...
}

 

 

4. 객체 생성을 캡슐화할 수 있다.

정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 하다.

 

웹 어플리케이션을 개발하다보면 계층 간에 데이터를 전송하기 위한 객체로 DTO(Data Transfer Object)를 정의해서 사용한다. DTO와 Entity 간에는 자유롭게 형 변한이 가능해야 한ㄴ데, 정적 팩토리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다.

public class CarDto {
  private String name;
  private int position;

  pulbic static CarDto from(Car car) {
    return new CarDto(car.getName(), car.getPosition());
  }
}


// Car -> CatDto 로 변환
CarDto carDto = CarDto.from(car);

 

만약 정적 팩토리 메서드를 쓰지 않고 DTO로 변환한다면 외부에서 생성자의 내부 구현을 모두 드러낸 채 해야할 것이다.

Car carDto = CarDto.from(car); // 정적 팩토리 메서드를 쓴 경우
CarDto carDto = new CarDto(car.getName(), car.getPosition); // 생성자를 쓴 경우

 

 

이처럼 정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것 뿐만 아니라, 우리가 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍할 수 있도록 도와준다. 도메인에서 "객체 생성"의 역할 자체가 중요한 경우라면 정적 팩토리 클래스를 따로 분리하는 것도 좋은 방법이 될 것이다. 다만 정적 팩토리 메서드도 단점이 있다.

 

1. private 생성자일 경우 상속 불가능

정적 팩토리 메서드로 클래스를 설계하면 생성자를 private로 설정하게 된다. 따라서 정적 팩토리 메서드를 적용하는 경우에는 상속을 이용한 확장이 불가능하다.

그치만? 이 부분은 단점이라기에는 오히려 장점이 될 수도 있다.

 

 

2. API 문서에서의 불편함

생성자는 하나의 자바 프로그래밍 언어의 스펙이기 때문에 JavaDoc 같은 문서에서 상단에 정의되어 있어 빠르게 그에 대한 스펙 검색을 할 수 있다. 반면 정적 팩토리 메서드는 개발자가 임의로 만든 것이기에 많~은 메서드들 중에서 해당 메서드를 찾아내야 한다.

따라서 클래스 설계자는 API 문서를 깔끔하게 작성할 필요가 있으며, 정적 팩토리 메서드를 작성할 때 네이밍 컨벤션을 지킴으로써 단점을 극복하기도 한다.

 

 

확실한 점은 정적 팩토리 메서드를 적절히 사용했을 때 얻을 수 있는 장점이 더 많다는 것이다.

 

 

우리가 이미 사용하고 있는 것들도 있다.ex) Optional의 of(), List의 of(), Integer의 valudOf() 등...

 

 

+ 정적 팩토리 메서드 네이밍 컨벤션

  • from : 하나의 매개 변수를 받아서 객체를 생성
  • of : 여러 개의 매개 변수를 받아서 객체를 생성
  • getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • newInstance | create : 새로운 인스턴스를 생성
  • get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.
 

 

 

 


https://tecoble.techcourse.co.kr/post/2020-05-26-static-factory-method/

 

정적 팩토리 메서드(Static Factory Method)는 왜 사용할까?

tecoble.techcourse.co.kr

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%83%9D%EC%84%B1%EC%9E%90-%EB%8C%80%EC%8B%A0-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

 

💠 정적 팩토리 메서드 패턴 (Static Factory Method)

Static Factory Method Pattern 정적 팩토리 메서드(Static Factory Method) 패턴은 개발자가 구성한 Static Method를 통해 간접적으로 생성자를 호출하는 객체를 생성하는 디자인 패턴이다. 우리는 지금까지 객체

inpa.tistory.com