| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 트러블슈팅
- 디자인 패턴
- Effective Java
- 스케줄러
- GoF 23
- redis
- 이펙티브 자바
- Spring Batch
- 자바
- 코드카타
- 추상클래스
- 템플릿 메서드 패턴
- lv1
- 프로그래머스
- 계산기
- spring boot
- DB
- 김영한
- 백엔드
- 토스
- Spring
- 빌더 패턴
- 성능 개선
- 스프링 배치
- 배치
- java
- 로드밸런서
- 프록시 패턴
- 스프링
- Til
- Today
- Total
김코딩
여긴 못지나간다.(@Valid) 본문
오늘은 Spring에서 검증(Validation)에 대한 글을 작성하였습니다.
처음에 @Valid에 대해 잘 모를 때는 이렇게 생각했습니다.
"음... DB에서도 NOT NULL 같은 걸로 검증을 해주는데,
굳이 컨트롤러에서 또 검증을 해야 하나?"
하지만 이제 알아버렸습니다.. DB에서 막는 건 너무 늦었다는 사실을요.
왜 @Valid가 필요할까?
사용자가 입력한 값은 절대 신뢰할 수 없습니다.
- 이름이 빈 채로 제출될 수 있고
- 이메일이 이상한 형식일 수 있으며
- 숫자여야 할 값이 문자로 들어올 수도 있습니다
이런 값이 그대로 서비스 로직이나 DB로 들어가면?
- 예외가 터지거나
- 데이터 무결성이 깨지거나
- 나중에 추적하기 어려운 버그로 이어집니다.
그래서 우리는 가장 앞단에서 잘못된 입력을 걸러낼 필요가 있습니다.
바로 그 역할을 하는 게 @Valid입니다.
@Valid의 기본 사용법
Spring에서는 @Valid를 사용해 컨트롤러에서 DTO의 유효성을 검사할 수 있습니다.
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinDto dto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "joinForm"; // 유효성 검사 실패 시
}
return "redirect:/home"; // 통과한 경우
}
- @Valid는 DTO에 선언된 검증 조건을 확인하고,
- BindingResult에 에러 여부와 메시지를 담습니다.
DTO에는 어떤 조건을 줄 수 있나요?
public class JoinDto {
@NotBlank(message = "이름은 필수입니다.")
private String name;
@Email(message = "이메일 형식이 아닙니다.")
private String email;
@Min(value = 18, message = "18세 이상만 가입 가능합니다.")
private int age;
}
이처럼 DTO 클래스에 각종 유효성 어노테이션을 붙이면,
잘못된 값은 컨트롤러 진입과 동시에 필터링됩니다.
자주 사용하는 유효성 검증 어노테이션 목록
| 어노테이션 | 설명 | 사용 대상 | 예시 |
| @NotNull | null이 아니어야 함 | 모든 타입 | @NotNull(message = "필수 입력입니다.") |
| @NotBlank | null, 빈 문자열, 공백만 있는 문자열 허용 X | 문자열 | @NotBlank(message = "이름은 필수입니다.") |
| @NotEmpty | null 또는 빈 값 허용 X (공백은 허용) | 문자열, 컬렉션 등 | @NotEmpty(message = "값을 입력해주세요.") |
| 이메일 형식이어야 함 | 문자열 | @Email(message = "이메일 형식이 아닙니다.") | |
| @Size(min, max) | 문자열 또는 컬렉션의 길이 제한 | 문자열, 리스트 등 | @Size(min = 2, max = 10, message = "2~10자여야 합니다.") |
| @Min | 지정한 최소값 이상이어야 함 | 숫자 | @Min(value = 1, message = "1 이상이어야 합니다.") |
| @Max | 지정한 최대값 이하이어야 함 | 숫자 | @Max(value = 100, message = "100 이하여야 합니다.") |
| @Positive | 양수여야 함 | 숫자 | @Positive(message = "양수여야 합니다.") |
| @PositiveOrZero | 0 이상 | 숫자 | @PositiveOrZero(message = "0 이상이어야 합니다.") |
| @Negative | 음수여야 함 | 숫자 | @Negative(message = "음수여야 합니다.") |
| @Pattern | 정규식 만족해야 함 | 문자열 | @Pattern(regexp = "^[0-9]{4}$", message = "4자리 숫자여야 합니다.") |
| @AssertTrue / @AssertFalse | 값이 true / false여야 함 | boolean, Boolean | @AssertTrue(message = "약관 동의는 필수입니다.") |
BindingResult란?
@Valid는 DTO에 정의된 유효성 조건을 검사합니다.
그런데 유효성 검증에 실패했을 때는 어떻게 처리될까요?
이때 예외를 던지지 않고, 검증 결과를 담아주는 객체가 바로 BindingResult입니다.
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinDto dto, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 검증 실패 시, 다시 폼으로 돌아가기
return "joinForm";
}
// 검증 성공 시, 가입 처리
return "redirect:/home";
}
BindingResult가 존재하면,
검증 실패 시에도 컨트롤러 메서드가 호출되고
hasErrors()로 직접 실패 여부를 판단할 수 있습니다.
BindingResult의 위치가 중요한 이유
BindingResult는 반드시 @Valid 바로 뒤에 위치해야 합니다.
중간에 다른 파라미터가 있으면, 스프링이 검증 결과를 바인딩할 수 없어 500 에러가 발생할 수 있습니다.
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinDto dto,
HttpServletRequest request, // 중간에 끼면 안 된다
BindingResult bindingResult) {
}
검증 실패 시 실제 동작 예시
예를 들어, 이런 입력값이 들어왔다고 해보겠습니다:
POST /join
Content-Type: application/x-www-form-urlencoded
name=
email=not-an-email
age=16
DTO
public class JoinDto {
@NotBlank(message = "이름은 필수입니다.")
private String name;
@Email(message = "이메일 형식이 아닙니다.")
private String email;
@Min(value = 18, message = "18세 이상만 가입 가능합니다.")
private int age;
}
컨트롤러 코드:
@PostMapping("/join")
public String join(@Valid @ModelAttribute JoinDto dto, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
// 에러 메시지 확인
bindingResult.getFieldErrors().forEach(error -> {
System.out.println(error.getField() + ": " + error.getDefaultMessage());
});
// 에러를 모델에 담아 폼으로 전달
model.addAttribute("errors", bindingResult);
return "joinForm";
}
return "redirect:/home";
}
결과:
name: 이름은 필수입니다.
email: 이메일 형식이 아닙니다.
age: 18세 이상만 가입 가능합니다.
사용자에게 에러 보여줄 때
템플릿 엔진(Thymeleaf 등)을 사용할 경우, BindingResult를 활용해 다음과 같이 에러 메시지를 출력할 수 있습니다.
<form th:action="@{/join}" method="post" th:object="${joinDto}">
<input type="text" th:field="*{name}" />
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></div>
<input type="email" th:field="*{email}" />
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}"></div>
<input type="number" th:field="*{age}" />
<div th:if="${#fields.hasErrors('age')}" th:errors="*{age}"></div>
<button type="submit">가입하기</button>
</form>
정리하기
왜 검증이 필요할까?
사용자 입력은 신뢰할 수 없으며,
DB나 서비스 레이어에서 처리하기엔 이미 늦습니다.
@Valid는 어떤 역할을 하나요?
DTO에 정의된 검증 조건을 기준으로
유효성 검사를 자동으로 수행합니다.
BindingResult는 왜 필요한가요?
검사 실패 시 예외를 발생시키지 않고
컨트롤러에서 에러를 직접 처리할 수 있게 해줍니다.
DTO에서 어떤 검증 조건을 사용할 수 있나요?
@NotNull, @NotBlank, @Email, @Min, @Pattern 등 다양한 조건을 조합해
입력값의 형식과 범위를 세밀하게 조정할 수 있습니다.
BindingResult의 위치는 중요합니다
@Valid 바로 뒤에 선언해야 하며, 그렇지 않으면 에러가 발생할 수 있습니다.
'스프링' 카테고리의 다른 글
| 스프링 MVC - 타입 변환은 어떻게 이루어질까? (Converter & Formatter) (0) | 2025.06.05 |
|---|---|
| 반복되는 로그인 체크, 필터 하나로 끝내기(Filter 사용법) (0) | 2025.05.22 |
| Spring Container와 Bean이 헷갈리는 이들에게 (0) | 2025.05.15 |
| 의존성 주입(Dependency Injection)이란? (0) | 2025.05.08 |
| 프로퍼티 바인딩이 뭔데 자꾸 헷갈려? (0) | 2025.05.01 |