정적 팩토리 메서드(static factory method)
개발을 하다보면 정적 팩토리 메서드를 자주 사용하게 된다.
그런데 문득 정적 팩토리 메서드를 왜 사용하는 것인지 정확히 알지 못한채 사용한다는 생각이 들어 정적 팩토리 메서드를 왜 사용해야하는지 한번 정리해보려고 한다.
정적 팩토리 메서드란?
우선 팩토리(factory)라는 용어는 GoF 디자인 패턴 중 팩토리 패턴에서 유래한 단어로, 객체를 생성하는 역할을 분리한다는 의미가 담겨 있다.
따라서 정적 팩토리 매서드란 객체 생성의 역할을 하는 클래스 메서드이다.
직접적으로 생성자를 통해 객체 생성을 하는 것이 아니라 객체 생성 역할을 대신해주는 정적 팩토리 메서드를 사용해 객체 생성을 하는 것이다.
왜 생성자 대신에 정적 팩토리 메서드를 통해 객체를 생성하는 것일까?
생상자와 정적 팩토리 메서드는 객체를 생성한다는 같은 역할을 하고 있지만, 그 활용도에서 엄청난 차이가 난다.
정적 팩토리 메서드의 장점을 하나하나 살펴보자.
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> lottoNumber) {
return new Lotto(lottoNumbers);
}
...
}
createAuttoLotto와 createMenualLotto 모두 로또 객체를 생성하고 반환하는 정적 팩토리 메서드이다. 이때 메서드의 이름만으로도 로또 객체를 자동으로 생성하는지, 수동으로 생성하는지 단번에 알 수 있다.
이처럼 정적 팩토리 메서드를 사용하면 해당 생성의 목적을 이름에 표현할 수 있어 가독성이 좋아지는 효과가 있다.
2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.
enum 같이 자주 사용되는 요소의 개수가 정해져 있다면 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만들 수 있다.
이때 정적 팩토리 메서드와 캐싱구조를 함께 사용하면 매번 새로운 객체를 생성할 필요가 없어진다.
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~45까지의 로또 번호는 enum으로도 만들 수 있지만 반복문을 통해 쉽게 45개의 인스턴스를 만들 수 있으므로 반복문을 사용했다.
이렇게 하면 미리 생성된 로또 번호 객체의 캐싱을 통해 새로운 객체 생성의 부담을 덜 수 있다는 장점이 있다.
이때 생성자의 접근 제한자를 private으로 설정해 객체 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있다는 것을 기억하자.
3. 객체 생성을 캡슐화할 수 있다.
정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 하다.
캡슐화(Encapsulization)
• 데이터 은닉
• 여기서는 생성자를 클래스의 메서드 안으로 숨기면서 내부 상태를 외부에 드러낼 필요없이 객체 생성 인터페이스를 단순화 시킬 수 있다.
웹 애플리케이션을 개발하다보면 계층 간 데이터를 전송하기 위한 객체로 DTO(Data Transfer Object)를 정의해서 사용한다.
DTO와 Entity간에는 자유롭게 형 변환이 가능해야 하는데, 이때 정적 팩토리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다.
public calss CarDTO {
private String name;
private int position;
public static CarDto from(Car car) {
return new CarDto(car.getName(), car.getPosition());
}
}
// Car → CarDto 변환
CarDto carDto = CarDto.from(car); // 정적 팩토리 메서드를 사용한 경우
CarDto carDto = new CarDto(car.getName(), car.getPosition()); // 생성자를 사용한 경우
정적 팩토리를 쓰지 않고 DTO로 변환한다면 외부에서 생성자의 내부 구현을 모두 드러내야 한다.
이처럼 정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것 뿐만 아니라, 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍 할 수 있도록 도와 준다.
도메인에서 "객체 생성"의 역할 자체가 중요한 경우라면 정적 팩토리 클래스를 따로 분리하는 것도 좋은 방법이 될 수 있다. 다만 팩토리 메서드만 존재하는 클래스를 생성할 경우 상속이 불가하다는 단점이 있으니 참고하여 사용해야 한다.
확실한 것은 정적 팩토 메서드를 적절히 사용했을 때 얻을 수 있는 장점이 더욱 많다는 것이다. 객체간 형 변환이 필요하거나, 여러 번의 객체 생성이 필요한 경우라면 생성자보다는 정적 팩토리 메서드를 사용해보도록 하자!
정적 팩토리 메서드 네이밍 컨벤션
정적 팩토리 메서드를 만드는데 있어 일반적으로 많이 사용되는 네이킹 컨벤션이다.
메서드 | 설명 |
from | 하나의 매개변수를 받아 객체를 생성 |
of | 여러개의 매개변수를 받아 객체를 생성 |
newInstance | Create | 새로운 인스턴스를 생성 |
get[other type] | 다른 타입의 인스턴스를 생성, 이전에 반환했던 것과 같을 수 있음 |
new[other type] | 다른 타입의 새로운 인스턴스를 생성 |
Reference