[Spring Boot] RestControllerAdvice 활용 예외 처리

이준영·2024년 11월 14일

Spring MSA 프로젝트

목록 보기
3/15
post-thumbnail

Java Exceptions

먼저 Java Exception에 대해 알아보자.

이전에 자바 강의(자바의 정석 기초편)를 들으며 정리해두었던 내용이다..

  1. 프로그램 오류
    • 컴파일 에러 : 컴파일 할 때 발생하는 에러
    • 런타임 에러 : 실행 할 때 발생하는 에러 (프로그램 종료)
    • 논리적 에러 : 작성 의도와 다르게 동작 (프로그램 종료X)
    • Java의 런타임 에러
      • 에러(error) : 프로그램 코드에 의해서 수습될 수 없는 심각한 오류
      • 예외(exception) : 프로그램 코드에 의해서 수습될 수 있는 다소 미약한 오류
      • 에러는 어쩔 수 없지만, 예외는 처리하자
    • 예외처리의 정의와 목적
      • 정의 : 프로그램 실행 시 발생할 수 있는 예외의 발생에 대비한 코드를 작성하는 것
      • 목적 : 프로그램의 비정상 종료를 막고, 정상적인 실행상태를 유지하는 것
  2. 예외 클래스의 계층 구조
  3. Exception과 RuntimeException
    • Exception : 사용자의 실수와 같은 외적인 요인에 의해 발생하는 예외
    • RuntimeException : 프로그래머의 실수로 발생하는 예외
  4. 예외 처리하기 : try-catch문
    • try-catch문에서의 흐름
      • try블럭 내에서 예외가 발생한 경우
        • 발생한 예외와 일치하는 catch블럭이 있는 지 확인한다.
        • 일치하는 catch블럭을 찾게 되면, catch 블럭 문장들을 수행하고 전체 try-catch문을 빠져나가서 그 다음 문장을 계속해서 수행한다.
        • 만일 일치하는 catch블럭을 찾지 못하면, 예외 처리가 안됨
      • try블럭 내에서 예외가 발생하지 않은 경우
        • catch블럭을 거치지 않고 전체 try-catch문을 빠져나가 수행을 계속한다.
  5. printStackTrace()와 getMessage()
    • printStackTrace() : 예외 발생 당시의 호출스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
    • getMessage() : 발생한 예외클래스의 인스턴스에 저장된 메시지를 얻을 수 있다.
  6. 멀티 catch 블럭
    • 내용이 같은 catch블럭을 하나로 합친 것(JDK1.7부터)
      try {
      	...
      } catch (ExceptionA | ExceptionB e) {
      	...
      }
  7. 예외 발생시키기
    • 연산자 new를 이용해서 발생시키려는 예외 클래스의 객체를 만든 다음
    • 키워드 throw를 이용해서 예외를 발생시킨다.
  8. checked exception, unchecked exception
    • checked exception : 컴파일러가 예외 처리 여부를 체크(예외 처리 필수)
    • unchecked exception : 컴파일러가 예외 처리 여부를 체크 안함(예외 처리 선택)
  9. 메서드에 예외 선언하기
    • 예외를 처리하는 방법 : try-catch문, 예외 선언하기
    • 메서드가 호출시 발생가능한 예외를 호출하는 쪽에 알리는 것(throws)
  10. finally 블럭
    • 예외 발생여부와 관계없이 수행되어야 하는 코드를 넣는다.
      try {
      	...
      } catch (ExceptionA | ExceptionB e) {
      	...
      } finally {
      	...
      }
  11. 사용자 정의 예외 만들기
    • 직접 예외 클래스를 정의할 수 있다.
    • 조상은 Exception과 RuentimeException 중에서 선택
  12. 예외 되던지기(exception re-throwing)
    • 예외를 처리한 후에 다시 예외를 발생시키는 것
    • 호출한 메서드와 호출된 메서드 양쪽 모두에서 예외처리하는 것
  13. 연결된 예외(chained exception)
    • 한 예외가 다른 예외를 발생시킬 수 있다.
    • 한 예외가 다른 예외를 발생시킬 수 있다.
    • 예외 A가 예외 B를 발생시키면, A는 B의 원인 예외(cause exception)
    • 사용하는 이유
      • 여러 예외를 하나로 묶어서 다루기 위해서
      • checked예외를 unchecked예외로 변경하려 할 떄
  14. hashCode()
    • 객체의 해시코드를 반환하는 메서드
    • Object클래스의 hashCode()는 객체의 주소를 int로 변환해서 반환
    • equals()를 오버라이딩하면, hashCode()도 오버라이딩해야 한다.
    • equals()의 결과가 true인 두 객체의 해시코드는 같아야 하기 때문
  15. toString(), toString()의 오버라이딩
    • toString() : 객체를 문자열(String)으로 변환하기 위한 메서드

