티스토리 뷰

728x90
반응형

 

 

 

지난 글까진 @ExceptionHandler와 @RestControllerAdvice를 이용한 예외 처리에 대해 알아보았다.

 

또한 예외 처리는 오류 처리(Trouble Shooting)라고도 불리며 오류 발생 시 대응하는 방법을 제시하는 개념이라고도 했다.

 

@ExceptionHandler에 대한 글에서 짧게 throw를 언급하면서 throw란 일종의 의도적 예외 발생이라고 적었었는데,

 

이번 글에선 바로 그 의도적 예외발생에 대해 다룬다.

 

들어가기 전에 먼저 이후에 다루게 될 예외에 대해 조금 더 파악해 보자.

 


에러(Error) vs. 체크 예외(Check Exception) vs. 언체크 예외(Uncheck Exception)

 

Error와 Exception은 공통적으로 Serializable 인터페이스를 구현한 Throwable 클래스를 상속받는다.

 

또한 실제로 발생하는 에러와 예외는 각각 Error와 Exception 클래스의 하위 클래스가 되며,

 

구체적인 차이를 간략하게 알아보면 아래와 같다.

 

  • 에러(Error) - 메모리 부족과 같이 시스템 레벨에서 발생, 개발자가 처리 불가, 컴파일 에러 X
  • 예외(Exception) - 유효성 검증과 같이 개발자가 구현한 로직에서 발생, 개발자가 처리 가능

이어서 예외는 다시 RuntimeException의 상속여부로 체크 예외와 언체크 예외로 나뉘는데, 그 차이는 아래와 같다.

 

  • 체크 예외(Checked Exception) - RuntimeException 상속 X

    • 복구 가능성이 있는 예외 → 반드시 예외 처리 코드(복구, 회피 등) 작성
    • 처리 코드를 작성하지 않으면 컴파일 불가능 - 실수에 의한 예외 처리 누락 방지
    • IOException, SQLException과 같은 RuntimeException 이외의 모든 예외
    • 실제 개발에서는 대부분 언체크 예외를 사용
  • 언체크 예외(Unchecked Exception) - RuntimeException 상속 O

    • 복구 가능성이 없는 예외 → 처리 코드 강제 X
    • 컴파일 시 체크하지 않고(Unchecked), 런타임에 발생하는 예외
    • NullPointerException, IndexOutOfBoundException과 같은 RuntimeException 예외
  • 사용자 정의 예외(Custom Exception)

    • Exception 혹은 RuntimeException을 상속받아 개발자가 직접 정의하는 예외
    • 구체적인 에러 메시지를 전달하기 편함

 

이 글에선 언체크 예외와 RuntimeException을 사용한 사용자 정의 예외의 예를 살펴본다.

 

 

언체크 예외(Unchecked Exception) 처리

 

상황

 

  1. A 사용자의 암호화폐 지갑에서 B 사용자에게 코인을 전송하는 요청을 처리하기 위한
    백엔드 서버 ↔ 블록체인 간 API 통신 과정에서 블록체인으로부터 A의 잔고가 부족하다는 메시지를 받아 프로세스 중단

    • 백엔드 서버 ↔ 외부 시스템 연동에서 발생하는 에러
    • 백엔드 서버에서 이런 예외가 발생하면 개발자가 할 수 있는 게 없음
    • 예외를 의도적으로 던져서 에러 정보와 클라이언트의 잔고 충전을 안내할 수 있음
  2. 지난 시간까지 작성한 코드에서 DB에 존재하지 않는 회원에 대한 조회를 요청

    • 시스템 내부에서 조회하려는 리소스(자원, Resource)가 없는 경우
    • 서비스 계층에서 예외를 의도적으로 던져 클라이언트에게 알릴 수 있음

이 글에선 당연히 지난 글까지 작성한 코드를  바탕으로 예외 처리를 적용해 보겠다.

 

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

 

자바에서 의도적으로 예외를 던질때 사용하는 키워드는 미리 언급했던 throw이다.

 

또한 throw가 쓰여진 예외는 메서드 바깥, 즉 메서드를 호출한 지점으로 던져지게 되는데

 

지난 글까지 작성한 서비스 계층에서 던져진 예외는 API 계층인 Controller의 핸들러 메서드가 잡아서 처리할 수 있게 된다.

 

더 정확하게는 예외처리를 공통화한 GlobalExceptionAdvice 클래스에서 처리하게 된다.

 

  • 서비스 계층에서 예외 던지기(throw)
package com.gnidinger.member.service;

import com.gnidinger.member.entity.Member;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MemberService {
		...

    public Member findMember(long memberId) {

        throw new RuntimeException("Member Not Found");
    }

		...
}

아직 작성된 데이터베이스가 없기 때문에 회원 정보를 조회할 수 없다.

 

throw 키워드를 이용하여 RuntimeException 객체에 예외 메시지를 포함해 메서드 밖으로 던진 것을 볼 수 있다.

 

  • GlobalExceptionAdvice에서 예외 잡기(catch)
package com.gnidinger.advice;

import com.gnidinger.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;

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

        return null;
    }
}

RuntimeException을 잡아서(catch) 처리하기 위해 handleResourceNotFoundException() 메서드를 추가했다.

 

메서드 안에 매개변수로 RuntimeException을 넣고, Throwable의 getMessage() 메서드로 메시지를 받아오게 된다.

 

