[TIL] - 정적 팩토리 메서드는 왜 사용할까?

김주형·2022년 9월 20일
0

TIL

목록 보기
15/37
post-thumbnail

Reference

이 글은 다음을 참고하여 작성되었습니다. 감사합니다. 🙇🏻‍♂️


작성 목적

  • 주변에서 정적 팩토리 메서드를 자주 사용한다는 이유만으로 그 용도와 역할을 제대로 모른 채 사용하는 것을 방지하기
  • 정적 팩토리 메서드란 무엇이고 어떤 장점이 있는지 알아보고 선택적인 응용 가능한 사람이 되기

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

정적(static). 팩토리(factory). 메서드(method)

GoF 디자인 패턴 중 팩토리 패턴에서 유래한 이 단어는 객체를 생성하는 역할을 분리하겠다는 취지가 담겨있다. (팩토리 패턴, 팩토리 클래스에 대해 더 알고 싶다면?)

-> 정적 팩토리 메서드란 객체 생성의 역할을 하는 클래스 메서드라는 의미로 요약해볼 수 있다.

정적 팩토리 메서드가 무엇인지 감이 안 온다면 예시를 통해 알아볼 수 있다.


다음 코드는 java.time 패키지에 포함된 LocalTime 클래스의 정적 팩토리 메서드이다.

// LocalTime.class
...
public static LocalTime of(int hour, int minute) {
  ChronoField.HOUR_OF_DAY.checkValidValue((long)hour);
  if (minute == 0) {
    return HOURS[hour];
  } else {
    ChronoField.MINUTE_OF_HOUR.checkValidValue((long)minute);
    return new LocalTime(hour, minute, 0, 0);
  }
}
...

// hour, minutes을 인자로 받아서 9시 30분을 의미하는 LocalTime 객체를 반환한다.
LocalTime openTime = LocalTime.of(9, 30);

위 예시 코드에서 본 LocalTime 클래스의 of 메서드처럼 직접적으로 생성자를 통해 객체를 생성하는 것이 아닌 메서드를 통해서 객체를 생성하는 것을 정적 팩토리 메서드라고 한다.

또 다른 예시로 enum의 요소를 조회할 때 사용하는 valueOf 도 정적 팩토리 메서드의 일종이라고 할 수 있다.
미리 생성된 객체를 “조회”를 하는 메서드이기 때문에 팩토리의 역할을 한다고 볼 수는 없지만, 외부에서 원하는 객체를 반환해주고 있으므로 결과적으로는 정적 팩토리 메서드라고 간주해도 좋다고 한다.

public enum Color {
  RED,
  BLUE;
}
...
Color redColor = Color.valueOf("RED");
Color blueColor = Color.valueOf("BLUE");

정적 팩토리 메서드 감잡기

정적 팩토리 메서드가 무엇이고, 어떤 역할을 하는지 감을 잡았다면 다음과 같은 의문이 들어야 정상이라고 한다.

Q : 객체를 생성하는 역할은 자바에서 제공하는 “생성자”가 하는데, 왜 정적 팩토리 메서드를 따로 만들어서 객체를 생성하는 것일까?
생성자와는 어떤 차이가 있나?

A : “생성자 대신 정적 팩토리 메서드를 고려하라”
- 조슈아 블로크 in “이펙티브 자바

벌써부터 정적 팩토리 메서드를 사용하는 것이 더 장점이 클것이라는 느낌이 오지 않냐고 묻는다.. 생성자와 정적 팩토리 메서드는 객체를 생성한다는 같은 역할을 하고 있지만 그 활용도는 엄연히 차이가 나기 때문이다. 실제로 정적 팩토리 메서드가 어떤 면에서 생성자보다 우위를 차지하고 있는지 하나씩 검증해보자.


1. 이름을 가질 수 있다.

  • 객체는 생성 목적과 과정에 따라 생성자를 구별해서 사용할 필요가 있다.
  • new라는 키워드를 통해 객체를 생성하는 생성자는 내부 구조를 잘 알고 있어야 목적에 맞게 객체를 생성할 수 있다.
  • 하지만 정적 팩토리 메서드를 사용하면 메서드 이름에 객체의 생성 목적을 담아 낼 수 있다.

예시 - 자동로또와 수동로또를 생성하는 팩토리 클래스

public class LottoFactory() {
  private static final int LOTTO_SIZE = 6;

  private static List<LottoNumber> allLottoNumbers = ...; // 1~45까지의 로또 넘버

  public static Lotto createAutoLotto() {
    Collections.shuffle(allLottoNumbers);
    return new Lotto(allLottoNumbers.stream()
            .limit(LOTTO_SIZE)
            .collect(Collectors.toList()));
  }

  public static Lotto createManualLotto(List<LottoNumber> lottoNumbers) {
    return new Lotto(lottoNumbers);
  }
  ...
}

createAutoLotto와 createMenualLotto 모두 로또 객체를 생성하고 반환하는 정적 팩토리 메서드이다. 메서드의 이름만 보아도 로또 객체를 자동으로 생성하는지, 아니면 수동으로 생성하는지 단번에 이해할 수 있을 것이라고 한다.

이처럼 정적 팩토리 메서드를 사용하면 해당 생성의 목적을 이름에 표현할 수 있어 가독성이 좋아지는 효과가 생긴다.


2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.

enum과 같이 자주 사용되는 요소의 개수가 정해져있다면 해당 개수만큼 미리 생성해놓고 조회(캐싱)할 수 있는 구조로 만들수 있다.
정적 팩터리 메서드와 캐싱구조를 함께 사용하면 매번 새로운 객체를 생성할 필요가 없어진다.

