정적 팩토리 메서드를 사용하는 이유

정적 팩토리 메서드(Static Factory method)란?

정적 팩토리 메서드란 객체 생성을 생성자가 아닌 정적(static) 메서드로 하는 것을 말한다.

“객체 생성은 생성자가 하는데, 왜 정적 팩토리 메서드를 따로 만들어서 객체 생성을 할까?”

1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다. 반면, 정적 팩토리 메서드를 사용하면 메서드 이름에 객체 생성 목적을 담아낼 수 있다.

예를들어, 학생 객체를 생성한다고 할 때 생성자 사용하는 경우, 정적 메서드를 사용하는 경우를 비교 해보자.

1.1 생성자 사용

public class Student {

	private String name;
	private int idNumber;

	public Student(String name) {
		this.name = name;
		this.idNumber = 99999;
	}

	public Student(int idNumber) {
		this.idNumber = idNumber;
		this.name = "대학생";
	}
}
Student student1 = new Student("슬로");
Student student2 = new Student(11222334);

Student 객체를 생성자를 오버라이딩 해서 사용하고 있다. 그런데 만약 개발자가 Student 객체 내부 구조에 대해 잘 알고 있지 않다면, 이름이나 학번을 생성자 인자로 보내야하는 사실을 모를 수 있다.

1.2 정적 팩토리 메서드 사용

public class Student {

	private String name;
	private int idNumber;

	private Student(String name, int idNumber) {
		this.name = name;
		this.idNumber = idNumber;
	}

	public static Student createByName(String name) {
		return new Student(name, 999999);
	}

    public static Student createByIdNumber(int idNumber) {
		return new Student("슬로", idNumber);
	}
}
Student student1 = Student.createByName("슬로");
Student student2 = Student.createByIdNumber(999999);

createByName, createByIdNumber 모두 학생 객체를 생성하고 반환하는 정적 메서드이다. 메서드 이름만 봐도 이름으로 생성하는지, 학번으로 생성하는지 단번에 이해할 수 있다. 이처럼 정적 팩토리 메서드를 사용하면 생성의 목적을 이름에 표현할 수 있기 때문에 가독성이 좋아지는 효과가 있다.

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

인스턴스를 새로 만들 필요 없이 새로 생성한 인스턴스를 캐싱하여 재활용할 수 있기 때문에 불필요한 객체 생성을 피할 수 있다. 또한 생성자의 접근 제한자를 private로 설정하여 불필요한 객체 생성을 막을 수 있다.

public class CarFactory {

	private CarFactory() {}

	private static class CarFactoryHolder {
		private static final CarFactory INSTANCE = new CarFactory();
	}

	public static CarFactory getInstance() {
		return CarFactoryHolder.INSTANCE;
	}
}

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

정적 팩터리 메서드를 사용하면, 반환할 객체의 클래스를 자유롭게 선택할 수 있게하는 유연성을 가질 수 있게 된다.

public static Grade of(int score) {
    if (score >= 90) {
        return new A();
    }

    if (score >= 80) {
        return new B();
    }

    // ...

    return new F();
}

class A extends Grade {
    // ...
}

class B extends Grade {
    // ...
}

4. 객체 생성을 캡슐화할 수 있다.

4.1 정적 팩터리 메서드를 사용하면 내부 구현을 외부에 들어내지 않고 DTO로 변환할 수 있다.

public class CarDto {

	private final String name;
	private final int location;

	public CarDto(final String name, final int location) {
		this.name = name;
		this.location = location;
	}

	public static CarDto from(Car car) {
		return new CarDto(car.getName().getName(), car.getLocation());
	}
}
// 정적 팩토리 메서드를 사용한 경우
CarDto carDto = CarDto.from(car);

// 생성자를 사용한 경우
// 내부가 이름, 위치 정보로 구현된다는 것을 알 수 있다.
CarDto carDto = new CarDto(car.getName(), car.getLocation());

4.2 객체가 가지는 멤버 변수가 아닌 데이터를 매개변수로 받아서 가공하여 처리할 수 있다.

정적 메소드에서 입력받은 자동차 이름 배열을 그대로 매개변수로 받아서 Car List로 가공한 뒤에 Cars 객체를 생성한다. 이처럼 정적 메소드에서 가공 처리를 한다면, controller나 view에서 각자 맡은 역할만 알맞게 수행할 수 있다.

public class Cars {

	private final List<Car> cars;

	private Cars(List<Car> cars) {
		cars = Collections.unmodifiableList(cars);
		validateDuplicate(cars);
		this.cars = cars;
	}

	public static Cars from(String[] names) {
		return new Cars(Arrays.stream(names)
			.map(Name::new)
			.map(Car::new)
			.collect(Collectors.toList()));
	}
}

단점

  • 상속을 하려면 public이나 protected 생성자가 필요하다. 그런데 정적 팩터리 메서드는 생성자를 private로 막아두기 때문에 정적 팩터리 메서드만 제공한다면 하위 클래스를 만들 수 없다.
  • 정적 팩터리 메서드는 프로그래머가 사용하기 위해서 찾는 시간이 오래 걸린다. 생성자처럼 API 설명에 명확히 드러나지 않아 사용자가 직접 방법을 찾아야한다.

정적 팩토리 메서드 네이밍 컨벤션

두 번째 단점을 보완하기 위해서 널리 알려진 규약을 따라 메서드 이름을 짓는 것이 좋다.

  • from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드

    Data d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드

    Set<Rank> faceCars = EnumSet.of(JACK, QUEEN, KING);
  • valueOf : from과 of의 더 자세한 버전

    BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance | getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.

    StackWalker luke = StackWalker.getInstance(options);
  • create | newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해서 반환함을 보장한다.

    Object newArray = Array.newInstance(classObject, arrayLen);
  • getType : getInstance와 같으나, 생성한 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.

    FileStore fs = Files.getFileStore(path);
  • newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 쓴다. Type은 팩터리 메서드가 반환할 객체의 타입이다.

    BufferedReader br = Files.newBufferedReader(path);
  • type : getType과 newType의 간결한 버전

    List<Complaint> litany = Collections.list(legacyLitany);

그래서 언제 정적 팩토리 메서드, 생성자를 사용할건데?

정적 팩토리 메서드

  • 생성자 명을 목적성 있게 설정하고 싶은 경우
  • 객체가 가지는 멤버 변수가 아닌 데이터를 받아서 가공해야 하는 경우
  • 여러 번의 객체 생성이 필요한 경우

생성자

  • 멤버변수에 저장할 데이터를 사용해서 객체를 생성하는 경우

이렇게 생성자, 정적 팩토리 메서드를 목적에 맞게 구분해서 사용한다면 어떤 장점이 있을까?

의미를 전달하거나 의도를 쉽게 파악할 수 있다. 생성자를 사용하면, 어떤 필드를 내부적으로 가지는지를 파악할 수 있다. 팩토리 메서드를 사용하면, 생성 목적을 파악하기 쉽고, 전달되는 데이터가 가공 처리를 거칠 수도 있겠다는 추측을 할 수 있다.

참고


Written by@슬로
느리지만 꾸준하게 나아갑니다.