Skip to content

Commit

Permalink
Merge pull request #39 from YAPP-Github/feature/#31
Browse files Browse the repository at this point in the history
[#31] Feature: Global Exception 처리 & Global Response 형식 지정
  • Loading branch information
rong5026 authored Jan 28, 2025
2 parents 9c64ce5 + b6eb7e4 commit 6e89c38
Show file tree
Hide file tree
Showing 18 changed files with 441 additions and 34 deletions.
15 changes: 9 additions & 6 deletions application/main-application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,21 @@ dependencies {
implementation 'com.fasterxml.uuid:java-uuid-generator:5.1.0'
}


bootJar {
archiveFileName = 'yapp-main-application.jar'
archiveFileName = 'yapp-main-application.jar'
enabled = true // 실행 가능한 모듈에서만 활성화
}

jar {
enabled = false
}

tasks.named('test') {
useJUnitPlatform()
}

bootJar {
enabled = true
springBoot {
mainClass = "org.mainapplication.MainApplication" // MainApplication 클래스의 패키지 경로
}

jar {
enabled = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,4 @@ public ResponseEntity<String> refreshAccessToken(@CookieValue("RefreshToken") St
String newAccessToken = tokenService.reissueAccessToken(refreshToken, response);
return ResponseEntity.ok(newAccessToken);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.mainapplication.global.constants.WebSecurityURI;
import org.mainapplication.global.filter.JwtAuthenticationFilter;
import org.mainapplication.global.filter.TestAuthenticationFilter;
import org.mainapplication.global.oauth2.handler.CustomAuthenticationEntryPoint;
import org.mainapplication.global.oauth2.handler.CustomOAuth2FailureHandler;
import org.mainapplication.global.oauth2.handler.CustomOAuth2SuccessHandler;
import org.mainapplication.global.oauth2.service.CustomOauth2UserService;
Expand Down Expand Up @@ -37,6 +38,7 @@ public class WebSecurityConfig {
private final CustomOauth2UserService customOauth2UserService;
private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;
private final CustomOAuth2FailureHandler customOAuth2FailureHandler;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final TestAuthenticationFilter testAuthenticationFilter;

private void defaultBasicFilterChain(HttpSecurity http) throws Exception {
Expand Down Expand Up @@ -73,23 +75,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
)
.exceptionHandling(exceptionHandling ->
exceptionHandling
.authenticationEntryPoint(customAuthenticationEntryPoint())
.authenticationEntryPoint(customAuthenticationEntryPoint.oAuth2EntryPoint())
)
.addFilterBefore(testAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// .addFilterBefore(jwtAuthenticaltionFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return (request, response, authException) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"로그인이 필요합니다.\"}");
};
}

@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ public final class WebSecurityURI {
"/auth/login/oauth2/code/google",
"/swagger-ui/**",
"/v3/api-docs/**",
"/swagger-resources/**"
"/swagger-resources/**",
"/common/health/**"
);

public static final List<String> CORS_ALLOW_URIS =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package org.mainapplication.global.error;

import java.io.Serializable;

import lombok.Getter;

@Getter
public abstract class CustomException extends RuntimeException implements Serializable {

private final transient ErrorCodeStatus errorCodeStatus;

protected CustomException(ErrorCodeStatus errorCodeStatus) {
super(errorCodeStatus.getMessage());
this.errorCodeStatus = errorCodeStatus;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.mainapplication.global.error;

import org.springframework.http.HttpStatus;

public interface ErrorCodeStatus {
String name();

HttpStatus getHttpStatus();

String getMessage();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mainapplication.global.error;

public record ErrorResponse(String errorClassName, String message) {

public static ErrorResponse of(String errorClassName, String message) {
return new ErrorResponse(errorClassName, message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package org.mainapplication.global.error;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.mainapplication.global.error.code.CommonErrorCode;
import org.mainapplication.global.response.GlobalResponse;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex,
Object body,
HttpHeaders headers,
HttpStatusCode statusCode,
WebRequest request) {

ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(), ex.getMessage());
return super.handleExceptionInternal(ex, errorResponse, headers, statusCode, request);
}

/**
* enum 타입 String의 값을 잘못입력 햇을 때
*/
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(
HttpMessageNotReadableException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("HttpMessageNotReadableException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/**
* 메소드 인자가 유효하지 않을 때 클라이언트에게 HTTP 400 상태 코드(Bad Request)를 반환.
* 주로 Valid, @valiated 에러시 발생
*/
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("MethodArgumentNotValidException : {}", ex.getMessage(), ex);
String errorMessage = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(), errorMessage);
final GlobalResponse response = GlobalResponse.fail(status.value(), errorResponse);
return ResponseEntity.status(status).body(response);
}

/**
* 지원되지 않는 HTTP 요청 메서드를 사용했을 때 클라이언트에게 HTTP 405 상태 코드(Method Not Allowed)를 반환
*/
@Override
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
HttpRequestMethodNotSupportedException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("HttpRequestMethodNotSupportedException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = CommonErrorCode.METHOD_NOT_ALLOWED;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/**
* 요청 주소가 없는 주소일 경우 404 상태코드(Not Found)
*/
@Override
protected ResponseEntity<Object> handleNoHandlerFoundException(
NoHandlerFoundException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("NoHandlerFoundException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = CommonErrorCode.NOT_FOUND_REQUEST_ADDRESS;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/**
* 요청 리소스가 없을 경우 (없는 주소로 요청)
*/
@Override
protected ResponseEntity<Object> handleNoResourceFoundException(
NoResourceFoundException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("NoResourceFoundException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = CommonErrorCode.NOT_FOUND_REQUEST_RESOURCE;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/**
* @RequestParam의 값이 없을 때
*/
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("MissingServletRequestParameterException : {}", ex.getMessage(), ex);

final ErrorCodeStatus errorCodeStatus = CommonErrorCode.REQUESTED_PARAM_NOT_VALIDATE;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/**
* @Valid나 @Validated 애노테이션을 사용하여 요청 본문 또는 메서드의 인자에 대한 유효성을 검사할 때 발생
* List<User>의 request를 받을 때 발생
*/
@Override
protected ResponseEntity<Object> handleHandlerMethodValidationException(
HandlerMethodValidationException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {

log.error("HandlerMethodValidationException : {}", ex.getMessage(), ex);

final ErrorCodeStatus errorCodeStatus = CommonErrorCode.REQUESTED_VALUE_NOT_VALIDATE;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/** enum type 일치하지 않아 binding 못할 경우 발생 주로 @RequestParam enum으로 binding 못했을 경우 발생 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
protected ResponseEntity<GlobalResponse> handleMethodArgumentTypeMismatchException(
MethodArgumentTypeMismatchException ex) {

log.error("MethodArgumentTypeMismatchException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/** CustomException 예외 처리 */
@ExceptionHandler(CustomException.class)
public ResponseEntity<GlobalResponse> handleCustomException(CustomException ex) {

log.error("CustomException : {}", ex.getMessage(), ex);
final ErrorCodeStatus errorCodeStatus = ex.getErrorCodeStatus();
final ErrorResponse errorResponse = ErrorResponse.of(errorCodeStatus.name(), errorCodeStatus.getMessage());
final GlobalResponse response = GlobalResponse.fail(errorCodeStatus.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(errorCodeStatus.getHttpStatus()).body(response);
}

/** 500번대 에러 처리 */
@ExceptionHandler(Exception.class)
protected ResponseEntity<GlobalResponse> handleException(Exception ex) {

log.error("Internal Server Error : {}", ex.getMessage(), ex);
final ErrorCodeStatus internalServerError = CommonErrorCode.INTERNAL_SERVER_ERROR;
final ErrorResponse errorResponse = ErrorResponse.of(ex.getClass().getSimpleName(),
internalServerError.getMessage());
final GlobalResponse response = GlobalResponse.fail(internalServerError.getHttpStatus().value(), errorResponse);
return ResponseEntity.status(internalServerError.getHttpStatus()).body(response);
}

/** Request Param Validation 예외 처리 */
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<GlobalResponse> handleConstraintViolationException(
ConstraintViolationException e) {
log.error("ConstraintViolationException : {}", e.getMessage(), e);

Map<String, Object> bindingErrors = new HashMap<>();
e.getConstraintViolations()
.forEach(
constraintViolation -> {
List<String> propertyPath =
List.of(
constraintViolation
.getPropertyPath()
.toString()
.split("\\."));
String path =
propertyPath.stream()
.skip(propertyPath.size() - 1L)
.findFirst()
.orElse(null);
bindingErrors.put(path, constraintViolation.getMessage());
});

final ErrorResponse errorResponse = ErrorResponse.of(e.getClass().getSimpleName(), bindingErrors.toString());
final GlobalResponse response = GlobalResponse.fail(HttpStatus.BAD_REQUEST.value(), errorResponse);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.mainapplication.global.error.code;

import org.mainapplication.global.error.ErrorCodeStatus;
import org.springframework.http.HttpStatus;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum CommonErrorCode implements ErrorCodeStatus {
// 서버 오류
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP method 입니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류, 관리자에게 문의하세요"),
METHOD_ARGUMENT_TYPE_MISMATCH(HttpStatus.BAD_REQUEST, "Enum Type이 일치하지 않거나 값이 없습니다."),
REQUESTED_PARAM_NOT_VALIDATE(HttpStatus.BAD_REQUEST, "Reqeust Param 값이 유효하지 않습니다"),
REQUESTED_VALUE_NOT_VALIDATE(HttpStatus.BAD_REQUEST, "메서드의 인자가 유효하지 않습니다."),
NOT_FOUND_REQUEST_ADDRESS(HttpStatus.NOT_FOUND, "잘못된 요청 주소입니다."),
NOT_FOUND_REQUEST_RESOURCE(HttpStatus.NOT_FOUND, "존재하지 않은 요청 주소입니다.");

private final HttpStatus httpStatus;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.mainapplication.global.error.exception;

import org.mainapplication.global.error.CustomException;
import org.mainapplication.global.error.ErrorCodeStatus;

public class CommonException extends CustomException {
public CommonException(ErrorCodeStatus errorCodeStatus) {
super(errorCodeStatus);
}
}
Loading

0 comments on commit 6e89c38

Please sign in to comment.