(Spring) 취향 기반 향수 추천 서비스 - 생성자 VS 빌더 고민

김준석·2023년 10월 25일
0

향수 추천 서비스

목록 보기
15/21
post-thumbnail

이 프로젝트를 진행하면서 거의 모든 도메인과 DTO에 Builder 패턴을 사용했습니다. 그렇게 한 이유는 오로지 가독성의 측면에서만 바라봤기 때문이었습니다. 하지만, Builder의 사용에 대해 고민을 할 필요성이 있어서 학습을 하였습니다.

Builder패턴이란?

  • 객체 생성을 위한 디자인 패턴 중 하나
  • 메서드 체이닝을 통해 객체를 생성하고, 해당 객체는 대개 불변성을 가집니다.
  • 필드의 모든 매개변수에 대한 각각의 생성자를 자동으로 생성한다고 나와있습니다.

  • 예제 자료를 보면 foo 필드와 bar 필드 모두 생성자가 각각 설정되어 있는 것을 확인할 수 있습니다.

빌더를 택한 이유

가독성 측면

  • 빌더를 적용한 경우
    public FeatureResponseDto showFeatureDetails(Long id) {
        Perfume perfume = perfumeService.findPerfumeById(id);

        return FeatureResponseDto.builder()
                .perfume(perfume)
                .scentFeature(selectScent(id))
                .moodFeature(selectMood(id))
                .seasonFeature(selectSeason(id))
                .maintenanceFeature(selectMaintenance(id))
                .build();
    }
  • 생성자를 사용한 경우
    public FeatureResponseDto showFeatureDetails(Long id) {
        Perfume perfume = perfumeService.findPerfumeById(id);

        return new FeatureResponseDto(selectScent(id), selectMood(id), selectSeason(id), selectMaintenance(id), perfume);
    }

사람마다 가독성의 기준이 다르기 때문에 어떨 지 모르겠으나, 제 기준에서는 빌더가 가독성이 더 좋다고 느껴져서 사용하였습니다.

현재 필드가 5개인데, 더 많을 경우 복잡성은 더 증가할 수 있습니다.
물론 필드가 많아지면 클래스를 분리해야하겠지만요..ㅎㅎ

편리함

생성자는 매개변수의 순서가 보장되어야 합니다.
반대로 빌더는 필드를 설정하는 순서에 구애받지 않기 때문에 원하는 순서로 필드를 생성할 수 있습니다.

위 그림처럼 순서를 제시해주긴 하지만, 필드가 많아질 경우 편의성이 떨어질 수 있습니다.

빌더 패턴의 위험성

    public A(String a, String b, String c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

생성자는 인스턴스를 생성할 때 모든 필드의 값이 다 들어가지 않으면 객체를 생성할 수 없습니다.

반면, 빌더는 선택적으로 값을 주입시킬 수 있습니다.

    public FeatureResponseDto showFeatureDetails(Long id) {
        Perfume perfume = perfumeService.findPerfumeById(id);

        return FeatureResponseDto.builder()
                .perfume(perfume)
                .scentFeature(selectScent(id))
                .moodFeature(selectMood(id))
                .seasonFeature(selectSeason(id))
                .maintenanceFeature(selectMaintenance(id))
                .build();
    }

이 경우를 예시로, perfume값을 임의로 뺀다 하더라도 인스턴스가 생성됩니다. 단, perfume은 null이 됩니다.

의도치 않게 필드를 세팅하지 않아도 인스턴스가 생성된다는 점에서 위험성이 존재합니다.

Trade Off

  • 생성자 -> 가독성이 떨어지고, 편의성이 떨어지지만 위험성이 없다.
  • Builder -> 가독성이 좋고, 편의성이 높지만, 위험성이 존재한다.

사용을 할 때 위와 같은 Trade Off를 고려해봐야 할 것 같습니다.

나는 어떻게 할 것인가?

위험성과 가독성의 사이에서 고민을 하다가, 기준을 세우기로 결정하였습니다.

  • 필드가 3개 이하인 경우
@Entity(name = "token")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Token {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "token_id", nullable = false)
    private Long tokenId;

    @NotNull
    private String refreshToken;

    @NotNull
    private Long memberId;

... 중략
}

필드가 3개 이하일 경우, 생성자를 사용해도 가독성이 떨어지지 않을 것이라고 생각이 들었습니다. 또한 순서가 헷갈리지 않아 불편함도 없을 것이라 느껴졌습니다.

  • 필드가 3개 초과인 경우
@Getter
@EntityListeners(AuditingEntityListener.class)
@Entity(name = "report")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Report {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "report_id", nullable = false)
    private Long reportId;

    @Enumerated(value = EnumType.STRING)
    private ReportType reportType;

    private String description;

    @CreatedDate
    private LocalDateTime reportDate;

    @Enumerated(value = EnumType.STRING)
    private ReportStatus reportStatus;

    @Embedded
    private ReportDetail reportDetail;
    
    ...중략

반면 위와같이 필드가 3개 이상일 경우, 빌더를 사용하는 것이 더 좋을 것 같다는 생각이 들었습니다. 단, null이 발생할 수 있는 위험성이 존재한다는 것을 인지한 채로 사용해야겠다고 느꼈습니다.

자바 라이브러리들은 어떨까?

  • 자바 List의 ImmutableCollections
       ListItr(List<E> list, int size) {
            this.list = list;
            this.size = size;
            this.cursor = 0;
            isListIterator = false;
        }

        ListItr(List<E> list, int size, int index) {
            this.list = list;
            this.size = size;
            this.cursor = index;
            isListIterator = true;
        }
  • 자바 ArrayList
public ArrayList(int initialCapacity) {
        ...
    }
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(Collection<? extends E> c) {
        ...
    }
  • DateTimeFromatterBuilder (builder 사용)
    public static DateTimeFormatter ofLocalizedTime(FormatStyle timeStyle) {
        Objects.requireNonNull(timeStyle, "timeStyle");
        return new DateTimeFormatterBuilder().appendLocalized(null, timeStyle)
                .toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
    }

단언하긴 힘들지만, DateTimeFormatter같은 경우엔 복잡한 객체 생성 과정 문제를 해결하기 위해 Builder를 적용했다고 생각이 듭니다..!
반면 ArrayList 구현체는 비교적 단순한 객체 생성, 성능의 문제로 생성자를 사용했다고 느껴졌습니다.

이 둘을 고민하면서 상황과 목적에 따라 적절한 방법을 채택해야겠다고 느껴졌습니다.!

profile
기록하면서 성장하기!

0개의 댓글