[Spring/Kotlin] form data array 바인딩 실패
view에서 multipart를 첨부하려고 form 데이터를 보내다가 에러를 마주했다.
{
"hashtagText": "",
"postTags[0].postTagSeq": "0",
"postTags[0].atrcTagSeq": "1142",
"postTags[0].checked": "false",
"postTags[1].postTagSeq": "0",
"postTags[1].atrcTagSeq": "1143",
"postTags[1].checked": "false",
"postTags[2].postTagSeq": "1020",
"postTags[2].atrcTagSeq": "1144",
"postTags[2].checked": "true",
"postTags[3].postTagSeq": "1021",
"postTags[3].atrcTagSeq": "1145",
"postTags[3].checked": "true",
"postTags[4].postTagSeq": "0",
"postTags[4].atrcTagSeq": "1146",
"postTags[4].checked": "false",
"postTags[5].postTagSeq": "1022",
"postTags[5].atrcTagSeq": "1147",
"postTags[5].checked": "true",
"postTitle": "숨이 막혀 메이데이~!~",
"postText": "따따따따따",
"attachment": []
}
백엔드에서 데이터를 받기 위한 DTO를 다음과 같이 설정했다.
data class postUpdateReq(
@Schema(defaultValue = "상태", required = true) val status: Status,
@Schema(description = "어드민 ID", required = true) var adminId: String?,
@Schema(description = "포스트 태그") val postTags: List<postTagReq>?,
@Schema(description = "첨부파일") val attachment: MultipartFile?,
)
data class postTagReq(
@Schema(description = "포스트 태그 번호") val postTagSeq: Long,
@Schema(description = "상위게시글 태그 번호") val upperPostTagSeq: Long,
@Schema(description = "포스트 체크 여부") val checked: Boolean,
)
보면 알다시피 List DTO를 선언했다.
그리고 컨트롤러 단에는 평소에 많이 사용하는 @RequestBodty 대신 @ModelAttribute 로 매핑하였다.
@PutMapping("/{postSeq}")
@Operation(summary = "포스트 수정", description = "포스트를 수정한다.")
fun updatepost(
@PathVariable postSeq: Long,
@ModelAttribute req: postUpdateReq
): BaseResponse<postDetailRes> {
...
}
이렇게 설정하면 된다고 생각하고 호출을 보냈는데 에러가 발생했다.
org.springframework.beans.InvalidPropertyException: Invalid property 'postTags[0]' of bean class [core.models.dto.postUpdateReq]: Illegal attempt to get property 'postTags' threw exception
Caused by: java.lang.IllegalStateException: Default value must not be null
default value가 null 이라니..?? 난 파라미터를 정확히 세팅해서 보냈는데 왜 null로 세팅이 됐을까.
@ModelAttribute 는 값을 객체로 바인딩할 때 프로퍼티 접근법을 사용한다. 프로퍼티 접근법에서는 해당 객체를 기본 생성자로 생성하고 setter를 사용하여 파라미터로 전달한 값을 객체에 주입한다.
코틀린 data class에서 val 로 선언하면 getter만 생성되고 setter가 생성되지 않는다. 그래서 바인딩이 되지 않았던 것이다.
data class postUpdateReq(
@Schema(defaultValue = "상태", required = true) val status: Status,
@Schema(description = "어드민 ID", required = true) var adminId: String?,
@Schema(description = "포스트 태그") var postTags: List<postTagReq>?,
@Schema(description = "첨부파일") val attachment: MultipartFile?,
)
그래서 위와 같이 라이드 태그를 var 로 선언하였으나 또 에러가 발생했다.
org.springframework.beans.NullValueInNestedPathException: Invalid property 'postTags' of bean class [core.models.dto.postUpdateReq]: Could not instantiate property type [core.models.dto.postTagReq] to auto-grow nested property path
org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:167)
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:153)
Caused by: java.lang.NoSuchMethodException: core.models.dto.postTagReq.<init>()
at java.base/java.lang.Class.getConstructor0(Class.java:3761)
at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2930)
at org.springframework.beans.AbstractNestablePropertyAccessor.newValue(AbstractNestablePropertyAccessor.java:925)
... 55 more
List로 들어갈 DTO의 기본생성자가 없기 때문이다. 그래서 아래와 같이 수정해야 한다.
data class postTagReq(
@Schema(description = "포스트 태그 번호") val postTagSeq: Long = 0L,
@Schema(description = "상위게시글 태그 번호") val upperPostTagSeq: Long = 0L,
@Schema(description = "포스트 체크 여부") val checked: Boolean? = null,
)
아니면 constructor 를 선언해도 된다.
물론 multipart 파일은 @RequestPart 으로 세팅하고 dto를 @RequestBody 데이터를 보내는 방법도 있다.
그러면 form에 선언한 파라미터를 한꺼번에 보내지 못하고 자바스크립트 조작을 해야한다.
프론트에서 Content-type은 한가지만 설정할 수 있기 때문이다.
무엇보다 이 원인을 해결하지 못하고 우회하는 것 같아 사용하지 않았다.