이 프로젝트를 진행하면서 거의 모든 도메인과 DTO에 Builder 패턴을 사용했습니다. 그렇게 한 이유는 오로지 가독성의 측면에서만 바라봤기 때문이었습니다. 하지만, Builder의 사용에 대해 고민을 할 필요성이 있어서 학습을 하였습니다.
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를 고려해봐야 할 것 같습니다.
위험성과 가독성의 사이에서 고민을 하다가, 기준을 세우기로 결정하였습니다.
@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개 이하일 경우, 생성자를 사용해도 가독성이 떨어지지 않을 것이라고 생각이 들었습니다. 또한 순서가 헷갈리지 않아 불편함도 없을 것이라 느껴졌습니다.
@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이 발생할 수 있는 위험성이 존재한다는 것을 인지한 채로 사용해야겠다고 느꼈습니다.
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;
}
public ArrayList(int initialCapacity) {
...
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
...
}
public static DateTimeFormatter ofLocalizedTime(FormatStyle timeStyle) {
Objects.requireNonNull(timeStyle, "timeStyle");
return new DateTimeFormatterBuilder().appendLocalized(null, timeStyle)
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
}
단언하긴 힘들지만, DateTimeFormatter같은 경우엔 복잡한 객체 생성 과정 문제를 해결하기 위해 Builder를 적용했다고 생각이 듭니다..!
반면 ArrayList 구현체는 비교적 단순한 객체 생성, 성능의 문제로 생성자를 사용했다고 느껴졌습니다.
이 둘을 고민하면서 상황과 목적에 따라 적절한 방법을 채택해야겠다고 느껴졌습니다.!