Spring

@ControllerAdivce를 사용한 예외 처리 로직 분리

voider 2021. 5. 15. 11:36

@ControllerAdvice를 사용하여 예외처리 로직 분리하기

과제 프로젝트를 개선하면서 받은 피드백 중 하나는 @ControllerAdvice를 통해 예외 처리 로직을 분리하라는 것이었습니다.
아래 코드를 보면 예외를 잡고 발생하는 예외마다 다른 Http Status code를 반환했습니다.
예시 코드

    @PostMapping
    fun requestApartmentToBeMoved(@RequestBody apartmentRequest: ApartmentRequest, httpSession: HttpSession)
            : ResponseEntity<String> {
        logger.info("apartmentRequest:{}", apartmentRequest)
z
        try {
            val apartment = apartmentViewService.getApartment(apartmentRequest.toEntity())
            httpSession.setAttribute("apartment", apartment)
        } catch (e: NotExistsApartmentException) {
            logger.error("error : {}", e.msg)
            return ResponseEntity(HttpStatus.UNPROCESSABLE_ENTITY)
        } catch (e: AlreadyReservedException) {
            logger.error("error: {} ", e.msg)
            return ResponseEntity(HttpStatus.CONFLICT)
        }

        return ResponseEntity(HttpStatus.OK)
    }

제가 보기에도 try - catch블록이 있는 함수는 이 함수가 정말로 해야 하는 일이 무엇인지 쉽게 알 수 없었고, 여러 핸들링 함수에 걸쳐 예외 처리 로직이 중복되는 문제도 있었습니다. 예외 처리 같은 핵심 로직을 벗어난 부가 로직을 횡단 관심(cross-cutting concern)이라고 부른다는 사실을 알게되었습니다. 이런 횡단 관심사를 분리하기 위해 스프링은 AOP(Aspect oriented programming)를 제공합니다. 피드백 받은 @ControllerAdvice또한 AOP가 제공하는 애노테이션 중 하나입니다. 이는 발생할 수 있는 예외를 한 곳에 모아서 전역적으로 처리할 수 있도록 하는 기능입니다. @ExceptionHandler와 함께 사용해서 try-catch를 사용해야 했던 문제를 해결할 수 있었습니다.

Controller Advice적용

@RestControllerAdvice
class ExceptionHandlerController {
    companion object : Logger

    @ExceptionHandler(value = [AlreadyReservedException::class, UnreservedUserException::class])
    fun conflictException(e: AlreadyReservedException) : ResponseEntity<String> {
        logger.error(e.msg)
        return ResponseEntity(HttpStatus.CONFLICT)
    }

    @ExceptionHandler(OutRangeDateException::class)
    fun outRangeDateExceptionHandler(e: OutRangeDateException): ResponseEntity<String> {
        logger.error(e.msg)
        return ResponseEntity(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
    }

    @ExceptionHandler(NotFoundException::class)
    fun notFoundExceptionHandler(e: NotFoundException): ResponseEntity<String> {
        logger.error(e.message)
        return ResponseEntity(HttpStatus.NO_CONTENT)
    }

    @ExceptionHandler(value = [NotExistsApartmentException::class, InvalidRequestStateException::class])
    fun notExistsApartmentExceptionHandler(e: NotExistsApartmentException): ResponseEntity<String> {
        logger.error(e.msg)
        return ResponseEntity(HttpStatus.UNPROCESSABLE_ENTITY)
    }
}

@ControllerAdvice를 적용한 뒤 컨트롤러 함수

    @PostMapping
    fun requestApartmentToBeMoved(@RequestBody apartmentRequest: ApartmentRequest, httpSession: HttpSession)
            : ResponseEntity<String> {
        logger.info("apartmentRequest:{}", apartmentRequest)

        val apartment = apartmentViewService.getApartment(apartmentRequest.toEntity())
        httpSession.setAttribute("apartment", apartment)

        return ResponseEntity(HttpStatus.OK)
    }

발생할 수 있는 예외를 @ExceptionHandler의 프로퍼티로 설정하고 예외가 발생하면 로그를 찍고 적절한 HTTP상태 코드를 반환하도록 처리했습니다. 이렇게 @ControllerAdvice를 적용한 다음 기존 컨트롤러의 코드는 더 짧고 명확해졌습니다.