가수면

[Spring Boot] 에러 핸들링 본문

Java

[Spring Boot] 에러 핸들링

니비앙 2024. 2. 20. 02:32

spring boot security의 사용과 rest API 서버일 때를 기준으로 한 에러 핸들링 방법을 정리한다.

인증

spring boot security를 사용하는 경우 requestMatchers를 통해 인증이 필요하지 않은 url을 예외 처리할 수 있으며, 그 외 url에 대해 인증 절차를 통과하지 못했을 경우 exceptionHandling를 이용해 전역적인 인증 에러를 발생시킬 수 있다.

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        http
.
.
.
                .exceptionHandling((exceptionHandling) ->
                        exceptionHandling.authenticationEntryPoint(new CustomAuthenticationEntryPoint()));
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setContentType("application/json");
        response.setStatus(ErrorCode.INVALID_TOKEN.getStatus().value());
        response.getWriter().write(Response.error(ErrorCode.INVALID_TOKEN.name()).toStream());
    }
}

전역 케이스

그러나 위처럼 exceptionHandling를 통해 에러를 핸들링하게 되면 따로 설정하지 않은 전역 에러에 인증 에러가 대신 반환되는 문제가 발생할 수 있다.

때문에 전역 에러에 대한 핸들링을 직접 설정해줄 필요성이 생기게 된다.

 

@ControllerAdvice, @RestControllerAdvice

공통적인 관심사를 컨트롤러 전반에 걸쳐 적용할 수 있게 해주는 어노테이션이다.

어노테이션이 적용된 클래스에 정의된 메서드는 애플리케이션의 모든 컨트롤러에 대한 공통적인 처리 로직(예: 예외 처리, 데이터 바인딩, 모델 객체 추가 등)을 담당하게 된다.

 

이때 전역 에러 처리에는 @ExceptionHandler 어노테이션이 사용된다.

 

1. 잘못된 url에 대한 요청

NoHandlerFoundException.class

잘못된 url로 요청을 보낼 경우 404에러를 발생시키는 에러다.

@ExceptionHandler(NoHandlerFoundException.class)
    public ResponseEntity<?> handleNoHandlerFoundException(NoHandlerFoundException e) {
        log.error("error occurs {}", e.toString());
        return ResponseEntity
                .status(e.getStatusCode())
                .body(Response.error(ErrorCode.URL_NOT_FOUND.name()));
    }

그 외 application.yml 등에 아래 설정을 해줘야 NoHandlerFoundException 에러가 발생한다.

spring.mvc.throw-exception-if-no-handler-found=true
spring.web.resources.add-mappings=false

첫번째 설정은 스프링 MVC가 요청에 대한 핸들러를 찾지 못했을 때 NoHandlerFoundException을 발생시키는 역할을 한다.(그러나 두번째 설정만 해줘도 되는 걸 보면 최신 버전에서는 default 값이 true로 바뀐 듯하다. 실제로 false로 설정해주게 되면 NoHandlerFoundException가 발생하지 않는다.)

두번째 설정의 경우 '정적 리소스 핸들링'을 비활성화 하는 설정이다. 스프링 부트는 정적 리소스에 대한 요청을 자동으로 처리하게 된다. 이 경우 존재하지 않는 URL에 대한 요청이 들어오면 스프링 부트는 정적 리소스를 찾으려 시도한다. 만약 찾지 못하면 내부적으로 처리하여 404 응답을 반환하게 되는데, 이 과정에서 NoHandlerFoundException가 발생하지 않기에 비활성화 시켜주는 설정이 되겠다.

 

2. 잘못된 메소드에 대한 요청

HttpRequestMethodNotSupportedException.class

잘못된 메소드로 요청했을 때 발생하는 에러다.

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResponseEntity<?> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("error occurs: {}", e.getMessage());
        return ResponseEntity
                .status(e.getStatusCode())
                .body(Response.error(ErrorCode.METHOD_NOT_ALLOWED.name()));
    }

 

3. 잘못된 body 형식에 대한 요청

HttpRequestMethodNotSupportedException.class

