기록

[JpaTest] Spring Bean Validation 활용 가이드 본문

교육/강의

[JpaTest] Spring Bean Validation 활용 가이드

youngyin 2025. 2. 4. 00:00

시작하면서

Spring Framework에서는 Bean Validation을 통해 데이터 유효성을 간편하게 검증할 수 있습니다. 이 글에서는 주요 어노테이션, Validation 적용 위치, 그리고 실제 사용 사례를 중심으로 Spring Bean Validation을 활용하는 방법을 알아보겠습니다.


1. 주요 어노테이션

Bean Validation에서 자주 사용되는 어노테이션은 다음과 같습니다:

  • @NotNull: 값이 null이 아니어야 함.
  • @NotEmpty: 빈 문자열("")이나 null이 아닌 값이어야 함 (컬렉션, 배열 등도 지원).
  • @NotBlank: 공백만으로 이루어진 문자열이 아닌 값을 검증.
  • @Positive: 양수 값만 허용.
  • @Size(min = , max = ): 문자열, 컬렉션, 배열 등의 크기 제약.
  • @Email: 이메일 형식 검증.

2. Validation 사용 사례

2.1 의존성 추가

먼저, spring-boot-starter-validation 의존성을 추가해야 합니다:

implementation 'org.springframework.boot:spring-boot-starter-validation'

2.2 DTO 클래스에서의 유효성 검사

DTO 클래스에서 필드 제약 조건을 정의하여 유효성을 검증할 수 있습니다:

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class OrderCreateRequest {

    @NotEmpty(message = "상품번호 리스트는 비어있을 수 없습니다.")
    private List<String> productNumbers;

    @Builder
    public OrderCreateRequest(List<String> productNumbers) {
        this.productNumbers = productNumbers;
    }

    public OrderCreateServiceRequest toServiceRequest() {
        return OrderCreateServiceRequest.builder()
                .productNumbers(productNumbers)
                .build();
    }
}

2.3 Controller 메서드에서 유효성 검사

Controller 메서드에서 @Valid 어노테이션을 사용하여 DTO 유효성 검사를 수행할 수 있습니다:

@RequiredArgsConstructor
@RestController
public class OrderController {

    private final OrderService orderService;

    @PostMapping("/api/v1/orders/new")
    public ApiResponse<OrderResponse> createOrder(@RequestBody @Valid OrderCreateRequest request) {
        LocalDateTime now = LocalDateTime.now();
        return ApiResponse.ok(orderService.createOrder(request.toServiceRequest(), now));
    }
}

요청 본문 유효성 검사가 실패하면 Spring은 자동으로 MethodArgumentNotValidException을 발생시킵니다. 이로 인해 클라이언트는 HTTP 400 상태 코드와 함께 기본적인 에러 메시지를 받게 됩니다. 컨트롤러에서는 일반적인 형식과 간단한 제약 조건만 검증하며, 세부적인 비즈니스 규칙은 서비스 계층에서 처리하는 것이 좋습니다.

2.4 비즈니스 로직에서의 유효성 검사

비즈니스 계층에서는 더 복잡한 도메인 로직과 비즈니스 규칙을 검증합니다. 예를 들어:

import jakarta.validation.Valid;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    public OrderResponse createOrder(@Valid OrderCreateServiceRequest request, LocalDateTime now) {
        if (request.getProductNumbers().size() > 20) {
            throw new IllegalArgumentException("상품 번호는 최대 20개까지 가능합니다.");
        }
        // 주문 생성 로직
        return new OrderResponse();
    }
}

사실 위 로직은 컨트롤러 단에서 어노테이션으로 Validation 처리가 가능합니다. 하지만 컨트롤러에서 모든 검증 로직을 처리하면 유지보수성과 코드의 명확성이 떨어질 수 있습니다. 특히 도메인 규칙이나 서비스 정책과 같은 복잡한 로직은 서비스 계층에서 처리하는 것이 더 적합합니다. 이를 통해 컨트롤러는 간단한 역할에 집중하고, 서비스 레이어는 비즈니스 로직의 변경에 유연하게 대응할 수 있습니다.

