🚀 Item 1 생성자 대신 정적 팩터리 메서드를 고려하라
생성자 대신 정적 팩터리 메서드를 고려하라
왜 그래야할까요?
먼저 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있습니다.
정적 팩터리 메서드(static factory method)는 인스턴스 생성을 위해 new 키워드를 사용하는 대신, 클래스 내부에 정적 메서드(static method)를 정의하여 객체를 반환하는 디자인 패턴입니다.
다음은 정적 팩터리 메서드의 예시입니다. 여기서는 Boolean 타입의 값을 반환하는 valueOf 메서드를 예로 들 수 있습니다.
public class Boolean {
private final boolean value;
private Boolean(boolean value) {
this.value = value;
}
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
}
그럼 이런식으로 정적 팩터리 메서드를 사용한다면 어떠한 장점이 있을까요?
1️⃣ 이름을 가질수 있다.
2️⃣ 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
3️⃣ 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
4️⃣ 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
5️⃣ 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도된다.
그럼 이에 대해 하나씩 알아보겠습니다.
1️⃣ 이름을 가질 수 있다.
먼저 아래의 예시를 보겠습니다.
public class Patient {
private String name;
private int age;
private boolean urgent;
private boolean minor;
public Patient(String name, int age, boolean urgent) {
this.name = name;
this.age = age;
this.urgent = urgent;
}
public Patient(String name, int age, boolean minor) {
this.name = name;
this.age = age;
this.minor = minor;
}
}
자바에서는 위와 같이 완전히 동일한 시그니처의 생성자가 같이 있을 수 없습니다. 물론 매개변수 순서를 바꿔서 사용하면 가능하긴 합니다. 하지만 이런식으로 한다면 이 코드를 사용하는 입장에서는 구분하기가 어려워집니다. 하지만 생성자를 만들기 위해서는 이름을 똑같게 만들어야 하기 때문에 어쩔수 없습니다. 이때 바로 사용할 수 있는데 바로 정적 팩터리 메서드입니다.
정적 팩터리 메서드를 사용하면 아래와 같이 이름을 붙여서 더 구분감있게 사용할 수 있습니다.
public class Patient {
private String name;
private int age;
private boolean urgent;
private boolean minor;
public static Patient urgentPatient(String name, int age) {
Patient patient = new Patient();
patient.name = name;
patient.age = age;
patient.urgent = true;
return patient;
}
public static Patient minorPatient(String name, int age) {
Patient patient = new Patient();
patient.name = name;
patient.age = age;
patient.minor = true;
return patient;
}
}
이런식으로 이름을 가질 수 있는 정적 팩터리 메서드를 사용하면 생성자의 시그니처가 중복되는 경우에 관해서 제약이 없기 때문에 정적 팩터리 메서드를 고려해보는 것도 좋습니다. 아울러 이름까지 가질 수 있으니 전달하고자 하는 바를 명확하게 전달할 수도 있습니다.
2️⃣ 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
정적 팩터리 메서드를 사용하면 호출될 때마다 객체를 새로 생성하지 않기 때문에 메모리 사용양이 줄어늘고 이로인해 성능을 향상시킬 수 있으며 객체 생성 비용이 높은 경우에는 특이 도움이됩니다.
그리고 언제 어느 인스턴스를 살아 있게 할지도 철저히 통제가 가능하고 이런 클래스를 인스턴스 통제 클래스라고 합니다.
인스턴스를 통제하면 아래와 같은 장점이 존재합니다.
- 클래스를 싱글턴으로 만들 수 있다. (Item 3)
- 인스턴스화 불가로 만들 수도 있다. (Item 4)
- 불변값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있다. (Item 5)
인스턴스 통제는 아래와 같이 구현할 수 있습니다.
public class Example {
private Example() {}
private static fianl Example EXAMPLE = new Example();
public static Example newInstance() {
return EXAMPLE;
}
}
3️⃣ 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
4️⃣ 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
세 번째 장점과 네 번째 장점은 묶어서 설명하겠습니다. 하위 타입 객체를 반환할 수 있는 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 엄청난 유연성을 선물합니다.
다양한 언어서비스 관련 코드로 예를 들어보겠습니다.
보통 생성자만을 사용한다면 아래와 같이 사용할겁니다.
public class KoreanHelloService implements HelloService {
@Override
public String hello() {
return "안녕하세요";
}
}
public class EnglishHellowService implements HelloSerice {
@Override
public String hello() {
return "hello";
}
}
이런식으로 생성자를 사용하면 딱 English Hello Serice나 Koeran Hello Serice만 가져올 수 있습니다.
왜냐하면 해당하는 클래스의 인스턴스만 만들어주기 때문이죠
하지만 정적 팩터리 메서드를 사용하면 리턴하는 반환타입에 얼마든지 호환 가능한 다른 타입들의 클래스 또는 인스턴스를 리턴이 가능하게 해줍니다. 여기서 네번째 장점인 매개변수에 따라서 각기 다른 클래스의 객체를 반환할 수도 있다는 것이 나옵니다.
아래의 코드는 정적 팩터리 메서드를 사용한 예시입니다.
public static HelloServiceFactory {
public static HelloService of(String lang) {
if(lang.equals("ko")) {
return new KoreanHelloService();
}
if(lang.equals("en"){
return new EnglishHelloSerivce();
}
}
}
하지만 여기서 변환된 것이 있습니다. 자바8 전에는 인터페이스에 정적 메서드를 선언할 수 없었지만 자바 8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한잎 루렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 없어졌습니다. 동반 클래스에 두었던 public 정적 멤버들 상당수를 그냥 인터페이스 자체에 두면 됩니다.
public interface HelloService {
static HelloSerive of(String lang) {
if(lang.equals("ko") {
return new KoreanHelloSerivce();
}
if(lang.equals("en"){
return new EnglishHelloSerice();
}
}
}
지금까지 장점을 알아보았는데 물론 단점도 존재합니다.
단점에는 아래와 같이 두 가지가 존재합니다.
- 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 클래스 하위 클래스를 만들 수 없다.
- 정적 팩터리 메서드는 프로그래머가 찾기 어렵다. 이에 대해 자세히 알아보겠습니다.
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의 간결한 버전. |
'정보' 카테고리의 다른 글
[Effective java] Item 5 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.03.03 |
---|---|
[Effective java] Item 4 인스턴스화를 막을려거든 private 생성자를 사용하라 (0) | 2024.02.25 |
싱글톤 (0) | 2023.11.26 |
[JEST] Mocking (0) | 2023.11.05 |
코드리뷰 잘 활용하기 (1) | 2023.10.23 |