
-1000원으로 보내거나, 상품 이름을 비워서 보낸다면 어떻게 해야 할까요?
당연히 서버는 이런 요청을 거절하고 친절하게 에러 이유를 알려줘야 합니다.
오늘은 build.gradle에 의존성을 직접 추가해야 합니다.
아래 전체 코드를 참고하여 수정해 주세요.// ... 기존 내용 생략 ...
dependencies {
// ... 기존 의존성 생략 ...
// [추가] 유효성 검사 라이브러리
implementation 'org.springframework.boot:spring-boot-starter-validation'
}AddProductRequest 클래스에 어노테이션을 붙입니다.
@NotBlank는 문자열이 비어있지 않은지 검사하고,
@Min(1000)은 숫자가 최소값 이상인지 검사합니다.package com.example.tdd_study.product.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class AddProductRequest {
@NotBlank(message = "상품명은 필수입니다.")
private String name;
@Min(value = 1000, message = "상품 가격은 최소 1000원 이상이어야 합니다.")
private int price;
public AddProductRequest(String name, int price) {
this.name = name;
this.price = price;
}
}@Valid 어노테이션을 파라미터 앞에 붙여야 합니다.package com.example.tdd_study.product;
import com.example.tdd_study.product.dto.AddProductRequest;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping("/products")
public void register(@RequestBody @Valid AddProductRequest request) {
productService.register(request.getName(), request.getPrice());
}
}400 Bad Request 에러가 발생하는지 테스트해 보겠습니다.
이것은 우리가 만든 안전장치가 잘 작동하는지 확인하는 중요한 테스트입니다.ProductControllerTest에 실패 케이스를 추가합니다.package com.example.tdd_study.product;
import com.example.tdd_study.product.dto.AddProductRequest;
import com.google.gson.Gson;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private ProductService productService;
@Test
void 상품_등록_성공() throws Exception {
// given
AddProductRequest request = new AddProductRequest("아메리카노", 4000);
Gson gson = new Gson();
String jsonContent = gson.toJson(request);
// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/products")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonContent))
.andExpect(MockMvcResultMatchers.status().isOk());
}
@Test
void 상품명은_필수다() throws Exception {
// given: 이름이 비어있는 잘못된 요청
AddProductRequest request = new AddProductRequest("", 4000);
Gson gson = new Gson();
String jsonContent = gson.toJson(request);
// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/products")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonContent))
.andExpect(MockMvcResultMatchers.status().isBadRequest()); // 400 에러 기대
}
@Test
void 가격은_1000원_이상이어야_한다() throws Exception {
// given: 가격이 너무 낮은 요청
AddProductRequest request = new AddProductRequest("아메리카노", 500);
Gson gson = new Gson();
String jsonContent = gson.toJson(request);
// when & then
mockMvc.perform(MockMvcRequestBuilders.post("/products")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonContent))
.andExpect(MockMvcResultMatchers.status().isBadRequest()); // 400 에러 기대
}
}
200 OK 응답이 내려온다.400 Bad Request 응답이 내려온다.@RestControllerAdvice입니다.MethodArgumentNotValidException이 바로 유효성 검사 실패 시 발생하는 예외입니다.package com.example.tdd_study.common;
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;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
// 에러가 발생한 필드(field)와 에러 메시지(message)를 맵에 담습니다.
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
}{
"name": "상품명은 필수입니다.",
"price": "상품 가격은 최소 1000원 이상이어야 합니다."
}spring-boot-starter-validation을 추가해야 검증 기능을 사용할 수 있습니다.@NotBlank, @Min 등을 사용하여 입력값의 규칙을 정했습니다.MockMvc를 사용하여 잘못된 데이터가 들어왔을 때 400 Bad Request가 발생하는지 검증했습니다.@RestControllerAdvice를 통해 에러 응답을 프론트엔드가 처리하기 쉬운 형태로 통일했습니다.다음 시간에는 지금까지 작성한 코드를 다시 한번 되돌아보며, 테스트하기 쉬운 코드와 어려운 코드의 특징을 분석하고 설계를 개선하는 시간을 갖겠습니다.
@ControllerAdvice를 활용한 다양한 에러 처리 패턴을 설명합니다.