๐ŸƒSpring Bean Validation๊ณผ API ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

2 minute read

Bean Validation

  • ํŠน์ • ํ•„๋“œ์— ๋Œ€ํ•œ ๊ฒ€์ฆ ๋กœ์ง์€ ๋Œ€๋ถ€๋ถ„ ๋นˆ ๊ฐ’์ธ์ง€ ์•„๋‹Œ์ง€, ํŠน์ • ํฌ๊ธฐ๋ฅผ ๋„˜๋Š”์ง€ ์•„๋‹Œ์ง€์™€ ๊ฐ™์ด ๋งค์šฐ ์ผ๋ฐ˜์ ์ธ ๋กœ์ง์ด๋‹ค.
  • ์• ๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด์„œ ๊ฒ€์ฆ๋กœ์ง์„ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ‘œ์ค€ํ™”ํ•œ ๊ฒƒ์ด Bean Validation์ด๋‹ค.

์˜์กด๊ด€๊ณ„ ์ถ”๊ฐ€

  • org.springframework.boot:spring-boot-starter-validation

์‚ฌ์šฉ๋ก€

public class Item {  
  
	private Long id;  
  
	@NotBlank  
	private String itemName;  
  
	@NotNull  
	@Range(min = 1000, max = 1000000)  
	private Integer price;  
  
	@NotNull  
	@Max(9999)  
	private Integer quantity;  
}  

ValidatorFactory

  • ์Šคํ”„๋ง์—์„œ๋Š” ์ง์ ‘ ์‚ฌ์šฉํ•  ์ผ์ด ์—†์ง€๋งŒ ์ฐธ๊ณ ๋กœ ์•Œ์•„๋‘”๋‹ค.
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();  
Validator validator = factory.getValidator();  
  
Set<ConstraintViolation<Item>> violations = validator.validate(item)  
for (ConstraintViolation<Item> violation : violations) {  
	violation.getMessage();	// ex: violation.message=1000์—์„œ 1000000 ์‚ฌ์ด์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค  
}  

์Šคํ”„๋ง๊ณผ Bean Validator

  • spring-boot-starter-validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ๋„ฃ์œผ๋ฉด ์Šคํ”„๋ง ๋ถ€ํŠธ๊ฐ€ ์ž๋™์œผ๋กœ Bean Validator๋ฅผ ์ธ์ง€ํ•˜๊ณ  LocalValidatorFactoryBean์„ ๊ธ€๋กœ๋ฒŒ Validator๋กœ ๋“ฑ๋กํ•œ๋‹ค.
  • @Valid๋‚˜ @Validated ์• ๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋‹ค. ์ „์ž๋Š” ์ž๋ฐ” ํ‘œ์ค€, ํ›„์ž๋Š” ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

์˜ˆ์ œ

@PostMapping("/add")  
public String addItem(@Validated @RequestBody Dto dto, BindingResult bindingResult) {  
	if (bindingResult.hasError()) {  
		log.info(bindingResult);  
		return bindingResult.getAllErrors();  
	}  
}  
  • ์‚ฌ์šฉ๋ฒ•์„ ์œ„ํ•œ ์ธ์œ„์ ์ธ ์˜ˆ์ œ์ด๋‹ค. ์‹ค์ œ๋กœ๋Š” Request๊ฐ€ Dto๋กœ ๋ณ€ํ™˜์ž์ฒด๊ฐ€ ์•ˆ๋˜์„œ ํ•ธ๋“ค๋Ÿฌ ์•ˆ์œผ๋กœ ๋“ค์–ด๊ฐ€์ง€ ๋ชปํ•˜๊ณ  ์ด๋ฏธ ์‹คํŒจํ•˜๊ฒŒ ๋œ๋‹ค.

API ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

  • ์˜ˆ์™ธ๋ฅผ ๋˜์กŒ์„๋•Œ(throw) ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด๋ถ€์—์„œ ์•„๋ฌด๋„ ๊ทธ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?
  • ์˜ˆ์™ธ๋Š” ์„œ๋ธ”๋ฆฟ์„ ํ˜ธ์ถœํ•œ WAS๊นŒ์ง€ ์ „๋‹ฌ๋œ๋‹ค. ๊ทธ๋ฆฌ๊ณ  MVC์˜ ๊ฒฝ์šฐ ์˜ˆ์™ธ ํŽ˜์ด์ง€, API์˜ ๊ฒฝ์šฐ 500๋ฒˆ๋Œ€์˜ ์„œ๋ฒ„ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
  • ์˜ˆ์™ธ์˜ ๊ธฐ๋ณธ์ฒ˜๋ฆฌ๋Š” response.sendError(statusCode, reason)์„ ํ†ตํ•ด WAS์— ์ „๋‹ฌ๋˜๊ณ , ๋‹ค์‹œ ์„œ๋ธ”๋ฆฟ์„ ํ†ตํ•ด ์—๋ŸฌํŽ˜์ด์ง€ ํ˜น์€ ์—๋Ÿฌ์ฒ˜๋ฆฌ ์„œ๋ธ”๋ฆฟ์ด ํ˜ธ์ถœ๋˜๋Š” ๊ตฌ์กฐ์ด๋‹ค.
  • ๋””ํ…Œ์ผํ•˜๊ฒŒ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์Šคํ”„๋ง์—์„œ๋Š” ExceptionResolver๋ฅผ ์ œ๊ณตํ•œ๋‹ค.

ExceptionResolver

  • ์šฐ์„ ์ˆœ์œ„๋Š” ExceptionHandlerExceptionResolver, ResponseStatusExceptionResolver, DefaultHandlerExceptionResolver์ˆœ์ด๋‹ค.

