Skip to content

Commit

Permalink
[feat] : 전역 에러 처리 및 에외처리 설정 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
chahyunsoo committed Mar 27, 2024
1 parent e4748dc commit a28df11
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package ussum.homepage.domain.user.exception;

import ussum.homepage.global.error.code.BaseErrorCode;
import ussum.homepage.global.error.exception.GeneralException;

public class UserNotFoundException extends GeneralException {
private BaseErrorCode baseErrorCode;
public UserNotFoundException(BaseErrorCode baseErrorCode) {
super(baseErrorCode);
}
}
39 changes: 39 additions & 0 deletions src/main/java/ussum/homepage/global/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package ussum.homepage.global;

import ussum.homepage.global.error.code.BaseCode;
import ussum.homepage.global.error.status.SuccessStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class ApiResponse<T> {
@JsonProperty("isSuccess")
private final Boolean isSuccess;

private final String code;
private final String message;

@JsonInclude(JsonInclude.Include.NON_NULL)
private final T data;

public static <T> ApiResponse<T> onSuccess(T data) {
return new ApiResponse<>(
true, SuccessStatus._OK.getCode(), SuccessStatus._OK.getMessage(), data);
}

public static <T> ApiResponse<T> onSuccess(String message, T data) {
return new ApiResponse<>(true, SuccessStatus._OK.getCode(), message, data);
}

public static <T> ApiResponse<T> onSuccess(BaseCode code, T data) {
return new ApiResponse<>(true, code.getReasonHttpStatus().getCode(),
code.getReasonHttpStatus().getMessage(), data);
}

public static <T> ApiResponse<T> onFailure(String code, String message, T data) {
return new ApiResponse<>(false, code, message, data);
}
}
9 changes: 9 additions & 0 deletions src/main/java/ussum/homepage/global/error/code/BaseCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ussum.homepage.global.error.code;

import ussum.homepage.global.error.dto.ReasonDto;

public interface BaseCode {
public ReasonDto getReason();

public ReasonDto getReasonHttpStatus();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package ussum.homepage.global.error.code;

import ussum.homepage.global.error.dto.ErrorReasonDto;

public interface BaseErrorCode {
public ErrorReasonDto getReason();

public ErrorReasonDto getReasonHttpStatus();
}
15 changes: 15 additions & 0 deletions src/main/java/ussum/homepage/global/error/dto/ErrorReasonDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ussum.homepage.global.error.dto;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ErrorReasonDto {
private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;
}
15 changes: 15 additions & 0 deletions src/main/java/ussum/homepage/global/error/dto/ReasonDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ussum.homepage.global.error.dto;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@Builder
public class ReasonDto {
private HttpStatus httpStatus;

private final boolean isSuccess;
private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package ussum.homepage.global.error.exception;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
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.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import ussum.homepage.global.ApiResponse;
import ussum.homepage.global.error.dto.ErrorReasonDto;
import ussum.homepage.global.error.status.ErrorStatus;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

/**
* 요청된 타입과 맞지 않는 파라미터가 전달될 때 발생
* <p>
* 작동 방식: 예외에서 속성 이름을 추출하고, "올바른 값이 아닙니다."라는 메시지와 함께
* 내부 메소드 handleExceptionInternalMessage로 전달
*/
@Override
protected ResponseEntity<Object> handleTypeMismatch(TypeMismatchException e, HttpHeaders headers,
HttpStatusCode status, WebRequest request) {

String errorMessage = e.getPropertyName() + ": 올바른 값이 아닙니다.";

return handleExceptionInternalMessage(e, headers, request, errorMessage);
}

/**
* MissingServletRequestParameterException을 처리
* 이 예외는 필수 요청 파라미터가 누락 되었을 때 발생
* <p>
* 작동 방식: 누락된 파라미터 이름을 추출하고, 동일한 "올바른 값이 아닙니다." 메시지를 사용하여 처리
*/
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException e,
HttpHeaders headers,
HttpStatusCode status, WebRequest webRequest) {

String errorMessage = e.getParameterName() + ": 올바른 값이 아닙니다.";

return handleExceptionInternalMessage(e, headers, webRequest, errorMessage);
}

/**
* MethodArgumentNotValidException을 처리
* 이 예외는 메소드 파라미터의 유효성 검사 실패 시 발생
* <p>
* 작동 방식: 각 필드 오류에 대한 정보를 수집하고, 이를 ErrorStatus._BAD_REQUEST와 함께
* handleExceptionInternalArgs 메소드로 전달
*/
@Override
public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers,
HttpStatusCode status, WebRequest webRequest) {

Map<String, String> errors = new LinkedHashMap<>();

e.getBindingResult().getFieldErrors().stream()
.forEach(fieldError -> {
String fieldName = fieldError.getField();
String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");

errors.merge(
fieldName,
errorMessage,
(existingErrorMessage, newErrorMessage) ->
existingErrorMessage + ", " + newErrorMessage);
}
);

return handleExceptionInternalArgs(e, HttpHeaders.EMPTY, ErrorStatus.valueOf("_BAD_REQUEST"),
webRequest, errors);
}

