기능 구현을 위해 5개의 조건이, 기본값 처리 로직을 위해 6개의 파라미터가 필요
hashtag 필드가 없다면 요청자의 username으로,통계 시작일이 없다면 오늘 기준 일주일 전,통계 종료일이 없다면 오늘로 초기화 필요Builder와 삼항연산자를 사용하고, 컨트롤러에서 예외처리했다.
@GetMapping("/statistics")
public Map<String, Integer> getStatistics(
@AuthenticationPrincipal UserDetails UserDetail,
@RequestParam(value = "hashtag") String hashtag,
@RequestParam(value = "type") String type,
@RequestParam(value = "start", required = false) LocalDate start,
@RequestParam(value = "end", required = false) LocalDate end,
@RequestParam(value = "value", defaultValue = "count") String value
) {
String username = userDetail.getUsername();
StatisticsQuery query = StatisticsQuery.builder()
.hashtag(hashtag == null ? username : hashtag)
.type(StatisticType.from(type))
.start(start == null ? LocalDate.now().minusDays(7) : start)
.end(end == null ? LocalDate.now() : end)
.value(StatisticValue.from(value))
.build();
if(query.getEnd().isBefore(query.getStart())) {
throw new IllegalArgumentException("검색 시작일이 종료일보다 미래여선 안됨");
}
// 이하 예외처리 생략
return statisticService.getStatistic(query);
}
}
이 코드는 마음에 들지 않았다.
컨트롤러에 DTO 생성 로직, 비즈니스 로직이 너무 섞여 있는 느낌이었기 때문이다.
1) 우선 여섯 개의 파라미터를 서비스로 넘기는 게 깔끔하지 않고,
2) 컨트롤러-서비스 간 DTO를 사용한다고 해도 불완전한 DTO 객체를 생성하는 게 찜찜했다.
3) 게다가 유효하지 않은 요청이라면, 서비스 레이어까지 갈 이유가 없다.
유효한 DTO 객체만 생성하고 싶다!
유효하지 않은 요청 데이터라면 최대한 빨리 쫓아내고 싶다!
이에 정적 팩토리 메소드를 사용하기로 했다.
public static StatisticRequest createWithDefaults(
String hashtag,
String type,
LocalDateTime start,
LocalDateTime end,
String value,
String email
) {
// 기본값 설정
String finalHashTag = (hashtag == null) ? email : hashtag;
StatisticType finalType = StatisticType.from(type);
LocalDateTime finalStart = (start == null) ? LocalDateTime.now().minusDays(7) : start;
LocalDateTime finalEnd = (end == null) ? LocalDateTime.now() : end;
StatisticValue finalValue = StatisticValue.from(value);
// 통계 시작일, 종료일 유효성 검사
// 2024-08-26, 2024-08-27이 제공될 때, 1을 반환
long daysBetween = ChronoUnit.DAYS.between(finalStart, finalEnd);
if(daysBetween < 0) {
// 시작일이 종료일보다 미래
throw new IllegalArgumentException(START_AFTER_END);
} else if(finalType == StatisticType.DATE && daysBetween >= 30) {
// 일별 통계 시 30일 초과 불가
throw new IllegalArgumentException(TOO_LONG_DATE_QUERY);
} else if(finalType == StatisticType.HOUR && daysBetween >= 7) {
// 시간별 통계 시 7일 초과 불가
throw new IllegalArgumentException(TOO_LONG_HOUR_QUERY);
}
// 유효한 `StatisticRequest` 객체를 생성하여 반환
return new StatisticRequest(finalHashTag, finalType, finalStart, finalEnd, finalValue);
}
생성자는 private으로 설정하여 정적 팩토리 메서드를 통해서만 객체를 만들 수 있도록 구현했다.
물론 같은 동작을 하는 생성자를 만들어 해결할 수도 있다.
하지만 위의 메소드와 같은 파라미터를 사용하는 생성자를 통해 DTO를 생성한다면, 그 생성자에 기본값 처리 로직이 있는지, 예외 처리 로직이 존재하는지 알리기 어렵다고 판단했다.
반면, 정적 팩토리 메소드는 이름을 가질 수 있다.
나의 경우 createWithDefaults라는 이름으로 기본값(과 예외처리) 로직이 포함되어 있음을 나타냈다.
1) 상속 불가
실컷 객체 초기화 로직을 만들어놓고 생성자를 무분별하게 개방해버리면 의미가 없으니까 생성자를 private으로 제한해야한다.
이 경우, 하위 클래스를 만들 수 없다는 단점이 생긴다.
하지만 객체 재사용이 필요하다면 포함 관계로 얼마든지 해결할 수 있을 것이다.
2) 프로그래머가 찾기 어렵다
IntelliJ가 잘 찾으니까 괜찮다.