Exception Handling - KimGyuBek/Threadly GitHub Wiki

์˜ˆ์™ธ ์ฒ˜๋ฆฌ


๊ฐœ์š”

MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ๋Š” DispatcherServlet ๋‚ด๋ถ€๊นŒ์ง€ ๋„๋‹ฌํ•ด GlobalExceptionHandler๊ฐ€ ์ฒ˜๋ฆฌํ•œ๋‹ค.

Security Filter์—์„œ ๋ฐœ์ƒํ•œ ์ธ์ฆ ์˜ˆ์™ธ๋Š” DispatcherServlet๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ๋ชป ํ•˜๊ณ  filter chain ๋‚ด๋ถ€์—์„œ ExceptionTranslatorFilter๊ฐ€ ๊ฐ€๋กœ์ฑˆ ํ›„ AuthenticationEntryPoint๊ฐ€ ์ฒ˜๋ฆฌํ•œ๋‹ค.


๋ชฉ์ฐจ

  1. MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ
  2. Filter ์˜ˆ์™ธ
  3. ์—๋Ÿฌ ์‘๋‹ต
  4. ๊ณ ๋ ค ์‚ฌํ•ญ

MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ

ํ๋ฆ„

mvc_exception_flow

  • Controller, Service, Repository์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋Š” DispatcherServlet๊นŒ์ง€ ๋„๋‹ฌ ํ›„ ์ฒ˜๋ฆฌ๋œ๋‹ค.
  • @RestControllerAdvice๋Š” ์˜ˆ์™ธ๋ฅผ ๊ฐ์ง€ํ•ด ErrorCode๋ฅผ ErrorResponse๋กœ ๋ณ€ํ™˜ ํ›„ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • RuntimeException์„ ์ƒ์†๋ฐ›๋Š” ์ปค์Šคํ…€ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค. (ex.UserException)
  • ์ด ๋ฐฉ์‹์„ ํ†ตํ•ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์˜ˆ์™ธ๋ฅผ ํ•œ ๊ณณ์—์„œ ์ผ๊ด€๋˜๊ฒŒ ์‘๋‹ตํ•  ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ์‹œ

Follow ์š”์ฒญ ์ฒ˜๋ฆฌ

//FollowCommandService.java
/*Follow ์š”์ฒญ ์ฒ˜๋ฆฌ*/
@Transactional
@Override
public FollowUserApiResponse followUser(FollowUserCommand command) {
//   ...
  /*์ž๊ธฐ ์ž์‹ ์„ ํŒ”๋กœ์œ  ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐœ์ƒ*/
  if (Objects.equal(command.userId(), command.targetUserId())) {
    throw new UserException(ErrorCode.SELF_FOLLOW_REQUEST_NOT_ALLOWED);
  }
//   ...
}
//GlobalExceptionHandler.java
/*์ „์—ญ ์• ์™ธ ์ฒ˜๋ฆฌ๊ธฐ*/
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

  //   ...
  /*User*/
  @ExceptionHandler(UserException.class)
  public ResponseEntity<ErrorResponse> handleUserException(UserException ex, WebRequest request) {

    return ResponseEntity.status(
            ex.getErrorCode().getHttpStatus()) //UserException ๊ฐ์ง€ ํ›„ ErrorResponse๋กœ ๋ณ€ํ™˜ ํ›„ ๋ฐ˜ํ™˜
        .body(new ErrorResponse(ex.getErrorCode()));
  }
//   ...
}

Security Filter ์˜ˆ์™ธ

ํ๋ฆ„

security_filter_exception_flow

  • Filter์—์„œ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ๋Š” DispatcherServlet๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ๋ชป ํ•œ๋‹ค.
  • @RestControllerAdvice์—์„œ ์ฒ˜๋ฆฌ ๋ถˆ๊ฐ€
  • SecurityFilterChain ๋‚ด๋ถ€์—์„œ ExceptionTranslationFilter๊ฐ€ ์˜ˆ์™ธ๋ฅผ ๊ฐ€๋กœ์ฑ„๊ณ  AuthenticationEntryPoint์„ ์ƒ์†๋ฐ›์€ ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • ์ด ๊ณผ์ •์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์˜ˆ์™ธ๋Š” AuthenticationException์„ ์ƒ์†๋ฐ›์•„์„œ ์ปค์Šคํ…€ ์˜ˆ์™ธ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค. (ex.UserAuthenticationException)
    • RuntimeException์„ ๋˜์ง€๊ฒŒ ๋˜๋ฉด 500 ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Œ
    • ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋ฅผ AuthenticationException์˜ ํ•˜์œ„ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ฒ˜๋ฆฌ

์˜ˆ์‹œ

JWT ํ† ํฐ ์ธ์ฆ