요청의 body가 잘못되었을 때 발생하는 에러다.

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<?> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error("error occurs: {}", e.getMessage());
        return ResponseEntity
                .status(HttpStatus.BAD_REQUEST)
                .body(Response.error(ErrorCode.HTTP_MESSAGE_NOT_READABLE.name()));
    }

HttpRequestMethodNotSupportedException.class에는 .getStatusCode()가 없어서 HttpStatus.BAD_REQUEST를 직접 지정해줘야 한다.

 

4. @Valid 조건을 충족시키지 않는 요청

MethodArgumentNotValidException.class

@RequestBody 등에 @Valid를 설정했을 때 그 조건을 충족하지 못할 경우 발생하는 에러다.

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException e) {
        log.error("error occurs {}", e.toString());
        String errorMessage = getJoinedErrorMessage(e);
        return ResponseEntity
                .status(e.getStatusCode())
                .body(Response.error(errorMessage));
    }

 

5. 런타임 에러

RuntimeException.class

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<?> handleRuntimeException(RuntimeException e) {
        log.error("error occurs {}", e.toString());
        return ResponseEntity
                .status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(Response.error(ErrorCode.DATABASE_ERROR.name()));
    }

 

6. 커스텀 에러

내가 만든 에러 핸들링 클래스.class

RuntimeException를 extends해서 만든 클래스로, 서비스 로직에서 예외 처리할 때 사용한 에러다.

    @Transactional
    public UserDto createUser(String username, String nickname, String password, String passwordConfirm) {
        if (!password.equals(passwordConfirm)) {
            throw new CustomExceptionHandler(ErrorCode.PASSWORDS_NOT_MATCHING, "Passwords do not match");
        }
    @ExceptionHandler(CustomExceptionHandler.class)
    public ResponseEntity<?> handleCustomException(CustomExceptionHandler e) {
        log.error("error occurs {}", e.toString());
        return ResponseEntity
                .status(e.getErrorCode().getStatus())
                .body(Response.error(e.getErrorCode().name()));
    }
package com.springboot.petProject.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class CustomExceptionHandler extends RuntimeException {

    private ErrorCode errorCode;
    private String message;

    public CustomExceptionHandler(ErrorCode errorCode) {
        this.errorCode = errorCode;
        this.message = null;
    }

    @Override
    public String getMessage() {
        if (message == null) {
            return errorCode.getMessage();
        } else {
            return String.format("%s. %s", errorCode.getMessage(), message);
        }

    }
}

 

7. 그 외

AccessDeniedException - 사용자가 자원에 접근할 권한이 없는 경우 발생하는 에러

@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<?> handleAccessDeniedException(AccessDeniedException e) {
    log.error("Access Denied: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.FORBIDDEN)
            .body(Response.error(ErrorCode.ACCESS_DENIED.name()));
}

 

DataIntegrityViolationException - 데이터베이스 작업 중 제약 조건 위반이 발생했을 때 발생하는 에러 (ex. 유니크 위반 등)

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<?> handleDataIntegrityViolationException(DataIntegrityViolationException e) {
    log.error("Data Integrity Violation: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.CONFLICT)
            .body(Response.error(ErrorCode.DATA_INTEGRITY_VIOLATION.name()));
}

 

EntityNotFoundException - 요청한 엔티티를 데이터베이스에서 찾을 수 없을 때 발생하는 에러

@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<?> handleEntityNotFoundException(EntityNotFoundException e) {
    log.error("Entity Not Found: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(Response.error(ErrorCode.ENTITY_NOT_FOUND.name()));
}

 

HttpMediaTypeNotSupportedException - 클라이언트가 요청에서 지원하지 않는 미디어 타입을 사용했을 때 발생하는 에러

@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseEntity<?> handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
    log.error("Media Type Not Supported: {}", e.getMessage());
    return ResponseEntity
            .status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
            .body(Response.error(ErrorCode.UNSUPPORTED_MEDIA_TYPE.name()));
}

'Java' 카테고리의 다른 글

[Spring Boot] S3 연결하기  (0) 2024.03.29
백엔드 성능 측정  (0) 2024.03.28
Junit5  (0) 2024.02.17
스프링 부트 서버에 https 설정  (0) 2024.01.23
Spring Security와 JWT  (0) 2024.01.09
Comments