/**
* ConstraintViolationException을 처리
* 이 예외는 Bean Validation API에 의해 유효성 검사 실패 시 발생
*
* 작동 방식: 유효성 검사 위반에 대한 메시지를 수집하고, ErrorStatus.valueOf를 사용하여 적절한 에러 상태(String)로 변환 후,
* handleExceptionInternalConstraint 메소드로 처리
*/
@ExceptionHandler
public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest webRequest) {
String errorMessage =
e.getConstraintViolations().stream()
.map(constraintViolation -> constraintViolation.getMessage())
.findFirst()
.orElseThrow(
() ->
new RuntimeException(
"ConstraintViolationException 추출 도중 에러 발생"));

return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, webRequest);
}

/**
* 일반 Exception을 처리
* 이 메소드는 처리되지 않은 다른 모든 예외를 캡처함
*
* 작동 방식: 예외의 메시지와 ErrorStatus._INTERNAL_SERVER_ERROR를 사용하여
* handleExceptionInternalFalse 메소드로 처리합니다.
*/
@ExceptionHandler
public ResponseEntity<Object> exception(Exception e, WebRequest webRequest) {
e.printStackTrace();

return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY,
ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), webRequest, e.getMessage());
}

/**
* 사용자 정의 예외 GeneralException을 처리함
*
* 작동 방식: GeneralException에서 ErrorReasonDto를 추출하고,
* 이를 handleExceptionInternal 메소드로 전달함
*/
@ExceptionHandler(value = GeneralException.class)
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest httpServletRequest) {

ErrorReasonDto errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();

return handleExceptionInternal(generalException, errorReasonHttpStatus, null, httpServletRequest);
}

private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDto reason,
HttpHeaders httpHeaders,
HttpServletRequest httpServletRequest) {

ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
WebRequest webRequest = new ServletWebRequest(httpServletRequest);

return super.handleExceptionInternal(e, body, httpHeaders, reason.getHttpStatus(), webRequest);
}

private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders httpHeaders,
ErrorStatus errorStatus,
WebRequest webRequest,
Map<String, String> errorArgs) {

ApiResponse<Object> body = ApiResponse.onFailure(errorStatus.getCode(), errorStatus.getMessage(), null);

return super.handleExceptionInternal(e, body, httpHeaders, errorStatus.getHttpStatus(), webRequest);
}


private ResponseEntity<Object> handleExceptionInternalMessage(Exception e, HttpHeaders httpHeaders,
WebRequest webrequest, String errorMessage) {

ErrorStatus badRequestErrorStatus = ErrorStatus._BAD_REQUEST;
ApiResponse<String> body = ApiResponse.onFailure(badRequestErrorStatus.getCode(), badRequestErrorStatus.getMessage(), errorMessage);

return super.handleExceptionInternal(e, body, httpHeaders, badRequestErrorStatus.getHttpStatus(), webrequest);
}

private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorStatus, HttpHeaders headers, WebRequest request) {
ApiResponse<Object> body = ApiResponse.onFailure(errorStatus.getCode(), errorStatus.getMessage(), null);

return super.handleExceptionInternal(e, body, headers, errorStatus.getHttpStatus(), request);
}

