프로젝트 개요 : 이것이 취업을 위한 백엔드 개발이다 클론코딩 - 상품관리 애플리케이션
프로젝트 환경 : IntelliJ, SpringBoot, MySQL
프로젝트 코드 : https://github.com/smkim9202/ProductManagement
@RestControllerAdvice 유효성 검사 예외 처리
예외 클래스도 패키지 이름이 있고, 예외를 처리 할 때는 패키지 이름보다 예외 이름이 더 중요
이름은 해당 예외에 관한 힌트를 제공, 상속 관계를 활용하면 모든 예외에 대해 하나하나 처리할 필요 없음
- 도메인 객체 : jakarta.validation.ConstraintViolationException
- 컨트롤러 : org.springframework.web.bind.MethodArgumentNotValidException
ConstraintViolationException
400에러 응답오도록 예외처리
GlobalExceptionHandler.java
package kr.co.api.product.management.presentation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<String> handleConstraintViolatedException(ConstraintViolationException ex){
//예외에 대한 처리
String errorMessage = "오류 발생";
return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
}
}
ConstraintViolationException 예외처리
도메인 객체에 대한 유효성 검사가 실패했을 경우 500에러가 던져지는 상황을 400 에러가 응답 오도록 예외처리
//요청
{"name":"연필","price":200,"amount":1000000000}
//응답 400 Bad Request
오류 발생
응답 바디에 에러 정보 추가
GlobalExceptionHandler.java
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorMessage> handleConstraintViolatedException(ConstraintViolationException ex){
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
List<String> errors = new ArrayList<>();
for(ConstraintViolation<?> constraintViolation : constraintViolations ) {
errors.add(constraintViolation.getPropertyPath() + ", " + constraintViolation.getMessage());
}
// 불변 리스트로 변환 (Stream의 toList()와 동일한 효과)
errors = List.copyOf(errors);
ErrorMessage errorMessage = new ErrorMessage(errors);
return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
}
ex.getConstraintViolations()
검증 실패 항목들을 Set 형태로 가져옴
getPropertyPath(), getMessage()
실패한 필드명, 필드의 검증 실패 메시지
List.copyOf()
원본 리스트를 복사하여 불변 리스트(추가/삭제 불가) 생성
이후에 새로운 요소 추가시 UnsupportedOperationException 발생
스트림에서 toList() 할 경우 스트림 결과를 리스트로 변환하고, Java16부터는 불변 리스트를 반환
ErrorMessage.java
package kr.co.api.product.management.presentation;
import java.util.List;
public class ErrorMessage {
private List<String> errors;
public ErrorMessage(List<String> errors) {
this.errors = errors;
}
public List<String> getErrors() {
return errors;
}
}
ErrorMessage 사용 이유
List<String> 형태로 반환하면 JSON 필드 이름 없이 문자열 배열 형태로 전달되어 어떤 정보인지 명확하게 파악되지 않음
유효성 검사 실패 외에도 여러 가지 에러 메시지를 일관된 형태로 제공하고 싶다면 커스텀한 에러 응답 클래스를 만드는게 좋음
//요청
{"name":"연필","price":200000000,"amount":1000000000}
//응답 400 Bad Request
{
"errors": [
"checkValid.validationTarget.amount, 9999 이하여야 합니다",
"checkValid.validationTarget.price, 1000000 이하여야 합니다"
]
}
응답 바디에 사용하는 메서드 노출 제거
GlobalExceptionHandler.java
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorMessage> handleConstraintViolatedException(ConstraintViolationException ex){
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
List<String> errors = new ArrayList<>();
for(ConstraintViolation<?> constraintViolation : constraintViolations ) {
errors.add(extractField(constraintViolation.getPropertyPath()) + ", " + constraintViolation.getMessage());
}
// 불변 리스트로 변환 (Stream의 toList()와 동일한 효과)
errors = List.copyOf(errors);
ErrorMessage errorMessage = new ErrorMessage(errors);
return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
}
private String extractField(Path path){
String[] splittedArray = path.toString().split("[.]");
int lastIndex = splittedArray.length - 1;
return splittedArray[lastIndex];
}
//요청
{"name":"연필","price":200000000,"amount":1000000000}
//응답 400 Bad Request
{
"errors": [
"amount, 9999 이하여야 합니다",
"price, 1000000 이하여야 합니다"
]
}
MethodArgumentNotVaildException
GlobalExceptionHandler.java
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorMessage> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex){
List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();
List<String> errors = new ArrayList<>();
for(FieldError fieldError : fieldErrors ) {
errors.add(fieldError.getField() + ", " + fieldError.getDefaultMessage());
}
// 불변 리스트로 변환 (Stream의 toList()와 동일한 효과)
errors = List.copyOf(errors);
ErrorMessage errorMessage = new ErrorMessage(errors);
return new ResponseEntity(errorMessage, HttpStatus.BAD_REQUEST);
}
//요청
{"name":"연필"}
//응답 400 Bad Request
{
"errors": [
"price, 널이어서는 안됩니다",
"amount, 널이어서는 안됩니다"
]
}
예외 처리 전략
Checked Exception과 Unchecked Exception
Checked Exception
- try-catch 문이 강제되는 예외
- Exception 클래스를 상속받는 예외들
Unchecked Exception
- try-catch 문이 강제되지 않는 예외
- RuntimeException 클래스를 상속받는 예외들
- 예외 처리 핸들러, 또는 필요하다면 try-catch문을 통해 예외 처리 가능
클래스 상속 관계도
- Throwable은 클래스(인터페이스 아님)
- Error와 Exception은 다름
- Error : OutOfMemoryError, StackOverflowError 메모리 관련해 발생한 문제, 로직 작성 어렵고 원래라면 발생하면 안됨
- Exception : 애플리케이션에서 자연스럽게 발생, 일반적으로 개발자가 적절히 처리하기 위한 로직 작성 가능
코드에서 발생하는 예외를 커스텀 예외로 바꿔 던지기
- NotSuchElementException : Optional이나 DB에서 id에 해당하는 요소를 찾지 못해서 발생하는 예외로 타입을 통해 서버 코드가 무엇으로 구현되어 있는지 클라이언트 코드에서 알게 되어 Repository 코드의 캡슐화를 깨드리는 예외
- Product를 찾지 못해 발생한 예외를 새로 정의 : Product처럼 식별자를 가지는 도메인 객체들이 추가될 것을 감안하여 범용적으로 작성
EntityNotFoundException.java
package kr.co.api.product.management.domain;
public class EntityNotFoundException extends RuntimeException{
public EntityNotFoundException(String message){
super(message);
}
}
도메인 계층에 넣은 이유
엔티티를 찾지 못했을 경우 발생하는 예외기 때문
레이어드 아키텍처에서 모든 계층은 도메인 계층을 의존할 수 있으므로, 모든 계층에서 사용되어야 하기 때문
ListProductRepository.java
public Product findById(Long id){
Product foundProduct = null;
for (Product product : products) {
if (product.sameId(id)) {
foundProduct = product;
break;
}
}
if (foundProduct == null) {
throw new EntityNotFoundException("Product를 찾지 못했습니다.");
}
return foundProduct;
}
GlobalExceptionHandler.java
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorMessage> handleEntityNotFoundExceptionException(
EntityNotFoundException ex
) {
List<String> errors = new ArrayList<>();
errors.add(ex.getMessage());
ErrorMessage errorMessage = new ErrorMessage(errors);
return new ResponseEntity<>(errorMessage, HttpStatus.NOT_FOUND);
}
404 Not Found를 응답 상태 코드로 사용한 이유
URL의 경로에 해당하는 자원을 찾지 못했다는 의미
URL 경로에 존재하는 id를 가진 Product가 없기 때문
//GET요청 : 존재하지 않는 ID
//응답 404 Not Found
{
"errors": [
"Product를 찾지 못했습니다."
]
}
'프로젝트 > clone coding' 카테고리의 다른 글
[백엔드 개발 : ProductManagement] 상품 관리 애플리케이션 기능 구현(MySQL) (0) | 2025.04.05 |
---|---|
[백엔드 개발 : ProductManagement] 상품 관리 애플리케이션 DB 연동(MySQL) (0) | 2025.04.04 |
[백엔드 개발 : ProductManagement] 유효성 검사 추가 (0) | 2025.03.20 |
[백엔드 개발 : ProductManagement] 상품 조회/수정/삭제 구현 (0) | 2025.03.19 |
[백엔드 개발 : ProductManagement] DTO와 getter, setter (0) | 2025.03.18 |