스프링 예외처리

Choco·2023년 1월 25일
1
post-thumbnail

서버에서 로직상 문제가 생기면 추후에 더 문제가 생기기전에 로직을 더이상 진행하지않고 예외처리를 해줘야한다.
스프링을 통한 REST API로 예외처리를 해보자

서블릿단에서 예외처리

일반적으로 서블릿에 들어온다음에 로직상 문제가 생겼을때에 예외처리 방법이다
@RestConrollerAdvice를 이용하면 Controller에서 발생하는 공통적인 예외처리를 한 클래스 내에서 할 수 있게 해준다.


@RestControllerAdvice
@Slf4j
public class ApiExceptionController {

    @ExceptionHandler
    public ResponseEntity<CustomBody> bindException(BindException e){
        CustomBody response = CustomBody.builder()
                .message("잘못된 요청")
                .detail("적절한 인자값이 들어오지 않았습니다")
                .build();
        log.error("적절한 인자값이 들어오지 않았습니다");
        return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
    }


    @ExceptionHandler
    public ResponseEntity<CustomBody> exHandle(CustomException e){
        return CustomBody.toResponseEntity(e.getErrorCode());
    }

}

우선 첫번째 메서드 부터 살펴보자

  • @ExceptionHandler는 예외처리를 하는 핸들러라고 이고 해당 메서드에 매개변수의 예외객체가 발생하면 호출된다. 이 메서드에선 BindException이 발생하면 호출된다.

  • CustomBody는 자신이 원하는 형태로 return하기 위해 따로 만든 객체이다

@Getter
@Setter
@Builder
public class CustomBody {

    private final LocalDateTime timestamp = LocalDateTime.now();
    private String message;
    private String detail;

    public static ResponseEntity<CustomBody> toResponseEntity(CustomMessage errorCode) {
        return ResponseEntity
                .status(errorCode.getHttpStatus())
                .body(CustomBody.builder()
                        .message(errorCode.getMessage())
                        .detail(errorCode.getDetail())
                        .build()
                );
    }

    @Builder
    public CustomBody(String message, String detail) {
        this.message = message;
        this.detail = detail;
    }

    public CustomBody() {
        this.message = "올바른 요청";
        this.detail = "정상적으로 처리되었습니다";
    }
}

다음으로 두번째 메서드를 살펴보자

@ExceptionHandler
public ResponseEntity<CustomBody> exHandle(CustomException e){
        
	return CustomBody.toResponseEntity(e.getErrorCode());

}
  • 이 메서드는 일반적으로는 예외가 발생하지 않지만 로직상에 문제가 생길것을 대비하여 따로 예외를 강제로 발생시켜야 할때 호출되는 메서드이다.
  • CustomExceptionRuntimeException을 상속받는다(일반적으로 나오는 오류는 대부분은 RuntimeException에게 상속받기 때문에 통일성을 주기 위해)
@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException{

    private final CustomMessage customMessage;

}

CustomMessage는 상황에 따라 전달해야할 메세지와 StatusCode를 나타낸다.

@Getter
@AllArgsConstructor
public enum CustomMessage {

    /* 200 OK : 올바른요청*/
    OK(HttpStatus.OK,"올바른 요청","정상적으로 처리되었습니다"),

    /* 400 BAD_REQUEST : 잘못된 요청 */
    INVALID_EMAIL(HttpStatus.BAD_REQUEST,"잘못된 요청", "이메일이 맞지 않습니다"),
    INVALID_restaurant(HttpStatus.BAD_REQUEST,"잘못된 요청", "해당 가게가 존재하지 않습니다"),

    /* 401 UNAUTHORIZED : 비인증 사용자 */
    INVALID_PASSWORD(HttpStatus.UNAUTHORIZED,"인증되지 않은 사용자","비밀번호가 맞지 않습니다"),

    /* 403 FORBIDDEN: 권한이 없는 사용자 */

    /* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
    CONFLICT_EMAIL(HttpStatus.CONFLICT,"데이터 중복","중복된 이메일 사용자가 있습니다"),
    CONFLICT_USER(HttpStatus.CONFLICT,"존재하지 않는 데이터","게시글 작성자의 고객 정보가 없습니다");

    private final HttpStatus httpStatus;
    private final String message;
    private final String detail;
}

예외를 강제로 발생시키는 예시를 보자

@PostMapping("/user/signup")
    public ResponseEntity<Object> signup(@Validated @RequestBody SignUpDto member) {
        
        Optional<Member> userCheck = memberservice.LoginCheck(member.getEmail());
       	
        if (userCheck.isPresent()) {
            throw new CustomException(CustomMessage.CONFLICT_EMAIL);
        }
        ...

    }

회원가입을 하는 Controller 메서드의 일부분인데 이메일이 중복 되면 안되므로 같은 이메일을 가진 유저가 있을때 예외를 발생시켜줬다.

Filter단에서 예외처리

Servlet에서 발생한 예외는 Servlet 로직상에서 예외처리를 해주었다. 하지만 Filter에서 발생하는 문제는 Servlet에 들어오기전에 발생하기에 Config에서따로 처리 해주어야 한다.

@Configuration
@AllArgsConstructor
public class SecurityConfig{

    private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
    private final CustomAccessDeniedHandler customAccessDeniedHandler;

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

       //예외처리
        http.exceptionHandling()
            .authenticationEntryPoint(customAuthenticationEntryPoint)
            .accessDeniedHandler(customAccessDeniedHandler);
        return http.build();
    }
}
  • authenticationEntryPoint는 인증을 하지 못할때 발생하는 에러를 처리 하는 메서드(401 에러)
  • 구체적인 에러를 나누고 원하는 대로 reponse하기 위해 따로 Custom해준다.
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {
        // 유효한 자격증명을 제공하지 않고 접근하려 할때 401에러
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        JSONObject reponseJson;

        if(authException instanceof BadCredentialsException){
            reponseJson = new JSONObject(CustomBody.builder().message("인증되지 않은 사용자").detail("이메일이나 비밀번호가 유효하지 않습니다").build());
        }
        else {
            reponseJson = new JSONObject(CustomBody.builder().message("인증되지 않은 사용자").detail("토큰의 정보가 유효하지 않습니다").build());
        }
        response.getWriter().print(reponseJson);
    }
}
  • accessDeniedHandler는 권한이 없을때 발생하는 에러를 처리 하는 메서드(403 에러)
  • 구체적인 에러를 나누고 원하는 대로 reponse하기 위해 따로 Custom해준다.

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        //필요한 권한이 없이 접근하려 할때 403
        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        JSONObject responseJson = new JSONObject(CustomBody.builder().message("권한이 없습니다").detail("해당 유저는 해당 url에 권한이 없습니다").build());
        response.getWriter().print(responseJson);
    }
}

0개의 댓글