시작하며
공용 에러를 정의하고 스웨거를 통해 에러에 대해서 문서화를 하는 방법에 대해서 이야기 하도록 하겠다.
이 게시물은 공통으로 사용할 에러의 형식을 만들어서 설정하는 부분을 먼저 이야기 하고 다음 게시물에서 swagger로 에러를 문서화 하는 방법에 대해서 이야기하도록 하겠다.
이펙티브 자바 item 38 번
확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 라는 내용이 있다.
public interface Operation {
double apply(double x, dobule y);
}
public enum BasicOperation implements Operation {
PLUS("+"){
public double apply(double x, dobule y) {return x + y; }
}
}
public enum ExtendedOperation implements Operation {
EXP("^"){
public double apply(double x, dobule y) {return Math.pow(x ,y ); }
}
}
출처 : 이펙티브 자바 p.233
위 내용처럼 인터페이스를 선언한뒤에 다형성을 사용해서
사용하는 부분에서는 Operation 인터페이스만 바라보도록 할 수 있다.
위 방식을 사용해서 에러코드들을 도메인 별로 분리하는 방법과 이후에 swagger를 통해서 문서화 하는 방법에 대해 알아보자.
에러 형식
다음과 같이 에러 형식을 통일하고, 이제 이것에 맞게 코드를 짜보려고 한다.
{
"success": false,
"status": 401,
"code": "AUTH_401_1",
"reason": "인증 시간이 만료되었습니다. 인증토큰을 재 발급 해주세요",
"timeStamp": "2024-09-19T11:24:42.948392",
"path": "요청시 패스정보입니다."
}
ErrorReason이라는 객체를 먼저 만든다.
package com.groomiz.billage.global.dto;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class ErrorReason {
private final Integer status;
private final String code;
private final String reason;
}
에러가 통일 된 형식을 가지고 싶기 때문에 다음과 같은 구조의 dto를 하나 제작해준다.
package com.groomiz.billage.global.dto;
import java.time.LocalDateTime;
import lombok.Getter;
@Getter
public class ErrorResponse {
private final boolean success = false;
private final int status;
private final String code;
private final String reason;
private final LocalDateTime timeStamp;
private final String path;
public ErrorResponse(ErrorReason errorReason, String path) {
this.status = errorReason.getStatus();
this.code = errorReason.getCode();
this.reason = errorReason.getReason();
this.timeStamp = LocalDateTime.now();
this.path = path;
}
public ErrorResponse(int status, String code, String reason, String path) {
this.status = status;
this.code = code;
this.reason = reason;
this.timeStamp = LocalDateTime.now();
this.path = path;
}
}
모든 Exception이 구현할 BaseErrorCode 인터페이스를 만들어준다.
public interface BaseErrorCode {
public ErrorReason getErrorReason();
String getExplainError() throws NoSuchFieldException;
}
이후 상수를 사용하기 편하게 static으로 사용할 클래스를 하나 만들어줬다.
package com.groomiz.billage.global.consts;
public class BillageStatic {
public static final String AUTH_HEADER = "Authorization";
public static final String BEARER = "Bearer ";
public static final String ACCESS_TOKEN = "ACCESS_TOKEN";
public static final String REFRESH_TOKEN = "REFRESH_TOKEN";
public static final int MILLI_TO_SECOND = 1000;
public static final int BAD_REQUEST = 400;
public static final int UNAUTHORIZED = 401;
public static final int FORBIDDEN = 403;
public static final int NOT_FOUND = 404;
public static final int CONFLICT = 409;
public static final int INTERNAL_SERVER = 500;
public static final Long NO_START_NUMBER = 1000000L;
public static final Long MINIMUM_PAYMENT_WON = 1000L;
public static final Long ZERO = 0L;
public static final String KAKAO_OAUTH_QUERY_STRING =
"/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code";
public static final String[] SwaggerPatterns = {
"/swagger-resources/**", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs",
};
}
BaseErrorCode를 가지고 있는 GlobalCodeException 객체를 만들겠다.
@Getter
@AllArgsConstructor
public class GlobalCodeException extends RuntimeException {
private BaseErrorCode errorCode;
public ErrorReason getErrorReason() {
return this.errorCode.getErrorReason();
}
}
대부분의 에러는 GlobalCodeException을 상속 받아서 에러 객체를 만들 것이다.
그러나 모든 에러가 이렇게 형식적인 건 아니고 동적으로 에러를 만들고 싶을 수 있다.
@Getter
@AllArgsConstructor
public class GlobalDynamicException extends RuntimeException {
private final int status;
private final String code;
private final String reason;
}
따라서 다음과 같은 GlobalDynamicException 클래스도 만들어줬다.
GlobalException Handler
ControllerAdvice를 이용해서 Exception handler를 대부분이 사용할텐데,
GlobalExceptionHandler 전체 코드는 다음과 같다.
전체적으로 주석으로 작성해뒀지만, 그래도 함수별로 천천히 코드를 설명해보겠다.
package com.groomiz.billage.global.exception;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
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 com.fasterxml.jackson.databind.ObjectMapper;
import com.groomiz.billage.global.dto.ErrorReason;
import com.groomiz.billage.global.dto.ErrorResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, Object body, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest(); // 예외가 발생한 URL과 같은 요청에 대한 세부 정보를 추출
String url = httpServletRequest.getRequestURL().toString();
HttpStatus httpStatus = (HttpStatus)status;
ErrorResponse errorResponse =
new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase(),
ex.getMessage(),
url); // 사용자 정의 ErrorResponse 객체를 생성
return super.handleExceptionInternal(ex, errorResponse, headers, status, request);
}
//주로 요청 본문이 유효성 검사를 통과하지 못할 때 발생합니다 (예: @Valid 어노테이션 사용 시) MethodArgumentNotValidException 예외를 처리하는 메서드
@SneakyThrows // 메서드 선언부에 Throws 를 정의하지 않고도, 검사 된 예외를 Throw 할 수 있도록 하는 Lombok 에서 제공하는 어노테이션입
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest();
String url = httpServletRequest.getRequestURL().toString();
Map<String, Object> fieldAndErrorMessages =
errors.stream()
.collect(
Collectors.toMap(
FieldError::getField, FieldError::getDefaultMessage));
String errorsToJsonString = new ObjectMapper().writeValueAsString(fieldAndErrorMessages);
HttpStatus httpStatus = (HttpStatus)status;
ErrorResponse errorResponse =
new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase(), errorsToJsonString, url);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
/** Request Param Validation 예외 처리
* 유효성 검사 제약 조건이 위반되었을 때 발생합니다. (예: @NotNull, @Size, @Email 어노테이션 사용 시)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> constraintViolationExceptionHandler(
ConstraintViolationException ex, HttpServletRequest request) {
Map<String, Object> bindingErrors = new HashMap<>(); // 유효성 검사 실패 필드와 해당 오류 메시지를 저장하기 위한 맵을 생성
// 예외 객체에서 유효성 검사 위반 항목들을 가져옴.
ex.getConstraintViolations()
.forEach(
constraintViolation -> {
//위반된 속성의 경로를 가져옵니다. 이 경로는 문자열로 변환되어 점(.)을 기준으로 분할됩니다
List<String> propertyPath =
List.of(
constraintViolation
.getPropertyPath()
.toString()
.split("\\."));
// 마지막 요소를 추출하여 실제 필드 이름을 가져옵니다. 예를 들어, 경로가 user.address.street라면 street가 추출됩니다.
String path =
propertyPath.stream()
.skip(propertyPath.size() - 1L)
.findFirst()
.orElse(null);
//위반된 필드 이름과 해당 오류 메시지를 맵에 저장
bindingErrors.put(path, constraintViolation.getMessage());
});
ErrorReason errorReason =
ErrorReason.builder()
.code("BAD_REQUEST")
.status(400)
.reason(bindingErrors.toString())
.build();
ErrorResponse errorResponse =
new ErrorResponse(errorReason, request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
@ExceptionHandler(GlobalCodeException.class)
public ResponseEntity<ErrorResponse> BillCodeExceptionHandler(
GlobalCodeException e, HttpServletRequest request) {
BaseErrorCode code = e.getErrorCode();
ErrorReason errorReason = code.getErrorReason();
ErrorResponse errorResponse =
new ErrorResponse(errorReason, request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
@ExceptionHandler(GlobalDynamicException.class)
public ResponseEntity<ErrorResponse> BillDynamicExceptionHandler(
GlobalDynamicException e, HttpServletRequest request) {
ErrorResponse errorResponse =
new ErrorResponse(
e.getStatus(),
e.getCode(),
e.getReason(),
request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(e.getStatus())).body(errorResponse);
}
//TODO: 이 경우 디코에 알림 가도록 구성해도 좋겠다.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception ex, HttpServletRequest request)
throws IOException {
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest(); // 예외가 발생한 URL과 같은 요청에 대한 세부 정보를 추출
String url = httpServletRequest.getRequestURL().toString();
log.error("INTERNAL_SERVER_ERROR", ex);
GlobalErrorCode internalServerError = GlobalErrorCode.INTERNAL_SERVER_ERROR;
ErrorResponse errorResponse =
new ErrorResponse(
internalServerError.getStatus(),
internalServerError.getCode(),
internalServerError.getReason(),
url);
return ResponseEntity.status(HttpStatus.valueOf(internalServerError.getStatus()))
.body(errorResponse);
}
}
RestControllerAdvice를 이용해서 ExceptionHandler를 이용하고자 한다.
혹시나 ControllerAdvice에 대한 이해가 부족하다면 아래 게시글을 참고하도록 하자.
https://mangkyu.tistory.com/205
1. handleExceptionInternal
@Override
protected ResponseEntity<Object> handleExceptionInternal(
Exception ex, Object body, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest(); // 예외가 발생한 URL과 같은 요청에 대한 세부 정보를 추출
String url = httpServletRequest.getRequestURL().toString();
HttpStatus httpStatus = (HttpStatus)status;
ErrorResponse errorResponse =
new com.groomiz.billage.global.dto.ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase(),
ex.getMessage(),
url); // 사용자 정의 ErrorResponse 객체를 생성
return super.handleExceptionInternal(ex, errorResponse, headers, status, request);
}
이 메소드는 기본적으로 ResponseEntityExceptionHandler에서 예외를 처리할 때 사용되는 메소드이다. 이 메소드를 오버라이드하여 사용자 정의 처리를 추가할 수 있다.
- ServletWebRequest : WebRequest 객체를 ServletWebRequest 로 캐스팅해서, HTTP 서블릿 관련 요청 정보를 추출한다. 이를 통해 HTTP 요청 URL, HTTP 메소드 등과 같은 정보를 얻을 수 있다.
- HttpServletRequest: 실제 서블릿 기반의 HTTP 요청 객체로, 이곳에서 예외가 발생한 URL을 추출하고 있다.
- getRequestURL().toString(): 예외가 발생한 요청의 URL을 문자열 형태로 추출한다. 이는 클라이언트에게 반환할 에러 응답 객체에 포함된다.
이후 사용자 정의 ErrorResponse 객체를 생성한다.
- HttpStatus httpStatus: HttpStatusCode 객체를 HttpStatus로 캐스팅하여 HTTP 상태 코드와 관련된 세부 정보를 얻는다.
- ErrorResponse: 사용자 정의 에러 응답 객체이다. 이 객체는 클라이언트에게 전달할 에러 메시지, HTTP 상태 코드, 예외 메시지, 발생한 URL 등을 포함한다.
- httpStatus.value(): HTTP 상태 코드를 숫자 형태로 가져온다. 예를 들어, 404 (NOT FOUND), 500 (INTERNAL SERVER ERROR) 등의 코드가 된다..
- httpStatus.getReasonPhrase(): 상태 코드에 해당하는 문구를 가져온다. 예를 들어, 404이면 "Not Found"와 같은 문구가 반환된다.
- ex.getMessage(): 발생한 예외의 메시지를 반환한다..
- url: 예외가 발생한 요청의 URL이다.
ErrorResponse 객체는 클라이언트에게 반환되게 된다.
return super.handleExceptionInternal(ex, errorResponse, headers, status, request);
이 부분에서 부모 클래스인 ResponseEntityExceptionHandler의 handleExceptionInternal 메소드를 호출하고, 그 결과를 반환한다.
여기서 두 번째 파라미터로 사용자 정의 ErrorResponse 객체를 전달하여, 기본 응답 대신 우리가 정의한 내용으로 클라이언트에게 응답이 전달된다.
2. handleMethodArgumentNotValid
//주로 요청 본문이 유효성 검사를 통과하지 못할 때 발생합니다 (예: @Valid 어노테이션 사용 시) MethodArgumentNotValidException 예외를 처리하는 메서드
@SneakyThrows // 메서드 선언부에 Throws 를 정의하지 않고도, 검사 된 예외를 Throw 할 수 있도록 하는 Lombok 에서 제공하는 어노테이션입
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest();
String url = httpServletRequest.getRequestURL().toString();
Map<String, Object> fieldAndErrorMessages =
errors.stream()
.collect(
Collectors.toMap(
FieldError::getField, FieldError::getDefaultMessage));
String errorsToJsonString = new ObjectMapper().writeValueAsString(fieldAndErrorMessages);
HttpStatus httpStatus = (HttpStatus)status;
ErrorResponse errorResponse =
new ErrorResponse(httpStatus.value(), httpStatus.getReasonPhrase(), errorsToJsonString, url);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
이 코드에서는 요청 본문이 유효성 검사를 통과하지 못할 때 발생하는 MethodArgumentNotValidException을 처리하는 메서드를 정의하고 있다. 이 예외는 Spring에서 @Valid나 @Validated와 같은 어노테이션을 사용하여 요청 본문에 대한 유효성 검사를 수행할 때, 필드 검사가 실패했을 경우 발생한다.
해당 메서드는 유효성 검사를 통과하지 못한 필드에 대한 오류 정보를 클라이언트에게 반환하는 역할을 한다.
- @SneakyThrows는 Lombok에서 제공하는 어노테이션으로, 메소드에서 throws 절을 명시적으로 선언하지 않고도 checked exception를 던질 수 있도록 해준다.
- 이 경우, 메소드 내부에서 ObjectMapper.writeValueAsString 메서드가 예외를 던질 수 있기 때문에, 이를 처리하기 위해 @SneakyThrows가 사용되었다. 이 어노테이션 덕분에 try-catch를 따로 작성하지 않아도 예외를 던질 수 있다.
List<FieldError> errors = ex.getBindingResult().getFieldErrors();
- 유효성 검사를 통과하지 못한 필드의 에러를 추출할 수 있다.
- getBindingResult(): 유효성 검사 실패에 대한 결과를 반환하는 메서드로, 이 메서드를 통해 검증 실패한 필드와 관련된 정보를 얻을 수 있다.
- getFieldErrors(): 실패한 필드의 리스트를 반환한다. 이 리스트는 각각의 FieldError 객체로, 각 필드의 유효성 검사 실패 정보 (필드명과 에러 메시지)를 포함하고 있다.
이후 위에서 설명했듯, 예외 발생 URL을 추출한다.
Map<String, Object> fieldAndErrorMessages =
errors.stream()
.collect(
Collectors.toMap(
FieldError::getField, FieldError::getDefaultMessage));
스트림을 사용해서 ieldError 객체 리스트에서 필드명(FieldError::getField)과 그에 해당하는 기본 에러 메시지(FieldError::getDefaultMessage)를 맵으로 변환한다.
여기서 key는 필드 이름이고, value는 해당 필드의 에러 메시지이다. 이 방식으로 유효성 검사를 통과하지 못한 모든 필드와 메시지를 매핑한다.
이후 ObjectMapper를 이용해서 JSON을 문자열로 바꾸고, 사용자 정의 ErrorResponse 객체 생성한다.
3. constraintViolationExceptionHandler
/** Request Param Validation 예외 처리
* 유효성 검사 제약 조건이 위반되었을 때 발생합니다. (예: @NotNull, @Size, @Email 어노테이션 사용 시)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> constraintViolationExceptionHandler(
ConstraintViolationException ex, HttpServletRequest request) {
Map<String, Object> bindingErrors = new HashMap<>(); // 유효성 검사 실패 필드와 해당 오류 메시지를 저장하기 위한 맵을 생성
// 예외 객체에서 유효성 검사 위반 항목들을 가져옴.
ex.getConstraintViolations()
.forEach(
constraintViolation -> {
//위반된 속성의 경로를 가져옵니다. 이 경로는 문자열로 변환되어 점(.)을 기준으로 분할됩니다
List<String> propertyPath =
List.of(
constraintViolation
.getPropertyPath()
.toString()
.split("\\."));
// 마지막 요소를 추출하여 실제 필드 이름을 가져옵니다. 예를 들어, 경로가 user.address.street라면 street가 추출됩니다.
String path =
propertyPath.stream()
.skip(propertyPath.size() - 1L)
.findFirst()
.orElse(null);
//위반된 필드 이름과 해당 오류 메시지를 맵에 저장
bindingErrors.put(path, constraintViolation.getMessage());
});
ErrorReason errorReason =
ErrorReason.builder()
.code("BAD_REQUEST")
.status(400)
.reason(bindingErrors.toString())
.build();
ErrorResponse errorResponse =
new ErrorResponse(errorReason, request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
이 코드는 위에서 본 것과 거의 같고 유효성 제약조건이 틀렸을 때 나오는 에러 메시지를 정의한다.
4. GlobalCodeExceptionHandler
@ExceptionHandler(GlobalCodeException.class)
public ResponseEntity<ErrorResponse> BillCodeExceptionHandler(
GlobalCodeException e, HttpServletRequest request) {
BaseErrorCode code = e.getErrorCode();
ErrorReason errorReason = code.getErrorReason();
ErrorResponse errorResponse =
new ErrorResponse(errorReason, request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(errorReason.getStatus()))
.body(errorResponse);
}
대부분 우리가 정의한 에러는 GlobalCodeException 클래스이기 때문에, 에러 메시지는 이렇게 만들어진다고 보면 된다.
5. GlobalDynamicExceptionHandler
@ExceptionHandler(GlobalDynamicException.class)
public ResponseEntity<ErrorResponse> GlobalDynamicExceptionHandler(
GlobalDynamicException e, HttpServletRequest request) {
ErrorResponse errorResponse =
new ErrorResponse(
e.getStatus(),
e.getCode(),
e.getReason(),
request.getRequestURL().toString());
return ResponseEntity.status(HttpStatus.valueOf(e.getStatus())).body(errorResponse);
GlobalDynamicException 같은 경우 우리가 미리 정의한 객체가 아닌 새롭게 만들어서 제작할 예정이다.
아직은 두개가 무슨 차이인지 감이 안 올 수도 있다. 일단 설정해두고 나중에 다시 이해하도록 하자.
6. handleException
//TODO: 이 경우 디코에 알림 가도록 구성해도 좋겠다.
@ExceptionHandler(Exception.class)
protected ResponseEntity<ErrorResponse> handleException(Exception ex, HttpServletRequest request)
throws IOException {
ServletWebRequest servletWebRequest = (ServletWebRequest)request;
HttpServletRequest httpServletRequest = servletWebRequest.getRequest(); // 예외가 발생한 URL과 같은 요청에 대한 세부 정보를 추출
String url = httpServletRequest.getRequestURL().toString();
log.error("INTERNAL_SERVER_ERROR", ex);
GlobalErrorCode internalServerError = GlobalErrorCode.INTERNAL_SERVER_ERROR;
ErrorResponse errorResponse =
new ErrorResponse(
internalServerError.getStatus(),
internalServerError.getCode(),
internalServerError.getReason(),
url);
return ResponseEntity.status(HttpStatus.valueOf(internalServerError.getStatus()))
.body(errorResponse);
}
handleException 에서 서버가 어떤 오류가 나더라도 위에있는 것에 걸리지 않았다면, 이 오류는 개발자가 생각한 오류가 아닐 것이다.
여기에서 디스코드나 메일 등으로 알림이 가도록 구성해준다면, 예기치 못한 오류에 대해서 모니터링 할 수 있다.
오류 정의하기
이제 오류를 처리하는 로직을 만들었으니, 어떻게 오류를 만들고 어떻게 throw해야 하는지 등에 대해서 작성해보겠다.
도메인 별로 에러를 정의할 것이고, 각 도메인마다 에러가 있다.
Auth에서 오류가 발생하는 경우를 생각해서 AuthException을 예시로 만들어보겠다.
1. 에러 정의하기
다음과 같이 GlobalCodeException을 상속 받는 Auth도메인에 있는 오류를 만들어준다.
package com.groomiz.billage.auth.exception;
import com.groomiz.billage.global.exception.GlobalCodeException;
import lombok.Getter;
@Getter
public class AuthException extends GlobalCodeException {
public AuthException(AuthErrorCode errorCode) {
super(errorCode);
}
}
이렇게 AuthException을 정의해주고 앞으로 Auth에 관련된 예외는 이걸 이용해서 던지면 된다.
2. 에러코드 정의하기
이런 Enum을 이용해서 에러를 정의한다.
형식은 다음과 같다.
package com.groomiz.billage.auth.exception;
import static com.groomiz.billage.global.consts.BillageStatic.*;
import java.lang.reflect.Field;
import java.util.Objects;
import com.groomiz.billage.global.anotation.ExplainError;
import com.groomiz.billage.global.dto.ErrorReason;
import com.groomiz.billage.global.exception.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum AuthErrorCode implements BaseErrorCode {
@ExplainError("accessToken 만료시 발생하는 오류입니다.")
TOKEN_EXPIRED(UNAUTHORIZED, "AUTH_401_1", "인증 시간이 만료되었습니다. 인증토큰을 재 발급 해주세요"),
@ExplainError("인증 토큰이 잘못됐을 때 발생하는 오류입니다.")
INVALID_TOKEN(UNAUTHORIZED, "AUTH_401_2", "잘못된 토큰입니다. 재 로그인 해주세요"),
@ExplainError("refreshToken 만료시 발생하는 오류입니다.")
REFRESH_TOKEN_EXPIRED(FORBIDDEN, "AUTH_403_1", "인증 시간이 만료되었습니다. 재 로그인 해주세요."),
@ExplainError("헤더에 올바른 accessToken을 담지않았을 때 발생하는 오류(형식 불일치 등)")
ACCESS_TOKEN_NOT_EXIST(FORBIDDEN, "AUTH_403_2", "알맞은 accessToken을 넣어주세요.");
private final Integer status;
private final String code;
private final String reason;
@Override
public ErrorReason getErrorReason() {
return ErrorReason.builder().reason(reason).code(code).status(status).build();
}
@Override
public String getExplainError() throws NoSuchFieldException {
Field field = this.getClass().getField(this.name());
ExplainError annotation = field.getAnnotation(ExplainError.class);
return Objects.nonNull(annotation) ? annotation.value() : this.getReason();
}
}
커스텀 어노테이션 제작
문서화를 하고 싶은 에러만 보여줄 수 있게 커스텀 어노테이션을 제작한다.
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ExplainError {
String value() default "";
}
사용 예시
public void logout(HttpServletRequest request, HttpServletResponse response) {
// 헤더에서 RefreshToken 가져오기
String refreshToken = request.getHeader("RefreshToken");
if (refreshToken == null || !refreshToken.startsWith("Bearer ")) {
throw new AuthException(AuthErrorCode.ACCESS_TOKEN_NOT_EXIST);
}
// Bearer 접두사 제거
refreshToken = refreshToken.substring(7);
// RefreshToken 만료 확인
if (jwtUtil.isExpired(refreshToken)) {
throw new AuthException(AuthErrorCode.REFRESH_TOKEN_EXPIRED);
}
// RefreshToken의 유효성 확인
String category = jwtUtil.getCategory(refreshToken);
String username = jwtUtil.getUsername(refreshToken);
if (!"RefreshToken".equals(category) || !redisService.checkExistsValue(refreshToken)) {
throw new AuthException(AuthErrorCode.INVALID_TOKEN);
}
// Redis에서 RefreshToken 삭제
redisService.deleteValues(username);
// 응답에서 RefreshToken 헤더를 제거
response.setHeader("RefreshToken", "");
}
로그아웃 로직을 가져와봤다.
여기서 오류가 나는 경우에 생성하는 AuthException을 보고 이렇게 오류를 정의할 수 있다는 것을 기억하자.
마치며
이번 게시물은 오류 처리를 하기 위한 내용이 주를 이룬다.
Exception 처리 같은 경우도 매번 프로젝트마다 거의 통일 된 형식을 이루는데, 이걸 사용하는 보일 플레이트격의 게시물을 만들고 싶어서 다음과 같은 글을 쓰게 되었다.
다음 게시물에서는 스웨거를 이용해서 지금까지 만든 Exception을 문서화하는 방법에 대해서 알아보도록 하겠다.
https://haward.tistory.com/251
'Backend' 카테고리의 다른 글
Aws SQS Spring 스캐줄러를 사용한 Listener 관리하기 (1) | 2024.10.06 |
---|---|
[spring swagger로 에러문서화하기] 에러를 문서로 만들기 (2/2) (2) | 2024.09.28 |
[k8s] kubectl 명령어가 동작하지 않을 때 (왜 쿠버네티스는 스왑 메모리 사용을 허용하지 않는가?) (1) | 2024.09.17 |
Spring Boot에서 Swagger를 통한 API 문서화 설정 가이드 (2) | 2024.09.13 |
Aws SQS Spring으로 Listener Stop Start하기 (0) | 2024.08.13 |