Spring Framework
비즈니스 로직에 대한 예외 처리, 사용자 정의 예외(Custom Exception)
ChaeHing
2023. 4. 18. 01:57
- 체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외이다.
- ClassNotFoundException 등
- 언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미한다.
- NullPointerException, ArrayIndexOutOfBoundsException 등
- RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.
개발자가 의도적으로 예외를 던질 수(throw) 있는 상황
- 백엔드 서버와 외부 시스템과의 연동에서 발생하는 에러 처리
- 시스템 내부에서 조회하려는 리소스(자원, Resource)가 없는 경우
의도적인 예외 던지기/받기(throw/catch)
- 서비스 계층에서 던져진 예외는 API 계층인 Controller의 핸들러 메서드 쪽에서 잡아서 처리
서비스 계층에서 예외(Exception) 던지기(throw)
@Service
public class MemberService {
...
...
public Member findMember(long memberId) {
// TODO should business logic
// (1)
throw new RuntimeException("Not found member");
}
...
...
}
GlobalExceptionAdvice 예외 잡기(catch)
- 공통 예외 처리에서 - Controller에 예외를 처리하기 때문
@RestControllerAdvice
public class GlobalExceptionAdvice {
...
...
@ExceptionHandler
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
System.out.println(e.getMessage());
// ErrorResponse 수정 코드 필요
return null;
}
}
- DB에서 조회되는 회원이 없는 경우 RuntimeException을 던지고(throw) (memberService)
- 던져진 RuntimeException을 예외처리 클래스에서 받는다(catch) (GlobalExceptionAdvice)
- 현재 코드는 회원 정보가 존재하지 않는 경우만 처리됨
- 회원 등록시 이미 존재하는 회원인 경우 (post)
- 패스워드 검증이 안되는 경우등
- 예외로 던질 수 있는 여러 상황이 존재한다.
- RuntimeException이라는 추상적인 예외를 던지기 때문에 구체적으로 어떤 예외가 발생한지 알기 어렵다.
사용자 정의 예외 (Custom Exception)
- RuntimeException과 같은 추상적인 예외가 아닌 구체적으로 표현할 수 있는 Custom Exception을 만들어서 예외를 던질 수있다.
- RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.
예외 코드
package com.codestates.exception;
import lombok.Getter;
public enum ExceptionCode {
MEMBER_NOT_FOUND(404, "Member Not Found");
@Getter
private int status;
@Getter
private String message;
ExceptionCode(int code, String message) {
this.status = code;
this.message = message;
}
}
- 서비스 계층에서 던질 Custom Exception에 사용할 ExceptionCode를 enum으로 정의
- 다양한 유형의 예외를 enum에 추가해서 사용 가능
BusinessLogicException
package com.codestates.exception;
import lombok.Getter;
public class BusinessLogicException extends RuntimeException {
@Getter
private ExceptionCode exceptionCode;
public BusinessLogicException(ExceptionCode exceptionCode) {
super(exceptionCode.getMessage());
this.exceptionCode = exceptionCode;
}
}
- 서비스 계층에서 사용할 BusinessLogicException이라는 Custom Exception을 정의
- BusinessLogicException은 RuntimeException을 상속하고 있으며 ExceptionCode를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공
- BusinessLogicException은 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있다.
Service 클래스
@Service
public class MemberService {
public Member findMember(long memberId) {
// should business logic
throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
}
- 서비스 계층에서 BusinessLogicException을 사용하여 예외를 던질 수 있다.
GlobalExceptionAdvice
- 서비스 계층에서 던진 예외를 처리하는 공통 예외 처리 클래스
package com.codestates.advice;
import com.codestates.exception.BusinessLogicException;
import com.codestates.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;
@RestControllerAdvice
public class GlobalExceptionAdvice {
@ExceptionHandler
public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
System.out.println(e.getExceptionCode().getStatus());
System.out.println(e.getMessage());
// TODO GlobalExceptionAdvice 기능 추가 1
final ErrorResponse response = ErrorResponse.of(e.getExceptionCode());
return new ResponseEntity<>(response, HttpStatus.valueOf(e.getExceptionCode()
.getStatus()));
}
// TODO GlobalExceptionAdvice 기능 추가 2
@ExceptionHandler
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
public ErrorResponse handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e){
// HttpRequestMethodNotSupportedException를 Argument로
//final ErrorResponse response = ErrorResponse.of(e);
// http stauts를 Argument로
final ErrorResponse response = ErrorResponse.of(HttpStatus.METHOD_NOT_ALLOWED);
return response;
}
// TODO GlobalExceptionAdvice 기능 추가 3
@ExceptionHandler
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(NullPointerException e){
// ExceptionCode(enum)를 Argument로
//final ErrorResponse response = ErrorResponse.of(ExceptionCode.INTERNAL_SERVER_ERROR);
// http stauts를 Argument로
final ErrorResponse response = ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR);
return response;
}
}
- 서비스 계층에서 던진 BusinessLogicException을 처리
- GlobalExceptionAdvice 기능 추가 1
- 없는 멤버를 조회할때 - DB에서 조회가 안될때
- HttpRequestMethodNotSupportedException을 처리하기 위한 handleHttpRequestMethodNotSupportedException()
- GlobalExceptionAdvice 기능 추가 2
- 클라이언트가 요청한 HTTP 메서드를 서버가 처리할 수 없는 경우에 발생
- 예를 들어, 클라이언트가 POST 메서드를 사용하여 /users 엔드포인트에 요청했지만 서버에서는 POST 메서드를 허용하지 않고 GET 메서드만 허용하는 경우
- NullpointerException 등과 같이 개발자가 구현 상의 실수로 발생하는 Exception을 처리하기 위한 handleException() 메서드
- GlobalExceptionAdvice 기능 추가 3
- 여기선 NullpointerException만
- @RestControllerAdvice에서 @ResponseStatus와 ResponseEntity
- 한 가지 유형으로 고정된 예외를 처리할 경우에는 @ResponseStatus
- 다양한 유형의 Custom Exception을 처리하고자 할 경우에는 ResponseEntity를 사용.
- BusinessLogicException
ErrorResponse
- 필요한 Error 정보만 담기 위해 Error 전용 Response 객체를 통해 데이터 가공
package com.codestates.response;
import com.codestates.exception.ExceptionCode;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.web.HttpRequestMethodNotSupportedException;
@Getter
public class ErrorResponse {
// status와 message 선언
private int status;
private String message;
// 생성자
public ErrorResponse(int status, String message) {
this.status = status;
this.message = message;
}
// GlobalExceptionAdvice 기능 추가 1 exceptionCode를 파라미터로 받을경우
public static ErrorResponse of(ExceptionCode exceptionCode) {
return new ErrorResponse(exceptionCode.getStatus(), exceptionCode.getMessage());
}
// GlobalExceptionAdvice 기능 추가 2 HttpRequestMethodNotSupportedException을 파라미터로 받을경우
public static ErrorResponse of(HttpRequestMethodNotSupportedException methodNotSupportedException) {
return new ErrorResponse(405, "Method Not Allowed");
}
// GlobalExceptionAdvice 기능 추가 2 , 3 httpstaus를 파라미터로 받을 경우
public static ErrorResponse of(HttpStatus httpStatus){
return new ErrorResponse(httpStatus.value(),httpStatus.getReasonPhrase());
}
}
- of() 메서드를 통한 객체 생성
- 생성자 오버로딩을 통해 각 기능별 ErrorResponse 객체 리턴
Response Body 확인
요약
- 체크 예외(Checked Exception)는 예외를 잡아서(catch) 체크한 후에 해당 예외를 복구하든가 아니면 회피를 하든가 등의 어떤 구체적인 처리를 해야 하는 예외이다.
- 언체크 예외(Unchecked Exception)는 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리를 할 필요가 없는 예외를 의미한다.
- RuntimeException을 상속한 예외는 모두 언체크 예외(Unchked Exception)이다.
- RuntimeException을 상속해서 개발자가 직접 사용자 정의 예외(Custom Exception)를 만들 수 있다.
- 사용자 정의 예외(Custom Exception)를 정의해서 서비스 계층의 비즈니스 로직에서 발생하는 다양한 예외를 던질 수 있고, 던져진 예외는 Exception Advice에서 처리할 수 있다.
- @ResponseStatus 애너테이션은 고정된 예외를 처리할 경우에 사용할 수 있다.
- HttpStatus가 동적으로 변경되는 경우에는 ResponseEntity를 사용한다.