본문 바로가기
독서이야기/이펙티브 자바 3 edition

[아이템1] 생성자 대신 정적 팩터리 메서드를 고려하라

by 사랑꾼이야 2022. 8. 7.
반응형

이 내용은 이펙티브 자바 Effective Java 3/E 를 읽으면서 정리한 내용을 포함하고 있습니다.

객체를 생성하기 위해서는 객체의 생성자(public) 를 통해서 얻을 수 있습니다. 하지만 생성자 이외에도 객체를 생성하기 위해서 사용하는 방법이 하나 더 있는데, 그것은 정적 팩터리 메서드 입니다.

정적 팩토리 메소드란?

객체 생성의 역할을 하는 클래스 메서드입니다. 많이 사용하는 클래스를 통해서 살펴보도록 하겠습니다.

날짜를 이용하기 위해서LocalDateTime.class 객체를 사용합니다. 이 클래스를 생성하기 위해서는 아래 일부 예시와 같이 사용하고 있습니다.

// LocalDateTieme.class
...
// 정적 팩토리 메소드 사용
public static LocalDateTime of(int year, Month month, int dayOfMonth, int hour, int minute) {
    LocalDate date = LocalDate.of(year, month, dayOfMonth);
    LocalTime time = LocalTime.of(hour, minute);
    return new LocalDateTime(date, time);
}
...
private LocalDateTime(LocalDate date, LocalTime time) {
    this.date = date;
  this.time = time;
}
  • 정적 팩토리 메소드인 of 를 통해서 year, month, dayOfMonth, hour, minute 매개변수를 받아서 내부적으로 생성자를 호출하는 구조입니다.

또 다른 예로는 Enum 클래스가 있습니다.

// enum
public enum MemberStatus {

    INPROGRESS, COMPLETE;
}


// MemberStatusTest.class
@Test
void 사용자_상태_조회() {
    final MemberStatus memberStatus = MemberStatus.valueOf("INPROGRESS");

    assertEquals(memberStatus, MemberStatus.INPROGRESS);
}
  • 정적 팩토리 메소드인 valueOf 를 통해서 내부적으로 조회하여 객체를 전달합니다.

정적 팩터리 메서드 는 객체를 생성하기 위해 사용하는 생성자와 결과는 동일하지만 차이가 없지만 장점들이 있습니다.

생성자와의 차이점

생성자와는 어떠한 차이점이 있으며, 어떠한 장점들이 있는지 확인을 해보도록 하겠습니다.

간단한 예제를 통해서 생성자를 사용한 방식과 정적 팩토리 메소드를 사용한 방식에 대해서 확인해보겠습니다.

생성자 사용

사용자 생성을 위해서 생성자를 이용해서 테스트 코드를 작성하였습니다.

@DisplayName("사용자 초기 생성을 생성자를 이용한다.")
@Test
void 사용자_생성() {
    final Member member = new Member(1L, "lee", 20, "seoul");

  assertAll(
    () -> assertEquals(member.getId(), 1L),
    () -> assertEquals(member.getMemberName(), "lee"),
    () -> assertEquals(member.getAge(), 20),
    () -> assertEquals(member.getAddress(), "seoul")
  );
}
  • new Member() 를 통해서 객체를 생성하였습니다.

정적 팩토리 메소드 사용

정적 팩토리 메소드를 사용해서 테스트 코드를 작성하였습니다.

@DisplayName("글 초기 생성을 정적 팩토리 메소드를 이용한다.")
@Test
void 글_생성() {
    final Write write = Write.initCreate(1L, "title", "content", "lee", "lee");

  assertAll(
      () -> assertEquals(write.getId(), 1L),
    () -> assertEquals(write.getTitle(), "title"),
    () -> assertEquals(write.getContent(), "content"),
    () -> assertEquals(write.getCreatedBy(), "lee"),
    () -> assertEquals(write.getUpdateBy(), "lee")
  );
}
  • Write.initCreate() 를 통해서 객체를 생성하였습니다.

어떠한 차이점이 느껴지셨나요? 정적 팩토리 메소드가 생성보다 좋은 점들에 대해서 알아보도록 하겠습니다.

1. 이름을 가질 수 있습니다.

생서자에 넘기는 매개변수와 생성자 자체만으로 반환될 객체의 특성을 제대로 설명할 수는 없습니다. 반면, 정적 팩토리 메소드는 이름만 잘 지어준다면 반환될 객체의 특성을 쉽게 나타낼 수 있습니다. 그리고 정적 팩토리 메소드를 이용해서 생성 목적과 과정에 따라서 생성자를 구별해서 사용할 필요가 있는데, 정적 팩토리 메소드를 사용하면 객체의 생성 목적 또한 담아낼 수 있습니다.

위에서 사용한 Write.initCreate 처럼 메소드 이름을 통해서 의미를 전달할 수 있습니다.

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

인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다.

