[Spring] 24. @Valid와 BindingResult
이전 글에서는 ResponseEntity에 대해 살펴봤습니다. ResponseEntity는 API 응답에서 상태 코드, Header, Body를 함께 제어할 수 있는 객체입니다.
이번 글에서는 사용자가 입력한 값이 올바른지 검사하는 @Valid와 검증 결과를 담는 BindingResult에 대해 정리하겠습니다. 회원가입, 게시글 작성, 로그인, 댓글 등록 같은 기능에서는 빈 값, 글자 수, 이메일 형식 같은 검증이 필요합니다.
핵심은 간단합니다. @Valid는 DTO에 작성한 검증 규칙을 실행하고, BindingResult는 검증 실패 결과를 Controller에서 확인할 수 있게 해줍니다.
1. 입력값 검증이 필요한 이유
웹 애플리케이션에서는 사용자가 항상 올바른 값을 입력한다고 가정하면 안 됩니다. 게시글 제목을 비워둘 수도 있고, 회원가입 이메일 형식이 잘못될 수도 있고, 비밀번호 길이가 너무 짧을 수도 있습니다.
게시글 제목 없음
비밀번호 2글자
이메일 형식 오류
수량이 0보다 작음
필수 체크박스 미선택
이런 값이 그대로 Service나 DB까지 전달되면 문제가 생길 수 있습니다. 따라서 Controller에 도착한 입력값을 먼저 검증하고, 문제가 있으면 다시 입력 화면으로 돌려보내는 처리가 필요합니다.
사용자 입력
→ Controller
→ 입력값 검증
→ 실패하면 입력 화면으로 돌아감
→ 성공하면 Service 호출
2. @Valid란?
@Valid는 DTO에 작성한 검증 규칙을 실행하라는 의미의 어노테이션입니다. Controller 메서드의 DTO 파라미터 앞에 붙여 사용합니다.
@PostMapping("/board/write")
public String write(@Valid BoardWriteDto boardWriteDto) {
return "redirect:/board/list";
}
위 코드에서 @Valid는 BoardWriteDto에 작성된 검증 규칙을 실행합니다. 하지만 검증 실패 결과를 직접 처리하려면 BindingResult를 함께 사용해야 합니다.
@PostMapping("/board/write")
public String write(
@Valid BoardWriteDto boardWriteDto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "board/write";
}
return "redirect:/board/list";
}
검증 실패가 있으면 bindingResult.hasErrors()가 true가 됩니다. 그 경우 다시 입력 화면으로 돌아가도록 처리할 수 있습니다.
3. 검증 어노테이션 종류
DTO 필드에는 여러 검증 어노테이션을 붙일 수 있습니다. 대표적인 검증 어노테이션은 다음과 같습니다.
| 어노테이션 | 의미 |
@NotBlank |
문자열이 null이거나 빈 문자열이거나 공백만 있는 경우를 허용하지 않습니다. |
@NotNull |
null 값을 허용하지 않습니다. |
@Size |
문자열, 배열, 컬렉션 등의 길이나 크기를 제한합니다. |
@Email |
이메일 형식인지 검사합니다. |
@Min |
숫자의 최솟값을 제한합니다. |
@Max |
숫자의 최댓값을 제한합니다. |
@Pattern |
정규식 패턴에 맞는지 검사합니다. |
Spring Boot 3 이상에서는 보통 jakarta.validation 패키지의 검증 어노테이션을 사용합니다.
4. 게시글 작성 DTO 예시
게시글 작성 기능을 기준으로 DTO를 만들어보겠습니다. 제목은 비어 있으면 안 되고, 내용은 최소 10자 이상이어야 한다고 가정하겠습니다.
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class BoardWriteDto {
@NotBlank(message = "제목을 입력해주세요.")
private String title;
@NotBlank(message = "내용을 입력해주세요.")
@Size(min = 10, message = "내용은 10자 이상 입력해주세요.")
private String content;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
이 DTO에는 검증 규칙이 들어 있습니다.
| 필드 | 검증 규칙 |
title |
비어 있으면 안 됩니다. |
content |
비어 있으면 안 되고, 10자 이상이어야 합니다. |
5. Controller에서 @Valid 사용하기
이제 Controller에서 @Valid를 사용해 DTO 검증을 실행해보겠습니다.
@Controller
@RequestMapping("/board")
public class BoardController {
private final BoardService boardService;
public BoardController(BoardService boardService) {
this.boardService = boardService;
}
@GetMapping("/write")
public String writeForm(Model model) {
model.addAttribute("boardWriteDto", new BoardWriteDto());
return "board/write";
}
@PostMapping("/write")
public String write(
@Valid BoardWriteDto boardWriteDto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "board/write";
}
boardService.write(boardWriteDto);
return "redirect:/board/list";
}
}
흐름은 다음과 같습니다.
POST /board/write
→ BoardWriteDto에 폼 값 바인딩
→ @Valid로 검증 실행
→ 실패 결과는 BindingResult에 저장
→ 오류가 있으면 board/write 화면으로 돌아감
→ 오류가 없으면 Service 호출
여기서 중요한 점은 BindingResult의 위치입니다.
6. BindingResult는 반드시 @Valid 대상 바로 뒤에 둔다
BindingResult는 검증 대상 바로 뒤에 와야 합니다. 이 순서가 매우 중요합니다.
@PostMapping("/write")
public String write(
@Valid BoardWriteDto boardWriteDto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "board/write";
}
return "redirect:/board/list";
}
좋은 순서는 다음과 같습니다.
@Valid 검증 대상
→ BindingResult
반대로 다음처럼 중간에 다른 파라미터가 끼면 검증 결과를 제대로 받지 못하거나 오류가 발생할 수 있습니다.
// 좋지 않은 예시
@PostMapping("/write")
public String write(
@Valid BoardWriteDto boardWriteDto,
Model model,
BindingResult bindingResult
) {
return "redirect:/board/list";
}
BindingResult는 반드시 자신이 받을 검증 대상 바로 다음에 두는 습관을 들이는 것이 좋습니다.
7. BindingResult로 오류 확인하기
BindingResult는 검증 실패 여부를 확인할 수 있습니다. 대표적으로 hasErrors()를 사용합니다.
if (bindingResult.hasErrors()) {
return "board/write";
}
오류가 있으면 입력 화면으로 다시 돌아갑니다. 이때 사용자가 입력한 값과 오류 정보가 함께 View로 전달될 수 있습니다.
특정 필드에 오류가 있는지 확인할 수도 있습니다.
if (bindingResult.hasFieldErrors("title")) {
System.out.println("제목 오류가 있습니다.");
}
모든 오류 메시지를 확인할 수도 있습니다.
bindingResult.getAllErrors().forEach(error -> {
System.out.println(error.getDefaultMessage());
});
8. Thymeleaf에서 오류 메시지 출력하기
Thymeleaf를 사용하는 경우 검증 오류 메시지를 화면에 출력할 수 있습니다.
<form th:action="@{/board/write}" th:object="${boardWriteDto}" method="post">
<input type="text" th:field="*{title}" placeholder="제목">
<p th:if="${#fields.hasErrors('title')}" th:errors="*{title}">
제목 오류
</p>
<textarea th:field="*{content}" placeholder="내용"></textarea>
<p th:if="${#fields.hasErrors('content')}" th:errors="*{content}">
내용 오류
</p>
<button type="submit">등록</button>
</form>
이 구조에서는 BoardWriteDto에 담긴 값과 검증 오류가 함께 화면에서 사용됩니다.
사용자 입력값 유지
+
필드별 오류 메시지 출력
입력값이 잘못되었을 때 사용자가 어떤 부분을 고쳐야 하는지 보여줄 수 있습니다.
9. JSP에서 오류 처리하기
JSP 프로젝트에서는 Thymeleaf처럼 th:field를 쓰지는 않습니다. 대신 Controller에서 오류 메시지를 Model에 직접 담거나, Spring form 태그를 사용할 수 있습니다.
가장 단순한 방식은 오류가 있을 때 다시 입력 화면으로 보내고, 필요한 메시지를 Model에 담는 방식입니다.
@PostMapping("/write")
public String write(
@Valid BoardWriteDto boardWriteDto,
BindingResult bindingResult,
Model model
) {
if (bindingResult.hasErrors()) {
model.addAttribute("errorMessage", "입력값을 다시 확인해주세요.");
return "board/write";
}
boardService.write(boardWriteDto);
return "redirect:/board/list";
}
JSP에서는 다음처럼 출력할 수 있습니다.
<c:if test="${not empty errorMessage}">
<p>${errorMessage}</p>
</c:if>
처음에는 공통 오류 메시지를 출력하는 방식으로 시작하고, 이후 필드별 오류 메시지 출력 방식으로 확장해도 됩니다.
10. 회원가입 DTO 예시
회원가입은 검증 규칙이 더 많을 수 있습니다. 아이디, 비밀번호, 이름, 이메일을 받는 DTO를 예로 들어보겠습니다.
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
public class MemberJoinDto {
@NotBlank(message = "아이디를 입력해주세요.")
@Size(min = 4, max = 20, message = "아이디는 4자 이상 20자 이하로 입력해주세요.")
private String memberId;
@NotBlank(message = "비밀번호를 입력해주세요.")
@Size(min = 8, message = "비밀번호는 8자 이상 입력해주세요.")
private String memberPw;
@NotBlank(message = "이름을 입력해주세요.")
private String memberName;
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
// getter, setter
}
Controller에서는 다음처럼 사용할 수 있습니다.
@Controller
@RequestMapping("/member")
public class MemberController {
@GetMapping("/join")
public String joinForm(Model model) {
model.addAttribute("memberJoinDto", new MemberJoinDto());
return "member/join";
}
@PostMapping("/join")
public String join(
@Valid MemberJoinDto memberJoinDto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return "member/join";
}
return "redirect:/member/login";
}
}
검증에 통과하지 못하면 다시 회원가입 화면으로 돌아갑니다. 검증에 통과하면 회원가입 처리를 진행하고 로그인 화면으로 이동할 수 있습니다.
11. @Valid와 Service 검증의 차이
@Valid는 주로 입력 형식 검증에 사용합니다. 예를 들어 비어 있는지, 길이가 맞는지, 이메일 형식인지 같은 검증입니다.
반면 Service에서는 비즈니스 규칙 검증을 처리하는 경우가 많습니다. 예를 들어 아이디 중복 여부, 게시글 수정 권한, 주문 가능 재고 같은 검증입니다.
| 구분 | @Valid 검증 | Service 검증 |
| 검증 성격 | 입력 형식 검증 | 비즈니스 규칙 검증 |
| 예시 | 필수값, 길이, 이메일 형식 | 아이디 중복, 권한, 재고 확인 |
| 위치 | Controller 진입 시점 | Service 로직 처리 시점 |
예를 들어 회원가입에서 이메일 형식 검사는 DTO의 @Email로 할 수 있습니다. 하지만 아이디가 이미 존재하는지는 DB 조회가 필요하므로 Service에서 처리하는 것이 자연스럽습니다.
@Valid
→ 입력값 모양 검사
Service
→ 실제 서비스 규칙 검사
12. API에서 @Valid 사용하기
REST API에서도 @Valid를 사용할 수 있습니다. JSON 요청을 @RequestBody로 받고, 동시에 검증을 실행할 수 있습니다.
@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
@PostMapping
public ResponseEntity<?> write(
@Valid @RequestBody BoardWriteDto boardWriteDto,
BindingResult bindingResult
) {
if (bindingResult.hasErrors()) {
return ResponseEntity
.badRequest()
.body("입력값을 확인해주세요.");
}
return ResponseEntity.ok("등록 성공");
}
}
이 구조에서는 JSON 요청 Body를 DTO로 변환한 뒤 검증까지 실행합니다. 검증 실패 시 400 Bad Request 응답을 내려줄 수 있습니다.
JSON 요청
→ @RequestBody로 DTO 변환
→ @Valid로 검증
→ 실패하면 400 Bad Request
→ 성공하면 처리 진행
실제 프로젝트에서는 API 오류 응답을 공통 형식으로 만들고, 예외 처리에서 한 번에 관리하는 경우도 많습니다. 이 내용은 이후 예외 처리 파트에서 다루면 좋습니다.
13. 자주 하는 실수
1) BindingResult 위치를 잘못 두는 경우
// 잘못된 예시
public String write(
@Valid BoardWriteDto boardWriteDto,
Model model,
BindingResult bindingResult
) {
return "board/write";
}
BindingResult는 반드시 @Valid가 붙은 검증 대상 바로 뒤에 둬야 합니다.
// 좋은 예시
public String write(
@Valid BoardWriteDto boardWriteDto,
BindingResult bindingResult,
Model model
) {
return "board/write";
}
2) DTO에 검증 어노테이션을 붙이지 않는 경우
@Valid를 붙였더라도 DTO 필드에 검증 어노테이션이 없으면 검사할 규칙이 없습니다.
public class BoardWriteDto {
private String title;
private String content;
}
검증하려는 필드에는 규칙을 작성해야 합니다.
public class BoardWriteDto {
@NotBlank(message = "제목을 입력해주세요.")
private String title;
@NotBlank(message = "내용을 입력해주세요.")
private String content;
}
3) Service에서 처리해야 할 검증까지 DTO에 넣으려는 경우
DTO 검증은 입력값의 모양을 검사하는 데 적합합니다. DB 조회가 필요한 검증은 Service에서 처리하는 것이 좋습니다.
DTO 검증에 적합
→ 비어 있는지
→ 길이가 맞는지
→ 이메일 형식인지
Service 검증에 적합
→ 아이디가 중복되는지
→ 수정 권한이 있는지
→ 재고가 충분한지
4) 검증 실패 후 redirect를 사용하는 경우
검증 실패 시에는 보통 redirect보다 입력 화면 View를 그대로 반환하는 경우가 많습니다. 그래야 사용자가 입력한 값과 오류 메시지를 다시 보여주기 쉽습니다.
if (bindingResult.hasErrors()) {
return "board/write";
}
검증 성공 후에는 redirect를 사용할 수 있습니다.
boardService.write(boardWriteDto);
return "redirect:/board/list";
5) DTO를 Bean으로 등록하는 경우
DTO는 요청 데이터를 담는 객체입니다. 보통 @Component로 Bean 등록하지 않습니다.
// 일반적으로 피하는 구조
@Component
public class BoardWriteDto {
}
DTO는 Controller 요청 시점에 값이 바인딩되는 데이터 객체로 보면 됩니다.
14. 전체 흐름 정리
게시글 작성 검증 흐름을 정리하면 다음과 같습니다.
1. 사용자가 게시글 작성 폼을 제출한다.
2. Spring이 요청 값을 BoardWriteDto에 바인딩한다.
3. @Valid가 DTO의 검증 어노테이션을 확인한다.
4. 제목이 비어 있거나 내용이 짧으면 검증 실패가 발생한다.
5. 실패 결과가 BindingResult에 담긴다.
6. Controller가 bindingResult.hasErrors()로 오류를 확인한다.
7. 오류가 있으면 다시 입력 화면을 반환한다.
8. 오류가 없으면 Service를 호출한다.
9. 처리 성공 후 목록 화면으로 redirect한다.
정리
@Valid는 DTO에 작성한 검증 규칙을 실행하는 어노테이션입니다. BindingResult는 검증 실패 결과를 담고, Controller에서 오류 여부를 확인할 수 있게 해줍니다.
| @Valid | DTO의 검증 규칙을 실행합니다. |
| BindingResult | 검증 실패 결과를 담습니다. |
| @NotBlank | 문자열이 비어 있거나 공백만 있는 경우를 막습니다. |
| @Size | 문자열이나 컬렉션의 크기를 제한합니다. |
| 이메일 형식을 검사합니다. |
처음에는 다음 기준만 꼭 기억하면 됩니다.
DTO 필드에 검증 규칙 작성
→ @NotBlank, @Size, @Email 등
Controller 파라미터에 @Valid 작성
→ 검증 실행
@Valid 바로 뒤에 BindingResult 작성
→ 검증 결과 확인
검증 실패
→ 입력 화면 다시 반환
검증 성공
→ Service 호출 후 redirect
연습해보기
아래 회원가입 DTO에 검증 규칙을 추가해보세요.
public class MemberJoinDto {
private String memberId;
private String memberPw;
private String email;
}
예시는 다음과 같습니다.
public class MemberJoinDto {
@NotBlank(message = "아이디를 입력해주세요.")
@Size(min = 4, max = 20, message = "아이디는 4자 이상 20자 이하로 입력해주세요.")
private String memberId;
@NotBlank(message = "비밀번호를 입력해주세요.")
@Size(min = 8, message = "비밀번호는 8자 이상 입력해주세요.")
private String memberPw;
@NotBlank(message = "이메일을 입력해주세요.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
}
다음 글에서는 검증 오류를 더 정리해서 처리하는 방법과 오류 메시지를 화면에 보여주는 방법을 조금 더 자세히 살펴보겠습니다.

'Tech Stack > Spring Boot' 카테고리의 다른 글
| [Spring] 25. 검증 오류 메시지 처리하기 (0) | 2026.07.19 |
|---|---|
| [Spring] 26. @ExceptionHandler와 @ControllerAdvice (0) | 2026.07.18 |
| [Spring] 23. ResponseEntity란? (0) | 2026.07.16 |
| [Spring] 22. @ResponseBody와 @RestController (1) | 2026.07.15 |
| [Spring] 21. @RequestBody란? (0) | 2026.07.14 |