이름을 가진 생성자 역할을 하는 정적 메서드(Effective Java Item 1).
List.of(1, 2, 3)
Optional.ofNullable(x)
Duration.ofSeconds(3)
Integer.valueOf(100)
GoF Factory Method와 무엇이 다른가
-
GoF Factory Method — 하위 클래스가 '무엇을 만들지' 결정한다
- 상속이 핵심 장치다
- 추상 메서드 createXxx() 를 하위가 구현한다
-
정적 팩토리 — 이름 있는 생성자일 뿐이다
- 상속과 무관하고, 패턴이라기보다 관용구다
실무 대화에서는 뭉뚱그려 '팩토리' 라 부르지만 면접에서는 이 구분을 묻는 경우가 있다
생성자에 없는 능력
-
① 이름을 가진다
- BigInteger.probablePrime(bits, rnd)
- new BigInteger(...) 로는 무엇을 만드는지 알 수 없다
-
② 매번 새 인스턴스를 만들지 않아도 된다
- Integer.valueOf(100) → 캐시된 인스턴스 반환 (-128~127)
- Boolean.valueOf(true) → 상수 반환
- new 는 반드시 새 객체를 만든다 — 선택권이 없다
-
③ 하위 타입을 반환할 수 있다
- List.of() → 원소 수에 따라 다른 내부 구현체를 준다
- Collections.unmodifiableList(x) → 구현 클래스를 감춘다
- 반환 타입을 인터페이스로 두면 구현을 자유롭게 바꿀 수 있다
-
④ 인자에 따라 다른 클래스를 줄 수 있다
- EnumSet.of(...) → 원소 64개 이하면 RegularEnumSet, 넘으면 JumboEnumSet
- 호출자는 이 사실을 몰라도 된다
단점
- public·protected 생성자가 없으면 상속할 수 없다
- (합성을 유도한다는 점에서 장점으로 보기도 한다)
- 생성자처럼 눈에 띄지 않아 문서를 봐야 찾을 수 있다
- 관례적인 이름을 쓴다: of · valueOf · from · getInstance · newInstance
실무에서의 판단
값 객체·불변 객체를 만들 때는 생성자를 private 으로 막고 정적 팩토리만 노출하는 편이 유리하다
- 검증을 한곳에 모을 수 있다
- 나중에 캐싱을 넣어도 호출부가 바뀌지 않는다
record 는 컴팩트 생성자로 검증하고, 필요하면 정적 팩토리를 함께 둔다
관례적인 이름과 그 의미
of / valueOf 인자를 그대로 담는다 List.of · Integer.valueOf
from 다른 타입에서 변환한다 Date.from(instant)
getInstance 인스턴스를 준다 (같을 수도) Calendar.getInstance()
newInstance 매번 새로 만든다 Array.newInstance()
create 새로 만든다 Files.createFile()
type / getType 다른 클래스의 인스턴스를 준다 Collections.list(e)
- 이름 자체가 계약이다 — getInstance 가 매번 새 객체를 준다면 오해를 부른다
캐싱이 만드는 실제 현상
Integer a = Integer.valueOf(127), b = Integer.valueOf(127);
a == b; // true — 캐시된 같은 인스턴스
Integer c = Integer.valueOf(128), d = Integer.valueOf(128);
c == d; // false — 128부터는 새 객체
기본 캐시 범위 -128 ~ 127 상한은 -XX:AutoBoxCacheMax=N 으로 조정할 수 있다
'작은 값에서는 멀쩡하다 큰 값에서만 틀어지는' 버그의 원인이다 캐시는 성능용이지 동일성 보장이 아니다 → 값 비교는 언제나 equals
면접 함정
- ❌ "정적 팩토리 = 팩토리 메서드 패턴" → 상속 기반의 GoF 패턴과 다르다.
- ❌ "getInstance면 싱글톤" → 이름 관례일 뿐, 매번 새 객체를 줄 수도 있다.