Exception(예외) 처리의 중요성

Member(회원) 관련 API들을 만들고 있는 중에, API를 만들면서 입력값 혹은 입력조건에 위배되는 값들이 들어온다면 예외처리를 해야한다고 느꼈습니다. 

이유를 정리해보면, 

1, Backend단에서 예외처리를 통해 DB와의 통신을 최소화하게 하기 위하여입니다.

2. 잘못된 데이터가 들어가는것을 막기 위하여 예외처리를 하기 위하여입니다.

3. 적절한 Exception을 통하여 클라이언트단에 피드백 전달하기 위하입니다.

API가 실제로 DB와 통신을 하기전에 예외처리를 진행함으로써 불필요한 입력값들을 예외처리하여 Server에 부하를 막아주고, 이런 처리 하나하나가 Server 운영에 도움이 되기에 꼼꼼한 Exception 처리는 중요합니다.

이런 생각으로 Spring Boot에서는 Exception 처리를 어떻게 하는것이 좋을까 라는 생각으로 알아보게되었습니다.

 

 

간단하게 알아보는 JAVA에서 제공하는 Exception 클래스의 구조도 예시

Java Class의 Error와 Exception 예시 ( 소수의 Exception 과 Error 를 넣었습니다. )

 

위의 Class 구조도 전체적인 구조

위의 그림을 보면, Java.lang.Object를 Throwable이 extends하고, 

Error와 Exception이 Throwable을 extends 하고 있습니다.

그리고 다시 구현체들이 Error와 Exception을 extends 하고 있습니다.

또 여기서 각각의 구현체들이 Error, Checked Exception, UnChecked Excepion 으로 나뉘어지고 있습니다.

 

Error

  • 말그대로 에러입니다. 시스템에 비정상적인 상황이 발생하여 프로그램을 실행시킬 수 없습니다.
  • 이떄는 동작중인 프로그램이 중단됩니다.

Exception

  • Checked Exception ( 체크 예외 )
    • 반드시 에러 처리를 해야합니다. (try/catch or throw)
    • 확인시점은 컴파일단계
    • 기본설정은 예외 발생시 트랜잭션 roll back 하지 않습니다. (설정으로 변경가능)
    • RuntimeException을 제외한 Exception의 하위 클래스입니다. (그림에는 IOException만 존재)
      • java.lang.CloneNotSupportedException: 복제가 지원되지 않을 때 발생합니다.
      • java.io.IOException: 입출력 작업 중에 예외가 발생했을 때 발생합니다.
      • java.lang.InterruptedException: 스레드가 인터럽트되었을 때 발생합니다.
      • java.lang.NoSuchMethodException: 요청한 메서드가 존재하지 않을 때 발생합니다.
      • java.lang.reflect.InvocationTargetException: 리플렉션을 통해 호출된 메서드 내에서 예외가 발생했을 때 발생합니다.
      • java.lang.reflect.ClassNotFoundException : 클래스를 못찾습니다.
      • java.io.FileNotFoundException,.  (그림에는 IOException 만 존재합니다.)
  • UnChecked Exception ( 비체크 예외 )
    • 반드시 에러처리를 하지 않아도 됩니다.
    • 확인시점은 실행중 단계
    • 예외 발생시 트랜잭션 roll back 합니다.
    • RunTimeException의 하위클래스입니다.
    • RunTimeException(실행중 예외)의 이름처럼 실행중에 발생할 수 있는 예외를 이미합니다.
      • java.lang.ArithmeticException: 산술 연산 도중에 발생한 예외를 나타냅니다.
      • java.lang.ArrayIndexOutOfBoundsException: 배열 인덱스가 범위를 벗어났을 때 발생합니다.
      • java.lang.NullPointerException: null 참조를 사용하여 객체에 접근하려고 할 때 발생합니다.
      • java.lang.IllegalArgumentException: 부적절한 인수가 전달되었을 때 발생합니다.
      • java.lang.IllegalStateException: 객체의 상태가 메서드 호출에 적합하지 않을 때 발생합니다.

만약 여기에 Exception을 추가할려고한다면 어떻게 해야할까? 

  • 아래 그림의 보면 Exception 의 하위클래스로 내가 원하는 Class를 선언하여 구현하면 될 것 입니다.
  • 아래처럼 직접 구현할떄는 Custom Exception을 해야할때일것입니다. 하지만, 저 같은경우 이번글에서는 가장 일반적인 방식의 예외처리를 진행할 것이므로 이번글에서는 구체적인 해당 내용은 없습니다.
public class SeminharHubMemberException extends Exception {
 ..........구현해야할코드들
 ..........
 .........
}

 

 

왜 @RestControllerAdvice + @ExceptionHandler를 통해 구현하는가? (try ~ catch, @RestControllerAdvice  + @ExceptionHandler )

1. 가장 Error처리를 하는데 일반적인 방식은 try ~ catch 문을 사용하는 방식입니다.

  • 이 방식 같은경우, 모든 에러처리를 할때마다 try ~ catch 문을 붙이기에 코드가 복잡해지고 관리가 힘든 단점이 있습니다.