DefaultHandlerExceptionResolver

  • ์Šคํ”„๋ง ๋‚ด๋ถ€์˜ ๊ธฐ๋ณธ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.

ResponseStatusExceptionResolver

  • ์˜ˆ์™ธ์— ๋”ฐ๋ผ HTTP Status ์ฝ”๋“œ๋ฅผ ์ง€์ •ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.
  • @ResponseStatus๊ฐ€ ๋‹ฌ๋ ค์žˆ๋Š” ์˜ˆ์™ธ, ResponseStatusException ์˜ˆ์™ธ ๋“ฑ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • ๋‚ด๋ถ€ ๊ตฌํ˜„์ ์œผ๋กœ๋Š” ์ด๊ฒƒ๋„ sendError()๋ฅผ ํ†ตํ•ด ๋‹ค์‹œ ์„œ๋ธ”๋ฆฟ์ด ํ˜ธ์ถœ๋˜๋Š” ๊ตฌ์กฐ์ด๋‹ค.
// @ResponseStatus  
@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "์ž˜๋ชป๋œ ์š”์ฒญ ์˜ค๋ฅ˜")  
public class BadRequestException extends RuntimeException {  
  
}  
  
// ResponseStatusException - ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋“ฑ ์˜ˆ์™ธ๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•˜๊ธฐ ์–ด๋ ค์šด ๊ฒฝ์šฐ ์‚ฌ์šฉ  
@GetMapping("/responseExceptionTest")  
public String responseStatusExample() {  
	throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error", new IllegalArgumentException());  
}  

ExceptionHandlerExceptionResolver

  • ๊ฐ€์žฅ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’๊ณ  ๊ฐ€์žฅ ๋งŽ์ด ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹์ด๋‹ค.
  • ์ง€์ •ํ•œ ์˜ˆ์™ธ ๋˜๋Š” ๊ทธ ์˜ˆ์™ธ์˜ ์ž์‹ ํด๋ž˜์Šค๋ฅผ ๋ชจ๋‘ ์žก์„ ์ˆ˜ ์žˆ๋‹ค.
@RestController  
public class ApiExceptionController {  
  
	// ์ปจํŠธ๋กค๋Ÿฌ ๋‚ด์— ์˜ˆ์™ธ์ฒ˜๋ฆฌ ํด๋ž˜์Šค ์ •์˜  
	@ResponseStatus(HttpStatus.BAD_REQUEST)  
	@ExceptionHandler(IllegalArgumentException.class)		// ์˜ˆ์™ธ ํด๋ž˜์Šค๋Š” ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.  
	public ErrorResult illegalExHandler(IllegalArgumentException e) {  
		log.error(e);  
		return new ErrorResult("BAD", e.getMessage());  
	}  
  
	// ๋™์ ์œผ๋กœ ์ƒํƒœ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ์„ ๋•Œ  
	@ExceptionHandler(IllegalArgumentException.class)		// ์˜ˆ์™ธ ํด๋ž˜์Šค๋Š” ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.  
	public ErrorResult illegalExHandler(IllegalArgumentException e) {  
		ErrorResult errorResult = new ErrorResult("BAD", e.getMessage());  
		return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);  
	}  
  
	// API  
	@GetMapping("/responseExceptionTest")  
	public String responseStatusExample() {  
		throw new IllegalArgumentException("์ž˜๋ชป๋œ ์ž…๋ ฅ");  
	}  
}  
  • ์œ„ ์ฝ”๋“œ๋Š” API๋ฅผ ์œ„ํ•œ ์ฝ”๋“œ์™€ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ฝ”๋“œ๊ฐ€ ์„ž์—ฌ์žˆ์–ด ์‘์ง‘๋„๊ฐ€ ๋–จ์–ด์ง„๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ @ControllerAdvice, @RestControllerAdvice ๋“ฑ์„ ์‚ฌ์šฉํ•ด ๋ถ„๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.
// ๋Œ€์ƒ์„ ์ง€์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ชจ๋“  ์ปจํŠธ๋กค๋Ÿฌ์— ๊ธ€๋กœ๋ฒŒ๋กœ ์ ์šฉ  
@ControllerAdvice  
  
// ํŠน์ • ์• ๋…ธํ…Œ์ด์…˜์„ ๊ฐ€์ง„ ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์ ์šฉํ•˜๋Š” ๊ฒฝ์šฐ  
@ControllerAdvice(annotations = RestController.class)  
  
// ํŠน์ • ํŒจํ‚ค์ง€ ์•ˆ์˜ ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์ ์šฉํ•˜๋Š” ๊ฒฝ์šฐ  
@ControllerAdvice("org.example.controllers")  
  
  
// ํŠน์ • ์ปจํŠธ๋กค๋Ÿฌ์—๋งŒ ์ ์šฉํ•˜๋Š” ๊ฒฝ์šฐ  
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbsController.class})  
public class ApiExceptionAdvice {  
  
	@ResponseStatus(HttpStatus.BAD_REQUEST)  
	@ExceptionHandler(IllegalArgumentException.class)		// ์˜ˆ์™ธ ํด๋ž˜์Šค๋Š” ์ƒ๋žตํ•  ์ˆ˜ ์žˆ๋‹ค.  
	public ErrorResult illegalExHandler(IllegalArgumentException e) {  
		log.error(e);  
		return new ErrorResult("BAD", e.getMessage());  
	}  
  
	// ๋‹ค๋ฅธ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ์ฝ”๋“œ  
	// โ€ฆ  
}