본문으로 바로가기
반응형

개요

대략적인 정적 팩토리 메소드의 장/단점은 아래와 같다. 참고로 반드시 정적 팩토리 메소드를 고려하라는 것이 아니라 상황에 맞게 사용하라는 의미이다.

장점

  • 이름을 가질 수 있다. (물론 동일한 시그니처의 생성자는 두개 가질 수 없다)
  • 호출될 때 마다 새로운 인스턴스를 새로 생성하지 않아도 된다. (Boolean.valufOf)
  • 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. (인터페이스 기반 프레임워크, 인터페이스에 정적 메소드)
  • 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. (EnumSet)
  • 정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

단점

  • 상속을  하려면 public 또는 protected 생성자가 필요하기 때문에 정적 팩토리 메소드만 제공해야 한다.
  • 정적 팩토리 메소드는 개발자가 찾기 어렵다.

장점 1

public class Order {
    private boolean prime;
    private boolean urgent;
    private Product product;

    public static Order primeOrder(Product product) {
        Order order = new Order();
        order.prime = true;
        order.product = product;
        return order;
    }

    public static Order urgentOrder(Product product) {
        Order order = new Order();
        order.urgent = true;
        order.product = product;
        return order;
    }
}

예를 들어 위처럼 Order 클래스가 있다고 가정해보자. prime과 urgent 필드의 값에 따라 Prime Order/Urgent Order 2가지 타입으로 나뉘게 되는데, 기본 생성자를 사용하는 대신 primeOrder(), urgentOrder()와 같이 static 타입으로 선언하고 생성자의 이름을 좀 더 명시적으로 작성하라는 의미이다. 

장점 2

자바의 생성자는 호출될 때 마다 매번 새로운 인스턴스를 만든다. 만약 객체를 생성할 때 마다 새로운 객체가 아닌 동일한 객체를 생성하게 하려면 어떻게 해야 할까?

public class Settings {
    private boolean useAutoSteering;
    private boolean useABS;
    private Difficulty difficulty;
    private Settings() {}

    private static final Settings SETTINGS = new Settings();
    public static Settings newInstance() {
        return SETTINGS;
    }
}

먼저 private 생성자를 통해 기본 생성을 막아둔다. 그리고 static 변수에 객체를 미리 생성해둔 후 newInstance라는 정적 팩토리 메소드를 생성해준다. 이렇게하면 기본 생성자를 통해 객체를 생성할 수 없기 때문에 newInstance 메소드를 사용할 수 밖에 없고, 여러번의 호출에도 동일한 객체를 반환한다. 이런 작업을 통해 객체의 생성을 자기 자신이 직접 컨트롤하겠다는 의미를 내포시킬 수 있다.

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

...

public static Boolean valueOf(boolean b) {
    return (b ? TRUE : FALSE);
}

Boolean의 valueOf 코드를 살펴보면 위처럼 정적 팩토리 메소드로 동작한다. 책에서는 Flyweight pattern이 같이 언급되는데, 이 패턴은 자주 사용하는 객체는 미리 생성해두고 재사용하는 패턴이다. 정적 팩토리 메소드 또한 미리 생성해두고 재사용하는 형태이기 때문에 비슷한 패턴을 언급했다고 한다.

장점 3, 4

public class HelloServiceFactory {
    public static HelloService of(String lang) {
        if (lang.equals("ko")) {
            return new KoreanHelloService();
        } else {
            return new EnglishHelloService();
        }
    }
}

세 번째 장점은 위 코드처럼 리턴타입은 인터페이스 타입으로 주었지만, 실제 리턴값은 구현체로 선언해줄 수 있다는 점이다. 또는 리턴값에 인터페이스가 아니라 클래스를 선언해두고 해당 클래스의 하위 클래스를 리턴할수도 있다.

HelloService service = HelloServiceFactory.of("ko");

위처럼 팩토리 메소드를 통해 클래스를 생성하면 실제 구현체(KoreanHelloService/EnglishHelloService) 타입이 아닌 인터페이스 타입(HelloService)로 가져오게 된다. 이를 통해 인터페이스 기반 코드를 강제할 수 있다.

public interface HelloService {
    static HelloService of(String lang) {
        if (lang.equals("ko")) {
            return new KoreanHelloService();
        } else {
            return new EnglishHelloService();
        }
    }
}

자바 8버전 이후부터는 인터페이스에 static 메소드를 선언할 수 있기 때문에 위처럼 인터페이스로 코드를 이동시켜도 된다. 따라서 별도의 팩토리 클래스를 만들지 않고 인터페이스를 통해 구현해도 된다.