2. @RestControllerAdvice와 @ExceptionHandler를 함꼐 사용하여 에러처리

  • Spring에서 제공하는 방식입니다.
  • 이 방식 같은경우 Client단과 Controller 단이 통신하는 그 과정에서 에러들을 확인하여 내보냅니다.
  • 전역적인 예외처리를 담당합니다. ( 컨트롤러 내의 모든 예외를 처리할 수 있습니다. )
  • 관리가 훨씬 쉬워지고, 코드와 에러처리가 어느정도 분리되어 관심사 분리가 가능해집니다.
  • @RestControllerAdvice 같은경우 예외 발생시 JSON의 형태로 반환하기 위해 사용합니다.

2번 방식을 사용하는것이 Spring에서 제공하는 Standard한 방식입니다

 

구현과정

1. ErrorResponseDTO 생성해주기

  • 에러형식을 하나로 맞춰주기 위한 DTO입니다.
@Getter
@Setter
public class ErrorResponseDTO {
    private String errorType; 
    private String errorCode;
    private String errorMessage;
}
  • 에러타입, 에러코드, 에러메세지를 가지고 있습니다.
  • 예시)
{
  "errorType": "Conflict",
  "errorCode": "409",
  "errorMessage": "이미 존재하는 회원아이디입니다."
}

 

2. SeminharHubExceptionHandler 생성해주기

  • 예외가 발생했을시 해당 SeminarHubExceptionHandler로 오게되고, 발생한 에러에 맞는 ExceptionHandler를 찾아서 해당 함수를 발생시킵니다.
@RestControllerAdvice
@Log4j2
public class SeminarHubExceptionHandler {

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<ErrorResponseDTO> ExceptionHandler(Exception e){
        ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO();
        HttpHeaders responseHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        log.info("-------------------Global ExceptionHandler-------------------");

        errorResponseDTO.setErrorType(httpStatus.getReasonPhrase());
        errorResponseDTO.setErrorCode(String.valueOf(httpStatus.value()));
        errorResponseDTO.setErrorMessage(e.getMessage());

        return new ResponseEntity<ErrorResponseDTO>(errorResponseDTO, responseHeaders, httpStatus);
    }

    @ExceptionHandler(value = DuplicateMemberException.class)
    public ResponseEntity<ErrorResponseDTO> DuplicateMemberExceptionHandler(Exception e){
        ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO();
        HttpHeaders responseHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.CONFLICT;
        log.info("-------------------Global DuplicateMemberExceptionHandler-------------------");

        errorResponseDTO.setErrorType(httpStatus.getReasonPhrase());
        errorResponseDTO.setErrorCode(String.valueOf(httpStatus.value()));
        errorResponseDTO.setErrorMessage(e.getMessage());

        return new ResponseEntity<ErrorResponseDTO>(errorResponseDTO, responseHeaders, httpStatus);
    }

}
  • 예시)
{
  "errorType": "Conflict",
  "errorCode": "409",
  "errorMessage": "123"
}

 

3. MemberController와 Service단에 Throw해주기

  • 저 같은경우 Service단에 에러처리를 했습니다.
  • throws를 하는 이유는 Throws를 통해서 @RestControllerAdvice가 에러가 발생한 줄 알고 작업을 진행합니다.

MemberServiceImpl.class

    @Override
    public Long register(MemberDTO memberDTO) throws DuplicateMemberException {
        Member member = dtoToEntity(memberDTO);
        log.info("=========================================");
        log.info(member);

        if(memberRepository.countByMember_id(memberDTO.getMember_id()) != 0){
            throw new DuplicateMemberException("이미 존재하는 회원아이디입니다.");
        }
        memberRepository.save(member);
        return member.getMember_no();
    }

MemberController.class

    @PostMapping(value ="")
    @Operation(summary = "Register a new member")
    public ResponseEntity<Long> register(@RequestBody MemberDTO memberDTO) throws DuplicateMemberException {
        log.info("-----------------register--------------");
        log.info(memberDTO);

        Long member_no = memberService.register(memberDTO);
        return new ResponseEntity<>(member_no, HttpStatus.OK);
    }

 

이렇게 되면 에러처리가 완료되었습니다.

 

느낀점들

Exception 처리를 Spring에서 제공하는 Annotation 하나로 모두 처리할 수 있어서 깔끔하게 처리한다는점이 너무 편리합니다. 

또 이번에는 Error처리를 진행하면서 따로 Custom Exception을 처리하지는 않은 상태인데,

지금으로써는 CustomException 처리의 필요성을 못느끼고있습니다.

이유로써는, 일단 지금은 Java에서 제공하는 Exception들을 통해 ErrorCode와 ErrorType, ErrorMessage를 통해서 백엔드단에서 적절하게 에러를 표현할 수 있다고 생각하고 있습니다.

예시로 들면, 위의

https://programming.guide/java/list-of-java-exceptions.html

이 곳에 들어가보면 Java에서 제공하는 모든 Exception List를 볼수 있습니다.

추후 개발을 진행해보다가 필요성을 느끼는 상황을 마주한다면 해당 내용에 대해 이후에 다시 포스팅해보도록 하겠습니다.

 

 

 

참고자료

 

+ Recent posts