Search
⚠️

@Setter 와 @Builder 그리고 Jackson 에러

태그
데이터 바인딩
분류
Spring Boot
목차

문제의 시작

@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]
Plain Text
복사

@Setter 를 왜 지양하는 걸까?

@Setter 를 사용하면 해당 코드가 업데이트인지 추가인지 알기 어렵습니다.
변경하면 안되는 중요한 값임에도 불구하고 변경 가능한 값으로 착각할 수 있습니다. 즉, 안정성을 보장할 수 없습니다.
OCP(Open-Closed Principle) 의 원리를 지킬 수 업습니다. OCP 는 확장에 대해서는 열려있어야하고, 수정에 대해서는 닫혀있어야 한다는 원리입니다.
그래서 프로젝트를 진행하던중 @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(); }
Java
복사

날짜 수정

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(); }
Java
복사
이렇게 코드를 작성하므로써 OCP(Open-Closed Principle) 의 원리를 지킬 수 있습니다. 또한 수정에관한 dto 를 따로 생성하여 생성과 수정을 코드만 보고 분리할 수 있습니다.

@Builder 에 대한 이해 높이기

@Builder

@Builder 는 기본적으로 빌더 어노테이션이 적용된 전체 필드에 대한 값을 요구합니다. 때문에 생성자가 반드시 필요합니다. 이때의 생성자는 매개변수가 있는 생성자 이며 매개변수가 있는 생성자가 없다면 자동으로 만들어 줍니다. 이때 @AllArgsConstructor 를 사용하여 직접 지정할 수도 있습니다.

@Entity

@Entity 의 경우 매개변수가 없는 기본 생성자를 필요로 하며, 없다면 자동으로 만들어 줍니다. 하지만 빌더와 마찬가지로 @NoArgsConstructor 를 사용하여 직접 지정할 수 있습니다.
만약 @Entity 와 @Builder 만 사용한다면 어떻게 될까요? 는 에서 자동으로 만든 생성자 때문에 생성자가 이미 만들어졌다고 판단하고 에서 만든 기본 생성자 때문에 생성자가 이미 만들어졌다고 판단해 충돌이 발생하게 됩니다. 이때문에 @NoArgsConstructor@AllArgsConstructor 까지 적어 각각의 생성자를 직접 지정해줘야 합니다.

Jackson 라이브러리

Jackson은 Java에서 객체를 JSON 문자열을 변환(역직렬화)하거나 JSON 문자열을 객체로 변환(직렬화)하는 기능을 제공하는 라이브러리 입니다. 즉, @RequestBody 로 프론트에서 값을 받아오면 dto 와 바인딩을 해주는 역할을 합니다. 이 라이브러리는 JSON 을 어떻게 객체로 변환하는 걸까요?
1.
ObjectMapper 와 기본생성자를 사용하여 객체를 생성합니다.
2.
Setter 혹은 Getter 를 이용하여 필드를 인식합니다.
3.
객체의 필드에 해당하는 값을 넣습니다.
때문에 기본생성자가 없다면 초장부터 길을 잃어버리고 맙니다.

문제 다시 뜯어보기

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]
Plain Text
복사
다시 천천히 살펴보면 문제는 명확했습니다. 기본 생성자가 없어서 직렬화를 할 수 없다고 합니다.
해당 리퀘스트를 찾아가보니 아래와 같이 어노테이션이 달려있었습니다.
@Getter @Builder @AllArgsConstructor public class PlannerDeleteRequest {
Java
복사
이를 아래와 같이 수정해주니 잘 해결되었습니다.
@Getter @Builder @AllArgsConstructor @NoArgsconstructor public class PlannerDeleteRequest {
Java
복사

결론

Controller 에서 응답을 전달하는 DTO 에 기본 생성자가 없었기에 나타난 문제였습니다. 때문에 이는 간단히 기본생성자를 추가함으로써 해결할 수 있었습니다. 에러로그가 상세하게 나오기 때문에 쉽게 수정방법은 알 수 있었으나, 과정을 알아야 하므로 annotation 을 정리해보는 시간을 가져보았습니다.