노션으로 다시 돌아갔습니다 😅

[예외 처리] 비즈니스 로직에 대한 예외 처리

by mignon25

체크 예외(Checked Exception) 와 언체크 예외(Unchecked Exception)

1. 체크 예외

  • 발생한 예외를 잡아서(catch) 체크한 후에, 해당 예외를 복구하든지 회피하든지 등의 구체적인 처리를 반드시 해야 하는 예외
  • Ex) ClassNotFoundException

 

2. 언체크 예외

  • 예외를 잡아서(catch) 해당 예외에 대한 어떤 처리할 필요가 없는 예외
  • RuntimeException을 상속한 예외
  • Ex) NullPointerException, ArrayIndexOutOfBoundsException

 

 

개발자가 의도적으로 예외를 던지는(throw) 경우는 어떤 상황일까?

(Case 1) 백엔드 서버와 외부 시스템과의 연동에서 발생하는 예외 처리

<상황>

  • 암호 화페 지갑과 연동하는 백엔드 서비스를 만든다고 가정
  • 일반적으로 암호 화폐 지갑은 블록체인과 직접적으로 API 통신을 거치는 경우가 많지만, 백엔드 서버를 한 번 거쳐서 블록체인과 통신하는 경우도 있을 수 있다.
  • A라는 사용자가 B라는 사용자에게 코인을 전송하기 위해 백엔드 서버가 블록체인과 API 통신하는 과정에서 블록체인으로부터 A 사용자의 코인 잔고가 부족하다는 메시지를 전달받고 프로세스가 중단되었다. 

< 대처 >

  • 이러한 예외 상황에서는 백엔드 서버 쪽에서 무언가 할 수 있는 것이 없다.
  • 잔고 부족 상황을 클라이언트 쪽에 즉시 알려서 클라이언트가 지갑에 잔고를 채우는 것이 최선의 방법

=> 이런 경우, 백엔드 서버 쪽에서 예외를 의도적으로 던져서 클라이언트 쪽에 에러 발생 원인을 알려줄 수 있다. 

 

(Case 2) 시스템 내부에서 조회하려는 리소스(자원, Resource)가 없는 경우

<상황>

  • 커피 주문 애플리케이션 가정
  • 회원 정보를 조회하기 위해 클라이언트 쪽에서 Controller의 getMember() 핸들러 메서드에 요청 전송
  • DB를 조회하니 해당 회원 정보가 없음.

<대처>

=> 서비스 계층에서 해당 회원 정보가 없다는 예외를 의도적으로 전송해 클라이언트 쪽에 알릴 수 있다.

 

 

의도적인 예외 던지기/받기 (throw/catch)

< throw/catch 과정 >

  • throw 키워드를 사용해 메서드 내에서 발생한 예외를 메서드 밖으로 던질 수 있다. 
  • 던지는 예외는 메서드 바깥, 즉 메서드를 호출한 지점으로 던져지게 된다.
  • 서비스 계층에서 예외를 던지면 => API 계층인 Controller의 핸들러 메서드 쪽에서 잡아서 처리

< Controller에서의 예외 처리 >

  • Controller에서 예외가 발생하면 Exception Advice에서 처리하도록 공통화 해둔 상태
  • 서비스 계층에서 던진 예외 => 결과적으로 Exception Advice에서 처리하게 된다. 

 

 

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

< throw - MemberService >

  • throw 키워드를 사용해 RuntimeException 객체에 적절한 예외 메세지를 포함한 후 메서드 밖으로 던지기
@Service
public class MemberService {
    ...
		...

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

		...
		...
}

 

< catch - GlobalExceptionAdvice >

  • 서비스 계층에서 던져진 RuntimeException 을 GlobalExceptionAdvice 클래스에서 받기
@RestControllerAdvice
public class GlobalExceptionAdvice {

	...

    // 1
    @ExceptionHandler
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
        System.out.println(e.getMessage());
        
        // 필요한 정보만 담은 에러 객체로 변환 처리 로직
        
        return null;
    }
}

 

 

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

  • 서비스 계층에서 의도적으로 던질 수 있는 예외 상황은 다양하다.
  • 그러므로 RuntimeException 을 그대로 전달 받게 되면 어떤 상황에서 던져진 예외인지 구분이 어렵다. 

=> RuntimeException 같은 추상적인 예외가 아닌, InSufficientBalanceException 같은 Custom Exception을 통해 해당 예외를 조금 더 구체적으로 표현해서 던질 수 있다. 

 

1. 예외 코드 정의

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;
    }

}
  • enum 을 정의하고, field를 정의하면, 생성자에서 정의한 대로 enum 의 파라미터 값이 적용된다. 
  • ExceptionCode를 enum 으로 정의하면 비즈니스 로직에서 발생하는 다양한 유형의 예외를 enum에 추가해서 사용할 수 있다.

 

2. BusinessLogicException 구현

  • 서비스 계층에서 사용할 Custom Exception 정의 - BusinessLogicException
public class BusinessLogicException extends RuntimeException {
    
    @Getter
    private ExceptionCode exceptionCode;

    public BusinessLogicException(ExceptionCode exceptionCode) {
        super(exceptionCode.getMessage());
        this.exceptionCode = exceptionCode;
    }
}
  • BusinessLogicException은 RuntimeException을 상속하고 있다.
  • ExceptionCode를 멤버 변수로 지정하여 조금 더 구체적인 예외 정보들을 제공해줄 수 있다. 
    • enum 에서 지정한 status, message 등
  • 그리고, 상위 클래스인 RuntimeException의 생성자 super로 예외 메세지 전달
    • 내가 원하는 메세지로 예외 메세지가 설정된 예외 객체 생성

 

3. 서비스 계층에 BuisinessLogicException 적용

@Service
public class MemberService {
    ...

    public Member findMember(long memberId) {
        // TODO should business logic

				// (1)
        throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
    }

    ...
}

 

4. Exception Advice 에서 BusinessLogicException 처리

@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...
		...

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

				// ErrorResponse 수정 로직

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

< 변경된 부분 >

  • 메서드 명
    • 서비스 계층의 비즈니스 로직에서 발생하는 예외를 처리하는 것이 목적
    • handleResourceNotFoundException -> handleBusinessLogicException 로 변경
  • 메서드 파라미터 변경
    • RuntimeException -> BusinessLogicException 을 전달 받는 것으로 변경
  • @ResponseStatus(HttpStatus.NOT_FOUND) 제거
    • @ResponseStatus 애너테이션은 고정된 HttpStatus를 지정한다.
    • 따라서, BusinessLogicException 과 같이 다양한 Status를 동적으로 처리할 수 없다. 
    • ResponseEntity를 사용해서 HttpStatus를 동적으로 지정하도록 변경

 

 

Additional Keywords - Checked Exception / Unchecked Exception

 

 

 

 

 

 

 

 

 

 

 

 

 

블로그의 정보

Mignon'S Dev Log

mignon25

활동하기