이펙티브 자바 3판
아이템 1. 생성자 대신 정적 팩토리 메소드를 고려하라
인스턴스 생성법
보통 클래스의 인스턴스를 생성하기 위해서는 public 생성자를 이용합니다.
이 생성자를 이용하여 인스턴스를 얻는 방법에 대해 문제점을 찾아보는 시각으로 접근을 해봅시다.
운이 좋으면 어떠한 상황에도 별다른 문제점이 없는 사용방법이 될 수도있고, 어쩌면 문제점을 발견하고 그 문제를 해결할 수 있는 다른 방법을 얻을 수 있을지도 모릅니다.
생성자를 이용한 인스턴스 생성의 문제점?
역시 예시를 직접 살펴보는 것이 더 잘 이해될 것이라고 생각하여 예시 코드를 작성해가며 생각해 보았습니다.
1. 생성된 인스턴스의 목적성
public class Order
{
private String menu;
private Long price;
public Order(String menu, Long price) {
this.menu = menu;
this.price = price;
}
public static void main(String[] args )
{
Order order = new Order("Americano", 1_500L);
}
}
메뉴와 가격을 파라미터로 입력받아서 주문을 생성하는 클래스입니다.
생성자를 이용하게되면 명시적으로 해당 객체가 어떤 목적으로 생성되었는지 나타낼 수 없습니다.
클래스 이름과 입력받는 파라미터로 어느정도 유추는 가능하겠지만, 유추를 한다는 사실부터 명시적이지 않다는 의미가 된다고 생각합니다.
2. 매번 새로운 인스턴스의 생성
이번엔 위 코드에서 주문한 내용을 아메리카노에서 카페라떼로 바꾼다고 생각해봅시다.
단, 한번 기록된 필드값을 변경할 수 없는 불변 클래스인 경우입니다.
public class Order
{
private final String menu;
private final Long price;
public Order(String menu, Long price) {
this.menu = menu;
this.price = price;
}
public static void main(String[] args )
{
Order order = new Order("Americano", 1_500L);
order = new Order("Caffe Latte", 2_000L);
}
}
final로 선언된 불변클래스이기 때문에 주문을 변경하기 위해서는 새로운 생성자를 사용하여 인스턴스를 생성해야합니다.
여기서 발생하는 문제점은 새로운 객체를 생성하기 때문에 메모리를 할당해줘야 한다는 것입니다.
Order order = new Order("Americano", 1_500L);
System.out.println("Before : " + order.hashCode());
order = new Order("Caffe Latte", 2_000L);
System.out.println("Change -> Caffe Latte : " + order.hashCode());
order = new Order("Americano", 1_500L);
System.out.println("Change -> Americano : " + order.hashCode());
아메리카노 -> 카페라떼 -> 아메리카노로 주문을 변경했다고 생각해봅시다.
이 경우 메모리를 새롭게 할당했는지 알아보기 위하여 hashCode를 찍어보게되면 전부 다른 결과가 나오는 것을 알 수 있습니다.
즉, 다시 아메리카노로 돌아왔음에도 새로운 메모리를 할당했다는 사실을 알 수 있죠.
3. 하위 타입의 객체를 반환할 수 있는가?
이번에는 기존의 Order를 2000원 보다 낮은 가격을 처리하는 SmallPriceOrder와 높은 가격을 처리하는 HighPriceOrder로 나누었다고 생각해봅시다.
public abstract class Order
{
private final String menu;
private final Long price;
Order(String menu, Long price) {
this.menu = menu;
this.price = price;
}
}
public class LowPriceOrder extends Order{
public LowPriceOrder(String menu, Long price) {
super(menu, price);
}
}
public class HighPriceOrder extends Order{
public HighPriceOrder(String menu, Long price) {
super(menu, price);
}
}
이 경우에서 Order의 하위클래스인 SmallPriceOrder와 HighPriceOrder의 인스턴스를 반환하는 것은 불가능한 것을 알 수 있습니다.
4. 반환할 객체의 클래스가 존재하지 않으면 반환할 수 없다.
만약 특정 FQCN을 입력받아서 해당 클래스의 인스턴스를 반환하려고 한다. 라고 생각해봅시다.
public 생성자를 이용한 방법에서는 해당 클래스의 인스턴스를 반환하는 것은 불가능합니다.
정적 팩토리 메소드
정적 팩토리 메소드는 위의 public 생성자를 통한 호출에서 생기는 문제점을 해결하기 위해 고안된 기술입니다.
클래스의 인스턴스를 반환하는 static 메소드를 의미합니다.
public class Order
{
private static final Order Americano = new Order("Americano", 1_500L);
private static final Order CaffeLatte = new Order("Caffe Latte", 1_500L);
private String menu;
private Long price;
private Order(String menu, Long price) {
this.menu = menu;
this.price = price;
}
//정적 팩토리 메소드
public static Order takeOrder(String menu, Long price){
switch(menu){
case "Americano" : return Order.Americano;
case "Caffe Latte" : return Order.CaffeLatte;
default: return new Order(menu, price);
}
}
public static void main(String[] args )
{
Order order = new Order("Americano", 1_500L);
}
}
팩토리 메소드 내부에서 이미 생성되어 있는 객체를 반환하거나, 새로운 객체를 생성하는 것 모두 가능합니다.
1. 생성된 인스턴스의 목적성 ( 해결 )
public static void main(String[] args )
{
Order order = Order.takeOrder("Americano", 1_500L);
order = Order.takeOrder("Caffe Latte", 2_000L);
}
정적 팩토리 메소드의 경우, 이름을 지정할 수 있기 때문에 생성된 인스턴스가 어떤 의미를 가지는지 명시할 수 있습니다.
정적 팩토리 메소드로 구현되어있는 java.math.BigInteger 클래스를 살펴보면 위와 같은 정적 팩토리 메소드 들을 확인할 수 있는데, 반환되는 인스턴스가 어떤 값인지 명확하게 알 수 있습니다.
2. 매번 새로운 인스턴스의 생성 ( 해결 )
아까처럼 아메리카노 -> 카페라떼 -> 아메리카노로 주문을 변경하는 상황을 가정해봅시다.
public static void main(String[] args )
{
Order order = Order.takeOrder("Americano", 1_500L);
System.out.println("Before : " + order.hashCode());
order = Order.takeOrder("Caffe Latte", 2_000L);
System.out.println("Change -> Caffe Latte : " + order.hashCode());
order = Order.takeOrder("Americano", 1_500L);
System.out.println("Change -> Americano : " + order.hashCode());
}
다시 아메리카노로 돌아왔음에도 hashCode가 변하지 않은 모습을 확인할 수 있습니다.
즉, 새롭게 생성된 객체가 아니라는 의미가 되겠네요.
java.lang.Boolean 클래스를 살펴보면 생성자가 모두 @Deprecated ( 사용되지 않음 ) 애노테이션이 붙은 것을 확인할 수 있으며, valueOf 메소드를 통해서 객체를 반환하는 것을 확인할 수 있습니다.
이렇게 매 호출마다 새로운 객체를 생성유무를 판단하고, 언제 어느시점에 인스턴스가 살아있게 할 지를 통제할 수 있는 클래스를 인스턴스 통제 클래스라고 합니다.
이렇게 인스턴스를 통제하게 되면 Boolean처럼 아예 인스턴스화를 막거나, 싱글턴이 되도록 만들 수 있습니다.
3. 하위 타입의 객체를 반환할 수 있는가? ( 해결 )
public abstract class Order
{
private String menu;
private Long price;
Order(String menu, Long price) {
this.menu = menu;
this.price = price;
}
//정적 팩토리 메소드
public static Order TakeOrder(String menu, Long price){
if(price <= 1_500L){
return LowPriceOrder.TakeOrder(menu, price);
}else{
return HighPriceOrder.TakeOrder(menu, price);
}
}
}
public class HighPriceOrder extends Order{
private static final HighPriceOrder CaffeLatte = new HighPriceOrder("Caffe Latte", 2_000L);
private HighPriceOrder(String menu, Long price) {
super(menu, price);
}
public static HighPriceOrder TakeOrder(String menu, Long price){
if(menu.equals("Caffe Latte")){
return CaffeLatte;
}
else{
return new HighPriceOrder(menu, price);
}
}
}
public class LowPriceOrder extends Order{
private static final LowPriceOrder Americano = new LowPriceOrder("Americano", 1_500L);
private LowPriceOrder(String menu, Long price) {
super(menu, price);
}
public static LowPriceOrder TakeOrder(String menu, Long price){
if(menu.equals("Americano")){
return Americano;
}else{
return new LowPriceOrder(menu, price);
}
}
}
정적 팩토리 메소드를 이용하게되면, Order클래스에서 입력받은 매개변수에 따라, 반환될 하위클래스를 지정할 수 있습니다.
public static void main(String[] args) {
Order order = Order.TakeOrder("Americano", 1_500L);
System.out.println(order.hashCode());
LowPriceOrder lowPriceOrder = LowPriceOrder.TakeOrder("Americano", 1_500L);
System.out.println(lowPriceOrder.hashCode());
}
hashCode를 찍어보면 동일한 객체임을 알 수 있죠.
매개변수에 따라 다른 하위클래스를 반환하는 기능은 java.util.EnumSet에서 확인할 수 있습니다.
EnumSet클래스를 살펴보면, public 생성자 없이 정적 팩토리 메소드만을 이용하여 하위 클래스의 인스턴스를 반환하는 것을 확인할 수 있습니다.
입력된 Enum타입의 원소개수가 64이하가 되면, RagularEnumSet을 반환하고 아니라면 JumboEnumSet을 반환하는 것을 확인할 수 있습니다.
EnumSet의 사용자는 하위 클래스의 존재를 모르며, 개발자는 상황에 따라 하위클래스를 삭제하거나 추가하는 식으로 확장할 수 있습니다.
4. 반환할 객체의 클래스가 존재하지 않으면 반환할 수 없다. ( 해결 )
DriverManager의 getConnection 메소드를 살펴보면, String 값을 받아서 Connection 인스턴스를 반환하는 것을 볼 수 있습니다.
반환되는 getConnection(url, info, Reflection.getCallerClass()) 메서드를 살펴보면 con이 null인지 판단하고 반환하는 것을 살펴볼 수 있습니다.
이 말은 자바 프로그램을 실행시키는 당시에 url과 연결된 클래스가 없을 수도 있다는 것을 의미합니다.
이러한 정적 팩토리 메소드의 유연함은 서비스 제공자 프레임워크 ( ex : jdbc )를 만드는 근간이 됩니다.
서비스 제공자 프레임워크는 3개의 핵심 컴포넌트로 구성됩니다.
- 서비스 인터페이스 : 구현체의 동작 정의
- 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
- 서비스 접근 API : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
JDBC 기준으로 Connection 클래스가 서비스 인터페이스 역할을 하며,
DriverManager.registerDriver 가 제공자 등록 API를,
DriverManager.getConnection이 서비스 접근 API의 역할을 합니다.
자바 6버전 이후로는 ServiceLoader라는 범용 서비스 제공자 프레임워크가 제공된다고 합니다.
단점 1. 상속을 통해 하위클래스를 만들 수 없다.
정적 팩토리 메소드에서 생성자는 private이기 때문에 public/protected 생성자를 필요로하는 하위클래스를 생성할 수 없습니다.
상속을 하지 못하도록 억제하기 때문에 동일한 인스턴스를 필드로 받아서 사용하는 식의 방법을 사용할 수 있습니다.
단점 2. 찾기가 어려울 수 있다.
public 생성자의 경우 시그니처가 항상 new 클래스명() 으로 고정되어있기 때문에 찾는데 수고를 들일 필요가 없습니다.
반면 정적 팩토리 메소드의 경우, 메소드의 이름을 찾고 해당 메소드가 정적 팩토리 메소드가 맞는지 확인하는 과정이 필요합니다.
이 문제는 JavaDoc을 사용하여 문서화를 함으로써 어느정도 해결할 수 있습니다.
이 단점때문에 정적 팩토리 메소드를 생성 시에 사용하는 일반적인 규칙이 있습니다.
- from : 하나의 매개변수로 해당 타입의 인스턴스를 반환.
- of : 여럿의 매개변수를 받아 적절한 타입의 인스턴스를 반환.
- valueOf : from과 of의 자세한 버전
- instance, getInstance : 매개변수가 있다면 명시한 인스턴스를 반환. 매 호출마다 다른 인스턴스가 반환될 수도 있음.
- create, newInstance : 매 호출마다 새로운 인스턴스를 생성해서 반환
- getType : Type의 인스턴스를 반환
- newType : Type의 인스턴스를 새로 생성하여 반환
- type : getType과 newType의 간결한 버전