
기존 프로젝트가 어느정도 갈무리하고 있어서 최근에 새로운 프로젝트를 시작했다. 역시나 기본 구조는 거의 비슷하게 가져갈 것이기 때문에 이전에 깊게 공부하지 못했던 부분은 벨로그에 남기면서 복습할 예정이다.
프로젝트를 하다보면 런타임 에러를 기획 의도대로 만드는 경우가 많다. 예를 들어 글을 수정할 때 작성자 외에는 권한을 주면 안되는데, 이는 코드를 작성하는 사람이 직접 validate를 해줘야한다.
이외에도 여러 에러들을 전역처리 해줄 수 있는 것이 @ControllerAdvice다.
[spring mvc architecture]

ControllerAdvice를 통한 에러 처리는 "DispatcherServlet"단에서 처리된다.
tip : 나중에 security때문에 filter를 두게 되는데, 이때 생기는 에러의 발생을 절대 컨트롤러어드바이스를 통해 처리하면 안된다.
이 DispatcherServlet에서 실행(doDispatch)을 하다가 에러가 발생하면(exception != null)
@ControllerAdvice로 정의된 빈이 주입된 handlerExceptionResolvers가 실행되는 구조다.
잠깐 exception 전역처리 전에, 우리는 exception 뿐만 아니라 결과가 제대로 나왔을 때의 형식도 통일할 것이다. 이렇게 하는 이유는 유지보수를 위함이며 프론트에게 통일된 결과를 전달하기 위함이다. 프로젝트에서는 백엔드만 존재하는 것이 아니라 여러 역할들이 존재하고 우리가 무엇을 어떻게 처리하든 통일된 결과를 전달해주어야 전달받는 입장에서도 분리된 역할을 문제 없이 수행할 수 있을 것이다. 이런것 또한 객체지향적인 코드가 아닐까 싶다.
@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponseDto<T> {
private final Boolean isSuccess;
private final Integer code;
private final String message;
private T result;
public static <T> ApiResponseDto<T> onSuccess(T result) {
return new ApiResponseDto<>(true, 2000, SuccessStatus._SUCCESS.getMessage(), result);
}
public static <T> ApiResponseDto<T> of(BaseCode code, T result) {
return new ApiResponseDto<>(true, code.getReasonHttpStatus().getCode(), code.getReasonHttpStatus().getMessage(), result);
}
public static <T> ApiResponseDto<T> onFailure(Integer code, String message, T data) {
return new ApiResponseDto<>(false, code, message, data);
}
}
controller에서는 ApiResponseDto.onSuccess()를 사용하여 결과값을 리턴하고 실패의 경우에는 controllerAdvice에서 ApiResponseDto.onFailure()과 같은 형식으로 실패처리해준다.
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
//생략
}
@ExceptionHandler
public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
Reason errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
}
private ResponseEntity<Object> handleExceptionInternal(Exception e, Reason reason,
HttpHeaders headers, HttpServletRequest request) {
ApiResponseDto<Object> body = ApiResponseDto.onFailure(reason.getCode(), reason.getMessage(), null);
WebRequest webRequest = new ServletWebRequest(request);
return super.handleExceptionInternal(
e,
body,
headers,
reason.getHttpStatus(),
webRequest
);
}
GeneralException을 상속받는 예외의 경우에 한해서 처리할 예외처리 메서드이다. 나중에 각 엔티티에 맞는 [Entity]Handler.class들을 만들고 이를 GeneralException으로부터 상속을 받게되어 사용할 것이다. 그렇게 되면 각 핸들러 클래스들은 이 메서드에게 검열이 된다.
@Getter
@Builder
public class Reason {
private HttpStatus httpStatus;
private final boolean isSuccess;
private final Integer code;
private final String message;
private final HashMap<String, String> result;
}
public interface BaseCode {
Reason getReason();
Reason getReasonHttpStatus();
}
단순히 메서드를 위한 인터페이스이다. 두 메서드 모두 reason을 반환하도록 되어있다.
@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException{
private BaseCode code;
@Override
public String getMessage() {
return code.getReason().getMessage();
}
public Reason getErrorReason() {
return this.code.getReason();
}
public Reason getErrorReasonHttpStatus() {
return this.code.getReasonHttpStatus();
}
}
이렇게 만들어진 BaseCode인터페이스는 ErroStatus와 SuccessStatus에게 상속된다. 각 status(Enum)은 ENUM(HttpStatus, code, message)을 담게되는데 Reason으로부터 해당 값들을 넘겨받게된다.
@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseCode{
// 서버 오류
_INTERNAL_SERVER_ERROR(INTERNAL_SERVER_ERROR, 5000, "서버 에러, 관리자에게 문의 바랍니다."),
_UNAUTHORIZED_LOGIN_DATA_RETRIEVAL_ERROR(INTERNAL_SERVER_ERROR, 5001, "서버 에러, 로그인이 필요없는 요청입니다."),
_ASSIGNABLE_PARAMETER(BAD_REQUEST, 5002, "인증타입이 잘못되어 할당이 불가능합니다."),
// 일반적인 요청 오류
_BAD_REQUEST(BAD_REQUEST, 4000, "잘못된 요청입니다."),
_UNAUTHORIZED(UNAUTHORIZED, 4001, "로그인이 필요합니다."),
_FORBIDDEN(FORBIDDEN, 4002, "금지된 요청입니다."),
//인증 관련 오류(4050 ~ 4099)
//회원 관련 오류(4100 ~ 4149)
MEMBER_NOT_FOUND(NOT_FOUND, 4100, "존재하지 않는 회원입니다"),
//프로모션 관련 오류(4150 ~ 4199)
PROMOTION_NOT_FOUND(NOT_FOUND, 4150, "존재하지 않는 회원입니다"),
PROMOTION_ONLY_CAN_BE_TOUCHED_BY_WRITER(BAD_REQUEST, 4151, "작성자가 아닌 유저는 프로모션을 수정이 불가합니다.");
private final HttpStatus httpStatus;
private final Integer code;
private final String message;
@Override
public Reason getReason() {
return Reason.builder()
.message(message)
.code(code)
.isSuccess(false)
.build();
}
@Override
public Reason getReasonHttpStatus() {
return Reason.builder()
.message(message)
.code(code)
.isSuccess(false)
.httpStatus(httpStatus)
.build();
}
}
ErrorStatus는 오류상황에 맞게 각 코드값이 고유값이다.(unique) 이렇게 설정해주는 이유는 우리가 throw GeneralException할 때 basecode 또한 들고 가져간다. 나중에 원하는 정보들을 꺼내려면 getReason() 메서드를 사용해야하는데, 이때 코드값이 마치 데이터베이스의 pk처럼 쓰여 원하는 enum value를 들고와 reason객체를 반환한다.
한마디로 code가 있으면 원하는 reason값을 반환받을 수 있는 것이다.
@Override
public Member getByMemberId(Long memberId) {
return memberRepository.findById(memberId)
.orElseThrow(() -> new MemberHandler(ErrorStatus.MEMBER_NOT_FOUND));
}

이전 프로젝트에서 같이 작업했던 팀원이 선진문물(?)처럼 가져온 방식인데 다시 만들고 구조를 뜯어보니 약간 벽을 느꼈다. 이 방식을 사용하다보면 진짜 객체지향적이다라는 생각이 들 수 밖에 없다. 단순히 에러는 throw Handler처리 해주면 알아서 결과를 내어주는데, 그 안에 들어가는 코드의 복잡도나 유기성이 대단하다는 생각이 든다. 무엇보다 한번 만들고 더이상 유지보수할 필요없이 사용만 하면 된다는 점이 진짜 좋은 것 같다.
이런걸 내가 생각할 수나 있을까? 싶다..