일반적으로 Class의 Instance를 생성하는 방법은 public Constructor를 사용하지만, 그와 별도로 Static factory method를 사용할 수 있습니다.

 

이러한 메서드는 instance를 반환하는 단순한 형태나 별도 로직을 포함하는 형태를 지니게 됩니다.

 

 

Effective Java에서는 이를 통해 여러 장점과 단점이 존재한다고 하는데. 하나하나 정리해보았습니다.

 

 

 

1. Class를 생성하는 행위, 특성에 대해 이름을 부여할 수 있다.


객체의 Constructor 그 자체와 넘기는 Parameter 만으로는 해당 객체의 특성이나 의미를 자세히 알아내기가 어렵고 이를 이해하기 위해 API 문서를 더 확인해야 할 수 있습니다. 이때 Static factory method의 Naming을 통해 뜻을 나타내는 건 개발자에게 좋은 정보가 될 수 있습니다.

List<Value> empties = new ArrayList<>();

List<Value> empties = ArrayListUtils.emptyValues();

위의 예제는 어떠한 비즈니스를 설정하지 않고 단순하게 작성한 것이기에 공감이 되지 않을 수 있습니다. 그런 분들은 다른 상황에서 좋은 케이스를 만나볼 수 있을 것이라고 생각합니다.

 

예제의 의미는 알겠지만 위의 장점을 동의하지 못하신다면 프로젝트, 비즈니스 이해도가 존재하지 않는 프로젝트로 투입되었을 때를 생각해보면 좋을 것 같습니다. 그럴 때에 값 객체나, 도메인 모델 등이 method 방식으로 생성이 된다면 후자가 더 좋은 가독성을 지닌다고 생각합니다.

 

그리고 동일한 시그니처를 가지는 Constructor는 하나만 존재할 수 있습니다. 점층적인 생성자 패턴을 통해 여러 생성자를 생성하고 다른 로직이나 값을 끼워 넣을 수 있지만, 이는 개발자에게 혼란을 줄 수 있습니다.

개인적으로 생성자에 로직이 들어가는 것은 좋다고 생각하지 않습니다.

 

하지만 Static factory method를 사용한다면 method의 이름을 통해 이러한 문제를 해결할 수 있습니다.

 

 

 

2. 객체를 사용할 때마다 새로운 인스턴스를 생성하지 않아도 된다.


Static factory method를 사용하면 내부에 특정 상태를 가지는 인스턴스를 생성하여 가지고 있다가 전달하거나, 사용한, 사용할 객체를 캐싱하여 계속해서 반환할 수 있습니다.

private static class IntegerCache {
        //...
    static final Integer cache[];

    // static block 초기화 로직

        public static Integer valueOf(int i) {
            if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
            return new Integer(i);
        }

캐싱과 관련해서는 Integer class의 cache 배열을 통해 확인할 수 있는데, 인자 값에 따라서 캐싱한 인스턴스를 반환하는 로직을 확인할 수 있습니다.

 

 

 

3. 반환 타입의 하위 타입 객체를 반환할 수 있다.


// Collections의 정적 메서드들

unmodifiableCollection()
unmodifiableSet()
unmodifiableSortedSet()
unmodifiableNavigableSet()
unmodifiableList()
UnmodifiableRandomAccessList()
...

이러한 정적 메서드들은 각각의 하위 구현체를 반환하고 있습니다. 개발자는 직접 그 구현체에 대한 API를 확인하지 않더라도 Static factory method를 통해 이를 반환받고 사용할 수 있습니다.

 

이는 API의 크기와 개발자가 알고 있는 범주를 최소화하고 명시된 메서드의 이름과 간략한 설명을 통해 어떠한 객체를 얻을지 알 수 있어 개발에 대한 편의성을 가지게 됩니다.

 

 

 

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.


public static List<SimpleGrantedAuthority> ofRole(Domain model) {
    return domain.hasRole() 
         ? Collections.unmodifiableList(new SimpleGrantedAuthority("ADMIN"))
         : ListUtils.emptyRoleList(); // 가상의 Static factory method를 사용하였다.
}

----

List<GrantedAuthority> role = ofRole(model);

권한 상태 정보를 가지는 특정 Domain model이 있다고 가정한 예제입니다. Static factory method 내부에서는 전달받은 model 권한 상태에 따라 두 가지 유형의 리스트를 전달하게 됩니다. 클라이언트에서는 이를 알 수 없지만 로직 자체는 정상적으로 동작하기에, 개인적으로는 이를 통해 상황에 따라 유연한 로직 처리도 가능할 것이라고 생각합니다.

 

그리고 Java API의 EnumSet의 경우에는 전달받은 인자의 길이에 따라서 RegularEnumSet이나 JumboEnumSet 등을 반환하게 되는데 이는 값에 따라 메모리 공간을 개선하는 효과를 가지기도 합니다. 즉 성능이나 메모리 관점에서의 최적화도 Static factory method를 통해 가능합니다.

 

 

 

5. 메서드가 작성되는 시점에는 객체 클래스가 존재하지 않아도 된다.


Static factory method를 구현하는 시점에 Interface를 반환 타입을 지정한다면, 이후 시점에서 Interface의 구현체들이 추가되고 별도 모듈을 통해 주입되더라도 문제없이 주입받아 동작되게 만들 수 있습니다.

 

JDBC와 같이 다양한 벤더들이 하나의 서비스 API를 구성하는 구조(서비스 제공자 프레임워크)에서 서비스 인스턴스를 생성하고 주입해주는 접근 API 모듈은 이러한 특성을 활용한 것입니다.

접근 API 모듈 즉, Driver Manager는 getConnection() 이란 정적 팩터리 메서드를 가지고 있는데, 프로젝트에서 정의한 데이터 소스, 의존성을 통해 MySQL에서 제공하는 JDBC Driver가 추가되고 해당 구현체가 구현한 커넥션을 반환받을 수 있습니다.

 

 

여기까지가 이펙티브 자바에서 다루는 장점입니다. 이번엔 단점을 보도록 하겠습니다.

 

 

 

1. 상속을 하려면 public이나 protected 생성자가 필요하므로 정적 팩터리 메서드만 제공하게 하면 하위 클래스를 만들 수 없다.


사실 이 것은 상속 관점에서의 제한적인 시선이라고 생각이 듭니다. 최근에는 객체에서 기능을 제공하기 위해서 결합성을 올리고, 하위 구현체가 잘못된 동작을 할 수 있는 상속 방식을 사용하는 것보다 상태 변수로 가지는 조합 방식을 더 선호하기 때문입니다.

 

그렇기에 조합 방식을 사용하여 개발하는 사람들에게는 오히려 장점이 된다고 생각이 듭니다.

 

 

 

2. Static factory method에 대한 설명은 프로그래머가 찾기 어렵다.


정적 팩터리 메서드는 생성자처럼 명확한 API 설명을 가지지 않으므로 의미상 이해하기 어려운 부분에 대해서는 찾는 것이 더 어렵게 된다고 합니다. 이건 Java doc의 문제점..?

 

이를 완화하기 위해서 Static factory method를 작성할 때 흔히 사용되는 명명 규약을 준수하는 것이 좋다고 합니다.

from, of, valueOf, getInstance 등이 존재합니다.

 

 

 

참고 자료

  • Effective Java 3판

+ Recent posts