정적 팩토리 메서드란 무엇인가?

정적 팩토리 메서드란,객체 생성을 책임지는 클래스 자기 자신의 정적 메서드를 말한다.

일반적으로 우리는 new 키워드를 사용해 클래스의 생성자를 직접 호출하여 객체를 생성한다.

Person = person = new Person("홍길동", 25)

하지만 정적 팩토리 메서드를 사용하면 , new 를 사용하는 대신 다음과 같이 메서드를 호출하여 객체를 얻게 된다.

Person person = Person.createWithNameAndAge("홍길동",25)

두 방식의 가장 큰 차이점은 객체 생성의 제어권이 어디에 있느냐이다.


왜 생성자 대신 정적 팩토리 메서드를 사용해야 할까? (장점)

정적 팩토리 메서드는 생성자에 비해 여러 가지 강력한 장점이 있는데 이 장점들이 바로 우리가 이 패턴을 배워야 하는 이유다.

1. 이름을 가질 수 있어 가독성이 좋다.

생성자는 반드시 클래스 이름과 동일해야 하므로 , 생성자가 여러 개일 경우 매개변수의 타입과 개수로만 구분해야한다. 이는 코드의 의도를 파악하기 어렵게 만들곤 한다.

문제점

// 이 생성자가 정확히 무엇을 하는지 이름만 보고는 알 수 없다.
// 주석이나 문서를 봐야만 이해할 수 있다.
Boolean trueValue = new Boolean(true)

해결책 (정적 팩토리 메서드 사용)

메서드에 목적에 맞는 이름을 붙여줌으로써 코드의 가독성을 크게 높일 수 있다.

// 메서드 이름만 봐도 'true' 값에 해당하는 Boolean 객체를 얻는 다는 것을 명확히 알 수 있다.
Boolean trueValue = Boolean.valueOf(true);

createAdultMember(String name) 와 같이 이름을 지으면, “성인 회원을 생성한다” 는 의도가 명확하게 드러난다.


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

생성자는 new 키워드와 함께 호출될 때마다 무조건 새로운 객체를 만든다. 하지만 정적 팩토리 메서드는 기존에 만들어 둔 객체를 반환하거나, 캐싱(caching) 전략을 사용할 수 있다.

  • 인스턴스 통제
    • 불필요한 객체 생성을 막아 메모리 사용을 줄이고 성능을 향상시킬 수 있다.
  • 싱글턴 패턴
    • 클래스의 인스턴스가 오직 하나만 존재하도록 보장할 수 있다.
  • 플라이웨이트 패턴
    • 자주 사용되는 객체를 미리 생성해두고 공유하여 재사용할 수 있다.

대표적인 예시로, Boolean.valueOf(boolean) 메서드는 truefalse 를 인자로 받으면 새로운 Boolean 객체를 만들지 않고, 미리 만들어 둔 Boolean.TRUE 또는 Boolean.FALSE 객체를 반환합니다.

Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
System.out.println(b1 == b2); // 출력 : true;

이처럼 불필요한 객체 생성을 제어하는 것은 애플리케이션의 성능 최적화에 매우 중요하다.


3. 반환 타입의 하위 타입 객체를 반환할 수 있다.

이것은 정적 팩토리 메서드의 가장 큰 유연성 중 하나다. 메서드의 선언된 반환타입은 인터페이스나 추상 클래스가 될 수 있고, 실제로는 그 하위 타입의 특정 구현체 객체를 반환할 수 있다.

장점

  • 구현 클래스를 숨길 수 있다.
    • API 사용자 입장에서는 구체적인 구현 클래스를 알 필요 없이 인터페이스만으로 객체를 다룰 수 있어, 객체 지향의 캡슐화가 강해진다.
  • 향후 구현 클래스가 변경되거나 추가되어도 API는 변경되지 않는다.

대표적인 예시로, Java의 Collections 프레임워크가 이 패턴을 아주 잘 활용하고 있다.

// 우리는 List 인터페이스 타입으로 받지만,
// 실제 반환되는 객체는 Collections 내부에 정의된 정적 멤버 클래스인
// EmptyList 타입의 객체이다.
List<String> emptyList = Collections.emptyList();

