@Setter 를 지우고 빌더 패턴을 적용하면서 아래와 같은 에러들이 발생하였습니다.
org.springframework.messaging.converter.MessageConversionException: Could not read JSON: Cannot construct instance of travelplanner.project.demo.planner.dto.request.CalendarCreateRequest (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])"{"dateTitle":"2023-09-04T15:04:16.920Z"}"; line: 1, column: 2]
오늘도 머리아픈 에러를 만나버린 김시은. @Setter 를 왜 지양하는지부터 짚어보기로 했습니다.
그래서 프로젝트를 진행하던중 @Setter 를 제거하고 빌더패턴을 적용하기로 하였습니다. 어떻게 변화 하였는지 코드로 한번 알아보겠습니다.
아래는 @Data 를 제거하고 빌더패턴으로 작성한 날짜 생성/수정 코드입니다.
public CalendarResponse createDate(Long plannerId, CalendarCreateRequest createRequest, String accessToken) {
Planner planner = validatingService.validatePlannerAndUserAccessForWebSocket(accessToken, plannerId);
Calendar buildRequest = Calendar.builder()
.dateTitle(createRequest.getDateTitle())
.planner(planner)
.createdAt(LocalDateTime.now())
.build();
calendarRepository.save(buildRequest);
List<ToDoResponse> scheduleItemList = toDoService.getScheduleItemList(buildRequest.getId());
return CalendarResponse.builder()
.dateId(buildRequest.getId())
.createAt(buildRequest.getCreatedAt())
.dateTitle(buildRequest.getDateTitle())
.plannerId(plannerId)
.scheduleItemList(scheduleItemList)
.build();
}
public CalendarResponse updateDate(Long plannerId, Long updateId, CalendarEditRequest updateRequest, String accessToken) {
Planner planner = validatingService.validatePlannerAndUserAccessForWebSocket(accessToken, plannerId);
Calendar calendar = validatingService.validateCalendarAccess(planner, updateId);
CalendarEditor.CalendarEditorBuilder editorBuilder = calendar.toEditor();
CalendarEditor calendarEditor = editorBuilder
.dateTitle(updateRequest.getDateTitle())
.build();
calendar.edit(calendarEditor);
calendarRepository.updatedateTitle(updateId, updateRequest.getDateTitle());
List<ToDoResponse> scheduleItemList = toDoService.getScheduleItemList(calendar.getId());
return CalendarResponse.builder()
.dateId(calendar.getId())
.createAt(calendar.getCreatedAt())
.dateTitle(calendar.getDateTitle())
.plannerId(plannerId)
.scheduleItemList(scheduleItemList)
.build();
}
이렇게 코드를 작성하므로써 OCP(Open-Closed Principle) 의 원리를 지킬 수 있습니다. 또한 수정에관한 dto 를 따로 생성하여 생성과 수정을 코드만 보고 분리할 수 있습니다.
@Builder 는 기본적으로 빌더 어노테이션이 적용된 전체 필드에 대한 값을 요구합니다. 때문에 생성자가 반드시 필요합니다. 이때의 생성자는 매개변수가 있는 생성자 이며 매개변수가 있는 생성자가 없다면 자동으로 만들어 줍니다. 이때 @AllArgsConstructor 를 사용하여 직접 지정할 수도 있습니다.
@Entity 의 경우 매개변수가 없는 기본 생성자를 필요로 하며, 없다면 자동으로 만들어 줍니다. 하지만 빌더와 마찬가지로 @NoArgsConstructor 를 사용하여 직접 지정할 수 있습니다.
만약 @Entity 와 @Builder 만 사용한다면 어떻게 될까요? 는 에서 자동으로 만든 생성자 때문에 생성자가 이미 만들어졌다고 판단하고 에서 만든 기본 생성자 때문에 생성자가 이미 만들어졌다고 판단해 충돌이 발생하게 됩니다. 이때문에 @NoArgsConstructor@AllArgsConstructor 까지 적어 각각의 생성자를 직접 지정해줘야 합니다.
Jackson은 Java에서 객체를 JSON 문자열을 변환(역직렬화)하거나 JSON 문자열을 객체로 변환(직렬화)하는 기능을 제공하는 라이브러리 입니다. 즉, @RequestBody 로 프론트에서 값을 받아오면 dto 와 바인딩을 해주는 역할을 합니다. 이 라이브러리는 JSON 을 어떻게 객체로 변환하는 걸까요?
때문에 기본생성자가 없다면 초장부터 길을 잃어버리고 맙니다. 🥲
org.springframework.messaging.converter.MessageConversionException: Could not read JSON: Cannot construct instance of travelplanner.project.demo.planner.dto.request.CalendarCreateRequest (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: (byte[])"{"dateTitle":"2023-09-04T15:04:16.920Z"}"; line: 1, column: 2]
다시 천천히 살펴보면 문제는 명확했습니다. 기본생성자가 없어서 직렬화를 할 수 없다고 합니다.
해당 리퀘스트를 찾아가보니 아래와 같이 어노테이션이 달려있었습니다.
@Getter
@Builder
@AllArgsConstructor
public class PlannerDeleteRequest {
이를 아래와 같이 수정해주니 잘 해결되었습니다.
@Getter
@Builder
@AllArgsConstructor
@NoArgsconstructor
public class PlannerDeleteRequest {
Controller 에서 응답을 전달하는 DTO 에 기본 생성자가 없었기에 나타난 문제였습니다. 때문에 이는 간단히 기본생성자를 추가함으로써 해결할 수 있습니다. 에러로그가 상세하게 나오기 때문에 쉽게 수정방법은 알 수 있었으나, 과정을 알아야 하므로 어노테이션을 정리해보는 시간을 가져봤습니다. 🤭