단점

장점 2에서 생성자가 아닌 우리가 생성한 정적 팩토리 메소드만을 통해 객체를 생성하게 만들기 위해선, private 생성자를 통해 객체 생성을 막아둬야 한다고 했다. 이를 통해 해당 클래스는 상속 또한 받을 수 없다는 단점이 생긴다.

두 번째 단점으로는 javadoc 문서화를 진행할 때 정적 팩토리 메소드는 생성자 목록에 나오지 않고 메소드 목록에 나오기 때문에 해당 메소드를 통해 객체를 생성해야한다는 사실을 쉽게 알기 힘들다. 따라서 특정 매개변수를 받아서 특정 인스턴스를 만드는 경우 of를, 미리 만들어져있는 인스턴스를 가져오려는 경우 getInstance를, 매번 새로운 인스턴스를 만드는 경우 newInstance 등의 네이밍을 가지는 컨벤션이 존재한다.

열거 타입

  • 상수 목록을 담을 수 있는 데이터 타입
  • 특정한 변수가 가질 수 있는 값을 제한할 수 있기 때문에 Type-Safety를 보장할 수 있다.
  • Enum의 경우 JVM내에 딱 하나의 인스턴스만 생성되기 때문에 싱글톤 패턴을 구현할 때 사용하기도 한다.
public class Order {
    private OrderStatus orderStatus;

    public static void main(String[] args) {
        Order order = new Order();
        if (order.orderStatus == OrderStatus.DELIVERED) {
            // 에러 발생 X
        }
        
        if (order.orderStatus.equals(OrderStatus.DELIVERED) {
            // NullPointerException 발생
        }
    }
}

참고로 만약 위처럼 orderStatus가 null일 때 equals() 메소드를 통해 비교하게 되면 NullPointerException이 발생하기 때문에 단순 == 비교하는것을 권장한다.

EnumMap/EnumSet

만약 Enum을 키로 가진 맵을 사용하고 싶은 경우 일반적인 HashMap등을 사용하려 할 수 있다. 하지만 EnumMap을 사용하는게 좋은데, 그 이유는 EnumMap은 내부적으로 배열로 구현되어 있어 타 Map에 비해 메모리 사용량이 적고 매우 빠른 성능을 보장해주기 때문이다. 실제로 EnumMap은 값을 찾을 때 Enum 상수가 인덱스 정보를 oridinal 메소드로 제공하고 있기 때문에 바로 검색이 가능하다. 하지만 HashMap의 경우 key값을 hash메소드로 변환하여 인덱스를 찾아야 하기 때문에 비교적 비효율적으로 동작한다.

Enum만을 담고 있는 Set을생성하는 경우에도 EnumSet을 사용하는것이 좋은데, EnumSet은 비트 연산을 통해 집합 연산을 매우 빠르게 수행해주기 때문이다. 

플라이웨이트 패턴

같은 객체가 자주 요청되는 경우 플라이 웨이트 패턴을 사용할 수 있다고 한다. 쉽게 설명해서 자주 사용되는 필드와 그렇지 않은 필드를 구분하고, 자주 사용되는 필드는 특정 장소에 저장해두고 사용 시 꺼내어 사용하는 방법을 말한다.

public class Article {
    private String name;
    private String font;

    public Article(String name, String font) {
        this.name = name;
        this.font = font;
    }
}

public class Client {
    public static void main(String[] args) {
        new Article("hide", "font1");
    }
}

예를 들어 위처럼 Article이라는 클래스가 존재한다고 가정해보자. name의 경우 변할 가능성이 높기에 재사용하기 좋아보이지 않는다. 하지만 font의 경우 프론트 단에서 font1, font2 등으로 제한해놓은 상황이라면, 재사용하기 좋은 케이스이다.

public class Font {
    String name;

    public Font(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class FontFactory {
    private Map<String, Font> cache = new HashMap<>();

    public Font getFont(String font) {
        if (cache.containsKey(font)) {
            return cache.get(font);
        } else {
            Font newFont = new Font(font);
            cache.put(font, newFont);
            return newFont;
        }
    }
}

public Client {
    public static void main(String[] args) {
        FontFactory fontFactory = new FontFactory();
        new Article("hide", fontFactory.getFont("font1").name);
    }
}

따라서 위처럼 Font를 객체로 분리하고 FontFactory에서는 Map을 생성하여 캐싱처리해두는 형태로 변경하여 재사용을 유도할 수 있다.

Reference

인프런 - 백기선님 이펙티브 자바 완벽 공략 1부

반응형