개발노트

25.02.18 Spring Boot MVC 바인딩의 순서 본문

TroubleShooting

25.02.18 Spring Boot MVC 바인딩의 순서

ddong-kka 2025. 2. 18. 22:42

개요

validation이 정상적으로 동작해야하는데 정상 동작하지않는 문제가 발생했다.

동일한 코드를 UserController에서는 동작하지만 DeliveryAddressController에서는 동작하지않는 아이러니한 상황이다.

import 도 전부 동일한데 왜 DeliveryAddressController에서는 validation이 에러가 발생한걸까?

 

문제점

 

controller

  @PostMapping
    public ResponseEntity<?> addAddress(
                                        @Valid @RequestBody AddressReqDto addressReqDto,
                                        @AuthenticationPrincipal PrincipalDetails principalDetails,
                                        BindingResult bindingResult ){

        if (bindingResult.hasErrors()){
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body(ValidationErrorResponse(bindingResult));
        }

        return ResponseEntity.status(HttpStatus.OK)
                .body(deliveryAddressService.addAddress(addressReqDto, principalDetails));
    }

    private Map<String, Object> ValidationErrorResponse(BindingResult bindingResult) {
        List<Map<String, String>> errors = bindingResult.getFieldErrors().stream()
                .map(fieldError -> Map.of(
                        "field", fieldError.getField(),
                        "message", fieldError.getDefaultMessage(),
                        "rejectedValue", String.valueOf(fieldError.getRejectedValue()) // 입력된 값도 포함
                ))
                .toList();

        return Map.of(
                "status", 400,
                "error", "Validation Field",
                "message", errors
        );
    }

 

 

DTO

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class AddressReqDto {

    @NotBlank(message = "test")
    private String deliveryAddress; //배송지명

    @NotBlank(message = "test")
    private String deliveryAddressInfo; // 배송지 주소

    private String detailAddress; // 배송지 상세 주소 (필수 아님)
}

 

 

ValidationErrorResponse 는 usercontroller에서도 테스트까지 마친 코드이다. 정상적으로 동작한다.

컴파일 될때는 문제가 없는 상황이다. 진짜 문제는 하나의 json 값을 누락시키고 dto에 전달했을 때 

@Valid 어노테이션이 있기에 유효성 검사가 동작해야한다. 당연히 동작할거라 생각한 테스트에서 이런 에러가
발생하였다.

 

deliveryAddress 를 제외하고 json을 전송한 예시

{
    "deliveryAddressInfo" : "대구 남산동",
    "detailAddress" : "할맥 골목"
}

 

Validation failed for argument [0] in public org.springframework.http.ResponseEntity<?> com.sparta.delivery.domain.delivery_address.controller.DeliveryAddressController.addAddress(com.sparta.delivery.domain.delivery_address.dto.AddressReqDto,com.sparta.delivery.config.auth.PrincipalDetails,org.springframework.validation.BindingResult): [Field error in object 'addressReqDto' on field 'deliveryAddress': rejected value [null]; codes [NotBlank.addressReqDto.deliveryAddress,NotBlank.deliveryAddress,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [addressReqDto.deliveryAddress,deliveryAddress]; arguments []; default message [deliveryAddress]]; default message [test]]

 

입력 검증이 되질않았다. dto 에서 필요한 데이터에 @NotBlank 어노테이션과 message 를 작성해두었음에 필드 자체가 null 로 인식되어 에러가 반환된것이다. 정상적으로 동작했으면 당연히 message가 반환되어야한다.

또 반환된 에러와 메시지가 BindingResult 에 담겨야하지만 컨트롤러 자체가 동작 하지않는다.

 

UserController에서사용하는 코드와 동일한 코드인데 다르게 동작하는것이 이해가 안되었다.

spring security 의 설정 문제 때문인가 싶어서 확인했지만 그것도 아니었고 JWT filter의 문제인가 싶었지만 그것도 아니다.

늦은 시간이였지만 튜터님께 슬랙으로 요청해 도움을 청했고 정말 감사하게도 응해주셨다.

 

에러의 이유

정말 허무하게도 컨트롤러 매개변수의 순서 문제였다. ͡ ͜ʖ ͡ ╭∩╮

Spring MVC의 검증 흐름이 문제의 핵심이다.

 

Spring MVC의 메서드 매개변수 바인딩 순서

Spring은 컨트롤러의 매개변수를 순차적으로 처리한다.

왼쪽에서 오른쪽 순서로 값을 바인딩하면서 유효성 검사를 수행한다는 의미이다.

나의 경우는

  1. dto에 대한 요청 본문(json)을 @ReuqestBody로 매핑
  2. @Valid에 의해 dto에 대핸 유효성 검사 수행
    1. 여기서 문제가 발생하면 BindingResult가 필요하다. 
  3. @AuthenticationPrincipal PrincipalDetails principalDetails 바인딩 수행
  4. BindingResult 바인딩 시도 (하지만 이미 MethodArgumentNotValidException  에러가 발생하여 실행되지않음)

 

BindingResult가 @Valid 바로 뒤에 있어야하는 이유

Spring은 @Valid 나 @Validated가 적용된 매개변수를 검증한 후, 검증 결과를 BindingResult에 저장해야 하는데

BindingResult가 바로 다음 순서에 있어야 이를 연결할수가 있다. 하지만 두 매개변수 사이에 다른 매개변수가 있으면 

spring은 BindingResult를 찾을 수 없다고 판단하고 MethodArgumentNotValidException를 발생시킨다.

 

@Valid와 같은 어노테이션은 유효성 검사를 위한 AOP처럼 동작한다. 

BindingResult 자체가 AOP 어노테이션이 없기 때문에 순서 보장이 반드시 필요한것이다.

 

 

요약

  1. @Valid 와 같은 어노테이션은 AOP처럼 동작해서, 메서드 호출 전에 유효성 검사를 수행한다. 검증된 결과는 BindingResult에 저장된다
  2. BindingResult는 @Valid가 검증한 결과를 받아야 하므로, @Valid 바로 뒤에 위치해야한 결과를 정상적으로 받을 수있다.