[Section 3] 비즈니스 로직 예외 처리

Kim·2022년 10월 26일
0

Boot Camp

목록 보기
37/64
post-thumbnail

Checked Exception vs Unchecked Exception

애플리케이션에서 발생하는 예외는 체크 예외와 언체크 예외로 구분할 수 있다.

체크 예외는 발생한 예외를 잡아(catch) 체크하고 해당 예외를 복구하거나 회피하는 등의 구체적인 처리를 하는 예외다.
대표적인 Java의 체크 예외는 ClassNotFoundException 등이 있다.

언체크 예외는 예외를 잡아서 해당 예외에 대한 처리를 할 필요가 없는 예외를 말한다. 대표적인 언체크 예외로는 NullPointerException, ArrayIndexOutOfBoundsException 등이 있다.

개발자가 코드를 잘못 작성하여 발생하는 오류들은 모두 RuntimeException을 상속한 예외이다.
그러나 Java나 Spring에서 수많은 RuntimeException을 지원해주지만 이를 이용하여 개발자가 직접 예외를 만들어야 하는 경우도 존재한다.


개발자가 의도적으로 예외를 던질 수 있는 상황 "throw"

백엔드 서버와 외부 시스템과의 연도에서 발생하는 에러 처리

은행거래 서비스를 만든다고 상상해보자. 안드로이드나 ios 기반의 서비스가 될 수도 있고 데스크탑의 애플리케이션이 될 수도 있다.
만약 사용자 A가 사용자 B에게 10,000원을 전송하려 했는데, A의 통장 잔고가 부족하다는 메시지를 전달 받고 프로세스가 중단되었다.
백엔드 서버에서 해당 예외가 발생한 경우, 이 예외를 복구할 수 있는 방법은 없다. 잔고가 부족한 상황을 클라이언트에게 알려 클라이언트가 잔고를 채우는 것이 해결 방법이 될 것이다.
이러한 경우 백엔드 서버에서 예외를 의도적으로 던져 클라이언트 쪽에 에러가 발생한 정보를 알려줄 수 있다.

시스템 내부에서 조회하려는 Resource가 없는 경우

지금 학습하고 있는 샘플 애플리케이션의 커피 주문 애플리케이션을 떠올려보자.
회원 정보를 조회하기 위해 클라이언트가 Controller의 getMember() 핸들러 메서드에 요청을 보냈다. 그런데 조회를 해보니 요청 받은 회원의 정보가 없을 수가 있다. 이러한 경우 서비스 계층에서 해당 회원 정보가 없다는 예외를 의도적으로 전송해 클라이언트에게 알려줄 수 있다.


의도적인 예외 던지기, 받기 "throw, catch"

Java에서는 throw 키워드를 사용해 예외를 메서드 바깥으로 던질 수 있다.
던진 예외는 메서드 바깥인 메서드를 호출한 지점으로 던져진다.
서비스 계층에서 예외를 던진 경우 Controller의 핸들러 메서드 쪽에서 잡아서 처리할 수 있다. 서비스 계층의 메서드는 API 계층인 Controller의 핸들러 메서드가 이용하기 때문이다.

서비스 계층에서 예외 던지기 (throw)

@Service
public class MemberService {
    ...

    public Member findMember(long memberId) {		
		// (1)
        throw new RuntimeException("Not found member");
    }
		...
}

회원 정보를 조회했을 때 조회된 회원이 없다는 가정하에 throw 키워드를 사용해 RuntimeException 객체에 적절한 예외 메시지를 포함한 후 메서드 밖으로 던졌다.

GlobalExceptionAdvice 예외 잡기 (catch)

@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...
		
	// (1)
    @ExceptionHandler
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
        System.out.println(e.getMessage());
		...
        return null;
    }
}

(1)과 같이 RuntimeException을 잡아 처리하기 위한 handleResourceNotFoundException() 메서드를 추가했다.
postman에서 MemberController의 getMember() 핸들러 메서드에 요청을 보내면 MemberService에서 RuntimeException을 던지고, GlobalExceptionAdvice의 handleResourceNotFoundException() 메서드가 해당 RuntimeException을 잡아 "Not found member" 예외 메시지를 콘솔에 출력한다.

handleResourceNotFoundException() 메서드의 문제점

