프로젝트의 초기 목적은 API서버를 만들어서 프론트엔드를 공부하시는 분들에게 간단한 블로그 API를 제공하면 좋을것 같다는 생각에 시작하게 됐었는데 무백스 스터디원분중 한 분이 프론트엔드로 참여하고 싶다는 의견이 있었고 좋은 기회라 생각되어 어찌저찌 협업 프로젝트가 됐습니다!
현재 진행정도가 많지 않았기에 지금까지 만들었던 API 문서를 통해 협업을 위한 점검을 갖는 시간을 가졌는데 예외 상황에 대해서 에러 메시지만을 전달해주는 경우 프론트측에서 처리할 수 있는 부분이 한정된다는 의견이 있었습니다.
따라서 예외 상황에 대해 에러 메시지와 에러 코드를 같이 응답할 수 있게 리팩토링이 필요했고, 리팩토링을 진행하면서 했던 고민들과 과정을 공유해보려 합니다!
✅ 리팩토링 요구사항
예외 처리에 대한 공통 응답 리팩토링
- 예외에 대한 Http status 제외 예외 메시지를 반환해야 한다.
- 예외에 대한 Http status 제외 공통 에러 코드를 반환해야 한다.
- 공통 에러 코드 포멧
0
: 요청 데이터 검증(valid) 실패-1000
: 예상치 못한 서버 오류1000
: 인증/인가 관련 에러코드2000
: Member(유저) 관련 에러 코드3000
: Post(게시글) 관련 예외 코드4000
: Comment(댓글) 관련 예외 코드5000
: Category(카테고리) 관련 예외 코드6000
: Recommend(추천) 관련 예외 코드
우선 공통 에러 코드 포멧은 위와 같이 정했습니다. 최초에는 100, 200, 300과 같이 100자리 단위로 끊어보려 했으나 Http Status와 굉장히 유사하다는 생각이 들어 1000의 자리로 결정하게 됐습니다
✅ 리팩토링 진행
ErrorCode의 위치
처음에는 단순히 에러 코드를 커스텀 예외에 포함시키고 끝내려고 했으나 몇가지 걸리는 부분이 있습니다.
일단 에러 코드에 대한 부분을 한번에 살펴볼 수 없었습니다. 현재 회원가입에서만 사용되는 커스텀 예외는 다음과 같습니다.
오로지 회원가입에서만 5개의 커스텀 예외를 작성했고, 이런 예외들은 서로다른 에러 코드를 반환해야 합니다. 에러 코드를 커스텀 예외에 각각 작성하게 되면 가독성이 너무 떨어집니다.
따라서 생각한 방식은 각 도메인의 exception 패키지에 XxxErrorCode라는 enum 타입을 두고 에러 코드들을 기록하는 방식입니다.
작성한 MemberErrorCode
는 아래와 같습니다.
public enum MemberErrorCode {
// 회원가입 관련
DUPLICATE_LOGIN_ID(2001),
DUPLICATE_NICKNAME(2002),
INVALID_LOGIN_ID_FORMAT(2003),
INVALID_PASSWORD_CONFIRMATION(2004),
INVALID_PASSWORD_FORMAT(2005);
private int errorCode;
MemberErrorCode(final int errorCode) {
this.errorCode = errorCode;
}
public int value() {
return errorCode;
}
}
위와 같이 작성하게 되면 각 도메인에 해당되는 예외만 한번에 관리할 수 있다는 장점이 있고, 특정 도메인의 정보가 해당 패키지 내부에서만 의존관계를 갖게되므로 조금더 응집력을 높일 수 있었습니다.
ControllerAdvice에게 에러 코드 전달하기
에러 코드의 위치를 정했으니 이제 ControllerAdvice에게 어떻게 에러 코드를 전달해줄지를 정해야 합니다.
현재 제가 작성한 예외의 구조는 다음과 같습니다.
의존관계를 간단하게 추상화 하면CustomException
-> HttpStatusException
-> BusinessException
-> RuntimeException
과 같이 예외가 전달됩니다.
ControllerAdvice에서는 이중에서 HttpStatusException에 대한 예외를 핸들링합니다.
@Slf4j
@RestControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(final BindingResult bindingResult) {}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(final BadRequestException e) {}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e) {}
}
결국 CustomException에서 HttpStatusException으로 에러 코드를 전달해주면 되기에 CustomException에서 XxxErrorCode를 HttpStatusException의 생성자를 통해 전달해주며 해결할 수 있었습니다.
// CustomException
public class DuplicateLoginIdException extends BadRequestException {
private static final String MESSAGE = "이미 존재하는 아이디 입니다.";
public DuplicateLoginIdException() {
super(MESSAGE, MemberErrorCode.DUPLICATE_LOGIN_ID.value());
}
}
// HttpStatusException
public class BadRequestException extends BusinessException {
private final int errorCode;
public BadRequestException(final String message, final int errorCode) {
super(message);
this.errorCode = errorCode;
}
public int getErrorCode() {
return errorCode;
}
}
따라서 advice
패키지는 받아온 int 타입의 에러코드를 응답 dto에 포함시켜 응답을 진행하면됩니다.
@RestControllerAdvice
public class ControllerAdvice {
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ErrorResponse> handleBadRequestException(final BadRequestException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage(), e.getErrorCode()));
}
}
✅ 관련 PR
refactor: 비즈니스 예외가 발생하면 예외 메시지 뿐만 아니라 에러 코드도 같이 응답하도록 변경 b
관련 이슈 #12 설명 기존에 비즈니스 로직을 처리하는 부분은 단순하게 예외 메시지만을 반환하고 있었다. 무백스 스터디원의 의견을 참고해 프론트에서 조금 더 다양한 기능을 할 수 있도록 예
github.com