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

Hanjmo·2024년 7월 3일
post-thumbnail

나는 다음과 같은 형태로 생성자를 private하게 만들고, 내부에서 생성자를 호출하는 방식으로 코드를 작성했다.

@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ErrorResponse {

    private HttpStatus httpStatus;
    private String message;

    public static ErrorResponse from(HttpStatus httpStatus, String message) {
        return new ErrorResponse(httpStatus, message);
    }
}

그러다 문득 왜 이런 코드를 작성하는가? 라는 의문이 들었는데, 뚜렷한 답을 내놓을 수 없어 이번에 정리해보려고 한다.

정적 팩토리 메서드

내가 작성한 방식은 디자인패턴 중 하나인 정적 팩토리 메서드 패턴이라고 불린다.

이름에서 알 수 있듯 인스턴스 생성(팩토리)을 static(정적) 메서드를 통해 내부적으로 처리하는 패턴이다.

사용 사례

정적 팩토리 메서드 패턴은 이미 많은 곳에서 사용되고 있으며, 우리는 자연스럽게 이를 사용하고 있다.

대표적인 예로 Java에서 제공하는 LocalDateTime의 of 메서드가 있다.

이는 내부적으로 LocalDate와 LocalTime의 of를 호출하면서 생성한 각 인스턴스를 생성자에 전달하고, 최종적으로 LocalDateTime이라는 인스턴스를 생성하여 반환한다.

이 외에도 List의 of, Integer의 valueOf 등 이미 많은 코드가 정적 팩토리 메서드 패턴으로 구현되어 있다.

정적 팩토리 메서드 특징

그렇다면 왜 멀쩡한 생성자를 가두고 정적 메서드를 통해 생성자를 간접적으로 호출하도록 권장할까?

이는 아래 특징들을 살펴보면 자연스레 납득이 갈 것이다.

생성 목적에 대한 네이밍이 가능하다

집을 지으려고 할 때 누군가는 색상을 커스텀하고 싶어하길 원하고, 또 다른 누군가는 멋진 수영장을 함께 짓고 싶을 수 있다.

이처럼 많은 이들의 요구사항을 수용하기 위해서는, 각 생성 목적에 따라 여러 생성자를 오버로딩해야 한다.

그리고나서 각 목적에 따라 House 객체를 생성하면 아마 다음과 같은 코드를 작성할 것이다.

House blueHouse = new House("blue");
House whiteHouse = new House();
House whiteHouseWithSwimmingPool = new House(true);
House blueHouseWithSwimmingPool = new House("blue", true);

이때 새로 들어온 개발자가 House 객체를 생성하는 코드를 작성해야 한다면 어떨까?

분명 각 생성자에 어떤 값이 들어가고 있는건지 파악하기 힘들 것이고, 내부 구조를 하나하나 다 까봐야 할 것이다. 이렇듯 생성자를 그대로 호출하게 된다면 번거로움을 느끼게 된다.

하지만 생성자를 직접 사용하지 않고 정적 메서드로 감싼다면, 다음과 같이 이름을 명시할 수 있게 된다.

House blueHouse = House.colorOf("blue");

이러면 객체 생성 목적을 파악하기 위해 내부 구조를 확인해야 한다는 번거로움이 사라질 것이다.

인스턴스 생성을 통제할 수 있다

인스턴스 생성을 메서드로 간접적으로 수행하기 때문에 인스턴스 생성에 대한 통제권을 얻을 수 있다.

예를 들어, 다음과 같이 getInstance라는 정적 팩토리 메서드를 사용하여 항상 같은 인스턴스만 반환하도록 할 수 있다.

class Singleton {
    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

이러한 생성 패턴을 싱글톤 패턴이라고 하는데, 인스턴스를 오직 하나만 생성하고 재사용함으로써 메모리를 절약할 수 있다.

상황에 따라 다른 객체를 반환하도록 분기할 수 있다

메서드는 생성자와 달리 값을 반환할 수 있기 때문에, 다음과 같이 상황에 따라 다른 객체를 생성하여 반환할 수 있다.

class House {
    private String color = "white";
    private boolean hasSwimmingPool = false;

    public static House getHouse(int money) {
        if (money < 1_000) {
            return new Villa();
        } else if (money < 50_000) {
            return new TownHouse();
        } else {
            return new Apartment();
        }
    }
}

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

애플리케이션을 개발하다 보면, 엔티티 객체를 보호하면서 계층 간 데이터를 전달하기 위해 DTO를 사용한다.

이때 DTO와 객체 사이의 변환 로직이 들어갈 수 밖에 없는데, 정적 팩토리 메서드를 사용하냐 안하냐에 따라 캡슐화 여부가 달라진다.

public HouseDto service(House house) {
		...
		
		HouseDto houseDto1 = HouseDto.from(house); // 정적 팩토리 메서드 사용
		HouseDto houseDto2 = new HouseDto(house.getColor(), house.getHasSwimmingPool()); // 생성자 직접 호출
        
		...
        
}

생성자를 직접 사용하는 경우 모든 인자를 그대로 넘겨줘야 하기 때문에, House 객체를 DTO로 변환하는 로직이 포함된 클래스가 House의 내부 속성까지 모두 알게 된다.

반면에 정적 팩토리 메서드를 사용하면 메서드 인자의 타입을 House로 지정하여 객체 자체를 전달할 수 있게 되고, 결과적으로 외부에서 House의 내부 속성을 알 필요가 없게 된다.

정적 팩토리 메서드 네이밍 규칙

정적 팩토리 메서드는 다른 메서드와 구분을 짓기 위해 네이밍 컨벤션이 존재한다.

그런데 이 정적 팩토리 메서드의 컨벤션은 거의 법칙 수준으로 자리잡고 있기 때문에, 각 네이밍의 역할을 잘 알고 사용하는 것이 좋다.

  • from: 하나의 매개변수를 전달 받아 객체 생성
  • of: 여러 개의 매개변수를 전달 받아 객체 생성
  • getInstance | instance: 인스턴스 생성 (이전에 반환했던 것과 같을 수 있음)
  • newInstance | create: 새로운 인스턴스 생성
  • get{생성할 객체의 타입}: 다른 타입의 인스턴스 생성 (이전에 반환했던 것과 같을 수 있음)
  • new{생성할 객체의 타입}: 다른 타입의 새로운 인스턴스 생성

위에서 네이밍 컨벤션이 법칙 수준이라고 했는데, 그 이유는 정적 팩토리 메서드를 사용했을 때 API 문서에서의 불편함 때문이다.

Java 공식문서를 보면, 생성자가 가장 상단에 정의되어 있기 때문에 그에 대한 스펙을 빠르게 찾을 수 있다.

하지만 정적 팩토리 메서드는 개발자가 임의로 만든 것이기 때문에 다른 메서드와 같이 아래에 위치하게 되고, 정적 팩토리 메서드를 찾기 힘들다는 문제가 발생한다.

따라서 정적 팩토리 메서드를 쉽게 찾을 수 있도록 네이밍 규칙을 다른 컨벤션보다 엄격하게 적용하는 것이다.

0개의 댓글