Validation이 뭐임?
검증을 의미한다.
특정 데이터(주로 클라이언트 요청 데이터)의 값이 유효한지 확인하는 단계를 의미한다.
즉 시스템이 미리 정의한 사양(specification)에 부합하고 있는지 검증 하는 것이다.
Controller의 주요한 역할 중 하나는 Validation이다. HTTP 요청이 정상인지 검증한다.
Validation의 역할
- 검증을 통해 적절한 메세지를 유저에게 보여줌
- 검증 오류로 인해 정상적인 동작을 하지 못하는 경우는 없어야 함
- 사용자가 입력한 데이터는 유지된 상태여야 함
Validation 사용 이유
- 주문서 작성 페이지에서 잘못된 입력값으로 인해 서버에 오류가 생긴다면? (휴대폰 번호에 숫자가 아닌 문자 집어넣기)
- 서버의 문제로 인해 작성 페이지에서 Error 페이지로 이동된다면?
- Error 페이지로 이동되어 작성중인 폼이 모두 리셋되어 다시 작성 한다면?
- 이러면 유저는 빡침.
검증의 종류
- 프론트 검증
- 해당 검증은 유저가 조작할 수 있음으로 보안에 취약하다
- 보안에 취약하더라도 필요하다
- ex) 비밀번호에 특수문자가 포함되어야 한다면 즉각적인 alert 기능으로 유저 사용성 증가.
- 서버 검증
- 프론트 검증 없이 서버에서만 검증한다면 유저 사용성이 떨어짐.
- API Spect을 정의해서 Validation 오류를 Response 예시에 남겨주어야 함.
- API 명세서를 잘 만들어야 그에 맞는 대응 가능
- 데이터베이스 검증
- Not Null, Default와 같은 제약조건을 설정
BindingResult
Spring에서 기본적으로 제공되는 Validation 오류를 보관하는 객체이다. 주로 사용자 입력 폼을 검증할 때 많이 쓰이고 Field Error와 ObjectError를 보관한다.

- Error 인터페이스를 상속받은 인터페이스이다.
- Error 인터페이스는 에러의 저장과 조회 기능을 제공한다.
- BindingResult는 addError()와 같은 추가적인 기능을 제공한다.
- Spring이 기본적으로 사용하는 구현체는 BeanPropertyBidingResult 이다.
컨트롤러의 메서드에서 파라미터에 BindingResult가 없는 경우
잘못된 요청(예를 들어 Long type에 문자열 데이터 대입)시 검증 오류(400 Bad Request)가 발생하고 Controller가 호출되지 않음.
// 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";
}
}

파라미터에 BindingResult가 있는 경우
잘못된 요청시 BindingResult에 오류가 보관되고 Controller는 정상적으로 호출된다.
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";
}
}


//에러코드 원래 옆으로 한줄임
allErrors = [Field error in object 'memberCreateRequestDto' on field 'point': rejected value [김옥쥐];
codes [typeMismatch.memberCreateRequestDto.point,typeMismatch.point,typeMismatch.java.lang.Long,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [memberCreateRequestDto.point,point];
arguments []; default message [point]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Long' for property 'point'; For input string: "김옥쥐"]]
BindingResult가 있는 경우 오류가 나도 Controller가 호출되는 이유
@ModelAttribute는 파라미터를 필드 하나하나에 바인딩한다.
파라미터에 Binding Result가 함께 있는 경우 그중 하나의 필드에 오류가 발생하면 해당 필드를 제외하고 나머지 필드들만 바인딩 된 후 Controller가 호출된다.
Bean Validation
특정 필드 검증의 경우 빈값, 길이, 크기, 형식 과 같은 간단한 로직이다. 이러한 로직들을 모든 프로젝트에 적용할 수 있도록 표준화 한 것이다.
객체의 필드나 메서드에 제약 조건을 설정하여, 올바른 값을 가지고 있는지 검증하는 표준화된 방법이다.
기술 표준 인터페이스이며 다양한 어노테이션들과 여러가지 인터페이스로 구성되어 있다.
Bean Validation 구현체인 Hibernate Validator를 사용한다.
생긴이유
검증 기능을 매번 BindResult 코드로 구현하는 것은 번거로움
코드 예시
@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";
}
이 경우 Controller의 크기가 너무 커지며 단일 책임 원칙에 위배됨
적용 예시
@Getter
public class SignUpRequestDto {
@NotBlank
private String name;
@NotNull
@Range(min = 1, max = 120)
private Integer age;
}
@NotNull
- null을 허용하지 않음
- 모든 타입을 허용
@NotEmpty
- null을 허용하지 않음
- 빈값("")을 허용하지 않음
- CharSequence, Collection, Map, Array 허용
@NotBlank
- null을 허용하지 않음
- 공백(" ")을 허용하지 않음. 하나 이상의 문자를 포함해야 함
- 빈값("")을 허용하지 않음
- CharSequence 타입 허용 (String은 CharSequence(Interface)의 구현체이다.)
@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 "회원가입 완료";
}
}
어노테이션을 적용시키는 것 만으로 Vaildation을 쉽게 적용 가능.
Controller에 개발자가 기본적인 검증 로직을 작성할 필요가 없어짐
RestController의 @RequestBody에도 사용 가능
Field Error, Object Error
Field Error
특정 필드의 값이 유효성 검증을 통과하지 못한 경우 발생하는 오류이다.
주로 단일 필드에 적용된 검증 애노테이션(@NotNull, @Size, @Min, @Max 등)에서 발생한다.
public class User {
@NotNull(message = "Name cannot be null")
private String name;
@Min(value = 18, message = "Age should be at least 18")
private int age;
}
name 필드에 null 값을 입력하면 메세지를 내가 적은 대로 보여주며 오류 발생. 메세지를 안적을 경우 디폴트 메세지가 출력됨.
age 필드에 18보다 값이 작으면 위와 같음.
Object Error
클래스 전체 또는 필드 간의 관계에 대한 유효성 검증을 통과하지 못한 경우 발생하는 오류이다.
예를 들어 두 필드 간의 관계를 검증할 때 Object Error를 통해 해당 오류를 BindingResult에 기록 가능하다.
주로 클래스 레벨의 검증 애노테이션(@Valid, @AssertTrue, @AssertFalse 등) 에서 발생한다
public class User {
private String password;
private String confirmPassword;
@AssertTrue(message = "Passwords do not match")
public boolean isPasswordMatching() {
return password != null && password.equals(confirmPassword);
}
}
password와 confirmPassword가 일치하지 않는 경우 내가 적은 메세지 오류 발생. 안적으면 디폴트 메세지 출력.
실무에서는 Object Error의 경우 복잡한 Validation이 필요하여 Java 코드로 직접 Validation한다고 한다.
Groups
다양한 유효성 검사 시나리오를 정의할 때 사용된다. 동일한 객체에 대한 검증을 상황에 따라 다르게 적용하고 싶을때 활용 가능하다.
Bean Validation이 충돌할때 사용하는 기능이다.(생성 api는 이름 필드가 필수이나 수정api는 필수가 아니라던지..)
@Validated를 사용해야 사용 가능하다!!!
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 "상품 수정이 완료되었습니다.";
}
}
저장할때

