|
| 1 | +package com.ceos22.cgv.common.config.exception; |
| 2 | + |
| 3 | +import com.ceos22.cgv.common.codes.ErrorCode; |
| 4 | +import com.ceos22.cgv.common.response.ErrorResponse; |
| 5 | +import lombok.extern.slf4j.Slf4j; |
| 6 | +import org.springframework.http.HttpStatus; |
| 7 | +import org.springframework.http.ResponseEntity; |
| 8 | +import org.springframework.http.converter.HttpMessageNotReadableException; |
| 9 | +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; |
| 10 | +import org.springframework.security.core.Authentication; |
| 11 | +import org.springframework.security.core.AuthenticationException; |
| 12 | +import org.springframework.security.authentication.AnonymousAuthenticationToken; |
| 13 | +import org.springframework.security.core.context.SecurityContextHolder; |
| 14 | +import org.springframework.web.HttpRequestMethodNotSupportedException; |
| 15 | +import org.springframework.web.bind.MethodArgumentNotValidException; |
| 16 | +import org.springframework.web.bind.MissingRequestHeaderException; |
| 17 | +import org.springframework.web.bind.MissingServletRequestParameterException; |
| 18 | +import org.springframework.web.bind.annotation.ControllerAdvice; |
| 19 | +import org.springframework.web.bind.annotation.ExceptionHandler; |
| 20 | +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; |
| 21 | +import org.springframework.web.servlet.resource.NoResourceFoundException; |
| 22 | +import org.springframework.validation.BindException; |
| 23 | + |
| 24 | +import jakarta.validation.ConstraintViolationException; |
| 25 | +import org.springframework.security.access.AccessDeniedException; |
| 26 | +import org.springframework.web.server.ResponseStatusException; |
| 27 | + |
| 28 | +@Slf4j |
| 29 | +@ControllerAdvice |
| 30 | +public class GlobalExceptionHandler { |
| 31 | + |
| 32 | + /** |
| 33 | + * @RequestBody 검증 실패 등 Parameter 값이 유효하지 않은 경우 |
| 34 | + */ |
| 35 | + @ExceptionHandler(MethodArgumentNotValidException.class) |
| 36 | + protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception){ |
| 37 | + log.error("handleMethodArgumentNotValidException", exception); |
| 38 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR); |
| 39 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 40 | + } |
| 41 | + |
| 42 | + /** |
| 43 | + * 이미 존재하는 nickname으로 회원가입을 시도하는 경우 (예시) |
| 44 | + */ |
| 45 | + @ExceptionHandler(IllegalArgumentException.class) |
| 46 | + public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException exception) { |
| 47 | + log.error("handleIllegalArgumentException", exception); |
| 48 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.DUPLICATE_NICKNAME_ERROR); |
| 49 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * parameter 바인딩 과정의 오류 (ex. @ModelAttribute 바인딩 실패 등) |
| 54 | + */ |
| 55 | + @ExceptionHandler(BindException.class) |
| 56 | + public ResponseEntity<ErrorResponse> handleBindException(BindException exception) { |
| 57 | + log.error("handleBindException", exception); |
| 58 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR); |
| 59 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 60 | + } |
| 61 | + |
| 62 | + /** |
| 63 | + * Body로 데이터가 넘어오지 않았을 경우 |
| 64 | + */ |
| 65 | + @ExceptionHandler(HttpMessageNotReadableException.class) |
| 66 | + protected ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException exception) { |
| 67 | + log.error("HttpMessageNotReadableException", exception); |
| 68 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.REQUEST_BODY_MISSING_ERROR); |
| 69 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 70 | + } |
| 71 | + |
| 72 | + /** |
| 73 | + * 필수 Request Parameter가 누락된 경우 |
| 74 | + */ |
| 75 | + @ExceptionHandler(MissingServletRequestParameterException.class) |
| 76 | + protected ResponseEntity<ErrorResponse> handleMissingServletRequestParameterException(MissingServletRequestParameterException exception) { |
| 77 | + log.error("handleMissingServletRequestParameterException", exception); |
| 78 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.MISSING_REQUEST_PARAMETER_ERROR); |
| 79 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * 필수 Request Header가 누락된 경우 |
| 84 | + */ |
| 85 | + @ExceptionHandler(MissingRequestHeaderException.class) |
| 86 | + protected ResponseEntity<ErrorResponse> handleMissingRequestHeaderException(MissingRequestHeaderException exception) { |
| 87 | + log.error("handleMissingRequestHeaderException", exception); |
| 88 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.MISSING_REQUEST_HEADER_ERROR); |
| 89 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 90 | + } |
| 91 | + |
| 92 | + /** |
| 93 | + * 타입 변환 실패 (예: /api/{id} 에 문자열 전달) |
| 94 | + */ |
| 95 | + @ExceptionHandler(MethodArgumentTypeMismatchException.class) |
| 96 | + protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException exception) { |
| 97 | + log.error("handleMethodArgumentTypeMismatchException", exception); |
| 98 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.INVALID_TYPE_VALUE); |
| 99 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 100 | + } |
| 101 | + |
| 102 | + /** |
| 103 | + * @RequestParam, @PathVariable 등에서의 제약(@NotNull 등) 위반 |
| 104 | + */ |
| 105 | + @ExceptionHandler(ConstraintViolationException.class) |
| 106 | + protected ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException exception) { |
| 107 | + log.error("handleConstraintViolationException", exception); |
| 108 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NOT_VALID_ERROR); |
| 109 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * 지원하지 않는 HTTP Method 호출 |
| 114 | + */ |
| 115 | + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) |
| 116 | + protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException exception) { |
| 117 | + log.error("handleHttpRequestMethodNotSupportedException", exception); |
| 118 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.METHOD_NOT_ALLOWED_ERROR); |
| 119 | + return new ResponseEntity<>(response, HttpStatus.METHOD_NOT_ALLOWED); |
| 120 | + } |
| 121 | + |
| 122 | + /** |
| 123 | + * 잘못된 주소로 요청 한 경우 |
| 124 | + */ |
| 125 | + @ExceptionHandler(NoResourceFoundException.class) |
| 126 | + protected ResponseEntity<ErrorResponse> handleNoResourceFoundException(NoResourceFoundException exception) { |
| 127 | + log.error("handleNoResourceFoundException", exception); |
| 128 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.NO_RESOURCE_FOUND_ERROR); |
| 129 | + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); |
| 130 | + } |
| 131 | + |
| 132 | + |
| 133 | + /** |
| 134 | + * @PreAuthorize 등 메서드 보안에서 인증이 없거나(anonymous) 권한이 부족한 경우 처리 |
| 135 | + * anonymous 이면 401, 그 외 권한 부족은 403 |
| 136 | + */ |
| 137 | + @ExceptionHandler(AccessDeniedException.class) |
| 138 | + protected ResponseEntity<ErrorResponse> handleAccessDeniedException(AccessDeniedException exception) { |
| 139 | + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); |
| 140 | + boolean isAnonymous = (auth == null) || (auth instanceof AnonymousAuthenticationToken); |
| 141 | + |
| 142 | + if (isAnonymous) { |
| 143 | + log.warn("Unauthorized (Anonymous) Access Exception: {}", exception.getMessage()); |
| 144 | + return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.UNAUTHORIZED_ERROR), HttpStatus.UNAUTHORIZED); |
| 145 | + } |
| 146 | + |
| 147 | + log.warn("Forbidden Access Exception: {}", exception.getMessage()); |
| 148 | + return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.FORBIDDEN_ERROR), HttpStatus.FORBIDDEN); |
| 149 | + } |
| 150 | + |
| 151 | + /** |
| 152 | + * 인증 과정에서의 일반 AuthenticationException 은 401로 반환 |
| 153 | + */ |
| 154 | + @ExceptionHandler({AuthenticationException.class, AuthenticationCredentialsNotFoundException.class}) |
| 155 | + protected ResponseEntity<ErrorResponse> handleAuthenticationException(RuntimeException exception) { |
| 156 | + log.warn("Authentication Exception: ", exception); |
| 157 | + return new ResponseEntity<>(ErrorResponse.fromErrorCode(ErrorCode.UNAUTHORIZED_ERROR), HttpStatus.UNAUTHORIZED); |
| 158 | + } |
| 159 | + |
| 160 | + /** |
| 161 | + * 서비스에서 던진 ResponseStatusException을 표준 ErrorResponse로 변환 |
| 162 | + */ |
| 163 | + @ExceptionHandler(ResponseStatusException.class) |
| 164 | + protected ResponseEntity<ErrorResponse> handleResponseStatusException(ResponseStatusException exception) { |
| 165 | + int status = exception.getStatusCode().value(); |
| 166 | + ErrorCode code; |
| 167 | + switch (status) { |
| 168 | + case 400 -> code = ErrorCode.BAD_REQUEST_ERROR; |
| 169 | + case 401 -> code = ErrorCode.UNAUTHORIZED_ERROR; |
| 170 | + case 403 -> code = ErrorCode.FORBIDDEN_ERROR; |
| 171 | + case 404 -> code = ErrorCode.NOT_FOUND_ERROR; |
| 172 | + case 405 -> code = ErrorCode.METHOD_NOT_ALLOWED_ERROR; |
| 173 | + case 409 -> code = ErrorCode.CONFLICT_ERROR; |
| 174 | + default -> code = ErrorCode.INTERNAL_SERVER_ERROR; |
| 175 | + } |
| 176 | + // 정확한 Error 사유는 로그로 기록 |
| 177 | + log.warn("ResponseStatusException: status={}, reason={}", status, exception.getReason()); |
| 178 | + |
| 179 | + // 클라이언트에는 표준화된 메시지 전달 (정확한 에러 발생 원인에 대한 보안 유지) |
| 180 | + return new ResponseEntity<>(ErrorResponse.fromErrorCode(code), HttpStatus.valueOf(status)); |
| 181 | + } |
| 182 | + |
| 183 | + /** |
| 184 | + * 그 외 모든 Exception 경우 |
| 185 | + */ |
| 186 | + @ExceptionHandler(Exception.class) |
| 187 | + protected final ResponseEntity<ErrorResponse> handleAllExceptions(Exception exception) { |
| 188 | + log.error("Exception", exception); |
| 189 | + final ErrorResponse response = ErrorResponse.fromErrorCode(ErrorCode.INTERNAL_SERVER_ERROR); |
| 190 | + return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); |
| 191 | + } |
| 192 | + |
| 193 | +} |
0 commit comments