//JwtAuthenticationFilter.java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  private final CustomAuthenticationEntryPoint authenticationEntryPoint;

  //   ...
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
    try {
//         ...
      /*๋ธ”๋ž™๋ฆฌ์ŠคํŠธ์— ๋“ฑ๋ก๋˜ ํ† ํฐ์ด๋ฉด ์˜ˆ์™ธ ๋ฐœ์ƒ*/
      if (authManager.isBlacklisted(token)) {
        throw new TokenException(ErrorCode.TOKEN_INVALID); //๋„๋ฉ”์ธ CustomException ๋ฐœ์ƒ
      }

//         ...
    } catch (TokenException e) {
      TokenAuthenticationException exception = new TokenAuthenticationException(
          e.getErrorCode()); //๋„๋ฉ”์ธ ์˜ˆ์™ธ -> AuthenticationException ํ•˜์œ„ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜
      authenticationEntryPoint.commence(request, response,
          exception); //AuthenticationEntryPoint ํ˜ธ์ถœํ•ด์„œ ๋ณ€ํ™˜๋œ ์˜ˆ์™ธ๋ฅผ ์ง์ ‘ ์ „๋‹ฌ
      return;
    }

    filterChain.doFilter(request, response);
  }
}
//CustomAuthenticationEntryPoint.java
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

  private final ObjectMapper objectMapper;

  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException authException) throws IOException, ServletException {

    //์‘๋‹ต ๊ธฐ๋ณธ ์„ค์ •
    response.setCharacterEncoding("UTF-8");
    response.setContentType("application/json; charset=utf-8");

    //์ปค์Šคํ…€ Authentication ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
    /*TokenAuthenticationException์ผ ๊ฒฝ์šฐ*/
    if (authException instanceof TokenAuthenticationException) {

      ErrorCode errorCode = ((TokenAuthenticationException) authException).getErrorCode();
      response.setStatus(errorCode.getHttpStatus().value()); //ErrorCode์— ๋งคํ•‘๋œ HttpStatus ์ถ”์ถœ
      objectMapper.writeValue(response.getWriter(), //MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ ์‘๋‹ต ํฌ๋งท๊ณผ ๋™์ผํ•˜๊ฒŒ ์‹คํŒจ ๋ฐ”๋”” ์ž‘์„ฑ
          ApiResponse.fail(errorCode));

      return;
    }
    if (authException instanceof UserAuthenticationException) { //๋‹ค๋ฅธ ์ปค์Šคํ…€ Authentication ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
//      ...
      return;
    }
    // ๊ธฐ๋ณธ ์ธ์ฆ ์˜ˆ์™ธ๋Š” 401๋กœ ํ†ต์ผ
    ErrorCode errorCode = ErrorCode.AUTHENTICATION_ERROR;
    response.setStatus(errorCode.getHttpStatus().value());
    objectMapper.writeValue(response.getWriter(),
        ApiResponse.fail(errorCode));

    return;
  }
}

์—๋Ÿฌ ์‘๋‹ต

MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ(DispatcherServlet๋‚ด๋ถ€)์™€ Security ์˜ˆ์™ธ(filter chain)์—๋„ ๋™์ผํ•œ ์‘๋‹ต ํฌ๋งท์„ ์ค€๋‹ค.

์˜ˆ์‹œ

MVC ๋‚ด๋ถ€ ์˜ˆ์™ธ ์‘๋‹ต

{
    "success": false,
    "code": "TLY3000",
    "message": "๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.",
    "data": {},
    "timestamp": "2025-09-26T20:07:29.398202"
}

Security Filter ๋ฐœ์ƒ ์˜ˆ์™ธ ์‘๋‹ต

{
    "success": false,
    "code": "TLY1001",
    "message": "์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.",
    "data": {},
    "timestamp": "2025-09-26T20:08:08.492155"
}

๊ณ ๋ ค์‚ฌํ•ญ

  • Security Filter ์˜ˆ์™ธ๋Š” DispatcherServlet๊นŒ์ง€ ๋„๋‹ฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ @ResponseBodyAdvice๋กœ ๊ณตํ†ต ์‘๋‹ต ํฌ๋งท์ด ์ ์šฉ๋˜์ง€ ์•Š๋Š”๋‹ค.
    • ๋”ฐ๋ผ์„œ AuthenticationEntryPoint์—์„œ ์ง์ ‘ ๊ณตํ†ต ์‘๋‹ต์„ ๋งŒ๋“ค์–ด์„œ ์‘๋‹ตํ•ด์•ผํ•œ๋‹ค.
  • ์ปค์Šคํ…€ ํ•„ํ„ฐ๊ฐ€ ExceptionTranslationFilter ๋ณด๋‹ค ์•ž์— ์žˆ๋Š” ๊ฒฝ์šฐ, ๋‹จ์ˆœํžˆ throw๋งŒ ํ•ด์„œ๋Š” EntryPoint๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค.
    • ํ•„ํ„ฐ ๋‚ด๋ถ€์—์„œ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ authenticationEntryPoint.commence()๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•˜๊ฑฐ๋‚˜ ์ปค์Šคํ…€ ํ•„ํ„ฐ๋ฅผ ExceptionTranslationFilter ๋’ค์— ๋“ฑ๋กํ•ด์„œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•˜๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

์ƒ์„ธ ์›์ธ/ํ•ด๊ฒฐ ๊ณผ์ •์€ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฌธ์„œ์ฐธ๊ณ 


๊ด€๋ จ ๋ฌธ์„œ

โš ๏ธ **GitHub.com Fallback** โš ๏ธ