수정할때

그런데 Bean Validation 충돌시 DTO 자체를 분리하면 되는데?
맞는말이다.
단순한 검증시 groups를 사용하고 복잡한 검증일시 DTO 분리가 나아보인다.
실무에서는 등록 폼과 수정 폼 자체를 분리해서 사용하기 때문에 DTO를 나누는게 보편적이라는 것 같다.
Validator
어노테이션을 선언해주면 검증이 완료되는 이유는 Validator(Validation사용하는 것)가 존재하기 때문이다
Spring Boot는 validation 라이브러리를 설정하면 자동으로 Bean Validator를 Spring에 통합되도록 설정해준다.
@Valid, @Validated 차이점
- @Valid는 JAVA 표준이고 @Validated는 Spring 에서 제공하는 어노테이션이다.
- @Validated 를 통해 Group Validation 혹은 Controller 이외 계층에서 Validation이 가능하다.(위의 Bean Validation에서 Groups 참조)
- @Valid 는 MethodArgumentNotValidException 예외를 발생시킴
- @Validated 는 ConstrainViolationException 예외를 발생시킴
Validator 적용
적용 전
- @ModelAttribute 각각의 필드 타입에 맞추어 바인딩 시도
- 성공 : Controller 정상 호출
- 실패 : TypeMismatch FieldError 발생
적용 후
- @ModelAttribute -> 각 필드 바인딩 -> 성공한 필드만 Bean Validation 적용
- 성공 : String 필드에 문자 입력 -> 바인딩 성공 -> String 필드에 Bean Validation 적용
- 실패 : Integer 필드에 문자입력 -> 바인딩 실패 -> bindingResult에 TypeMismatchFieldError 추가 -> 바인딩에 실패한 필드는 값이 없음(null) -> Bean Validation 적용하지 않음
Bean Validator는 바인딩에 실패한 필드는 Beand Validation을 적용하지 않는다.
1. 바인딩에 성공한 필드가 Bean Validation을 적용하는 의미가 있기 때문이고
2. 성능을 위해서 이기도 하다.
첫번째 이유에 대한 설명으로는 클라이언트 측에서 기본적인 검증을 통해 입력 오류를 줄인다던지, 서버 측에서 검증 로직을 구현하여 바인딩 오류를 처리할 수 있다. 이러면 바인딩에 실패한 필드가 검증 단계까지 도달하지 않는다.
즉 바인딩에 실패한 필드는 애초에 적절한 단계에서 필터링 된다.
두번째 이유에 대한 설명으로는 Bean Validation은 서버 측에서 이루어지기 때문에, 모든 필드를 일일이 검증하는 건 시스템에 부담을 준다. 특히 대규모 애플리케이션에서는 모든 필드에 대해 검증을 수행하면 성능 저하가 발생한다.
따라서 바인딩에 실패한 필드는 사용자 입력에 포함되지 않은 데이터이므로 굳이 검증을 수행할 필요가 없다고 판단된다.
'spring' 카테고리의 다른 글
| scheduleJPA 트러블슈팅 (1) | 2025.02.07 |
|---|---|
| @ModelAttribute 와 @RequestBody (1) | 2025.02.05 |
| Bean 등록 수동 VS 자동 (0) | 2025.02.04 |
| 같은 타입의 Bean 충돌 해결(@Qualifier @Primary) (0) | 2025.02.04 |
| 의존관계 주입 / @RequiredArgsConstructor (0) | 2025.02.04 |