정적 팩토리 메서드란 무엇인가?
정적 팩토리 메서드란,객체 생성을 책임지는 클래스 자기 자신의 정적 메서드를 말한다.
일반적으로 우리는 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)
메서드는 true
나 false
를 인자로 받으면 새로운 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 를 제공하고 싶다면
무조건 생성자를 만들기 전에 정적 팩토리 메서드를 우선적으로 고려하는 습관을 들여보자.