강의에서는 다음과 같은 기준을 제시했습니다:

  1. 기본적인 null 체크, 빈값 체크는 Validation 어노테이션으로 처리해도 됩니다.
  2. 도메인 규칙(예: 데이터베이스 타입 제약)이나 서비스 정책과 관련된 로직은 서비스 레이어에서 처리해야 합니다.

그 이유는 정책이나 도메인 규칙이 변경되더라도 DTO를 변경하지 않고 유지하기 위함입니다. DTO는 주로 요청과 응답의 형식을 정의하는 역할을 하기 때문에, 비즈니스 로직이 DTO에 섞이지 않도록 설계하는 것이 유지보수에 유리합니다. 만약 DTO에 비즈니스 로직이 섞이게 되면, 정책 변경 시 DTO와 관련된 모든 계층을 수정해야 할 가능성이 높아집니다.

컨트롤러와 비즈니스 레이어를 나누는 이유는 다음과 같습니다:

  1. 유지보수성: 컨트롤러는 단순히 요청을 받아들이고 응답을 반환하는 역할만 하도록 하고, 정책 변경 시 비즈니스 로직만 수정합니다.
  2. 재사용성: 동일한 검증 로직을 여러 곳에서 재사용할 수 있습니다.
  3. 책임 분리: 각 계층이 자신의 역할에 충실하도록 설계합니다.

3. 유효성 검사 실패 처리

Spring에서는 유효성 검사 실패 시 예외를 처리하기 위한 전역 예외 처리기를 설정할 수 있습니다:

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<String> handleValidationExceptions(MethodArgumentNotValidException ex) {
        String errorMessage = ex.getBindingResult().getFieldErrors()
            .stream()
            .map(error -> error.getField() + " : " + error.getDefaultMessage())
            .reduce("", (msg1, msg2) -> msg1 + "\n" + msg2);

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorMessage);
    }
}

@ControllerAdvice는 Spring MVC에서 예외 처리, 모델 데이터 설정, 데이터 바인딩과 같은 기능을 전역적으로 제공하는 데 사용됩니다.

@ControllerAdvice와 @RestControllerAdvice

  • @RestControllerAdvice는 @ControllerAdvice와 동일한 기능을 제공하지만, RESTful 웹 서비스에 특화된 예외 처리 방식입니다. @ResponseBody가 기본적으로 포함되어 있어 메서드의 반환값이 JSON 형식으로 변환됩니다.
    • @ControllerAdvice: JSP/Thymeleaf와 같은 뷰를 사용하는 애플리케이션에 적합합니다.
    • @RestControllerAdvice: JSON/XML 형식의 응답을 처리하는 RESTful 애플리케이션에 적합합니다.

결론

Spring Bean Validation은 간단한 어노테이션으로 데이터 유효성을 보장하며, Controller와 Business Layer에서 각각의 역할에 맞게 사용될 때 가장 큰 장점을 발휘합니다.

  • 컨트롤러: 기본적인 형식 검사와 간단한 제약 조건 검사.
  • 비즈니스 레이어: 도메인 로직과 비즈니스 규칙에 따른 유효성 검사.

이렇게 계층 간 책임을 명확히 분리하면, 코드의 유지보수성과 재사용성을 높이고, 정책 변경 시 최소한의 수정으로 대응할 수 있습니다.

테스트 방법으로는 JUnit과 같은 테스트 프레임워크를 사용하여 DTO와 비즈니스 로직의 유효성 검증을 자동화할 수 있습니다. 예를 들어, MockMvc를 활용하여 Controller 계층의 요청과 응답을 테스트하거나, Validator를 직접 호출하여 DTO의 유효성을 확인할 수 있습니다.

 
Comments