Integer 클래스는 캐싱된 범위를 미리 만들어두고 전달된 매개변수 i 가 범위안에 있다면 객체를 새로 생성하지 않고 생성된 객체를 전달합니다. 그렇지 않을 경우에는 생성자를 통해서 새롭게 생성합니다.

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

이렇게, 인스턴스를 제어하여서 싱글턴으로 만들수도 있고, 인스턴스화 불가로 만들수 있습니다. 또한 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장할 수 있습니다.

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

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

아래는 Collections 호출을 통해서 List 객체를 생성할 수 있습니다. 이외에도 정적 팩토리 메서드를 사용해서 map, queue, set 등 다양한 객체를 생성할 수 있습니다.

@Test
void collectionsCreateTest() {
    final String element = "list";
    List<String> stringList = Collections.singletonList(element);
}

Collections.class 에서 제공하는 정적 팩토리 메소드를 통해서 객체를 생성할 수 있습니다. 내부 구현과 상관없이 Collections 이라는 클래스만 알고 있어도 쉽게 생성이 가능합니다.

// Collections.class
public static <T> List<T> singletonList(T o) {
    return new Collections.SingletonList(o);
}

// (Interface) Collection.class
public interface Collection<E> extends Iterable<E> {
    int size();

    boolean isEmpty();

    boolean contains(Object var1);

    Iterator<E> iterator();
    ...
}

// Collections.class
    private static class SingletonList<E> extends AbstractList<E> implements RandomAccess, Serializable {
        private static final long serialVersionUID = 3093736618740652951L;
        private final E element;

        SingletonList(E obj) {
            this.element = obj;
        }

        public Iterator<E> iterator() {
            return Collections.singletonIterator(this.element);
        }

        public int size() {
            return 1;
        }

        public boolean contains(Object obj) {
            return Collections.eq(obj, this.element);
        }
      ...
    }

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

웹 어플리케이션 개발시, DTO에서는 Entity를 형 변환하여 주로 사용합니다. 형 변환을 통해 DTO는 Entity의 불필요한 내용을 제외하고 화면으로 전달하여 줄 수 있습니다.

DTO는 객체안에 생성자를 안으로 숨기면서 내부 상태를 외부에 드러내지 않고 정적 팩토리 메소드를 제공하여 내부 구현과 상관없이 쉽게 변환할 수 있습니다. 개발자가 형 변환을 위해서는 WriteResponse.from(write) 만 알고 있으면 됩니다.

public class WriteResponse {

    private Long id;
    private String title;
    private String content;
    private LocalDateTime created;
    private String createdBy;

    private WriteResponse(final Long id, final String title, final String content, final LocalDateTime created
            , final String createdBy) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.created = created;
        this.createdBy = createdBy;
    }

    public static WriteResponse from(final Write write) {
        return new WriteResponse(write.getId(), write.getTitle(), write.getContent(), write.getCreated(), write.getCreatedBy());
    }
  ...
}

이렇게 생성한 정적 팩토리 메소드는 다음과 같이 사용할 수 있습니다.

// Write 객체
final Write write = Write.initCreate(1L, "title", "content", "lsh");
// WriteResponse 응답 객체
final WriteResponse response = WriteResponse.from(write);

정적 팩터리 메소드 명명 방식

  • from
    • 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드입니다.
    • Date d = Date.from(instant);
  • of
    • 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드입니다.
    • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • valueOf
    • from과 of의 더 자세한 버전입니다.
    • BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE);
  • instance 또는 getInstance
    • (매개변수를 받는다면) 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스를 보장하지는 습니다.
    • StackWalker luke = StackWalker.getInstance(optioins);
  • create 또는 newInstance
    • instance 또는 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장합니다.
    • Object newArray = Array.newInstance(classObject, arrayLen);
  • getType
    • 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용합니다.
    • Type 은 팩터리 메서드가 반환할 객체의 타입입니다.
    • FileStore fs = Files.getFileStore(path);
  • newType
    • 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용합니다.
    • Type 은 팩터리 메서드가 반환할 객체의 타입입니다.
    • BufferedReader br = Files.newBufferedReader(path);
  • type
    • getTypenewType 의 간결한 버전입니다.
    • List<Complaint> litany = Collections.list(legacyLitany);

정리

  • 정적 팩토리 메소드는 생성자의 역할을 대신하면서 좀 더 가독성이 좋은 코드를 작성할 수 있도록 합니다. 핵심적인 특징은 다음과 같습니다.
    • 정적 팩토리 메소도는 이름을 가질 수 있어서 사용하는 사람이 의도를 이해할 수 있습니다.
    • 정적 팩토리 메소드는 객체 생성을 캡슐화할 수 있어서 내부 상태를 외부에 들어내지 않고 외부에 필요한 부분만 제공할 수 있습니다. 이를 사용하는 사람은 내부 상태를 몰라도 됩니다.
  • 객체간 형 변환이 필요하거나, 여러 번의 객체 생성이 필요하면 생성자보다는 정적 팩토리 메서드를 고려해봅시다.

참조

반응형

댓글