정적 팩터리 메서드

베루스·2022년 2월 17일
5

이펙티브 자바(3판)Item 1. 생성자 대신 정적 팩터리 메서드를 고려하라의 내용을 살펴보려합니다.
일반적으로 객체를 생성하는 방법은 public 생성자를 사용하는 방법인데, 이펙티브 자바에서는 정적 팩터리 메서드의 장단점을 파악하고 필요 시 정적 팩터리 메서드의 사용을 권장하고 있습니다. 책에서 언급한 장점과 단점을 한번 살펴보고, 어떤 상황에서 정적 팩터리 메서드를 사용하는 것이 좋을지 알아보겠습니다.

정적 팩터리 메서드란?

정적 팩터리 메서드는 아래의 코드와 같이 객체를 생성해 반환하는 정적 메서드입니다.

public class Car {
	...
    
	public static Car create() {
		return new Car();
	}
}

일반적으로 public 생성자를 사용해 원하는 클래스의 객체를 생성하지만, 위와 같은 정적 팩터리 메서드를 사용해 객체를 생성할 수 있습니다.

정적 팩터리 메서드의 장점

1. 이름을 가질 수 있다.

생성자는 항상 클래스와 동일한 이름을 가져야합니다. 생성자에 전달하는 매개변수를 통해 원하는 생성자를 선택할 수 있는데, 이는 객체의 특성을 제대로 표현하지 못할 수 있습니다.

public class Car {
	private String name;
    
    public Car(String name) {
    	...
    }
    
    public static Car createByName(String name) {
    	return new Car(name);
    }
}

public class App {
	public static void main(String[] args) {
    	Car car1 = new Car("베루스");
        Car car2 = Car.createByName("베루스");
    }
}

단순하게 생성자를 이용해 원하는 자동차의 이름을 가지는 객체를 생성할 때 보다 정적 팩터리 메서드를 사용할 때 좀 더 명확한 의미를 가지고 객체를 생성할 수 있습니다.
또한, 생성자는 하나의 시그니처로 한개만 정의할 수 있습니다. 만약, 동일한 타입의 파라미터를 사용해 객체를 생성하고 싶을 때에는 문제가 발생할 수 있습니다. 하지만, 정적 팩터리 메서드는 다른 이름을 통해 다양한 시그니처를 가질 수 있기 때문에 생성자보다 좀 더 명확한 의미를 가질 수 있습니다.

public class Car {
	private String name;
    private String model;
    private int position;
    
    public Car(String name, int position) {
    	...
    }
    
    public Car(int position, String model) {
    	...
    }
    
    public static Car createByNameAndPosition(String name, int position) {
    	return new Car(name, position);
    }
    
    public static Car createByModelAndPosition(String model, int position) {
    	return new Car(position, model);
    }
}

위의 코드에서는 Car 객체를 생성할 때 사용할 생성자를 파라미터 타입의 순서를 바꿔서 정할 수 있습니다. String이 먼저올 때에는 이름을 초기화하고, int가 먼저올 때에는 모델명을 초기화하는데 개발자에게 많은 혼란을 줄 수 있습니다. (정확한 객체 생성을 위해 Car 클래스의 생성자 구현을 확인해야겠지요...) 정적 팩터리 메서드를 사용하면 createByNameAndPosition이나 createByModelAndPosition과 같은 이름을 통해 개발자들의 혼란을 제거해줄 수 있습니다.

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

생성자를 사용해 객체를 생성할 때에는 매번 새로운 객체가 생성됩니다. 하지만, 기존에 생성된 하나의 객체만을 재사용하는 싱글턴 패턴이나 기존 객체를 재사용해도 되는 불변 객체의 경우에는 항상 새로운 객체가 필요하지 않습니다. 정적 팩터리 메서드를 사용하면, 객체 생성을 통제할 수 있습니다.

public class Car {
	private static Car VERUS = new Car("베루스");
    
    ...
    
	public static Car verus() {
    	return VERUS;
    }
}

위의 코드와 같이 정적 팩터리 메서드를 사용하면 이미 생성된 VERUS객체를 항상 가져올 수 있습니다. 객체 생성을 제어하기 때문에 이전에 만들어둔 객체를 캐시하여 재활용할 수 도 있습니다.

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

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

