일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- MSA
- Java
- 아키텍처
- 객체지향원칙
- spring boot
- 멀티 모듈
- JWT
- testcode
- Til
- querydls
- docker
- swagger
- aop
- JPA
- Kafka
- Github Actions
- Intellij
- 어노테이션
- EC2
- 프로그래머스
- DevOps
- trouble shooting
- AWS
- CI/CD
- springboot
- Redis
- rabbitmq
- 유효성 검사
- 테스트 코드
- algorithm
- Today
- Total
개발노트
25.02.18 Spring Boot MVC 바인딩의 순서 본문
개요
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을 전송한 예시
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은 컨트롤러의 매개변수를 순차적으로 처리한다.
왼쪽에서 오른쪽 순서로 값을 바인딩하면서 유효성 검사를 수행한다는 의미이다.
나의 경우는
- dto에 대한 요청 본문(json)을 @ReuqestBody로 매핑
- @Valid에 의해 dto에 대핸 유효성 검사 수행
- 여기서 문제가 발생하면 BindingResult가 필요하다.
- @AuthenticationPrincipal PrincipalDetails principalDetails 바인딩 수행
- BindingResult 바인딩 시도 (하지만 이미 MethodArgumentNotValidException 에러가 발생하여 실행되지않음)
BindingResult가 @Valid 바로 뒤에 있어야하는 이유
Spring은 @Valid 나 @Validated가 적용된 매개변수를 검증한 후, 검증 결과를 BindingResult에 저장해야 하는데
BindingResult가 바로 다음 순서에 있어야 이를 연결할수가 있다. 하지만 두 매개변수 사이에 다른 매개변수가 있으면
spring은 BindingResult를 찾을 수 없다고 판단하고 MethodArgumentNotValidException를 발생시킨다.
@Valid와 같은 어노테이션은 유효성 검사를 위한 AOP처럼 동작한다.
BindingResult 자체가 AOP 어노테이션이 없기 때문에 순서 보장이 반드시 필요한것이다.
요약
- @Valid 와 같은 어노테이션은 AOP처럼 동작해서, 메서드 호출 전에 유효성 검사를 수행한다. 검증된 결과는 BindingResult에 저장된다
- BindingResult는 @Valid가 검증한 결과를 받아야 하므로, @Valid 바로 뒤에 위치해야한 결과를 정상적으로 받을 수있다.
'TroubleShooting' 카테고리의 다른 글
25.04.13 Spring Redis hashOps.keys()로 패턴 조회 안 되는 이유와 SCAN을 사용한 해결법 (0) | 2025.04.13 |
---|---|
25.04.10 Jackson LocalDateTime 직렬화 오류 해결 (0) | 2025.04.09 |
2025.03.22 RabbitMQ concurrency 설정과 비관적 락을 활용한 재고 감소 동시성 제어 (0) | 2025.03.25 |
25.03.21 사가 패턴 무한 재시도 방지 (0) | 2025.03.21 |
25.03.17 RabbitMQ message 직렬화 문제 해결 (0) | 2025.03.17 |