@ModelAttribute 데이터 바인딩이 되지 않는 오류

stoph·2022년 12월 11일
1

문제가 발생한 배경

  1. 썸네일 업로드 기능을 구현하기 위해서 application/json 에서
    multipart/formdata 타입으로 변경
  2. 클라이언트에서 FormData 객체에 데이터를 담아서 서버에 요청
  3. DTO 객체에 데이터가 바인딩이 되지 않는 오류 발생

클라이언트 요청 부분

const formData = new FormData();
    formData.append('memberId', memberId);
    formData.append('thumbnail', 'thumbnail');
    formData.append('description', description);
    formData.append('title', title);
    formData.append('content', content);
    formData.append('tags', tags);

    fetch("/post/write", {
        method: 'POST',
        body: formData
        })
        .then((response) => response.text())
        .then((data) => {
            window.location.href = '/post/' + data;
        })
        .catch((error) => {
            console.error('실패', error);
    });

DTO 부분

@NoArgsConstructor
@Getter
public class PostRequestDto {

    private String thumbnail;
    private String description;
    private String title;
    private String content;
    private String tags;

    @Builder
    public PostRequestDto(String thumbnail, String description, String title, String content, String tags) {
        this.thumbnail = thumbnail;
        this.description = description.trim();
        this.title = title.trim();
        this.content = content.trim();
        this.tags = !tags.isBlank() ? PostUtils.tagParsing(tags) : "";
    }

    public Post toPost(Member member) {
        return Post.builder()
                .member(member)
                .thumbnail(this.thumbnail)
                .description(this.description)
                .title(this.title)
                .content(this.content)
                .build();
    }

    public List<Tag> getTagList() {
        if (!tags.isBlank()) {
            return Stream.of(this.tags.split("#"))
                    .filter(s -> s.length() > 0)
                    .map(s -> Tag.builder().tagName(s.toLowerCase()).build())
                    .collect(Collectors.toList());
        } else {
            return new ArrayList<>();
        }
    }
}

검색하기 전에 스스로 원인 찾아보기

  1. 클라이언트에서 Content-Type 헤더에 직접적으로 타입을 명시하여 요청

    • 동일하게 바인딩 실패
  2. @RequestParam 으로 각 파라미터를 직접 받아보기

    • 데이터가 서버까지 정상적으로 전달되는 것 확인

이후에, @ModelAttribute의 데이터 바인딩에 대해서 검색했다.


데이터 바인딩 작동 원리

깊은 코드 레벨까지 이해하기는 어려워서 핵심만 정리

전제 조건

  • public 생성자만 여러 개 작성하거나 그 외 생성자만 여러 개 작성하는 경우에는 디폴트 생성자를 작성해야 한다.

    디폴트 생성자를 찾을 수 없다는 에러가 발생함 (이유는 아직 파악 못함)
  1. 가장 먼저 public 이면서 인자 수가 가장 적은 생성자를 찾는다.

    • public 으로 선언된 생성자가 없는 경우
      public 외에 접근제한자로 선언된 생성자 중에서 인자 수가 가장 적은 생성자를 찾는다.
  2. 1번의 조건에 맞는 생성자를 통해서 새 인스턴스를 생성한다.

    • 기본 생성자가 아닐 경우
      인자 이름과 클라이언트 요청 파라미터 이름이 일치하는 것이 있다면 해당 인자에 파라미터 값을 넣어서 생성
  3. setter 메서드를 통해서 클라이언트에게 받은 파라미터를 각각의 필드에 값을 넣어준다.

    • 생성자 생성을 통해 값을 넣어주는 것과 상관없이 동작
    • setter 메서드가 존재하지 않으면 동작 안함
    • 기본 생성자를 통해 인스턴스를 생성할 때, 데이터를 바인딩 해주기 위해서 존재하는 것

내 코드는 무엇이 문제였나

문제의 원인은 @NoArgsConstructor 때문이었다.

엔티티를 작성할 때 @NoArgsConstructor 를 작성하는 것이 습관이 되어버린 탓에 DTO 에도 남발한 것이 문제였다.

  1. public 생성자 중에 인자 수가 가장 적은 생성자는 디폴트 생성자이고
  2. setter 메서드를 정의하지 않았기 때문에 데이터 바인딩이 이루어지지 않음

느낀 점

Lombok을 너무 남용하지 말자...


참고

https://blog.karsei.pe.kr/59#%EC%--%A-%ED%--%-D%EB%--%-C%--%EC%--%-D%EC%--%B-%EC%-E%--%--%EC%--%-D%EC%--%B-

1개의 댓글

comment-user-thumbnail
2023년 11월 18일

이것 때문에 하루를 날렸는데 감사합니다...

답글 달기