이 글에서 얻는 것
- 검증(Validation)을 “컨트롤러에 if문”으로 붙이지 않고, 경계에서 일관되게 처리하는 구조를 만들 수 있습니다.
- Bean Validation(
@NotBlank,@Email등)의 기본 흐름과 예외 타입들을 이해하고, 어떤 예외를 어떻게 응답으로 바꿀지 기준이 생깁니다. - 공통 응답/에러 규약을 설계할 때 꼭 넣어야 할 필드(에러 코드/필드 에러/traceId)를 정리할 수 있습니다.
1) Validation은 어디에서 해야 하나
검증은 레이어별로 목적이 다릅니다.
- 요청 경계(Controller): “형식/필수값/범위” 같은 입력 검증(빠르게 4xx로 종료)
- 도메인/서비스(Service): “비즈니스 규칙” 검증(재고 부족, 권한 없음 같은 의미 있는 실패)
즉, Bean Validation은 “요청 형식”을 빠르게 거르는 데 매우 좋고, 비즈니스 규칙까지 맡기면 오히려 예외 흐름이 꼬일 수 있습니다.
2) Bean Validation 기본 흐름
2-1) DTO에 제약 조건 선언
public class CreateUserRequest {
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 아닙니다.")
private String email;
@NotBlank(message = "이름은 필수입니다.")
@Size(max = 20, message = "이름은 20자 이하여야 합니다.")
private String name;
}
2-2) 컨트롤러에서 @Valid로 트리거
@PostMapping("/users")
public ResponseEntity<UserDto> create(@Valid @RequestBody CreateUserRequest req) {
return ResponseEntity.ok(service.create(req));
}
@Valid가 붙으면, 실패 시 MethodArgumentNotValidException이 발생합니다(일반적으로 @RestControllerAdvice에서 처리).
추가로 알아두면 좋은 것:
- 쿼리 파라미터/경로 변수 검증에서
ConstraintViolationException이 나오는 경우가 있습니다. - 중첩 객체 검증은 필드에
@Valid를 추가해야 내려갑니다.
3) 공통 에러 응답 규약: ‘고정되는 것’이 실무에서 힘이다
에러 응답이 흔들리면 프론트/모바일/다른 서버가 매번 예외 처리를 새로 작성합니다. 그래서 최소한 아래는 고정하는 게 좋습니다.
code: 기계가 처리하기 위한 코드(문자열/Enum)message: 사용자/클라이언트가 이해할 메시지(필요하면 i18n)errors: 필드 단위 상세(검증 실패일 때 특히 중요)traceId: 로그/트레이싱과 연결되는 키path,timestamp: 운영에서 확인하기 좋은 맥락
예시:
{
"code": "VALIDATION_ERROR",
"message": "입력 값을 확인하세요.",
"errors": [{"field":"email","message":"이메일 형식이 아닙니다."}],
"traceId": "abc-123"
}
4) @RestControllerAdvice로 Validation 예외를 응답으로 바꾸기
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex,
HttpServletRequest request) {
List<ErrorDetail> details = ex.getBindingResult().getFieldErrors().stream()
.map(err -> new ErrorDetail(err.getField(), err.getDefaultMessage()))
.toList();
ErrorResponse body = ErrorResponse.validation(details, request.getRequestURI());
return ResponseEntity.badRequest().body(body);
}
}
핵심 포인트:
- Validation 실패는 대체로
400 Bad Request로 응답합니다. - “비즈니스 실패(예: 재고 부족)”와 “입력 형식 실패(Validation)”는 코드/응답을 분리하는 게 좋습니다.
5) 자주 하는 실수
- Validation 에러를 200으로 응답하거나, 메시지/형식이 엔드포인트마다 달라지는 경우
- 서버 로그에 동일 예외를 여러 번 찍는 경우(필터/핸들러/인터셉터 중복)
- DTO 검증과 비즈니스 규칙을 섞어서 “무슨 에러인지”가 모호해지는 경우
연습(추천)
- 필드 에러가 2개 이상일 때 응답이 어떻게 나오는지, 클라이언트가 어떻게 표시하면 좋은지까지 설계해보기
messages.properties로 검증 메시지를 분리하고(i18n), 코드/메시지의 역할을 구분해보기- Validation 실패와 BusinessException 실패를 각각 다른 포맷/코드로 내려보는 예제를 만들어보기
💬 댓글