위의 코드는 Integer 클래스의 일부분입니다. valueOf메서드를 사용해 Integer 객체를 생성할 경우, 특정 범위에 한해서 IntegerCache에 미리 저장된 Integer 객체를 반환합니다. 이런 식으로 기존에 생성된 객체를 사용하는 정적 팩터리 메서드의 경우, 생성 비용이 크지만 같은 객체를 자주 사용할 때 성능적으로도 장점이 될 수 있습니다.

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

정적 팩터리 메서드의 반환 타입이 인터페이스나 부모 클래스일 경우, 해당 타입의 하위 타입 객체를 반환할 수 있게 해줍니다. 이는 유연성을 향상시키는 효과가 나타나는데, OCP 원칙을 준수하는 코드를 만들 수 있게 해줍니다.

public interface Reader {
	String read();
    
    static Reader defaultReader() {
    	return new FileReader();
    }
}

public class FileReader implements Reader {
	public String read() {
    	...
    }
}

위의 코드의 경우 defaultReader 정적 팩터리 메서드에서 Reader 타입의 객체를 반환하는데, 하위 타입인 FileReader 객체를 생성해 반환합니다. 정적 팩터리 메서드를 사용하는 입장에서는 구체 클래스의 존재를 모르기 때문에 새로운 Reader 하위 타입의 객체가 필요해도 수정없이 사용할 수 있습니다.

public class Application {
	public void readSomething() {
    	Reader reader = Reader.defaultReader();
        String data = reader.read();
        ...
    }
}

위의 readSomething메서드는 어떤 하위 Reader 객체인지 상관없이, 재사용할 수 있습니다. 이는 새로운 요구사항으로 인해 Reader 하위 타입 객체가 필요할 경우, defaultReader 메서드의 구현만 변경해주면 됩니다. 이렇게 OCP 원칙을 준수하게 되면, 수정에는 닫혀있고 확장에는 열려있는 구조를 가질 수 있습니다.
자바 8전에는 인터페이스에 static 메서드를 정의할 수 없어, Collections, Arrays와 같은 인스턴스화 불가인 클래스를 만들어 정적 팩터리 메서드를 구현했습니다. 하지만, 자바 8부터는 인터페이스에 public static 메서드를 정의할 수 있게 되었고, 정적 팩터리 메서드를 위의 코드와 같이 인터페이스에 정의할 수 있게 되었습니다.

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

반환 타입의 하위 타입 객체이기만 하면 어떤 객체든 상관없이 정적 팩터리 메서드에서 반환할 수 있습니다. 현재 파악할 수 없는 기능을 가진 객체여도, 반환 타입의 하위 타입이기만 하면 문제없이 사용할 수 있습니다.

public interface Reader {
	String read();
    
    static Reader createByType(String type) {
    	if (type.equals("file")) {
        	return new FileReader();
        }
    	return new DbReader();
    }
}

public class FileReader implements Reader {
	public String read() {
    	...
    }
}

public class DbReader implements Reader {
	public String read() {
    	...
    }
}

위의 코드와 같이 매개변수를 이용해 원하는 객체를 가져올 수 있습니다. 언젠가 FileReader가 필요없어져 삭제되거나, 새로운 기능을 담은 Reader 하위 클래스가 추가되어도 사용자(클라이언트)는 문제없이 사용할 수 있습니다. (3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.에서 살펴본 유연성 덕분이죠 ㅎㅎ)

5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. (이 부분은 정확히 이해되지 않아 잘못된 내용일 가능성이 매우 높습니다 ㅠㅠ)

정적 팩터리 메서드는 하위 타입의 객체를 반환할 수 있기 때문에, 현재 시점에 반환할 객체가 존재하지 않아도 미래에 추가된 객체를 반환할 수 있도록 할 수 있습니다. 이런 특징은 서비스 제공자 프레임워크에서 나타난다고 합니다. 책에서 소개하는 대표적인 서비스 제공자 프레임워크JDBC인데 기능에 필요한 인터페이스가 정의되어 있으면, 미래에 제공자들이 구현한 구현체를 인터페이스와 함께 정의된 정적 팩터리 메서드를 통해 클라이언트가 사용할 수 있답니다.(흠...) 이를 통해, 구현체들을 클라이언트에 제공하는 역할을 프레임워크가 통제하여, 클라이언트를 구현체로부터 분리해줍니다. (이 문장은 책의 내용 거의 그대로 복사해왔는데, DIP와 관련된 내용 같습니다 ㅎㅎ)

