Develop

[Spring Boot] 게시판 - 05. Validation 검증 (입력값 유효성 검사) 본문

백엔드/게시판 만들기

[Spring Boot] 게시판 - 05. Validation 검증 (입력값 유효성 검사)

230801 2025. 11. 23. 06:08

안녕하세요 .ᐟ

게시판 프로젝트 5일차입니다.

 

오늘은 사용자 입력값을 검증하는 Validation을 구현했습니다.

 

 

 

오늘의 학습 목표

  • Validation이 필요한 이유
  • @Valid와 Bean Validation 어노테이션
  • DTO에 검증 로직 적용
  • 검증 실패 시 에러 응답 처리

 

Validation을 왜 해야 할까?

// 기존 코드 - 검증 없음
@PostMapping
public ResponseEntity<PostResponse> createPost(@RequestBody PostCreateRequest request) {
    return ResponseEntity.ok(postService.createPost(request, userId));
}

 

 

1. 잘못된 데이터가 서비스 레이어까지 도달함
- 빈 문자열, null 값이 그대로 DB에 저장될 수 있음
- 서비스 로직에서 NullPointerException 발생 가능

2. 보안 취약점
- 악의적인 사용자가 비정상적인 데이터를 전송할 수 있음
- SQL Injection, XSS 등의 공격에 노출

3. 데이터 무결성 훼손
- 이메일 형식이 아닌 값이 이메일 컬럼에 저장
- 너무 긴 문자열로 인한 DB 에러

 


목표: Controller 진입 시점에 검증

요청 → Controller(@Valid) → Service → Repository → DB
                   ↓ 실패
            400 Bad Request
           (어떤 필드가 왜 틀렸는지 알려줌)

 

 


 

검증은 어디서 해야 할까?

Controller 입력값 형식 검증 빈 값, 이메일 형식, 문자열 길이
Service 비즈니스 규칙 검증 작성자 본인 확인, 중복 체크
Repository 데이터 무결성 Unique 제약조건, FK 체크

 

이번에 구현할 것

  • Controller 레벨: @Valid + Bean Validation 어노테이션
  • Service 레벨: 비즈니스 규칙 (이미 4일차에 구현)

 


 

사용할 도구

Bean Validation 어노테이션

@NotNull null 불가 필수값
@NotEmpty null, 빈 문자열("") 불가 컬렉션, 문자열
@NotBlank null, 빈 문자열, 공백만 있는 문자열 불가 문자열 필수값
@Size 크기 제한 문자열 길이, 컬렉션 크기
@Email 이메일 형식 검증 이메일 필드
@Min / @Max 숫자 범위 나이, 수량
@Pattern 정규표현식 검증 전화번호, 비밀번호

 

 

@NotNull vs @NotEmpty vs @NotBlank

String value = "";        // 빈 문자열
String value = "   ";     // 공백만 있는 문자열
String value = null;      // null

@NotNull    → null만 불가 (빈 문자열 허용)
@NotEmpty   → null, 빈 문자열 불가 (공백만 있는 문자열 허용)
@NotBlank   → null, 빈 문자열, 공백만 있는 문자열 모두 불가

 

결론: 문자열 필수값에는 @NotBlank를 사용하자!

 


 

구현하기

1. 의존성 확인

Spring Boot 3.x에서는 spring-boot-starter-validation이 필요합니다.

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

 

2. Request DTO에 검증 어노테이션 추가

 

UserCreateRequest

@Getter
@NoArgsConstructor
public class UserCreateRequest {

    @NotBlank(message = "이메일은 필수입니다.")
    @Email(message = "올바른 이메일 형식이 아닙니다.")
    private String email;

    @NotBlank(message = "비밀번호는 필수입니다.")
    @Size(min = 8, max = 20, message = "비밀번호는 8~20자여야 합니다.")
    private String password;

    @NotBlank(message = "닉네임은 필수입니다.")
    @Size(min = 2, max = 10, message = "닉네임은 2~10자여야 합니다.")
    private String nickname;
}

 

 

 

3. Controller에 @Valid 적용

  • @RequestBody 앞에 @Valid를 붙이면 자동으로 검증됩니다!
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    @PostMapping
    public ResponseEntity<UserResponse> createUser(
            @Valid @RequestBody UserCreateRequest request) {  // @Valid 추가!
        return ResponseEntity.ok(userService.createUser(request));
    }

    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UserUpdateRequest request) {  // @Valid 추가!
        return ResponseEntity.ok(userService.updateUser(id, request));
    }
}

 

 

 

4. 검증 실패 시 에러 처리 (GlobalExceptionHandler)

  • 4일차에 구현한 GlobalExceptionHandler에 이미 추가되어 있습니다.
@RestControllerAdvice
public class GlobalExceptionHandler {

    // @Valid 검증 실패 시 발생
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        
        List<ErrorResponse.FieldError> fieldErrors = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(error -> ErrorResponse.FieldError.builder()
                        .field(error.getField())
                        .value(error.getRejectedValue() != null ? 
                               error.getRejectedValue().toString() : "")
                        .reason(error.getDefaultMessage())
                        .build())
                .collect(Collectors.toList());

        ErrorResponse response = ErrorResponse.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .code("4000")
                .message("잘못된 입력값입니다")
                .fieldErrors(fieldErrors)
                .build();

        return ResponseEntity.badRequest().body(response);
    }
}

 

 

검증 흐름 정리

1. 클라이언트가 POST /users 요청

2. Controller에서 @Valid가 UserCreateRequest 검증
   - email이 빈 값? → MethodArgumentNotValidException 발생
   - password가 7자? → MethodArgumentNotValidException 발생

3. GlobalExceptionHandler가 예외 catch

4. fieldErrors에 실패한 필드 정보 담아서 응답

 

 


 

 

Validation vs 비즈니스 예외 차이

발생 시점 Controller 진입 전 Service 로직 실행 중
예외 타입 MethodArgumentNotValidException BusinessException (커스텀)
응답 코드 400 Bad Request 상황에 따라 다름 (403, 404, 409 등)
fieldErrors 포함 (어떤 필드가 틀렸는지) 미포함
예시 빈 값, 형식 오류, 길이 초과 리소스 없음, 권한 없음, 중복

 


 

느낀 점

@Valid의 편리함

  • Controller에 @Valid 하나만 붙이면 자동으로 검증됩니다.
  • 검증 로직이 DTO에 모여있어 관리가 쉬웠습니다.
  • Service 코드가 깔끔해졌습니다. (입력값 검증 코드 불필요)

 

명확한 에러 메시지의 중요성

  • message 속성으로 사용자 친화적인 메시지를 제공할 수 있었습니다.
  • 프론트엔드 와 협업 시 어떤 필드가 왜 틀렸는지 바로 알 수 있습니다.

 

검증 계층 분리

  • 형식 검증 → Controller (Bean Validation)
  • 비즈니스 검증 → Service (Custom Exception)
  • 역할이 명확히 분리되어 코드가 깔끔해졌습니다.