API 사용자는 EmptyList 라는 구체적인 클래스의 존재를 몰라도 List 를 사용하는 데 아무런 문제가 없다. 이렇게 API 를 유연하게 설계하고 내부 구현을 숨기는 것은 좋은 라이브러리 설계의 핵심이다.


4. 제네릭 타입 인스턴스 생성을 단순화한다.

Java 7 에서 다이아몬드 연산자(<>) 가 도입되기 전까지, 제네릭 타입을 생성하려면 생성자에서도 타입 파라미터를 반복해서 적어줘야했는데, 정적 팩토리 메서드는 컴파일러가 타입을 추론할 수 있도록 도와 코드를 간결하게 만든다.

// 과거의 방식
Map<String, List<String>> map = new HashMap<String, List<String>>();

// 정적 팩토리 메서드를 활용하면 타입을 한번만 명시할 수 있다.
public static <K, V> HashMap<K, V> newInstance() {
    return new HashMap<K, V>();
}
Map<String, List<String>> map = HashMap.newInstance();

현재는 다이아몬드 연산자로 대부분 해결되지만, 더 복잡한 제네릭 구조에서는 여전히 유용하게 사용될 수 있다.


정적 팩토리 메서드의 단점

물론 장점만 있는건 아니다. 다음과 같은 단점도 명확히 알아야한다.

1. 상속이 불가능하다.

만약 클래스가 public 이나 protected 생성자 없이 오직 private 생성자와 정적 팩토리 메서드만 제공한다면, 그 클래스는 다른 클래스가 상속 할 수 없다. 하지만 이는 때로는 불변 클래스를 만들거나 상속을 의도적으로 막기 위한 장점이 될 수도 있다.

2. API 문서에서 찾기 어려울 수 있다.

JavaDoc과 같은 API 문서 생성 도구는 생성자를 다른 메서드와 구분하여 명확하게 보여준다. 하지만 정적 팩토리 메서드는 다른 일반적인 정적 메서드와 구분이 어려워, 개발자가 클래스의 인스턴스화 방법을 즉시 찾기 어려울 수 있다.

이러한 단점을 보완하기 위해 , 개발자들 사이에선 정적 팩토리 메서드에 널리 사용되는 명명 규칙이 있다.


흔히 사용되는 명명 규칙

정적 팩토리 메서드의 ‘찾기 어렵다’ 는 단점을 극복하기 위해 , 다음과 같은 이름을 주로 사용한다.


1. from 또는 of 하나의 매개변수를 받아 해당 타입의 인스턴스를 반환하는 형 변환 메서드.

  • Date date = Date.from(instant);
  • Set<Rank> rank = EnumSet.of(JACK, QUEEN, KING);

2. valueOf: from이나 of의 더 상세한 버전.

  • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);

3. getInstance 또는 instance: 싱글턴 인스턴스를 반환할 때 주로 사용. 매개변수가 있을 수도 있음

  • StackWalker walker = StackWalker.getInstance(options);

4. newInstance 또는 create: 매번 새로운 인스턴스를 생성하여 반환함을 보장.

  • Object newArray = Array.newInstance(classObject, arrayLen);

마지막 정리

구분 생성자 (Constructor) 정적 팩토리 메서드 (Static Factory Method)
이름 클래스 이름으로 고정 자유롭게 지정 가능 (가독성 👍)
객체 생성 호출 시마다 항상 새 객체 생성 기존 객체 반환, 캐싱 등 제어 가능 (성능 👍)
반환 타입 클래스 자신 타입으로 고정 하위 타입 등 유연하게 반환 가능 (유연성 👍)
상속 public/protected일 경우 가능 생성자가 private이면 불가능
사용 시점 단순하고 직관적인 객체 생성 시 복잡한 생성 로직, 유연성, 성능 최적화 필요 시

핵심은 public 생성자로 객체를 만드는 것이 나쁜 것은 아니다. 하지만 객체를 생성하는 로직이 조금이라도 복잡해지거나 , 성능 최적화가 필요하거나, 유연한 API 를 제공하고 싶다면 무조건 생성자를 만들기 전에 정적 팩토리 메서드를 우선적으로 고려하는 습관을 들여보자.