예외가 발생했을 때, 종류별로 처리할 수 있으면 좋다.
이를 대비해 Exception 디렉터리를 생성해, 각각의 오류마다 해당 결과를 전달해준다.
kotlin 기반으로 작성
✔️ Exception 계층
@ExceptionHandler
: Controller 계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능 (Service, Repository에서 발생하는 에러는 제외한다.)
@Controller 클래스안에서
~
@ExceptionHandler
fun handle(ex: IOException): ResponseEntity<String>{
}
~
@Controller
로 선언된 클래스 안에서 @ExceptionHandler
어노테이션으로 메서드 안에서 발생할 수 있는 에러를 처리할 수 있다.
✔️ 여러 개의 Exception 처리하는 방법
@ExceptionHandler
의 value 값으로 어떤 Exception을 처리할 것인지 넘겨줄 수 있다.
⇒ 만약, value를 설정하지 않으면 모든 Exception을 잡게 되기 때문에, Exception을 구체적으로 적어주는 것이 좋다.
@Controller 안이다.
@ExceptionHandler(value = [FileSystemException::class, RemoteException::class])
fun handle(ex: IOException): ResponseEntity<String>{
}
@ExceptionHandler(value = [Exception1::class, Exception2::class])
와 같이 명시해주면 된다.
@ControllerAdvice
: @Controller
와 handler
에서 발생하는 에러들을 모두 잡아준다.
@ControllerAdvice
안에서 @ExceptionHandler
를 사용하여 에러를 잡을 수 있다.
(ControllerAdvice는 java로 함)
@ControllerAdvice
public class ExceptionHandlers {
@ExceptionHandler(FileNotFoundException.class)
public ResponseEntity handleFileException() {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
✔️ 범위 설정
@ControllerAdvice
는 모든 에러를 잡아주기 때문에, 일부 에러만 처리하고 싶을 경우에는 따로 설정을 해주면 된다.
(1) 어노테이션
(2) basePackages (+basepackagesClasses
)
(3) assignableTypes
// 1.
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// 2.
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// 3.
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
basePackages
: 탐색 패키지 지정
org.example.controllers
: 패키지, 하위 패키지까지 모두 탐색
basePackagesClasses
: 탐색 클래스 지정, 클래스의 맨 위에 있는 package부터 시작
💡 참고
어노테이션, 베이스 패키지 등 설정자들은 runtime시 수행되기 때문에 너무 많은 설정자들을 사용하면 성능이 떨어질 수 있다.
@RestControllerAdvice
: @ControllerAdvice
와 @ResponseBody
을 가지고 있다.
@Controller
처럼 작동하며 @ResponseBody
를 통해 객체를 리턴할 수 있다.
✔️ @RestControllerAdvice
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
// ...
}
@ControllerAdvice
: @Component
어노테이션을 가지고 있어 컴포넌트 스캔을 통해 스프링 빈으로 등록된다.
@RestControllerAdvice
: @Controlleradvice
와 @ResponseBody
어노테이션으로 이루어져 있고 HTML 뷰 보다는 ResponseBody로 값을 리턴할 수 있다.
현재 게시판(Board) id를 찾던 중 BoardResourceNotFoundException
이 발생
✔ 현재 Exception 구조
@RestControllerAdvice
class GlobalExceptionHandler {
/* 설명
RESTful API에서 발생하는 예외 처리를 담당
예외 처리를 통해 클라이언트에게 적절한 에러 응답을 보내는 것이 중요하다.
Spring Framework에서 예외 처리를 담당하는 @RestControllerAdvice 클래스인 GlobalExceptionHandler
(1) handleCustomException
- CustomException 발생 시 호출되는 메소드
- CustomException에서 발생한 ErrorCode 객체를 이용하여 ErrorResponse 객체를 생성하여 반환
(2) handleBindException
- BindException 발생 시 호출되는 메소드
- BindException에서 발생한 BindingResult 객체를 이용하여 ErrorResponse 객체를 생성하여 반환
*/
private val logger = KotlinLogging.logger {}
@ExceptionHandler(value = [CustomException::class])
protected fun handleCustomException(e: CustomException): ResponseEntity<ErrorResponse?>? {
return ErrorResponse.toResponseEntity(e.getErrorCode())
}
}
CustomException
의 하위 Exception
들도 handleCustomException
에 포함된다.
e: CustomException
CustomException
자식 계층에 BoardResourceNotFoundException
이 있다.
✔ BoardErrorCode
import org.springframework.http.HttpStatus
import study.kotlin.boardtoyproject.common.exception.ErrorCode
enum class BoardErrorCode(override val status:HttpStatus, override val message: String) : ErrorCode{
// 400 BAD REQUEST
INVALID_PARAMETER(HttpStatus.BAD_REQUEST, "파라미터 값을 확인해주세요."),
// 404 NOT FOUND
BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 방 번호 입니다."),
YOUR_NOT_MEMBER(HttpStatus.NOT_FOUND, "회원이 아닙니다."),
RESOURCE_NOT_FOUND_EXCEPTION(HttpStatus.NOT_FOUND, "현재 요청사항을 찾지 못했습니다. id를 다시 확인해주세요."),
SAVED_BOARD_NOT_FOUND(HttpStatus.NOT_FOUND, "저장된 게시판이 아닙니다."),
// 409 CONFLICT 중복된 리소스
ALREADY_SAVED_BOARD(HttpStatus.CONFLICT, "이미 저장된 방입니다."),
// 500 INTERNAL SERVER ERROR
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 에러가 발생했습니다. 서버에 연락주세요.")
}
✔ CustomException
open class CustomException(private val errorCode: ErrorCode) : RuntimeException() {
// 404, 409 에러들은 따로 잡아주어야 하기 때문에 필요한 클래스
fun getErrorCode(): ErrorCode{
return errorCode
}
}
404, 409 에러
들은 따로 잡아주어야 하기 때문에 필요한 클래스
✔ BoardResourceNotFoundException
import study.kotlin.boardtoyproject.common.exception.CustomException
import study.kotlin.boardtoyproject.common.exception.ErrorCode
// 현재 요청사항을 찾지 못했습니다. id를 다시 확인해주세요.
class BoardResourceNotFoundException private constructor(errorCode: ErrorCode) : CustomException(errorCode){
companion object {
val CODE = BoardErrorCode.RESOURCE_NOT_FOUND_EXCEPTION
}
constructor() : this(CODE)
}
companion object
: static 전역 변수 선언
CODE
: enum에서 생성한 변수이다.
constructor
, 보조 생성자(Secondary Constructor)를 통해 CODE를 private constructor(errorCode: ErrorCode)
→ CustomException
constructor으로 넘겨준다.
BoardResourceNotFoundException는 CustomException의 자식 계층이다.
data class ErrorResponse(
val timestamp: LocalDateTime = LocalDateTime.now(),
val status: Int,
val message: String?,
val errors: List<FieldError>
){
// static
companion object {
// 메서드
fun toResponseEntity(errorCode: ErrorCode): ResponseEntity<ErrorResponse?> {
return ResponseEntity.status(errorCode.status).body(ErrorResponse(errorCode))
}
}
constructor(errorCode: ErrorCode) : this(
status = errorCode.status.value(),
message = errorCode.message,
errors = emptyList()
)
}
errorCode
: status(상태), message(메시지) 를 담아서 toResponseEntity
를 호출했다.
body
에 ErrorResponse(errorCode)
보조 생성자가 실행된다.
보조 생성자
constructor(errorCode: ErrorCode) : this(
status = errorCode.status.value(),
message = errorCode.message,
errors = emptyList()
)
결국 status와 body에 각각의 데이터를 담아서 ResponseEntity
가 반환된다.
사용자는 이것을 보고 어떤 오류인지 쉽게 판단할 수 있게 된다.
참고 : https://velog.io/@kiiiyeon/스프링-ExceptionHandler를-통한-예외처리