REST API를 만들다 보면 가장 먼저 부딪히는 문제가 있다.
- 필수값을 안 보냈을 때는 어떻게 할 것인가?
- 문자열 길이가 너무 길면?
- 숫자인데 음수면?
- 이런 요청을 그대로 서비스까지 보내도 되는가?
이 문제를 해결하기 위해 스프링에서는 컨트롤러 입구에서 요청 DTO를 검증하고, 문제가 있으면 비즈니스 로직으로 들어가기 전에 400(Bad Request) 로 차단하는 방식을 기본으로 사용한다.
이 글의 목표는 Validation이 “무엇이고”, “어디서”, “어떤 역할”을 하는지 흐름을 잡는 것이다.
0. Validation은 “어디에서” 하는가 (큰 그림)
먼저 위치부터 정리해야 한다.
Client(JSON 요청)
↓
Controller ← DTO + Validation (@Valid)
↓
Service ← 비즈니스 규칙
↓
Repository
- Validation은 컨트롤러 + DTO에서 수행
- 서비스는 “검증이 끝난 값만 들어온다”는 전제로 동작
즉,
- Validation = 입력값이 형식적으로 올바른가
- Service 로직 = 업무 규칙상 가능한가
이렇게 역할이 분리된다.
1. Bean Validation이란 무엇인가
Bean Validation은 “값 검증 규칙을 어노테이션으로 선언하는 표준 방식”이다.
특징은 다음과 같다.
- DTO 필드 위에 @NotBlank, @Min 같은 규칙을 붙인다
- 컨트롤러에서 @Valid를 사용하면 스프링이 검증을 실행한다
- 규칙을 위반하면 예외가 발생하고, 기본적으로 400 응답이 내려간다
핵심은 이거다.
검증 로직을 컨트롤러/서비스에 if문으로 흩뿌리지 않고, “입력 데이터 자체의 규칙”을 DTO에 모아둔다.
2. 왜 DTO에서 Validation을 하나
DTO는 외부 요청 데이터를 담는 객체다.
- JSON 요청은 신뢰할 수 없다
- 어떤 값이 올지 알 수 없다
- 형식이 깨진 요청은 서비스까지 갈 필요가 없다
그래서 DTO는 다음 역할을 가진다.
- 요청 데이터 구조 정의
- 요청 데이터 형식 검증
즉 DTO + Validation은 👉 “이 요청은 최소한 말이 되는 입력인가?”를 판단하는 관문이다.
3. 의존성 설정 (@Valid가 동작하려면)
Spring Boot에서는 아래 스타터가 필요하다.
- spring-boot-starter-validation
이 의존성이 있어야 @Valid와 Bean Validation 어노테이션이 동작한다.
(Spring Boot 3 기준으로는 Jakarta Validation 기반)
4. 요청 DTO 예시 (Shift 생성)
public record ShiftCreateRequest(
@NotBlank(message = "근무처는 필수입니다.")
@Size(max = 50, message = "근무처는 50자 이하여야 합니다.")
String employerName,
@NotNull(message = "시급은 필수입니다.")
@Min(value = 9860, message = "시급이 너무 낮습니다.")
Integer hourlyWage,
@NotNull(message = "근무시간(분)은 필수입니다.")
@Min(value = 1, message = "근무시간은 1분 이상이어야 합니다.")
Integer minutesWorked
) {}
이 DTO는 다음을 보장한다.
- 필수값이 빠지지 않았는가
- 문자열 길이가 적절한가
- 숫자 범위가 정상인가
👉 DB, 비즈니스 규칙과는 무관하게 “입력 데이터의 형태”만 검증한다.
5. 컨트롤러에서 Validation이 실행되는 흐름
@RestController
@RequestMapping("/api/shifts")
public class ShiftController {
private final ShiftService shiftService;
public ShiftController(ShiftService shiftService) {
this.shiftService = shiftService;
}
@PostMapping
public ResponseEntity<Void> create(
@RequestBody @Valid ShiftCreateRequest req
) {
shiftService.create(req);
return ResponseEntity.status(201).build();
}
}
실제 흐름은 다음 순서다.
- 클라이언트가 JSON 요청을 보낸다
- 스프링이 JSON을 DTO로 바인딩한다 (@RequestBody)
- @Valid가 붙어 있으면 Bean Validation 실행
- 규칙 위반 시 예외 발생
- 컨트롤러 메서드는 실행되지 않고 400 응답 반환
중요한 포인트: 검증에 실패하면 서비스는 호출되지 않는다.
6. 검증 실패 시 응답은 어떻게 처리되나
기본 동작은 다음과 같다.
- 상태 코드: 400 Bad Request
- 바디: 검증 실패 정보 포함
응답 포맷을 커스터마이징하는 방법은 전역 예외 처리(@ControllerAdvice) 단계에서 다룬다.
지금 단계에서는
- 검증이 어디서 실행되는지
- 실패하면 어디에서 차단되는지
이 두 가지만 이해하면 충분하다.
7. 자주 헷갈리는 포인트 정리
(1) @NotNull vs @NotBlank
- @NotNull → null만 막음
- @NotBlank → null + 빈 문자열 + 공백 문자열 모두 막음
👉 문자열 필수값은 @NotBlank가 기본
(2) int 대신 Integer를 쓰는 이유
- int는 null을 표현할 수 없다
- “값이 안 왔다”를 검증하려면 Integer + @NotNull이 적절하다
(3) @Valid는 어디에 붙이나
- 요청 DTO를 받을 때 컨트롤러 메서드 파라미터에 붙인다
@RequestBody @Valid ShiftCreateRequest req
8. Entity에도 Validation을 붙일 수 있나?
기술적으로는 가능하다.
- @Entity 필드에도 Bean Validation 어노테이션을 붙일 수 있다
- JPA 동작 시 검증이 실행되도록 설정할 수도 있다
다만 현재 학습 단계에서는 다음처럼 역할을 분리하는 것이 좋다.
- DTO: 요청 데이터 검증
- Entity: 도메인 상태와 관계 표현
그래서 이 단계에서는
👉 Entity에 Validation은 다루지 않고,
👉 DTO + 컨트롤러 검증 흐름에 집중한다.
9. 한 문장 요약
Validation은 DTO에 규칙을 선언하고, 컨트롤러에서 @Valid로 실행하여 형식이 잘못된 요청을 서비스 진입 전에 차단하기 위한 장치다.
'백엔드' 카테고리의 다른 글
| [스프링 로드맵] E-1. 전역 예외 처리 기본 - 응답을 하나로 묶는 이유 (1) | 2026.01.20 |
|---|---|
| [스프링 로드맵] D-2. Validation 응답 설계 - BindingResult vs 예외 처리 (0) | 2026.01.20 |
| [스프링 로드맵] C-3. Repository 실전 - JpaRepository가 해주는 것들 (1) | 2026.01.19 |
| [스프링 로드맵] C-2. 엔티티 설계 - @Entity와 기본 생성자의 의미 (0) | 2026.01.19 |
| [스프링 로드맵] C-1. JPA 큰 그림 - JPA vs Hibernate vs Spring Data JPA (0) | 2026.01.19 |