패키지 구조
상속 구조
ConcurCouponException 예외 클래스는 비즈니스 로직을 작성하다 발생하는 예외를 모아둘 최상위 클래스이다. ConcurCouponException을 상속받은 구조로 비즈니스 로직 관련 예외를 만든다.
예를 들어 조회 대상이 없는 경우에 대한 예외를 정의하는 NotFoundException 등을 만든다.
ErrorCode
public enum ErrorCode {
//400: BAD_REQUEST
INVALID_REQUEST_ARGUMENT("잘못된 요청입니다."),
COUPON_OVER_AMOUNT("발급 가능한 쿠폰 수량을 초과했습니다."),
COUPON_OVER_AMOUNT_PER_MEMBER("한사람당 발급 가능한 쿠폰 수량을 초과했습니다."),
NOT_ISSUED_TIME("쿠폰 발급 가능한 시간이 아닙니다."),
//401 UNAUTHORIZED
EXPIRED_AUTH_TOKEN("만료된 로그인 토큰입니다."),
INVALID_AUTH_TOKEN("올바르지 않은 로그인 토큰입니다."),
NOT_BEARER_TOKEN_TYPE("Bearer 타입의 토큰이 아닙니다."),
NEED_AUTH_TOKEN("로그인이 필요한 서비스입니다."),
INCORRECT_PASSWORD_OR_ACCOUNT("비밀번호가 틀렸거나, 해당 계정이 없습니다."),
DUPLICATE_ACCOUNT_USERNAME("해당 계정이 존재합니다."),
INVALID_REFRESH_TOKEN("존재하지 않은 리프래쉬 토큰으로 재발급 요청을 했습니다."),
EXPIRED_REFRESH_TOKEN("만료된 리프래쉬 토큰입니다."),
//403 FORBIDDEN
NOT_ENOUGH_PERMISSION("해당 권한이 없습니다."),
//404 NOT_FOUND
COUPON_GROUP_NOT_FOUND("존재하지 않은 그룹 쿠폰입니다."),
MEMBER_NOT_FOUND("존재하지 않는 멤버입니다."),
COUPON_NOT_FOUND("존재하지 않은 쿠폰입니다."),
//500 INTERNAL_SERVER_ERROR
INTERNAL_SERVER_ERROR("서버 내부에 문제가 발생했습니다."),
FOR_TEST_ERROR("테스트용 에러입니다.");
private final String message;
ErrorCode(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
ConcurCouponException
public abstract class ConcurCouponException extends NestedRuntimeException {
private final ErrorCode errorCode;
protected ConcurCouponException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
protected ConcurCouponException(ErrorCode errorCode, Throwable cause) {
super(errorCode.getMessage(), cause);
this.errorCode = errorCode;
}
protected ConcurCouponException(ErrorCode errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public ErrorCode getErrorCode() {
return errorCode;
}
}
BadRequestException
public class BadRequestException extends ConcurCouponException {
public BadRequestException(ErrorCode errorCode) {
super(errorCode);
}
}
NotFoundException
public class NotFoundException extends ConcurCouponException {
public NotFoundException(ErrorCode errorCode) {
super(errorCode);
}
}
ConcurCouponExceptiond에서 NestedRuntimeException을 상속하는 이유
- 예외 체인의 유지: 예외가 발생했을 때, 원래 예외를 포착하고 이를 새로운 예외로 감싸서 던질 때 유용합니다. 이를 통해 원래의 예외 메시지와 스택 트레이스를 보존할 수 있습니다. 이는 문제를 디버깅할 때 유용합니다.
public class CustomBusinessException extends NestedRuntimeException {
public CustomBusinessException(String msg) {
super(msg);
}
public CustomBusinessException(String msg, Throwable cause) {
super(msg, cause);
}
}
- Spring 프레임워크와의 통합: Spring 프레임워크는 내부적으로 많은 예외를 NestedRuntimeException을 사용하여 처리한다. 따라서 이를 상속받는 커스텀 예외 클래스를 사용하면 Spring의 예외 처리 메커니즘과 더 잘 통합될 수 있다.
ErrorResponse
public record ErrorResponse(
ErrorCode errorCode,
String message
) {
public static ErrorResponse from(ConcurCouponException concurCouponException) {
return ErrorResponse.from(concurCouponException.getErrorCode());
}
public static ErrorResponse from(ErrorCode errorCode) {
return new ErrorResponse(errorCode, errorCode.getMessage());
}
}
GlobalExceptionHandler
@RestControllerAdvice를 사용한 예외 처리 핸들러로 모든 컨트롤러에서 발생하는 예외를 중앙에서 한꺼번에 처리할 수 있다.
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger("ErrorLogger");
private static final String LOG_FORMAT_INFO = "\\n[INFO] - ({} {})";
private static final String LOG_FORMAT_WARN = "\\n[WARN] - ({} {})";
private static final String LOG_FORMAT_ERROR = "\\n[ERROR] - ({} {})";
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handle(BadRequestException e, HttpServletRequest request) {
logError(e,request);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e));
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErrorResponse> handle(NotFoundException e, HttpServletRequest request) {
logError(e,request);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e));
}
private void logError(Exception e, HttpServletRequest request) {
log.error(LOG_FORMAT_ERROR, request.getMethod(), request.getRequestURI(), e);
}
}
적용하지 않았을 때 throw new NotFoundException이나 BadRequestException을 발생한다면
{
"timestamp": "2024-06-06T05:41:32.927+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/v1/coupon-groups/1/member/1/members-coupons"
}
GlobalExceptionHandler 적용하면
{
"errorCode": "COUPON_GROUP_NOT_FOUND",
"message": "존재하지 않은 그룹 쿠폰입니다."
}
예외처리 사용 예시
private Member findMemberById(final Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new NotFoundException(ErrorCode.MEMBER_NOT_FOUND));
}
'Backend > Spring | Spring Boot' 카테고리의 다른 글
[Spring] Filter와 OncePerRequestFilter (0) | 2024.03.13 |
---|---|
[SpringBoot] @PostConstructd와 @PreDestroy (0) | 2024.02.22 |
[Error] TransactionRequiredException: Executing an update/delete query 에러 해결 방법 (1) | 2023.01.27 |
[spring-data-jpa] @Transactional 의 readonly 옵션과 성능 향상 (0) | 2023.01.27 |
[Spring Boot] Lombok이란? Lombok 추가하기 (0) | 2023.01.21 |