이전 포스트에서 만든 공통 응답을 비즈니스 에러에서도 동일하게 내려주고싶어졌다.
왜 why? 프론트 개발자 입장에선 그게 당연~히 편하다
응답에 대한 Type Interface를 미리 정의 해두면 성공이든 실패든 예외처리가 간편하게 가능하니까
먼저 ErrorCode interface를 정의해둔다
public interface ErrorCode {
int getCode();
String getMessage();
}
뭐 별건 없다 단순한 인터페이스 이제 이걸 상속받을 도메인 별 ErrorEnum을 생성한다
@Getter
@AllArgsConstructor
public enum UserErrorCode implements ErrorCode {
DUPLICATE_USER_ID(HttpStatus.CONFLICT.value(), "중복 되는 아이디 입니다."),
NOT_MATCH_PASSWORD_CONFIRM(HttpStatus.BAD_REQUEST.value(), "비밀번호와 비밀번호 확인이 일치하지 않습니다"),
NOT_FOUND_USER(HttpStatus.NOT_FOUND.value(), "로그인 한 유저를 찾을 수 없습니다"),
BAD_REQUEST_PASSWORD(HttpStatus.BAD_REQUEST.value(), "로그인 정보를 다시 확인하세요"),
HANDLE_ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "로그인이 필요합니다."),
CANT_ACCESS(HttpStatus.UNAUTHORIZED.value(), "접근권한이 없습니다"),
FAIL_KAKAO_LOGIN(HttpStatus.BAD_REQUEST.value(), "카카오 로그인 실패!"),
;
private final int code;
private final String message;
}
ErrorCode의 code부분을 HttpStatus로 리턴받게 할 수도 있지만 커스텀 에러코드의 사용을 생각해서 int로 선언하였다.
그럼 이렇게 생성한 ErrorCode를 실제 사용할 CustomException 파일을 만들어보자
@Getter
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
private final String errorMessage;
public CustomException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
this.errorMessage = errorCode.getMessage();
}
}
RuntimeException 을 상속받아 생성자에 super로 넣어주면 끝!
이제 잘 만든 에러들을 미리 정의해둔 공통 응답 폼에 맞춰 aop단에 만들어보자
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<Response> handleException(Exception e) {
Response errorResponse = new Response(false, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage(), e.getCause());
// 에러 응답 생성
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body(errorResponse);
}
@ExceptionHandler(CustomException.class)
public ResponseEntity<Response> handleCustomException(CustomException e) {
// 에러 정보를 담은 ErrorResponse 객체 생성
Response errorResponse = new Response(false, e.getErrorCode().getCode(), e.getErrorMessage(), null);
// 에러 응답 생성
return ResponseEntity.status(e.getErrorCode().getCode()).body(errorResponse);
}
}
@RestControllerAdvice
어노테이션을 붙이면 json으로 응답을 내려줄 수 있다.
@ControllerAdvice
를 사용하면 에러 페이지를 노출 시켜 줄 수 있다.
정의해둔 에러 외에 개발자의 실수로 인한 런타임 에러도 일단 잡아서 같은 응답으로 내려줘야 하기때문에 Exception.class 받는 메서드와 CustomException.class를 받는 메서드로 나누어 작성한다.
응답은 ResponseEntity로 본인이 만든 공통 폼 DTO를 반환해주면 된다.
이제 실제로 비즈니스 로직에 Error를 throw해보자
private User validUser(Long userId) {
return userRepository.findById(userId).orElseThrow(() -> new CustomException(UserErrorCode.NOT_FOUND_USER));
}
아주 간단하게
throw new CustomException(ErrorCode.Error)
형식으로 지정해주면 비즈니스 로직을 타다 throw를 만나면 반환해준다.
여기까지 하면 모든 에러가 공통 처리가 되었을까?
정답은 아니다. 그 이유는 Spring security 를 이용한 로그인및 인증처리 방식에서
인증처리는 보통 컨트롤러에 실제 요청이 도달하기 전 Filter 단에서 처리가 되는 반면,
AOP는 컨트롤러에 요청이 도달하기 직전에 실행이 되므로
Filter가 AOP보다 앞단에서 실행된다고 생각하면 된다.
그렇다면 인증처리와 같은 Filter 단에서 나오는 에러는 Spring security에서의 에러가 뿌려지게 되므로 내가 예상한 에러 응답과 다른 응답이 뿌려지는데
이에 대해선 순서에 맞게 로그인및 회원가입을 포스팅 한 후 작성하도록 하겠다.