진행 순서는 아래와 같다.

 

  1. Postman으로 MemberController의 getMember()에 조회 요청
  2. MemberService에서 RuntimeException을 던짐
  3. GlobalExceptionAdvice의 handleResourceNotFoundException() 메서드가
    RuntimeException을 잡아 예외 메시지인 “Member Not Found”를 콘솔에 출력
  • handleResourceNotFoundException() 방식의 문제점

    • 회원 정보를 제외한 여러 다양한 예외(비밀번호, 겹치는 아이디 등)에 적절하지 못하다.
    • 서비스 계층에서 RuntimeException을 던지고(throw), API 계층에서 그대로 잡는 문제
      → 예외의 의도와 구체적인 예외 발생에 대한 정보를 얻는 것이 어렵다.

계속해서 위 문제점을 해결하기 위해 사용자 정의 예외(Custom Exception)를 사용하는 법을 알아보자.

 

 

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

 

위에서 handleResourceNotFoundException()를 이용한 방식의 문제점을 살펴봤다.

 

문제는 다양하고 구체적인 예외 처리 표현을 만드는 데 있었는데,

 

앞으로 살펴볼 사용자 정의 예외(Custom Exception)를 사용해 이를 해결할 수 있다.

 

먼저 상위 패키지에 exception 폴더를 생성해 서비스 계층에서 던질 정보를 담은 ExceptionCode를 enum으로 작성한다.

package com.gnidinger.exception;

import lombok.Getter;

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에 대해선 아래 글에 정리했다.

 

2022.07.20 - [개발/Java] - [Java]열거형(enum), 애너테이션(Annotation)

 

[Java]열거형(enum), 애너테이션(Annotation)

열거형(enum) 열거형(enum)은 Enumerated Type에서 왔다. 말 그대로 열거형. 열거형은 변하지 않는 한정된(연관된) 상수 데이터를 다루는 데 최적화된 타입이다. <동, 서, 남, 북> 이나 <봄, 여름, 가을, 겨

gnidinger.tistory.com

위와 같이 ExceptionCode를 enum으로 정의하면 다양한 유형의 예외를 직접 추가해서 사용할 수 있게 된다.

 

이어서 이번엔 서비스 계층에서 사용할 Custom Exception 클래스를 정의한다.

package com.gnidinger.exception;

import lombok.Getter;

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

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

Custom Exception 클래스인 BusinessLogicException은 RuntimeException을 상속받고 있으며

 

ExceptionCode를 멤버 변수로 지정, 생성자를 통해 구체적인 예외 정보를 제공할 수 있다.

 

BusinessLogicException이라는 이름을 통해 비즈니스 로직에서 발생하는 예외를 담당한다는 것을 명확히 했으며,

 

상속받은 상위 클래스인 RuntimeException의 생성자(super)로 예외 메시지를 전달하는 것도 확인할 수 있다.

 

이렇게 만들어낸 BusinessLogicException은 GlobalExceptionAdviceErrorResponse를 조작하는 것으로

 

다양한 상황에서 ExceptionCode 정보만 바꿔가며 예외를 던질 수 있게 된다.

 

계속해서 서비스 계층에 방금 생성한 Custom Exception을 적용해 보자.

package com.gnidinger.member.service;

import com.gnidinger.exception.BusinessLogicException;
import com.gnidinger.exception.ExceptionCode;
import com.gnidinger.member.entity.Member;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class MemberService {
    ...
    public Member findMember(long memberId) {
        
        // throw new RuntimeException("Member Not Found"); // 기존 코드
        throw new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND);
    }
    ...
}

회원 정보를 조회하는 findMember() 메서드에서 기존에 RuntimeException을 던지던 것을

 

BusinessLogicException에 구체적인 예외 정보(ExceptionCode)를 담아 던지도록 수정했다.

 

enum형으로 작성했던 ExceptionCode에서 MEMBER_NOT_FOUND를 불러온 것을 확인할 수 있다.

 

마지막으로 예외를 모아 처리하는 GlobalExceptionAdvice에서 던져진 예외를 처리하도록 하자.

package com.gnidinger.advice;

import com.gnidinger.exception.BusinessLogicException;
import com.gnidinger.response.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    ...

//    기존 코드
//    @ExceptionHandler
//    @ResponseStatus(HttpStatus.NOT_FOUND)
//    public ErrorResponse handleResourceNotFoundException(RuntimeException e) {
//        System.out.println(e.getMessage());
//
//        return null;
//    }

    @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()으로 변경되어 목적이 명확해졌다.
  • 메서드 파라미터가 BusinessLogicException으로 변경되어 목적이 명확해졌다.
  • @ResponseStatus 대신 ResponseEntity를 사용해서 다양한 상태를 담을 수 있게 되었다.

    • @ResponseStatus는 고정된 HttpStatus를,  ResponseEntity는 다양한 유형의 예외를 동적으로 처리할 수 있다.

실제로 테스트를 위해 getMember() 메서드에 요청을 보내면 콘솔에 아래와 같은 메시지가 출력되는 것을 확인할 수 있다.

 

 

이제 이렇게 전달받은 예외 정보를 클라이언트에 출력하면 API - 서비스 계층의 연동이 끝난다.

 

클라이언트 출력 부분은 따로 글을 파겠다.

 

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함