티스토리 뷰

etc/TIL

스프링 부트에서 예외 처리하는 방법

기억용블로그 2022. 4. 29. 19:59
728x90

코드 출처: https://velog.io/@peppermint100/Spring-Boot-%EC%98%88%EC%99%B8-%EC%B2%98%EB%A6%AC

 

스프링부트의 예외 처리에 있어서 가장 핵심은 값에 대한 validation을 계속 순차적으로 진행하면서 생기는 exception을 단 하나의 객체에서만 처리하고 그 객체를 위로 던지는 것이다.

 

	@PostMapping("/login")
	
    //모든 exception을 담아줄 response객체를 return.
    public ResponseEntity<TokenContainingResponse> login(@RequestBody LoginRequest loginRequest) throws Exception {
    	
        //service layer에서 login 처리 후 token을 리턴
        String token = userService.loginAndGenerateToken(loginRequest);

	//response에 HttpStatus와 message, token을 담는다.
        TokenContainingResponse response = new TokenContainingResponse(HttpStatus.OK, Controller.LOG_IN_SUCCESS_MESSAGE, token);
        
        //exception을 위한 response와 ok 리턴.
        return new ResponseEntity<>(response, HttpStatus.OK);

 

Exception이 발생하든 발생하지 않든 항상 HttpStatus는 OK여야 한다. 서버에서 따로 만든 에러 처리 페이지가 아닌 브라우저 엔진에서 직접 보여주는 에러 화면과 에러 메시지는 유저 만족도를 최악으로 떨어트린다.

 

//exception을 처리 하는 단 하나의 클래스
public class TokenContainingResponse {
    private HttpStatus httpStatus;
    private String message;
    private String token;
}

 

 

@Service
...
public String loginAndGenerateToken(LoginRequest loginRequest) throws Exception {

		//Optional로 처리하며 null 발생시 Exception을 던짐
        String email = Optional.ofNullable(loginRequest.getEmail()).orElseThrow(EmptyValueExistException::new);
        String password = Optional.ofNullable(loginRequest.getPassword()).orElseThrow(EmptyValueExistException::new);

        Optional<User> user = userRepository.findByEmail(email);

		//Optional로 처리하여 null 발생시 Exception
        if(!user.isPresent()){
            throw new UserNotExistException();
        }

        try{
            authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getEmail(),
                        loginRequest.getPassword()
            ));
            
            //login 로직 후 실패시 Exception
        }catch(Exception e){
            throw new LoginFailException();
        }

        CustomUserDetails userDetails = userDetailsService.loadUserByUsername(user.get().getEmail());

        String token = jwtUtil.generateToken(email);

        return token;
    }

 

이후에 Exception 객체에 대해 정의한다.

@AllArgsConstructor
@Getter
public class ApiException {

//exception으로 client에 어떤 값을 보여줄지 정한다.
    private final String message;
    private final HttpStatus httpStatus;
    private final ZonedDateTime timestamp;
}

//service에서 생긴 exception은 RuntimeException을 상속받는다.
public class UserNotExistException extends RuntimeException{
}

 

 

//Exception을 한 곳에 집중하여 핸들링할 수 있게 도와주는 Bean
@ControllerAdvice
public class ApiExceptionHandler {

//처리하게 될 Exception을 매핑
    @ExceptionHandler(value = {UserNotExistException.class})
    public ResponseEntity<Object> handleUserNotExistException(UserNotExistException e){

        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

	//위에서 정의한 exception에 값을 채워준다.
        ApiException apiException = new ApiException(
                ExceptionMessage.USER_NOT_EXIST_MESSAGE,
                httpStatus,
                
                //UTC time에 normalized()한 시간.
                ZonedDateTime.now(ZoneId.of("Z"))
        );

        return new ResponseEntity<>(apiException, httpStatus);
    }

 

ControllerAdvice의 강력한 점은 어떤 Controller에서 exception이 발생하든 명시해둔 exception이 발생하면 (지금의 경우는 UserNotExistException) 

이 ExceptionHandler에 매핑되어 미리 지정한 처리를 하게 되는 것이다.

 


 

하지만 Spring Security 과정 중에서 생기는 Exception에 대해서는 Controller에서 핸들링 할 수 없는 문제가 생긴다.

 

이 문제를 해결하기 위해서는

 

@RestController
@RequestMapping("/exception")
public class ExceptionHandleController {

    @GetMapping("/jwt")
    public void JwtException(){
        throw new UserNotExistException();
    }
}

Security 과정 중에서 생기는 exception도 똑같은 이름으로 던지도록 해둔다.

 

 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers(<URLS>)
                .permitAll()
                .antMatchers(<URLS>)
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                
                //앞으로 security에서 생길 모든 exception을 처리해주는 곳을 설정
                .exceptionHandling().authenticationEntryPoint(new AuthenticationExceptionHandler())
                
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
    }

 

HttpSecurity를 받는 configure 메서드 설정에서 exception 핸들링을 AuthenticationExceptionHandler에서 할 수 있게 설정한 후

 

@Component
public class AuthenticationExceptionHandler implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
      httpServletResponse.sendRedirect("/exception/jwt");
  }
}

 

response를 받아 위에 Security 중에 생기는 jwt의 exception을 던질 url을 redirect하면 

이 url에서 발생하는 exception을 @ControllerAdvice가 캐치하여 핸들링 할 수 있게 된다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함