정적 팩터리 메서드의 단점

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

객체 생성을 위해 정적 팩터리 메서드만 제공할 경우, 생성자는 private 접근 제어자를 가져야합니다. 이는 해당 클래스의 상속을 제한하기 때문에, 상속을 사용하지 못하는 문제가 발생합니다. 하지만, 이펙티브 자바(3판)에서는 상속보다 합성의 사용을 유도하고, 어차피 불변 타입의 경우 이 제약을 지켜야한다는 점에서 장점이 될 수 있다고 합니다.

2.정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

생성자처럼 API 설명에 명확히 드러나지 않아 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화하는 방법을 알아내야합니다.(흠... 이건 먼말인지...)
일단,이펙티브 자바(3판)에서는 메서드 이름을 널리 알려진 규약을 따라 짓는 방법으로 해당 문제를 완화해줘야한다고 합니다. 그래서, 책에서 소개하는 정적 팩터리 메서드의 명명법은 아래와 같습니다.

  • from
    • 매개변수 하나를 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
    • 예) Date.from(instance);
  • of
    • 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
    • 예) List.of(a, b, c);
  • valueOf
    • from과 of보다 더 자세한 버전
    • 예) Integer.valueOf(1);
  • instance 혹은 getInstace
    • 매개변수가 있다면 매개변수에 맞는 객체를 반환하지만, 같은 객체임을 보장하지 않음
    • 예) StackWalker.getInstance(options);
  • create 혹은 newInstance
    • instance나 getInstance와 같지만, 항상 새로운 객체를 생성해 반환함을 보장
    • Array.newInstance(classObject, arrayLen);
  • getType
    • getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스의 팩터리 메서드를 정의할 때 사용
    • 예) Files.getFileStore(path);
  • newType
    • newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스의 팩터리 메서드를 정의할 때 사용
    • 예) Files.newBufferedReader(path);
  • type
    • getType과 newType의 간결한 버전
    • 예) Collections.list(legacyLitany);

정리

정적 팩터리 메서드를 사용하면 앞서 소개한 장단점을 가지게 됩니다. 켄트 벡의 구현 패턴에서 공장 메서드 파트를 살펴보면

정적 메서드는 추상 타입을 반환할 수 있으며, 의도가 담긴 별도의 이름을 가질 수 있다. 그러나 공장 메서드를 사용하면 복잡성이 증가하므로, 공장 메서드는 이득이 있을 경우에만 사용해야한다.
...
객체를 생성하는 것보다 복잡한 작업을 하는 경우 유용하다. 그러나 코드를 읽을 때, 공장 메서드를 볼 때마다 객체 생성 이외에 어떤 작업이 일어나는가 하는 의문을 갖게 된다. 따라서, 코드 독자의 시간을 아껴주고 싶다면, 평범한 객체 생성을 하는 경우에는 생성자를 사용하라. 객체 생성 이외에 다른 의도가 있을 경우에만 공장 메서드를 사용하라.

또 다른 책인 리팩토링생성자를 팩토리 메서드로 전환 파트의 동기 절을 살펴보면

생성자를 팩토리 메서드로 전환을 실시해야 할 가장 확실한 상황은 분류 부호를 하위클래스로 바꿀 때 발생한다. 분류 부호를 사용해 작성한 객체가 있는데 현시점에서 하위클래스가 필요해졌다. 어느 하위클래스를 사용할지는 분류 부호에 따라 달라진다. 하지만 생성자는 요청된 객체의 인스턴스 반환만 할 수 있다. 따라서 생성자를 팩토리 메서드로 바꿔야 한다.

두 가지 책에서 공통적으로 정적 팩터리 메서드의 하위 타입 객체 반환을 통한 유연성을 중요시하고 있습니다. 명확한 의미를 가진 이름을 사용하기 위해 정적 팩터리 메서드를 사용해도 좋지만, 복잡성이 증가할 수 있는 위험이 있어 평범한 객체의 경우 평범한 생성자를 사용할 것을 켄트 벡은 강조하고 있습니다. 정적 팩터리 메서드를 적용하기 전에 이펙티브 자바, 켄트 벡의 구현 패턴, 리팩토링에서 설명하는 정적 팩터리 메서드의 장단점을 기억하고 현 상황에서 정말 필요한지 고민한 다음 적용하면 좋을 것 같습니다~😁

0개의 댓글