Spring 기초/Spring 기초 지식

[Spring] @ModelAttribute 바인딩 실패와 해결

Stark97 2024. 2. 1. 02:21
반응형
 
 
 

@ModelAttribute에서 데이터 바인딩을 위해서  꼭 필요한 메서드가 존재한다.

📌 서론

컨트롤러에서 매게 변수로 dto객체를 받을 때 발생하는 바인딩 오류는 정말 간단하게 해결되지만 자주 발생하는 오류이기도 하다. 그만큼 바인딩에 대해서는 신경 쓰지 않고 알아서 적용되겠지?라는 생각을 가지고 어노테이션을 사용하게 되기 때문인데 수많은 api를 만들어 보게 되지만 바인딩도 @RequestBody인지 @ModelAttribute인지에 따라 그 방식이 다르다. 이번에는 @ModelAttribute를 사용했을 때 나의 실수로 인해 바인딩에 실패했던 내용을 소개하고자 한다.

 

1. 오류가 발생한 상황

오류가 발생한 코드 분석

  • 아래의 API에 요청을 보냈을 때 바인딩 오류가 발생했다.
/**  
 * 마이페이지 수정  
 */  
@PostMapping("/member/myPage/update")  
public ResponseEntity<ResponseDto<Void>> update(
        @Valid
        @ModelAttribute UpdateMyPageRequestDto dto
) {  
    MultipartFile profileImage = dto.getProfileImage();  
    myPageUseCase.updateMyPage(myPageConverter.updateRequestDtoToDomain(dto), profileImage);  
    return ResponseEntity.ok(  
            ResponseDto.success()  
    );  
}

이 API가 요청으로 받는 dto는 다음과 같이 작성했다.

@ToString  
@Getter  
@NoArgsConstructor  
public class UpdateMyPageRequestDto {  

    @NotBlank  
    private String nickname;        // 닉네임  

    @Size(max = 300)  
    private String introduction;    // 한줄소개  
    private MultipartFile profileImage; // 프로필 이미지  
    private Integer deleteFileOrder;       // 삭제할 file order  
    private String birth;  
    private String gender;
}

안드로이드에서는 아래의 인터페이스로 retrofit 요청을 보냈다.

interface MyPageService {  

    // 프로필 수정  
    @Multipart  
    @POST("/member/myPage/update")  
    suspend fun updateProfile(  
        @Part("nickname") nickname: RequestBody,  
        @Part("introduction") introduction: RequestBody?,  
        @Part profileImage: MultipartBody.Part?,  
        @Part("deleteFileOrder") deleteFileOrder: RequestBody?,  
        @Part("birth") birth: RequestBody?,  
        @Part("gender") gender: RequestBody?  
    ): Response<ResponseDto<Void>>

여기까지의 내용만 봤을 때 무엇 때문에 바인딩 오류가 발생했는지 알겠는가? 지금부터 발생한 오류를 분석해 보자

 

2. 오류 분석

오류 로그 분석

  • 지금 api에 요청을 보내면 오류가 발생한다. 오류 로그에는 아래와 같은 정보가 남아있었다.
MethodArgumentNotValidException occurred

Validation failed for argument [0] in public org.springframework.http.ResponseEntity<com.recipia.member.adapter.in.web.dto.response.ResponseDto<java.lang.Void>> 
[Field error in object 'updateMyPageRequestDto' on field 'nickname': rejected value [null]; codes
arguments []; default message [nickname]]; default message [공백일 수 없습니다]]

닉네임은 공백일 수 없다는 @Valid 에러 발생

  • 이것만 봐서는 @Valid 오류라고 생각할 수 있지만 근본적인 문제는 nickname을 세팅하지 못하는 바인딩 오류다.

에러 로그 분석
에러 로그 분석

요청 디버깅 진행

  • 이 오류를 파악하기 위해 내가 정말 nickname을 보내지 않았는지 안드로이드에서 요청을 보내면서 디버깅을 해봤는데 닉네임과 다른 데이터를 문제없이 잘 보내고 있다는 것을 알 수 있었다.

안드로이드 디버깅
안드로이드 디버깅

📌 오류 분석 결과

안드로이드에서 데이터를 제대로 보내는 것으로 봐선 스프링에서 @ModelAttribute를 사용하여 멀티파트 요청 데이터를 바인딩할 때 발생한 문제인 것으로 보인다.

 

3. 오류 해결

오류 해결

  • @ModelAttribute에서 발생한 오류라고 판단한 직후 바로 Dto객체를 확인했다. Dto에는 @Getter 어노테이션만이 사용되고 있었는데 @ModelAttribute는 @Setter가 필요하다는 것을 순간 놓쳤던 내 잘못이었다.

  • 이 오류를 해결하기 위해 dto 클래스에 @Setter를 포함하는 Lombok의 @Data를 적어줬더니 안드로이드에서 요청으로 보낸 데이터를 잘 바인딩하는 것을 확인할 수 있었다.
/**  
 * 마이페이지 수정 요청을 담당하는 request dto  
 */@ToString  
@Data  
@NoArgsConstructor  
public class UpdateMyPageRequestDto {  

    @NotBlank  
    private String nickname;        // 닉네임  

    @Size(max = 300)  
    private String introduction;    // 한줄소개  
    private MultipartFile profileImage; // 프로필 이미지  
    private Integer deleteFileOrder;       // 삭제할 file order  
    private String birth;  
    private String gender;
}

 

4. 마무리

결론

  • 에러 메시지를 다시 한번 보면 nickname 필드에 대한 바인딩 실패가 언급되었기 때문에, 이는 nickname 필드에 값을 설정할 세터 메서드가 없어서 발생한 것이라는 것을 알 수 있다.

  • 처음에는 @Valid의 오류라고 생각해서 조금 헷갈렸는데 @NotBlank는 단지 nickname 필드에 값이 성공적으로 바인딩된 후, 그 값이 빈 문자열이 아닌지 검증하는 과정에서 사용되는 것이지, 바인딩 과정 자체와는 직접적인 관련이 없다.

  • 따라서, 문제의 핵심은 @ModelAttribute가 객체에 데이터를 바인딩하는 과정에서 @Setter 메서드가 필요하다는 것이고, nickname 값이 null로 나타나는 것은 nickname 필드에 데이터를 설정할 세터 메서드가 없기 때문에 발생한 바인딩 문제였던 것이다.
반응형