java 에는 Checked Exception
과 Unchecked Exception
이 존재합니다.
이둘은 헷갈리지만 사실상 큰 차이가 존재합니다.
RuntimeException을 상속하지 않는 클래스는 Checked Exception
로 분류할 수 있고,
RuntimeException을 상속하는 클래스는 Unchecked Exception
으로 분류할 수 있습니다.
@Transactional
rollbakFor
에 기본 옵션이 들어가있기 때문에 예외 발생 시 롤백 처리됨@RestponseStatus(code= HttpStatus.NOT_FOUND, reason = "Data Not Found")
public class DataNotFoundException extends RuntimeExcetpion{}
@ResponseStatus
어노테이션을 사용할 수 습니다.throw new ResponseStatusException(HttpStatus.NOT_FOUND, "No Data");
위와 같은 이유로 Spring Exception 처리는 다른 방법으로 하게 되었습니다.
ErrorCode
: 핵심, 모든 예외 케이스를 이곳에서 관리함.CustomException
: 기본적으로 제공되는 Excetpion 외에 사용ErrorResponse
: 사용자에게 JSON 형식으로 보여주기 위해 에러 응답 형식 지정GlobalExceptionHandler
: Custom Exception Handler @ControllAdvice
: 프로젝트 전역에서 발생하는 Exception을 잡기 위한 클래스@ExceptionHandler
: 특정 Exception을 지정해서 별도로 처리해줌# applicatioin.yml
server:
error:
include-exception: false //response에 Exception을 표시할지
include-message: always //response에 Exception Message를 표시할지(never | always " on_param)
include- stacktrace: on_param //response에 Stack Trace를 표시할지(never | always " on_param)
whitelabel.enabled: true // 에러 발생 시 Spring 기본 에러 페이지 노출 여부
@Getter
@AllArgsConstructor
public enum ErrorCode {
/* 400 BAD_REQUEST : 잘못된 요청 */
INVALID_REFRESH_TOKEN(BAD_REQUEST, "리프레시 토큰이 유효하지 않습니다"),
MISMATCH_REFRESH_TOKEN(BAD_REQUEST, "리프레시 토큰의 유저 정보가 일치하지 않습니다"),
CANNOT_FOLLOW_MYSELF(BAD_REQUEST, "자기 자신은 팔로우 할 수 없습니다"),
/* 401 UNAUTHORIZED : 인증되지 않은 사용자 */
INVALID_AUTH_TOKEN(UNAUTHORIZED, "권한 정보가 없는 토큰입니다"),
UNAUTHORIZED_MEMBER(UNAUTHORIZED, "현재 내 계정 정보가 존재하지 않습니다"),
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
MEMBER_NOT_FOUND(NOT_FOUND, "해당 유저 정보를 찾을 수 없습니다"),
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "로그아웃 된 사용자입니다"),
NOT_FOLLOW(NOT_FOUND, "팔로우 중이지 않습니다"),
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
DUPLICATE_RESOURCE(CONFLICT, "데이터가 이미 존재합니다"),
;
private final HttpStatus httpStatus;
private final String detail;
}
@ResponseStatusException
과 비슷해 보이지만, 차이점은 개발자가 정의한 새로운 Exception을 모두 한 곳에서 관리하고 재사용 할수 있습니다.@Getter
@AllArgsConstructor
public class CustomException extends RuntimeException {
private final ErrorCode errorCode;
}
@Getter
@Builder
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String error;
private final String code;
private final String message;
public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
return ResponseEntity
.status(errorCode.getHttpStatus())
.body(ErrorResponse.builder()
.status(errorCode.getHttpStatus().value())
.error(errorCode.getHttpStatus().name())
.code(errorCode.name())
.message(errorCode.getDetail())
.build()
);
}
}
ResponseEntity<ErrorResponse>
로 변환해줍니다.@ControllerAdvice
는 프로젝트 전역에서 발생하는 모든 예외를 잡아줍니다.@ExceptionHandle
는 발생한 특정 예외를 잡아서 하나의 메서드에서 공통 처리해 줄 수 있게 해줍니다.@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(value = { ConstraintViolationException.class, DataIntegrityViolationException.class})
protected ResponseEntity<ErrorResponse> handleDataException() {
log.error("handleDataException throw Exception : {}", DUPLICATE_RESOURCE);
return ErrorResponse.toResponseEntity(DUPLICATE_RESOURCE);
}
@ExceptionHandler(value = { CustomException.class })
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("handleCustomException throw CustomException : {}", e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
}
@RestControllerAdvice
를 사용합니다.@RequiredArgsConstructor
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Transactional
public boolean follow(Long memberId) {
Member currentMember = getCurrentMember();
// 팔로우할 상대방 정보가 없는 경우
Member targetMember = memberRepository.findById(memberId)
.orElseThrow(() -> new CustomException(MEMBER_NOT_FOUND));
// 자기 자신을 팔로우 하려는 경우
if (currentMember.equals(targetMember)) {
throw new CustomException(CANNOT_FOLLOW_MYSELF);
}
// code...
}
}