
예외 발생에 대한 처리는 try~catch 구문으로 컨트롤한다.
가독성을 해치고, 관리에 용이하지 않다.
한 곳에서 혹은 한 모듈안에서 해결해보자.
💡 게시글 도메인 중
게시글 아이디(pk)로 찾을 수 없거나,
게시글 수정/삭제에 대한 권한이 없는 경우 예외를 발생시키고
클라이언트에게 메시지를 전달해보자
위의 시나리오대로 CustomException 을 만들고
ExceptionHandler 를 통해 예외를 핸들링해보자.
🎯 CustomException
만약, 게시글을 찾을 수 없거나 수정/삭제에 대한 권한이 없는데 접근한 경우
발생시킬 Post~Exception 을 만든다.
현재 스터디에서는 Exception 과 ErrorCode 를 전역적으로 사용하기 때문에 최상위 클래스로 두어 타입을 묶어준다.
🎯 Global-AppException
모든 커스텀 익셉션은 AppException 을 상속한다.
AppErrorCode 를 매개변수로 받아 메시지를 전달한다.
public class AppException extends RuntimeException {
@Getter
private final AppErrorCode errorCode;
public AppException(AppErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
🎯 Global-AppErrorCode
마찬가지로 전역적으로 사용할 에러 코드의 인터페이스다.
메시지와 Http 상태 코드의 게터를 구현해야한다.
public interface AppErrorCode {
String getMessage();
HttpStatus getHttpStatus();
}
🎯 게시글 아이디(pk)로 찾을 수 없을 때 발생 시킬 Exception
게시글의 아이디(pk)로 조회했을 때 찾을 수 없다면 발생시킬 Exception 이다.
public class PostNotFoundByIdException extends AppException {
@Getter
private final Long id;
public PostNotFoundByIdException(AppErrorCode errorCode, Long id) {
super(errorCode);
this.id = id;
}
}
🎯 게시글 ErrorCode
AppErrorCode 인터페이스의 구현체로 만든다.
enum 은 열거형 상수들의 모음이다.
@RequiredArgsConstructor, @Getter 어노테이션으로
간단하게 구현이 가능하다.
해당 ErrorCode 가 어떤 ErrorCode 인지 네이밍을 통해 의도를 드러내자.
@RequiredArgsConstructor
@Getter
public enum PostErrorCode implements AppErrorCode {
NOT_FOUND_POST_BY_ID_ERROR("존재하지 않는 게시글입니다. Id : ", HttpStatus.BAD_REQUEST),
UNAUTHORIZED_POST_UPDATE_ERROR("본인 글만 업데이트할 수 있습니다.", HttpStatus.BAD_REQUEST),
UNAUTHORIZED_POST_DELETE_ERROR("본인 글만 삭제할 수 있습니다.", HttpStatus.BAD_REQUEST),
;
private final String message;
private final HttpStatus httpStatus;
}
🎯 Service
비즈니스 로직을 처리하는 부분에서의 코드다.
예외를 던지는 메서드를 내부 메서드로 따로 분리를 했다.
@Service
@RequiredArgsConstructor
public class PostCommandService implements PostCommandUsecase {
/*
* ...
*/
private User loadUserFrom(Long userId) {
return userRepository.findById(userId)
.orElseThrow(() ->
new UserNotFoundByIdException(UserErrorCode.NOT_FOUND_USER_BY_ID_ERROR));
}
private Post loadPostFrom(Long id) {
return postRepository.findById(id)
.orElseThrow(() ->
new PostNotFoundByIdException(PostErrorCode.NOT_FOUND_POST_BY_ID_ERROR, id));
}
}
loadUserFrom : 유저의 ID 로 유저를 찾는 메서드다. 존재하지 않을 경우 예외를 던진다.
loadPostFrom : 게시글의 ID로 게시글을 찾는 메서드다. 존재하지 않을 경우 예외를 던진다.
(User~Exception 도 같은 방법으로 만들었다.)
🎯 ExceptionHandler
예외를 던지는 곳은 Service 에서 비즈니스 로직을 처리할 때다.
캐치를 하는 곳은 Controller 가 아닌 ExceptionHandler 이다.
캐치하는 코드를 만들자.
@RestControllerAdvice
@Slf4j
public class PostExceptionHandler {
@ExceptionHandler(PostNotFoundByIdException.class)
public ResponseEntity<?> catchPostNotFoundByIdException(PostNotFoundByIdException e) {
String errorMessage = e.getMessage() + e.getId();
int httpStatus = e.getErrorCode().getHttpStatus().value();
log.error("catchPostNotFoundByIdException : " + errorMessage);
return ResponseEntity
.status(httpStatus)
.body(errorMessage);
}
}
@RestControllerAdvice : Spring MVC 에서 예외 처리를 전역적으로 관리하기 위한 어노테이션이다.
@Slf4j : 에러 로깅을 위한 어노테이션이다.
@ExceptionHandler : 특정 예외 타입에 대한 처리 메서드를 정의한다.
@ExceptionHandler(xxx.class) : xxx.class 익셉션이 발생할 경우 캐치를 하겠다는 뜻이다. 여러 개의 class를 명시할 수 있다.
해당 클래스 익셉션 발생시 캐치 후 클라이언트에게 상태 코드와 메시지를 전달한다.