서비스 계층에서 의도적으로 던질 수 있는 예외 상황은 다양하게 존재할 수 있어 handleResourceNotFoundException()의 메서드명은 적절하지 않다.
그리고 추상적인 RuntimeException을 그대로 전달 받는 것 또한 바람직하지 않다.

서비스 계층에서 RuntimeException을 그대로 던지고 Exception Advice에서 RuntimeException을 그대로 잡는 것은 예외 의도가 명확하지 않고, 구체적으로 어떤 예외가 발생했는지 예외 정보를 얻기가 어렵다.


사용자 정의 예외(Custom Exception) 사용

앞선 예시로 잔고 부족으로 인한 에러 메시지를 백엔드 서버 쪽에 전송한 경우를 다시 떠올려보자.
서버 쪽에서 RuntimeException과 같은 추상적인 예외가 아닌 InsufficentBalanceException 같은 해당 예외를 더 구체적으로 표현할 수 있는 Custom Exception을 만들어 예외를 던질 수 있다.

예외 코드 정의

public enum ExceptionCode {
    MEMBER_NOT_FOUND(404, "Member Not Found");

    @Getter
    private int status;

    @Getter
    private String message;

    ExceptionCode(int status, String message) {
        this.status = status;
        this.message = message;
    }
}

위와 같이 서비스 계층에 던질 Custom Exception에 사용할 ExceptionCode를 enum으로 정의한다.
ExceptionCode를 enum으로 정의하면 비즈니스 로직에서 발생하는 다양한 유형의 예외를 enum에 추가하여 사용할 수 있다.

BusinessLogicException 구현

public class BusinessLogicException extends RuntimeException {
    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}

서비스 계층에서 사용할 Custom Exception인 BusinessLogicException을 정의한다.

BusinessLogicExceptionRuntimeException을 상속하고 있고, ExceptionCode를 멤버 변수로 지정하여 생성자를 통해서 조금 더 구체적인 예외 정보들을 제공할 수 있다.
또한, 서비스 계층에서 개발자가 의도적으로 예외를 던져야 하는 다양한 상황에서 ExceptionCode 정보만 바꿔가며 던질 수 있다.

BusinessLogicException을 서비스 계층에 적용

@Service
public class MemberService {
    ...
    
    public Member findMember(long memberId) {
		// (1)
        throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
    }
    ...
}

서비스 계층에서 RuntimeException을 던지던 것을 BusinessLogicException에 구체적인 예외 정보(ExceptionCode)를 던지도록 변경한다.
회원 정보가 존재하지 않는다는 MEMBER_NOT_FOUNDBusinessLogicException 생성자의 파라미터로 전달했다.

BusinessLogicException 처리

서비스 계층에서 던진 BusinessLogicException을 Exception Advice에서 처리하면 된다.

@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...

    @ExceptionHandler
    public ResponseEntity handleBusinessLogicException(BusinessLogicException e) {
        System.out.println(e.getExceptionCode().getStatus());
        System.out.println(e.getMessage());

		...

        return new ResponseEntity<>(HttpStatus.valueOf(e.getExceptionCode().getStatus()));
    }
}

변경된 부분은 아래와 같다.

  • 메서드명 변경
    메서드명이 서비스 계층의 비즈니스 로직 처리에서 발생하는 예외를 처리하는 것을 목적으로 하므로 메서드명을 handleBusinessLogicException으로 변경했다.

  • 메서드 파라미터 변경
    RuntimeException을 파라미터로 전달 받던 것을 BusinessLogicException을 전달 받는 것으로 변경했다.

  • @ResponseStatus(HttpStatus.NOT_FOUND) 제거
    @ResponseStatus 애너테이션은 고정된 HttpStatus를 지정하기 때문에 BusinessLogicException과 같이 다양한 Status를 동적으로 처리할 수 없어, ResponseEntity를 사용해서 HttpStatus를 동적으로 지정하도록 변경했다.

404
Member Not Found

변경된 코드를 테스트하기 위해 postman에서 MemberController의 getMember() 핸들러 메서드에 요청을 전송하면 위과 같은 출력 결과를 확인할 수 있을 것이다.

@ResponseStatus vs ResponseEntity

하나의 유형으로 고정된 예외를 처리할 경우 @ResponseStatus로 HttpStatus를 지정하여 사용하면 된다.
BusinessLogicException처럼 다양한 유형의 Custom Exception을 처리하고자 할 경우 ResponseEntity를 사용하면 된다.


참고 자료

0개의 댓글