private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorStatus,
HttpHeaders httpHeaders, HttpStatus httpStatus,
WebRequest webRequest, String errorPoint) {

ApiResponse<String> body = ApiResponse.onFailure(errorStatus.getCode(), errorStatus.getMessage(), errorPoint);

return super.handleExceptionInternal(e, body, httpHeaders, httpStatus, webRequest);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ussum.homepage.global.error.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import ussum.homepage.global.error.code.BaseErrorCode;
import ussum.homepage.global.error.dto.ErrorReasonDto;

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {
private BaseErrorCode baseErrorCode;

public ErrorReasonDto getErrorReason() {
return this.baseErrorCode.getReason();
}

public ErrorReasonDto getErrorReasonHttpStatus() {
return this.baseErrorCode.getReasonHttpStatus();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ussum.homepage.global.error.exception;

import ussum.homepage.global.error.code.BaseErrorCode;

public class InvalidValueException extends GeneralException {
private BaseErrorCode baseErrorCode;
public InvalidValueException(BaseErrorCode baseErrorCode) {
super(baseErrorCode);
}
}
74 changes: 74 additions & 0 deletions src/main/java/ussum/homepage/global/error/status/ErrorStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ussum.homepage.global.error.status;

import ussum.homepage.global.error.code.BaseErrorCode;
import ussum.homepage.global.error.dto.ErrorReasonDto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {
//기본(전역) 에러
_INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,"COMMON_500", "서버에서 요청을 처리 하는 동안 오류가 발생했습니다."),
_BAD_REQUEST(HttpStatus.BAD_REQUEST,"COMMON_400", "입력 값이 잘못된 요청 입니다."),
_UNAUTHORIZED(HttpStatus.UNAUTHORIZED,"COMMON_401", "인증이 필요 합니다."),
_FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON_403", "금지된 요청 입니다."),

//User 관련 에러
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "USER_001", "사용자를 찾을 수 없습니다."),

//Group 관련 에러
GROUP_NOT_FOUND(HttpStatus.NOT_FOUND,"GROUP_001","그룹을 찾을 수 없습니다."),
ALREADY_GROUP_EXIST(HttpStatus.NOT_FOUND,"GROUP_002","같은 code를 가진 그룹이 존재한다."),

//Member 관련 에러
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,"MEMBER_001","멤버를 찾을 수 없습니다."),

//Board 관련 에러
BOARD_NOT_FOUND(HttpStatus.NOT_FOUND,"BOARD_001","게시판을 찾을 수 없습니다."),

//Post 관련 에러
POST_NOT_FOUND(HttpStatus.NOT_FOUND,"POST_001","게시글을 찾을 수 없습니다."),

//ACL 관련 에러
ACL_PERMISSION_DENIED(HttpStatus.FORBIDDEN,"ACL_001","ACL에 권한이 없습니다."),
INVALID_TARGET(HttpStatus.BAD_REQUEST,"ACL_002","잘못된 ACL을 입력하였습니다."),

//Role 관련 에러
ROLE_PERMISSION_DENIED(HttpStatus.FORBIDDEN,"ROLE_001","필요한 Role을 가지고 있지 않습니다."),

//Token 관련 에러
INVALID_TOKEN(HttpStatus.BAD_REQUEST,"TOKEN_001","토큰이 올바르지 않습니다."),

//Body 에러
INVALID_BODY(HttpStatus.BAD_REQUEST, "BODY_ERROR", "Body가 올바르지 않습니다.");




private final HttpStatus httpStatus;
private final String code;
private final String message;

@Override
public ErrorReasonDto getReason() {
return ErrorReasonDto.builder()
.isSuccess(false)
.code(code)
.message(message)
.build();
}

@Override
public ErrorReasonDto getReasonHttpStatus() {
return ErrorReasonDto.builder()
.isSuccess(false)
.httpStatus(httpStatus)
.code(code)
.message(message)
.build();
}


}
Loading

0 comments on commit a28df11

Please sign in to comment.