개발 가이드 백엔드 입력값 검증 가이드
최종 수정:

입력값 검증 가이드

Bean Validation 기반 서버사이드 검증 규칙

입력값 검증 가이드

기본 원칙

  • 클라이언트 검증은 UX 보조용이며, 서버사이드 검증이 반드시 병행되어야 한다.
  • 모든 외부 입력값은 서버에서 검증한다.
  • 검증 실패 시 GlobalExceptionHandler가 400 응답으로 처리한다.

Bean Validation 어노테이션

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PostCreateReq {

    @NotBlank(message = "제목은 필수입니다.")
    @Size(max = 200, message = "제목은 200자 이내여야 합니다.")
    private String title;

    @NotBlank(message = "내용은 필수입니다.")
    @Size(max = 50000, message = "내용이 너무 깁니다.")
    private String content;

    @NotBlank(message = "카테고리는 필수입니다.")
    private String category;

    @Email(message = "올바른 이메일 형식이 아닙니다.")
    @Size(max = 254)
    private String email;

    @Min(value = 1, message = "최소 1 이상이어야 합니다.")
    @Max(value = 100, message = "최대 100 이하여야 합니다.")
    private Integer pageSize;
}

주요 검증 어노테이션

어노테이션설명
@NotNullnull 불허
@NotBlanknull, 빈 문자열, 공백만 있는 값 불허
@NotEmptynull, 빈 컬렉션 불허
@Size(min, max)문자열/컬렉션 크기 제한
@Min(value)숫자 최솟값
@Max(value)숫자 최댓값
@Email이메일 형식
@Pattern(regexp)정규식 패턴
@Positive양수
@PositiveOrZero0 이상

컨트롤러에서 @Valid 적용

@PostMapping
public ResponseEntity<ApiResponse<PostDetailRes>> regPost(
        @RequestBody @Valid PostCreateReq req) {
    return ResponseEntity.status(HttpStatus.CREATED)
            .body(ApiResponse.ok(postService.regPost(req)));
}

// 경로 변수는 @Validated + 제약조건
@GetMapping("/{id}")
public ResponseEntity<ApiResponse<PostDetailRes>> getPost(
        @PathVariable @Positive(message = "ID는 양수여야 합니다.") Long id) {
    return ResponseEntity.ok(ApiResponse.ok(postService.getPost(id)));
}

GlobalExceptionHandler 검증 실패 처리

// MethodArgumentNotValidException — @Valid 실패 시 발생
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Void>> handleValidation(MethodArgumentNotValidException e) {
    String message = e.getBindingResult().getFieldErrors().stream()
            .map(fe -> fe.getField() + ": " + fe.getDefaultMessage())
            .collect(Collectors.joining(", "));
    return ResponseEntity.badRequest().body(ApiResponse.fail(message));
}

실패 응답 예시:

{
    "success": false,
    "data": null,
    "error": "title: 제목은 필수입니다., category: 카테고리는 필수입니다."
}

비즈니스 검증 (Service)

Bean Validation 이외의 비즈니스 규칙은 Service에서 검증한다.

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostService {

    private final PostRepository postRepository;

    @Transactional
    public PostDetailRes regPost(PostCreateReq req) {
        // 비즈니스 검증 — 중복 제목 확인
        if (postRepository.existsByTitle(req.getTitle())) {
            throw new IllegalArgumentException("이미 존재하는 제목입니다: " + req.getTitle());
        }
        Post post = Post.builder()
                .title(req.getTitle())
                .content(req.getContent())
                .build();
        return PostDetailRes.from(postRepository.save(post));
    }
}

커스텀 검증 어노테이션

반복되는 검증 패턴은 커스텀 어노테이션으로 추출한다.

// 허용 카테고리 검증 어노테이션
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidCategoryValidator.class)
public @interface ValidCategory {
    String message() default "허용되지 않은 카테고리입니다.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 검증 구현체
public class ValidCategoryValidator implements ConstraintValidator<ValidCategory, String> {

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) return true;  // null은 @NotBlank로 처리
        return CategoryConstant.ALLOWED_CATEGORIES.contains(value);
    }
}

검증 체크리스트

  • [ ] 요청 VO에 Bean Validation 어노테이션 적용
  • [ ] 컨트롤러 메서드 파라미터에 @Valid 적용
  • [ ] 비즈니스 검증은 Service에서 처리
  • [ ] 검증 실패 메시지는 사용자가 이해할 수 있는 한국어로 작성
  • [ ] GlobalExceptionHandler에서 검증 실패 처리 확인
  • [ ] 파일 업로드: 확장자 + MIME 타입 이중 검증
AI 문서 검색

현재 페이지 내용을 기반으로 질문하세요.