@Setter 와 @Builder 그리고 Jackson 에러

시은·2024년 2월 18일
0
post-thumbnail

문제의 시작

@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 를 왜 지양하는 걸까? 🤷🏻‍♀️

  • @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();
    }

날짜 수정

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 에 대한 이해 높이기

@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]

다시 천천히 살펴보면 문제는 명확했습니다. 기본생성자가 없어서 직렬화를 할 수 없다고 합니다.

해당 리퀘스트를 찾아가보니 아래와 같이 어노테이션이 달려있었습니다.

@Getter
@Builder
@AllArgsConstructor
public class PlannerDeleteRequest {

이를 아래와 같이 수정해주니 잘 해결되었습니다.

@Getter
@Builder
@AllArgsConstructor
@NoArgsconstructor
public class PlannerDeleteRequest {

결론

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

도움이 되었던 블로그

[Spring] @RequestBody에 기본생성자만 필요하고 Setter는 필요없는 이유 - 1

[Spring] @RequestBody가 빈 생성자가 필요한 이유 (hint. ObjectMapper)

profile
창의력 대장이 되기 위한 여정

0개의 댓글