일반적으로 클래스의 인스턴스를 얻는 방법으로는 public 생성자다.
public class Product {
private String name;
private int price;
private String description;
private ProductStatus productStatus;
public Product(String name, int price, String description, ProductStatus productStatus){
this.name = name;
this.price = price;
this.description = description;
this.productStatus = productStatus;
}
}
public enum ProductStatus{
WAITING,
RELEASE,
DISCONTINUE
}
일반적인 클래스에선 public 생성자만응로도 충분히 생성이 가능하지만, 생성자 외에 정적 팩터리 메서드를 제공하면 사용자 입장에서 의도한 대로 인스턴스를 만들기 쉬워지는 경우가 종종 있다.
정적 팩터리 메서드의 대표적인 예시는 Boolean의 valueOf() 메서드가 있다.
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
메서드는 기본 타입인 boolean값을 받아와 Boolean 객체로 만들어 반환하고 있다.
책 에서는 BinInteger(int, int, Random)과 정적 팩터리 메서드인 BigInteger.probablePrime중 어느 쪽이 '값이 소수인 BigInteger를 반환하다'는 의미인지 생각해 보라고 한다. 앞서 작성한 public 생성자 코드를 예로 예제를 만들어 보자면
만약 Product클래스의 메인 생성자인 (name, price, description, productStatus)만 보고 어떤 특성을 가진 Product인지 파악하기 어렵다.
또한, 하나의 시그니처로는 하나의 생성자를 만들 수 있는데, 정적 팩터리 메서드는 이름을 가질 수 있으므로 하나의 시그니처로 여러 개의 정적 팩터리 메서드를 만들어 인스턴스를 반환할 수 있다.
public class Product {
private String name;
private int price;
private String description;
private ProductStatus productStatus;
public Product(String name, int price, String description, ProductStatus productStatus){
this.name = name;
this.price = price;
this.description = description;
this.productStatus = productStatus;
}
public static Product waitingMember(String name, int price, String description) {
return new Product(name, price, description, ProductStatus.WAITING);
}
public static Product releaseMember(String name, int price, String description) {
return new Product(name, price, description, ProductStatus.RELEASE);
}
public static Product discontinueMember(String name, int price, String description) {
return new Product(name, price, description, ProductStatus.DISCONTINUE);
}
}
public enum ProductStatus{
WAITING,
RELEASE,
DISCONTINUE
}
위와 같은 생성자로 ProductStatus를 구분하는 것보단 같은 행위를 가진 여러개의 정적 팩터리 메서드를 만들면, 사용자 입장에선 혼동의 여지 없이 특정 상태의 Product 인스턴스를 생성할 수 있게 된다.
public static Boolean valueOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
Boolean의 valueOf() 메소드는 인스턴스를 미리 캐싱해뒀다가 반환해 주는 것을 알 수 있다. 이러한 생성 비용이 큰 객체가 자주 요청되는 상황이라면 성능을 상당히 높여줄 수 있고, 플라이웨이트 패턴도 이와 비슷한 기법으로 볼 수 있다.
String[] strs = {"alpha", "beta", "charlie"};
List<String> lst = Arrays.asList(strs);
해당 코드는 간단하게 문자열 배열을 list로 변환하는 코드이 이다 이런 Arrays유틸 클래스의 asList() 메서드를 사용해본 적이 있다면 해당 장점을 이해할 수 있을 것이다.
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
실제 asList 구현 코드를 확인하면 List의 하위 구현체인 ArrayList로 값을 래핑하여 반환하는데, 이때 사용자는 이러한 구현체까지 알 필요가 없다. 즉 반환 객체의 클래스를 자유롭게 선택할 수 있다는 유연성은 개발자가 구현체를 공개하지 않고 구현체를 반환할 수 있기에 API를 작게 유지할 수 있다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다. 이점은 파라미터의 값에 따랄 다른 하위 타입을 반환할 수 있도록 한다.
만약 어떤 값에 따라 ProductStatus를 반환하고 싶다면, 정적 팩터리를 만들고 그 안에 비교 로직을 세우면 될 것이다.
public enum ProductStatus{
WAITING(0,30),
RELEASE(31,60),
DISCONTINUE(61,100);
private final int min;
private final int max;
ProductStatus(int min,int max){
this.min = min;
this.max = max;
}
public static ProductStatus of(int num){
return Arrays.stream(values())
.filter(checkProductStatus(num))
.findFirst()
.orElseThrow(()-> new NoSuchElementException("해당 ProductStatus를 찾을 수 없습니다."))
}
private static Predicate<ProductStatus> checkProductStatus(int num){
return ele -> ele.min <= num && ele.max >= num;
}
}
하지만 이 제약은 상속보다 컴포지션을 사용하도록 유도하고 불변 타입으로 만드려면 이 제약을 지켜야 하기에 오히려 장점으로 받아들일 수 도있다.
생성자 처럼 API 설명에 명확히 드러나지 않기에 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다. 그렇기 때문에 API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 사용하여 문제를 완화하여야 한다.