ControllerAdvice

https://mangkyu.tistory.com/205
ControllerAdvice에 대한 설명은 해당 블로그에서 자세히 설명하고 있어 참조하였다.

@Log4j2
@RestControllerAdvice
public class GlobalExceptionHandler {

    // DB 관련 에러
    @ExceptionHandler(DataAccessException.class)
    public ResponseEntity<?> handleDataAccessException(DataAccessException e) {
        log.error("[USER ERROR] {}, {}", ResponseCode.USER_DATA_ACCESS_ERROR.getCode(), e.getMessage());
        return ResponseEntity
                .internalServerError()
                .body(MessageResponse.of(ResponseCode.USER_DATA_ACCESS_ERROR));
    }

    // 파라미터 유효성 검증 에러
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException e) {
        log.error("[USER ERROR] {}, {}", ResponseCode.USER_REQUEST_PARAM_INVALID.getCode(), e.getMessage());
        List<String> errors = e.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(fieldError -> fieldError.getField() + ": " + fieldError.getDefaultMessage())
                .toList();

        return ResponseEntity
                .badRequest()
                .body(MessageResponse.of(ResponseCode.USER_REQUEST_PARAM_INVALID, errors));
    }

    // 사용자 에러
    @ExceptionHandler(UserException.class)
    public ResponseEntity<?> handleUserException(UserException e) {
        log.error("[USER ERROR] {}, {}", e.getResponseCode().getCode(), e.getMessage());
        return ResponseEntity
                .badRequest()
                .body(MessageResponse.of(e.getResponseCode()));
    }

}
  • RestControllerAdvice를 활용해 전역예외처리가 가능하도록 하였다.
@PostMapping
public ResponseEntity<?> join(
	@Valid @RequestBody JoinRequest request) {

	Boolean isExists = userService.isExistUser(request.getUsername());

	if (isExists) {
		throw new UserException(ResponseCode.USER_ALREADY_EXIST);
	}

	UserEntity newUser = userService.join(request);

	return ResponseEntity
		.status(HttpStatus.CREATED)
		.body(MessageResponse.success(newUser));
}
  • UserException을 throw 하면 GlobalExceptionHandler에서 응답 처리한다.
  • 또한 에러 로그, 파라미터 에러 상세 내용 처리 등 각 예외 처리를 한 곳에서 관리할 수 있다.
@Getter
@RequiredArgsConstructor
public enum ResponseCode {

    SUCCESS(0,"Success"),
    FAIL(-1,"Fail"),

    // 1xxx: User Exceptions
    USER_REQUEST_PARAM_INVALID(-1000, "User Request Parameter Invalid"),
    USER_ALREADY_EXIST(-1001, "User Already Exist"),
    USER_NOT_FOUND(-1002, "User Not Found"),
    USER_DATA_ACCESS_ERROR(-1003, "User Data Access Error"),
    ;

    private final int code;
    private final String message;
}
  • 실패에 대한 에러 응답 코드는 4자리 음수 값으로 설정하였다.
  • 임시로 1000번대 에러는 User로 정하였지만 추후 기능이 추가되면 정리 하면 좋을 것 같다.

이어서

현재 GlobalExceptionHandler에서 에러 처리를 하며 로그를 출력하고 있다.
이 부분을 Spring AOP를 활용하여 로깅시스템을 구현해보려 한다.

profile
환영합니다!

0개의 댓글