예시 - 로또 번호 생성 메서드 again

1부터 45까지의 로또 번호를 enum으로도 만들 수 있지만 LottoNumber 클래스 안에서 반복문을 통해 쉽게 45개의 인스턴스를 만들 수 있으므로 후자의 방법을 사용했다.

public class LottoNumber {
  private static final int MIN_LOTTO_NUMBER = 1;
  private static final int MAX_LOTTO_NUMBER = 45;

  private static Map<Integer, LottoNumber> lottoNumberCache = new HashMap<>();

  static {
    IntStream.range(MIN_LOTTO_NUMBER, MAX_LOTTO_NUMBER)
                .forEach(i -> lottoNumberCache.put(i, new LottoNumber(i)));
  }

  private int number;

  private LottoNumber(int number) {
    this.number = number;
  }

  public LottoNumber of(int number) {  // LottoNumber를 반환하는 정적 팩토리 메서드
    return lottoNumberCache.get(number);
  }

  ...
}

여기서 짚고 넘어가야할 점은 미리 생성된 로또 번호 객체의 캐싱을 통해서 새로운 객체 생성의 부담을 덜 수 있다는 장점도 있지만, 생성자의 접근 제한자를 private으로 설정함으로써 객체 생성을 정적 팩토리 메서드로만 가능하도록 제한할 수 있다는 것이다. 이를 통해 정해진 범위를 벗어나는 로또 번호의 생성을 막을 수 있다는 장점을 확보할 수 있게 된다.


3. 하위 자료형 객체를 반환할 수 있다.

하위 자료형 객체를 반환하는 정적 팩토리 메서드의 특징은 상속을 사용할 때 확인할 수 있다. 이는 생성자의 역할을 하는 정적 팩토리 메서드가 반환값을 가지고 있기 때문에 가능한 특징이다.

예시2

  • 시험 점수에 따라 결정되는 하위 등급 타입을 반환하는 정적 팩토리 메서드이다.
  • Basic, Intermediate, Advanced 클래스가 Level라는 상위 타입을 상속받고 있는 구조이며,
    다음과 같이 분기처리를 통해 하위 타입의 객체를 반환할 수 있다.
public class Level {
  ...
  public static Level of(int score) {
    if (score < 50) {
      return new Basic();
    } else if (score < 80) {
      return new Intermediate();
    } else {
      return new Advanced();
    }
  }
  ...
}

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

정적 팩토리 메서드는 객체 생성을 캡슐화하는 방법이기도 하다.

  • 캡슐화(Encapsulization)란?
    본래 캡슐화의 의미는 데이터의 은닉이지만,
    여기서는 생성자를 클래스의 메서드 안으로 숨기면서 내부 상태를 외부에 드러낼 필요없이 객체 생성 인터페이스 단순화 시킬 수 있음을 의미한다.

웹 어플리케이션을 개발하다보면 계층 간에 데이터를 전송하기 위한 객체로 DTO(Data transfer object)를 정의해서 사용한다.

DTO와 Entity간에는 자유롭게 형 변환이 가능해야 하는데, 정적 팩터리 메서드를 사용하면 내부 구현을 모르더라도 쉽게 변환할 수 있다고 하니 사실인지 검증해보자.

public class CarDto {
  private String name;
  private int position;

  pulbic static CarDto from(Car car) {
    return new CarDto(car.getName(), car.getPosition());
  }
}


// Car -> CatDto 로 변환
CarDto carDto = CarDto.from(car);

만약 정적 팩토리 메서드를 쓰지 않고 DTO로 변환한다면 외부에서 생성자의 내부 구현을 모두 드러낸 채 해야할 것이다.

Car carDto = CarDto.from(car); // 정적 팩토리 메서드를 쓴 경우
CarDto carDto = new CarDto(car.getName(), car.getPosition); // 생성자를 쓴 경우
  • 이처럼 정적 팩토리 메서드는 단순히 생성자의 역할을 대신하는 것 뿐만 아니라, 좀 더 가독성 좋은 코드를 작성하고 객체지향적으로 프로그래밍할 수 있도록 도와 준다.
  • 도메인에서 “객체 생성”의 역할 자체가 중요한 경우라면 정적 팩토리 클래스를 따로 분리하는 것도 좋은 방법이 될 것이다.
  • 다만 팩토리 메서드만 존재하는 클래스를 생성할 경우 상속이 불가하다는 단점이 있으니 참고하여 사용할 필요가 있다고 한다.

확실한 점은 정적 팩토리 메서드를 적절히 사용했을 때 얻을 수 있는 장점이 더 많다는 것이다. 객체간 형 변환이 필요하거나, 여러 번의 객체 생성이 필요한 경우라면 생성자보다는 정적 팩토리 메서드를 사용해보자.


정적 팩토리 메서드 이해도 높이기 - 네이밍 컨벤션

다음은 정적 팩토리 메서드의 용도와 역할에 대해 이해도를 높이는데 도움이 되고
일반적으로 많이 사용되는 네이밍 컨벤션이라고 한다.

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

  • from : 하나의 매개 변수를 받아서 객체를 생성
  • of : 여러개의 매개 변수를 받아서 객체를 생성
  • getInstance | instance : 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • newInstance | create : 새로운 인스턴스를 생성
  • get[OtherType] : 다른 타입의 인스턴스를 생성. 이전에 반환했던 것과 같을 수 있음.
  • new[OtherType] : 다른 타입의 새로운 인스턴스를 생성.
profile
근면성실

0개의 댓글