평소에 정적 팩터리 메서드에 대해 어느정도 알고 있었지만 뭔가 나만의 기준이 모호하다는 것을 느꼈다.
사용하다보면 뭔가 규칙성이 크게 없다는 느낌을 자주 받았다.
그러다가 아래와 같은 리뷰를 받고 정리하고 넘어가야겠다고 생각했다.
아래 코드에 위에 같은 리뷰를 받았다.
public class Deck {
private final Queue<Card> cards;
private Deck(final List<Card> cards) {
this.cards = new LinkedList<>(cards);
}
public static Deck createShuffledDeck(final ShuffleStrategy shuffleStrategy) {
final List<Card> generatedCards = generate();
final List<Card> shuffledCards = shuffleStrategy.shuffle(generatedCards);
return new Deck(shuffledCards);
}
public static Deck from(final List<Card> cards) {
return new Deck(cards);
}
...
}
지금 상황은 이미 정적팩터리 메서드가 있기 때문에 생성자를 `private`으로 변경한다.
`private`으로 변경한 이유는 이전에도 미션을 할 때 정적 팩터리 메서드를 사용하면 생성자를 써야할지 정적팩터리를 써야할지 헷갈렸기 때문에 둘 중 하나만 쓰는 것을 기준으로 잡고 코드를 짜고 있다.
즉 생성자를 먼저 사용하고 만약 `private` 생성자로 인해 컴파일 에러가 표기가 되면 "아 ! 정적 팩터리 메서드를 사용해야하구나"하고 인지하고 코드를 짜고 있다.
실제로 리뷰어님에게 물어봄
각설하고 정리하자면 현재 상황에서는 앞선 리뷰어님이 말씀하신 `from`보다는 `create` 혹은 `newInstance`가 더 어울리다고 생각한다.
하지만 아래의 경우에는 조금 다르다고 생각한다.
public class BetAmount {
private final long betAmount;
private BetAmount(final long betAmount) {
validateBetAmount(betAmount);
this.betAmount = betAmount;
}
public static BetAmount from(final String betAmount) {
try {
return new BetAmount(Long.parseLong(betAmount));
} catch (final NumberFormatException e) {
throw new IllegalArgumentException("베팅 금액은 숫자로만 작성해주세요.");
}
}
}
위 경우는 parsing 작업을 해주기 때문에 생성자로만은 표현하기에는 어렵다고 생각했다.
그래서 `from`을 사용했다.
왜 많은 이름중에 `from`을 사용했는지는 정적팩터리메서드에 대한 설명에 포함시켜놓았다.
그럼 이제 정적 팩터리 메서드에 대해 알아보겠다.
먼저 정적 팩터리 메서드를 알아보기전에 생성자에 대해 알아보자.
🚀 생성자
public class Car {
private String model;
private boolean electric;
private boolean convertible;
public Car(String model, boolean electric) {
this.model = model;
this.electric = electric;
}
public Car(String model, boolean convertible) {
this.model = model;
this.electric = convertible;
}
}
생성자는 시그니처가 같으면 컴파일러는 이를 구분할 수 없다.
생성자 오버로딩은 매개변수의 타입, 개수, 순서가 달라야 한다. (이름은 영향이 없다)
만약 서로 다른 의미를 가진 두 생성자를 만들고 싶다면, 매개변수의 개수를 늘리거나 다른 타입의 매개변수를 사용하는 방법이 있다.
하지만 순서만 바꿔도 아래처럼 가능하긴하다.
public class Car {
private String model;
private boolean electric;
private boolean convertible;
public Car(String model, boolean electric) {
this.model = model;
this.electric = electric;
}
public Car(boolean convertible, String model) {
this.electric = convertible;
this.model = model;
}
}
정적 팩터리 메서드
하지만 이보다 더 좋은 방법이 있는데 바로 정적 팩터리 메서드를 사용하는 것이다.
public class Car {
private String model;
private boolean electric;
private boolean convertible;
private Car(String model, boolean electric, boolean convertible) {
this.model = model;
this.electric = electric;
this.convertible = convertible;
}
public static Car createElectricCar(String model) {
return new Car(model, true, false);
}
public static Car createConvertibleCar(String model) {
return new Car(model, false, true);
}
public static Car createElectricConvertibleCar(String model) {
return new Car(model, true, true);
}
}
1️⃣ 이름을 가질 수 있다.
이렇게하면 생성자가 이름은 갖는 것처럼 행동할 수 있다.
이게 바로 정적 팩터리 메서드의 첫 번째 장점이다.
나는 이 장점이 엄청나다고 생각하다.
사실 개발자는 코드를 작성하는 시간보다 코드를 읽는 시간이 더 많다 비율로보면 코드 읽기(80%) : 코드 작성 (20%) 정도 된다고 본다.
이때 그냥 생성자를 사용하는 것보다 이름을 사진 정적 팩터리 메서드를 사용해서 객체를 생성하는게 코드의 가독성을 압도적으로 올린다고 생각한다.
물론 정적 팩터리로 이름을 부여할 필요가 없는 그냥 생성의 기능만 하는 생성자일 경우는 억지로 정적팩터리 메서드를 사용할 필요는 없다. 오히려 이런 경우는 안티패턴이 될 가능성이 크다.
2️⃣ 인스턴스를 새로 생성 X
두 번째 장점은 호출될 때 마다 인스턴스를 새로 생성하지 않아도 된다는 점이다.
이 덕분에 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다.
public class Example {
private Example() {}
private static fianl Example EXAMPLE = new Example();
public static Example newInstance() {
return EXAMPLE;
}
}
예를 들어 세팅 역할을 하는 객체를 미리 만들어 놓고 어차피 같은 값을 세팅한다면 계속 인스턴스를 만들어가면서 낭비할 필요가 없으니 위와 같이 사용하면 좋다.
자바에서 대표적으로 사용하는 정적 팩터리 메서드는 `BooleanValueOf(boolean)`이다.
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE: Boolean.FALSE;
}
미리 생성된 상수 객체(`Boolean.TRUE`, `Boolean.FALSE`)를 재사용하여 메모리 사용을 최적화한다.
3️⃣ 반환 타입의 하위 타입 객체를 반환할 수 있는 능력
세 번째는 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이다.
public class KoreanHelloService implements HelloService {
@Override
public String hello() {
return "안녕하세요";
}
}
public class EnglishHellowService implements HelloSerice {
@Override
public String hello() {
return "hello";
}
}
이런식으로 생성자를 사용하면 딱 `English Hello Serice` or `Koeran Hello Serice`만 가져올 수 있다.
왜냐하면 해당하는 클래스의 인스턴스만 만들어주기 때문이다.
하지만 정적 팩터리 메서드를 사용하면 리턴하는 반환타입에 얼마든지 호환 가능한 다른 타입들의 클래스 또는 인스턴스를 리턴이 가능하게 해준다.
4️⃣ 매개변수에 따라 각기 다른 클래스의 객체를 반환
여기서 네 번째 장점인 "매개변수에 따라서 각기 다른 클래스의 객체를 반환할 수도 있다"가 등장한다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다.
public static HelloServiceFactory {
public static HelloService of(String lang) {
if(lang.equals("ko")) {
return new KoreanHelloService();
}
if(lang.equals("en"){
return new EnglishHelloSerivce();
}
}
}
위 예시에서는 `KoreanHelloService`, `EnglishHelloService`가 HelloService의 구현체이기 때문에 입력 받은 매개변수에 따라 해당되는 클래스를 반환할 수 있다.
하지만 단점도 존재한다.
- 상속을 하려면 `public`이나 `protected` 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- why? 보통 정적 팩터리 메서드를 만들면 생성자를 `private`로 설정하기 때문에
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
이에 대해 자세히 알아보겠습니다.
1. 상속을 위해 정적 팩터리 메서드만 제공하면 클래스 하위 클래스를 만들 수 없다.
당연합니다. 보통 정적 팩터리 메서드를 사용한다면 private 생성자를 사용하기에 상속을 할 수 없다.
public class Example {
private Example() {}
private static fianl Example EXAMPLE = new Example();
public static Example newInstance() {
return EXAMPLE;
}
}
하지만 상속보다 컴포지션을 사용(Item 18)하도록 유도하고 불변 타입(Item 17)으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아드일 수도 있다.
물론 정적팩터리 메서드를 사용하면서 생성자를 private으로 하지 않는 경우도 있기는 합니다. 즉 생성자를 허용하는 경우도 있다.
예를들면 List가 그렇다.
public class Example {
public static void main(String[] args) {
List<String> list = new ArrayLIst<>();
List.of("test", "test1");
}
}
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 합니다. 따라서 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야합니다.
흔히 사용하는 명명 방식들
명명 규칙 | 설명 |
`from` | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드 |
`of` | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드 |
`valueOf` | from 과 of 의 더 자세한 버전 |
`instance` or `getInstance` | (매개 변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만 같은 인스턴스임을 보장하지는 않는다. |
`create` or `newInstance` | instance 혹은 getInstance와 같지만 매번 새로운 인스턴스를 생성해 반환함을 보장한다. |
`getType` | getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. |
`newType` | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. |
`type` | getType과 newType의 간결한 버전 |
📌 Reference
- Effective Java Item 1
'JAVA' 카테고리의 다른 글
조합 ? 상속 ? (1) | 2025.03.12 |
---|---|
isBlank() vs isEmpty() (0) | 2025.02.28 |
Comparable 인터페이스 너 뭐야! (1) | 2025.02.26 |
추상 클래스보다는 인터페이스를 우선하라 - 이펙티브 자바 Item 20 (0) | 2025.02.24 |