Validation
📌 특정 데이터(주로 클라이언트의 요청 데이터)의 값이 유효한지 확인하는 단계를 의미한다.
- Controller의 주요한 역할 중 하나는 Validation 이다. HTTP 요청이 정상인지 검증한다.
Validation을 사용하는 이유
- 주문서 작성 페이지에서 잘못된 입력값으로 인해 서버에 오류가 발생한다면?
- ex) 휴대폰 번호에 숫자가 아닌 문자가 들어간 경우
- 서버의 문제로 인해 작성 페이지에서 Error 페이지로 이동된다면?
- Error 페이지로 이동되어 작성중인 폼이 모두 리셋되어 다시 작성해야 한다면?
- 이러한 서비스의 유저는 굉장한 불편함을 겪게된다.
Validation의 역할
- 검증을 통해 적절한 메세지를 유저에게 보여주어야 한다.
- 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 한다.
- 사용자가 입력한 데이터는 유지된 상태여야 한다.
실제 서버를 운영하다보면 기능의 의도와 다른 다양한 사용 방법들을 보게된다. ex) Enter로 입력이 완료되도록 만들었지만 누군가는 Click, Tab + Enter를 누르듯 |
검증의 종류
- 프론트 검증
- 해당 검증은 유저가 조작할 수 있음으로 보안에 취약하다.
- 보안에 취약하지만 그럼에도 꼭 필요하다
- ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 가능 → 유저 사용성 증가
- 서버 검증
- 프론트 검증없이 서버에서만 검증한다면 유저 사용성이 떨어진다.
- API Spec을 정의해서 Validation 오류를 Response 예시에 남겨주어야 한다.
- API 명세서를 잘 만들어야 그에 맞는 대응을 할 수 있다.
- 서버 검증은 선택이 아닌 필수이다.
- 데이터베이스 검증
- Not Null, Default와 같은 제약조건을 설정한다.
- 최종 방어선의 역할을 수행한다.
기본적으로 프론트, 서버, 데이터베이스 모두 검증을 꼼꼼하게 하는것이 바람직하다. Validation으로 수많은 Error와 문제들을 방지할 수 있다. |
BindingResult
📌 Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체이다. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관한다.
- Errors 인터페이스를 상속받은 인터페이스이다.
- Errors 인터페이스는 에러의 저장과 조회 기능을 제공한다.
- BindingResult는 addError() 와 같은 추가적인 기능을 제공한다.
- Spring이 기본적으로 사용하는 구현체( 특정 인터페이스나 추상 클래스를 실질적으로 구현한 클래스 )는 BeanPropertyBindingResult 이다.
파라미터에 BindingResult가 없는 경우
@Data
public class MemberCreateRequestDto {
private Long point;
private String name;
private Integer age;
}
// View 반환
@Controller
public class BingdingResultController {
@PostMapping("/v1/member")
public String createMemberV1(@ModelAttribute MemberCreateRequestDto request, Model model) {
// Model에 저장
System.out.println("/V1/member API가 호출되었습니다.");
model.addAttribute("point", request.getPoint());
model.addAttribute("name", request.getName());
model.addAttribute("age", request.getAge());
// Thymeleaf Template Engine View Name
return "complete";
}
}
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Member 생성이 완료되었습니다!</h1>
<ul>
<li><span th:text="${point}">포인트</span></li>
<li><span th:text="${name}">이름</span></li>
<li><span th:text="${age}">나이</span></li>
</ul>
</body>
</html>
파라미터에 BindingResult가 있는 경우
@Controller
public class BindingResultController {
@PostMapping("/v2/member")
public String createMemberV2(
// 1. @ModelAttribute 뒤에 2. BindingResult가 위치한다.
@ModelAttribute MemberCreateRequestDto request,
BindingResult bindingResult,
Model model
) {
System.out.println("/V2/member API가 호출되었습니다.");
// BindingResult의 에러 출력
List<ObjectError> allErrors = bindingResult.getAllErrors();
System.out.println("allErrors = " + allErrors);
// Model에 저장
model.addAttribute("point", request.getPoint());
model.addAttribute("name", request.getName());
model.addAttribute("age", request.getAge());
return "complete";
}
}
@ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다. 파라미터에 Binding Result가 함께 있는 경우 만약 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출된다. |
Bean Validation
📌 특정 필드 검증의 경우 빈값, 길이, 크기, 형식 과 같은 간단한 로직이다. 이러한 로직들을 모든 프로젝트에 적용할 수 있도록 표준화 한 것이 Bean Validation이다.
@PostMapping("/v3/member")
public String createMemberV3(
// 1. @ModelAttribute 뒤에 2. BindingResult가 위치한다.
@ModelAttribute MemberCreateRequestDto request,
BindingResult bindingResult,
Model model
) {
System.out.println("/V3/member API가 호출되었습니다.");
// 3. Validation
if (request.getAge() == null || request.getAge() < 0) {
// BindingResult FieldError 추가
bindingResult.addError(
new FieldError("request", "age", "age 필드는 필수이며 0 이상의 값이어야 합니다.")
);
}
// error 처리
if (bindingResult.hasErrors()) {
System.out.println("Error를 처리하는 로직");
// error 페이지 반환
return "error";
}
// Model에 저장
model.addAttribute("point", request.getPoint());
model.addAttribute("name", request.getName());
model.addAttribute("age", request.getAge());
return "complete";
}
실행결과
- 검증 기능을 매번 BingdingResult 예시처럼 코드로 구현하는것은 번거로운 일이다.
- 이 경우 Controller의 크기가 너무 커지며 단일 책임 원칙에 위배된다.
- 개발자들은 불편함을 참지 않는다.
Bean Validation
- 객체의 필드나 메서드에 제약 조건을 설정하여, 올바른 값을 가지고 있는지 검증하는 표준화된 방법
- Bean Validation은 기술 표준 인터페이스이다.
- 다양한 Annotation들과 여러가지 Interface로 구성되어 있다.
- Bean Validation(인터페이스) 구현체인 Hibernate Validator를 사용한다.
Hibernate Validator의 Hibernate는 ORM JPA Hibernate와 이름만 같고 상관없는 기술이다. |
Bean Validation 적용 예시
@Getter
public class SignUpRequestDto {
@NotBlank
private String name;
@NotNull
@Range(min = 1, max = 120)
private Integer age;
}
@Controller
public class BeanValidationController {
@PostMapping("/model-attribute")
public String beanValidationV1(
@Validated @ModelAttribute SignUpRequestDto dto
) {
// 로직
// ViewName
return "complete";
}
}
@RestController
public class BeanValidationRestController {
@PostMapping("/request-body")
public String beanValidationV2(
@Validated @RequestBody SignUpRequestDto dto
) {
// 로직
// 문자 Data 반환
return "회원가입 완료";
}
}
- Annotation을 적용시키는것 만으로 Validation을 아주 쉽게 적용할 수 있다.
- Controller에 개발자가 기본적인 검증 로직을 작성할 필요가 없어졌다.
- RestController의 @RequestBody에도 사용할 수 있다.
Field Error
📌 스프링의 유효성 검증(Validation) 과정에서 발생하는 필드 수준의 에러 정보를 나타내는 객체
- 유효성 검증 실패 시, 스프링은 각 필드에 대한 검증 결과를 FieldError 객체로 저장하며, 이를 통해 어떤 필드에서 어떤 에러가 발생했는지를 확인 가능
Bean Validation 적용
- 코드예시
- 의존성 추가(build.gradle)
implementation 'org.springframework.boot:spring-boot-starter-validation' |
- implementation 뒤에 version이 따로 적혀있지 않은 이유 → Spring Boot 기능
- External Libraries
- 프로젝트에 적용된 라이브러리 목록
- 파일경로 jakarta.validation-api-${version}.jar
- jakarta.validation.constraints
- 사용할 수 있는 다양한 Annotation들을 확인할 수 있다.
- 파일경로 org.hibernate.validator:hibernate-validator:${version}
- 위 Annotation 들이 동작하게 만들어주는 Validator
- jakarta.validation-api : Interface
- org.hibernate.validator:hibernate-validator : 구현체
실제코드 import
- @Range Annotation은 Hibernate Validator 에서만 동작하는것
- @NotBlank, @NotNull 은 validation 표준 인터페이스
사용된 Annotation 정리
- @NotBlank
- null을 허용하지 않는다.
- 공백(” “)을 허용하지 않는다. 하나 이상의 문자를 포함해야한다.
- 빈값(””)을 허용하지 않는다.
- CharSequence 타입 허용
- String은 CharSequence(Interface)의 구현체이다.
- @NotNull
- null을 허용하지 않는다.
- 모든 타입을 허용한다.
- @NotEmpty
- null을 허용하지 않는다.
- 빈값(””)을 허용하지 않는다.
- CharSequence, Collection, Map, Array 허용
@Data
public class TestDto {
// 테스트 하고싶은 Annotation으로 변경 가능
@NotBlank
private String stringField;
@NotNull
@Range(min = 1, max = 9999)
private Integer integerField;
}
import static org.assertj.core.api.Assertions.assertThat;
public class BeanValidationTest {
@Test
void beanValidation() {
// Spring과 통합하면 아래 두줄의 코드는 사용하지 않는다.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
// Test 하고싶은 상황을 만들어서 검증 가능
TestDto dto = new TestDto();
dto.setStringField(" ");
dto.setIntegerField(1);
// DTO를 검증
Set<ConstraintViolation<TestDto>> violations = validator.validate(dto);
// 검증 결과가 예상대로 발생했는지 확인
// 검증에 걸린 필드가 있어야 함
assertThat(violations).isNotEmpty();
// 2개의 제약 위반 발생
assertThat(violations.size()).isEqualTo(2);
// Validation에 걸린 내역을 출력
for(ConstraintViolation<TestDto> violation : violations) {
// 아래의 결과에 Message가 있으면 Validation에 걸린것.
// Default Message가 있기 때문에 출력됨
// Message를 수정하고싶다면 Annotation 속성값(message="입력")으로 설정할 수 있다.
// 결과가 비어있으면 Validation에 걸리지 않은것.
System.out.println("violation = " + violation.getMessage());
}
}
}
출력결과
- 테스트 통과
- Default Message가 출력된다.
import가 org.hibernate.validator로 시작하면 하이버네이트를 사용할 때만 제공되는 검증 기능으로 다른 구현체로 validator를 교체하였을 경우 동작하지 않는다. 하지만 org.hibernate.validator를 대부분 사용한다. |
Validator
📌 유효성 검증(Validation) 인터페이스입니다. 특정 객체에 대한 데이터 유효성을 검사하고, 유효하지 않은 경우 오류 메시지를 생성하여 처리할 수 있도록 설계되었습니다. 이 인터페이스는 커스텀 유효성 검증 로직을 구현하거나, 스프링의 유효성 검증 프레임워크와 통합할 때 사용됩니다.
- 단순히 Annotation을 선언해주면 검증이 완료되는 이유는 Validator(Validation을 사용하는것)가 존재하기 때문이다.
- Spring Boot는 validation 라이브러리를 설정하면 'org.springframework.boot:spring-boot-starter-validation'자동으로 Bean Validator를 Spring에 통합되도록 설정해준다.
동작 순서
- Spring Boot Application 실행 시 자동으로 Bean Validator가 통합된다.
- LocalValidatorFactoryBean 을 Global Validator로 등록한다.
- Class Diagram
- Global Validator가 Default로 적용되어 있으니 @Valid, @Validated 만 적용하면 된다.
- Bean Validation Annotation이 있으면 검증을 수행한다. ex) @NotNull, @NotBlank, @Max 등등..
- Validation Error가 발생하면 FieldError, ObjectError를 생성하여 BindingResult에 담아준다.
@Valid, @Validated 차이점
- @Valid 는 JAVA 표준이고 @Validated 는 Spring 에서 제공하는 Annotation이다.
- @Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.
- @Valid 는 MethodArgumentNotValidException 예외를 발생시킨다.
- @Validated 는 ConstraintViolationException 예외를 발생시킨다.
Validator 적용
- Validator 적용 전
- @ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
- 성공 : Controller 정상 호출
- 실패 : TypeMismatch FieldError 발생
- @ModelAttribute 각각의 필드 타입에 맞추어 바인딩(변환) 시도
- Validator 적용 후
- @ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용
- Integer 타입 필드에 문자가 오면 애초에 검증의 의미가 없다.
- 성공 : String 필드에 문자입력 → 바인딩 성공 → String 필드에 Bean Validation 적용
- 실패 : Integer 필드에 문자입력 → 바인딩 실패 → bindingResult에 TypeMismatch FieldError 추가 → 바인딩에 실패한 필드는 값이 없음(null) → Bean Validation 적용하지 않음
- @ModelAttribute → 각 필드 바인딩 → 성공한 필드만 Bean Validation 적용
Bean Validator는 바인딩에 실패한 필드는 Bean Validation을 적용하지 않는다. 바인딩(변환)에 성공한 필드만이 Bean Validation을 적용하는 의미가 있다. |
에러 메세지
📌 Spring의 Bean Validation은 Default로 제공하는 Message들이 존재하고 임의로 수정할 수 있다
Error Message
- Bean Validation을 적용하고 BindingResult에 등록된 검증 오류를 확인해보면 오류가 Annotation 이름으로 등록되어 있다.
@Data
public class TestDto {
@NotBlank
private String stringField;
@NotNull
@Range(min = 1, max = 9999)
private Integer integerField;
}
@Slf4j
@RestController
public class BeanValidationController {
@PostMapping("/error-message")
public String beanValidation(
@Validated @ModelAttribute TestDto dto,
BindingResult bindingResult
) {
// bindingResult Field Error 출력
if (bindingResult.hasErrors()) {
return String.valueOf(bindingResult.getFieldError());
}
// 성공시 문자열 반환
return "회원가입 성공";
}
}
Postman
- integerField 는 비워두고 stringField 만 값(”문자열”) 입력
- 출력결과
Field error in object 'testDto' on field 'integerField': rejected value [null]; codes [NotNull.testDto.integerField,NotNull.integerField,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testDto.integerField,integerField]; arguments []; default message [integerField]]; default message [널이어서는 안됩니다] |
NotNull 오류 코드를 기반으로 MessageCodesResolver 를 통해 메세지 코드 생성
Spring 에서는 오류 메시지 코드관리를 위해 MessageCodesResolver 인터페이스의 구현체인 DefaultMessageCodesResolver를 기본으로 사용한다. |
에러 메세지 수정하기
- NotNull.Object.fieldName
- Annotation의 message 속성 사용
@Data
public class TestDto {
@NotBlank(message = "메세지 수정 가능")
private String stringField;
}
- NotNull.fieldName(MessageSource)
- 필드명에 맞춘 사용자 정의 Message
- NotNull.FieldType(MessageSource)
- 필드 타입에 맞춘 사용자 정의 Message
- NotNull
- Annotation 자체에 포함된 Default Message
- 네가지 방법중 Annotation의 message 속성을 사용하여 에러 메세지를 수정하면 됩니다.
MessageSource란 Spring에서 지원하는 인터페이스로 메세지의 국제화를 위해 사용된다. |
Object Error
- 필드 단위가 아닌 객체 전체에 대한 오류를 나타낸다. 예를들어 두 필드 간의 관계를 검증할 때 ObjectError를 통해 해당 오류를 BindingResult에 기록할 수 있다.
지금까지 위에서 배운 내용은 객체가 아닌 필드 단위를 검증해서 발생하는 Field Error에 대한 내용 |
- @ScriptAssert
- 코드예시
- 비밀번호와 비밀번호 확인 필드가 동일한지 검증
- 코드예시
@Data
@ScriptAssert(lang = "javascript", script = "_this.password === _this.confirmPassword", message = "Passwords do not match")
public class UserRegistrationDto {
@NotBlank(message = "Password is required")
private String password;
@NotBlank(message = "Confirm password is required")
private String confirmPassword;
}
- 실제로 @ScriptAssert는 제약사항 때문에 사용하지 않는다.
- 실무에서는 훨씬 복잡한 Validation들이 필요하지만 대응이 불가능하다.
- ex) 다른 객체끼리의 비교 혹은 DB조회 결과와 비교
- Object Error의 경우 Java 코드로 직접 Validation 한다.
- Java 코드로 구현하기
- 요구사항 : 총 구매 가격이 10000원 이상이여야 한다.
- price * count ≥ 10000
- 요구사항 : 총 구매 가격이 10000원 이상이여야 한다.
@Getter
@AllArgsConstructor
public class OrderRequestDto {
@NotNull
@Range(min = 1000)
private Integer price;
@NotNull
@Range(min = 1)
private Integer count;
}
@Slf4j
@RestController
public class BeanValidationController {
@PostMapping("/object-error")
public String objectError(
@Validated @ModelAttribute OrderRequestDto requestDto,
BindingResult bindingResult
) {
// 합이 10000원 이상인지 확인
int result = requestDto.getPrice() * requestDto.getCount();
if (result < 10000) {
// Object Error
bindingResult.reject("totalMin", new Object[]{10000, result}, "총 합이 10000 이상이어야 합니다.");
}
// Error가 있으면 출력
if (bindingResult.hasErrors()) {
log.info("errors={}", bindingResult);
return bindingResult.getAllErrors().get(0).getDefaultMessage();
}
// 성공로직 ...
return "성공";
}
}
- Object Error는 로직으로 구현하면 된다.
- Postman
Bean Validation의 충돌
📌 등록, 수정 API에서 각각 다른 Validation이 적용된다면?
요구사항
- 상품
- id (식별자)
- name (이름)
- price (가격)
- count (재고)
- 상품 등록 API
- 식별자 값은 필수가 아니다.
- name은 null, “”, “ “을 허용하지 않는다.
- price는 10 ~ 10000 사이의 숫자로 생성한다.
- count는 1 ~ 999 사이의 숫자로 생성한다.
- 상품 수정 API
- 식별자 값이 필수이다.
- name은 null, “”, “ “을 허용하지 않는다.
- price는 무제한으로 허용한다.
- count는 1 ~ 999 사이의 숫자로 생성한다.
일반적으로 수정 API를 만들 때 식별자 id값을 항상 Controller에서 받도록 구성한다. HTTP 요청은 사용자가 임의로 변경하여 요청할 수 있음으로 항상 서버에서 최종적으로 추가 검증을 진행 해야한다. ex) 게시글 수정시 요청자 본인이 쓴 글인지 확인한다. |
Product 저장 API
@Data
public class ProductRequestDto {
// 식별자는 Database에서 자동생성
@NotBlank
private String name;
@NotNull
@Range(min = 10, max = 10000)
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ConflictValidationController {
@PostMapping("/product")
public String save(
@Validated @ModelAttribute ProductRequestDto requestDto
) {
log.info("생성 API가 호출 되었습니다.");
// Validation 성공시 repository 저장로직 호출
return "상품 생성이 완료되었습니다";
}
}
Product 수정 API
@Data
public class ProductRequestDto {
@NotBlank
private String name;
// price 무제한 요구사항 반영
@NotNull
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ConflictValidationController {
@PutMapping("/product/{id}")
public String update(
@PathVariable Long id,
@Validated @ModelAttribute ProductRequestDto test
) {
log.info("수정 API가 호출 되었습니다.");
// Validation 성공시 repository 수정로직 호출
return "상품 수정이 완료되었습니다.";
}
}
@PathVariable의 required 속성의 기본값은 true이다.
해결방법
- 저장할 Object를 직접 사용하지 않고 SaveRequestDto, UpdateRequestDto 따로 사용한다.
- Bean Validation의 groups 기능을 사용한다.
groups
- Bean Validation의 groups 속성은 다양한 유효성 검사 시나리오를 정의할 때 사용된다. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을 때 groups를 활용할 수 있다.
// 저장용 group
public interface SaveCheck {
}
// 수정용 group
public interface UpdateCheck {
}
@Data
public class ProductRequestDtoV2 {
// 저장, 수정 @NotBlank Validation 적용
@NotBlank(groups = {SaveCheck.class, UpdateCheck.class})
private String name;
// 사용하는 모든곳에서 @NotNull Validation 적용
@NotNull
// 저장만 @Range 반영
@Range(min = 10, max = 10000, groups = SaveCheck.class)
private Integer price;
@NotNull
@Range(min = 1, max = 999)
private Integer count;
}
@Slf4j
@RestController
public class ProductController {
@PostMapping("/v2/product")
public String save(
// 저장 속성값 설정
@Validated(SaveCheck.class) @ModelAttribute ProductRequestDtoV2 requestDtoV2
) {
log.info("생성 API가 호출 되었습니다.");
// Validation 성공시 repository 저장로직 호출
return "상품 생성이 완료되었습니다";
}
@PutMapping("/v2/product/{id}")
public String update(
@PathVariable Long id,
// 수정 속성값 설정
@Validated(UpdateCheck.class) @ModelAttribute ProductRequestDto test
) {
log.info("수정 API가 호출 되었습니다.");
// Validation 성공시 repository 수정로직 호출
return "상품 수정이 완료되었습니다.";
}
}
groups VS DTO 분리
📌 Bean Validation의 충돌이 발생하는 경우 대부분 DTO를 분리하는 방법이 적절하다.
- groups VS DTO 분리
- groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
- 가독성이 떨어지고 코드 복잡도가 올라간다.
- 실무에서는 등록 폼과 수정 폼 자체를 분리해서 사용하기 때문에 DTO 분리 방법을 사용하면 된다.
- 단, 네이밍은 일관성있게 작성해야 한다.(SaveRequestDto, UpdateRequestDto)
- groups 속성을 사용하면 등록과 수정시 각각 다르게 Validation이 적용된다.
- DTO 분리
- 실제로 간단한 프로젝트를 개발해보면 저장, 수정시 Request가 비슷한 경우가 있다.
- 각각의 장단점이 존재하지만 어설프게 하나로 합칠 경우 유지보수시 엄청난 경험을 할 수 있다.
- RequestDto가 변한다는건 해당 API의 스펙 자체가 변경되어 많은 수정이 발생한다.
- 실무에서는 거의 발생하지 않는 경우기 때문에 간단한게 아니라면 대부분 분리하도록 하자!
- @Validated VS @Valid
- @Validated
- 속성값이 존재한다.
- spring이 제공하는 Annotation
- @Valid
- 속성값이 존재하지 않는다, groups 기능 지원하지 않는다.
- groups 기능을 사용하려면 @Validated를 사용해야 한다.
- Java 표준 Annotation
@ModelAttribute, @RequestBody
📌 @Valid, @Validated는 @ModelAttribute뿐만 아니라 @RequestBody에도 적용할 수 있다. @ModelAttribute는 요청 파라미터 혹은 Form Data(x-www-urlencoded)를 다룰 때 사용하고 @RequestBody 는 HTTP Body Data를 Object로 변환할 때 사용한다.
@RequestBody 적용
@Data
public class ExampleRequestDto {
@NotBlank
private String field1;
@NotNull
@Range(min = 1, max = 150)
private Integer field2;
}
@Slf4j
@RestController
public class RequestBodyController {
@PostMapping("/example")
public Object save(
@Validated @RequestBody ExampleRequestDto dto,
BindingResult bindingResult
) {
log.info("RequestBody Controller 호출");
if(bindingResult.hasErrors()) {
log.info("validation errors={}", bindingResult);
// Field, Object Error 모두 JSON으로 반환
return bindingResult.getAllErrors();
}
// 성공 시 RequestDto 반환(의미 없음)
return dto;
}
}
- Rest API 요청의 세가지 경우의 수
- 성공 요청: 성공
- Controller 정상 호출
- 응답 반환
- 실패 요청: JSON을 객체로 변환하는 것 자체가 실패
- field2에 String 입력
- JSON → Object 변환 실패
- 중요! Controller가 호출되지 않는다.
- 반드시 JSON → Object로 변환이 되어야 Validation이 진행된다.
- 검증 오류 요청: JSON을 객체로 변환하는 것은 성공, 검증에서 실패
- field2의 값에 범위를 넘어서는 값 입력 @Range(max = 150)
- bindingResult.getAllErrors() 가 MessageConverter에 의해 JSON으로 변환되어 반환된다.
- Controller를 실제로 호출한다.
- log로 작성한 bindingResult error들이 콘솔에 출력된다.
정리
- @ModelAttribute와 @RequestBody 차이점
- @ModelAttribute
- 각각의 필드 단위로 바인딩한다.
- 특정 필드 바인딩이 실패하여도 나머지 필드는 정상적으로 검증 처리할 수 있다.
- 특정필드 변환 실패
- 컨트롤러 호출, 나머지 필드 Validation 적용
- @RequestBody
- 필드별로 적용되는것이 아니라 객체 단위로 적용된다.
- MessageConverter가 정상적으로 동작하여 Object로 변환하여야 Validation이 동작한다.
- 특정필드 변환 실패
- 컨트롤러 미호출, Validation 미적용
- @ModelAttribute
- 추가내용
- bindingResult.getAllErrors()는 FieldError와 ObjectError 모두 반환한다.
- Spring은 MessageConverter를 이용해 Error 객체들을 변환하여 응답한다.
- RequestDTO 의 경우, 생성, 수정, 삭제, 모두 비슷하게 생겼어도 따로 분리해서 사용하자.
- 작성한 코드는 예시일 뿐 실제로는 API Spec에 맞는 응답을 만들어 클라이언트에 전달 해야한다.
- @ControllerAdvice