애플리케이션을 사용하는 여러 상황에서 보다 직관적으로 예외 메세지를 확인하기 위해 사용자 정의 예외 클래스를 이용해 예외 처리를 진행했다.
CheckedException, UncheckedException
사용자 정의 예외 클래스를 만들어 구현하는 방법 이전에 간단하게 CheckedException과 UnchekcedException에 대해 알아보자.
자바에서는 크게 RuntimeException을 기준으로 CheckedException과 UncheckedException을 구분한다.
RuntimeException은 이름에서도 알수 있듯이 실행 시점에 예외를 판단한다.
예를 들어, ArrayIndexOutOfBoundException과 같은 예외는 실행 시점에 배열의 인덱스를 벗어나 참조를 하는 경우 발생하는데 우리는 코드를 작성하는 시점에 해당 예외를 처리하지 않는다.
즉, 컴파일 시점에 Uncheck하기 때문에 UncheckedException으로 판단할 수 있다.
반대로, RuntimeException을 상속하지 않는 예외들은 모두 컴파일 시점에 Checked되기 때문에 CheckedException이다.
대표적으로, ClassNotFoundException과 같은 예외들은 컴파일 시점에 에러메세지를 보여주고 해당 예외들은 try-catch, thorws 를 이용해 예외를 처리해줘야 한다.
Custom Exception
우리는 애플리케이션을 사용할 때 여러가지 상황에서 보다 직관적인 예외 메세지를 확인하기 위해 CustomException을 사용하는 것이 목적이었다.
예를 들면, 가입된 파티 정보를 확인하려는데 이미 해체된 파티인 경우에는 NoSuchPartyException과 같이 직관적인 클래스 명과 그에 따른 예외 메세지를 던져줌으로 써 이를 클라이언트에 전달해주기 위해 사용했다.
애플리케이션을 사용하는 시점이라고 특정했기 때문에 실행 시점에 예외를 처리할 것이다. 따라서, RuntimeException을 상속하는 Custom Exception 클래스를 생성했다.
package com.dev.nbbang.party.domain.party.exception;
import com.dev.nbbang.party.global.exception.NbbangException;
public class NoSuchPartyException extends RuntimeException {
private final NbbangException nbbangException;
public NoSuchPartyException(String message, NbbangException nbbangException) {
super(message);
this.nbbangException = nbbangException;
}
public NoSuchPartyException(NbbangException nbbangException) {
super(nbbangException.getMessage());
this.nbbangException = nbbangException;
}
public NbbangException getNbbangException() {
return this.nbbangException;
}
}
우리는 예외가 발생할 때 흔히 try-catch문을 사용하는데 이 때 catch(Exception e)에 예외 클래스를 지정해주고 e.getMessage(), e.printStackTrace()를 이용해 예외 메세지를 확인한다.
e.getMessage()는 위의 Exception Class의 상속관계 중 최상위인 Throwable 클래스에 정의되어 있다. 그렇다면 Throwable까지 우리가 지정한 메세지를 전달해주어야한다.
위의 CheckedException, UncheckedException에서 각 클래스간 상속관계를 다시 확인해보자.
RuntimeException은 Exception을 상속하고, Exception은 Throwable을 상속하는 것을 확인할 수 있다.
실제로 RuntimeException, Exception 클래스를 확인해보면 생성자로 message를 전달 받고 해당 생성자는 super(message)를 이용해 다시 부모 클래스로 메세지를 전달해주는 것을 확인할 수 있다.
따라서, 우리는 RuntimeException을 상속받을 것이기 때문에 RuntimeException 클래스에만 우리가 지정한 예외 메세지를 전달해 주면 된다.
public NoSuchPartyException(String message, NbbangException nbbangException) {
super(message);
this.nbbangException = nbbangException;
}
우리의 목적은 해당 메세지를 담아 예외가 나는 상황이 발생했을 때 클라이언트까지 해당 메세지를 상황에 맞게 전달하는 것이 목적이었다.
따라서 동일한 사용자 정의 예외클래스를 사용하더라도 메시지를 변경해서 사용했다.
이외에도 Enum 타입을 사용자 정의 예외 클래스에 전달해줌으로써 특정 메세지를 공통적으로 사용하는 것도 가능하게 구현했다.
package com.dev.nbbang.party.global.exception;
public enum NbbangException {
NO_CREATE_PARTY ("BE001", "Doesn't Create Party"),
NOT_FOUND_PARTY("BE002", "Not Found Party"),
NOT_FOUND_OTT("BE101", "Not Found Ott Platform"),
NO_CREATE_QUESTION("BE201", "Doesn't Create Question"),
NOT_FOUND_QNA("BE202", "Not Found Qna"),
FAIL_TO_DELETE_QNA("BE203", "Fail To Delete Qna"),;
private String code;
private String message;
NbbangException(String code, String message) {
this.code = code;
this.message = message;
}
public String getCode() {
return code;
}
public String getMessage() {
return message;
}
}
위와 같이 에러 코드와 메세지를 하나의 Enum 타입으로 관리함으로써 상황에 맞게 공통적인 예외 메세지 및 에러코드를 확인할 수 있게 되었다.
실제 동작 확인
그렇다면 위에서 예시를 든 가입된 파티 정보를 조회하는데 이미 파티가 해체된 경우거나 등록되지 않은 파티인 경우 메세지를 클라이언트에 메세지를 던져주는 실제 동작을 확인해보자.
/**
* 파티 아이디를 이용해 파티 정보를 조회한다.
* @param partyId 고유 파티 아이디
* @return PartyDTO 조회한 파티 데이터 정보
*/
@Override
public PartyDTO findPartyByPartyId(Long partyId) {
// 1. 고유한 파티 아이디로 파티 정보 조회
Party findParty = Optional.ofNullable(partyRepository.findByPartyId(partyId))
.orElseThrow(() -> new NoSuchPartyException("등록되지 않았거나 이미 해체된 파티입니다.", NbbangException.NOT_FOUND_PARTY));
return PartyDTO.create(findParty);
}
고유한 파티 번호로 파티 정보를 조회할 때 데이터베이스에 해당 파티 번호가 없는 경우 null을 return 하기 때문에 Optional을 이용해 null인 경우 사용자 정의 예외를 호출하도록 로직을 작성했다.
Web Layer는 요청이 들어오는 경우 위의 로직을 실행하고 예외가 발생하는 경우를 try-catch로 처리하고 e.getMessage()를 메세지로 담아 클라이언트로 return 했다.
@GetMapping(value = "/{partyId}")
public ResponseEntity<?> findPartyByPartyId(@PathVariable(name = "partyId") Long partyId) {
log.info("[Party Controller - Find Party By Party Id] 파티 조회");
try {
// 파티 아이디로 파티 조회하기
PartyDTO findParty = partyService.findPartyByPartyId(partyId);
return ResponseEntity.ok(CommonSuccessResponse.response(true, findParty, "파티 조회에 성공했습니다."));
} catch (NoSuchPartyException e) {
log.warn("[Party Controller - Find Party By Party Id] message : " + e.getMessage());
return ResponseEntity.ok(CommonResponse.response(false, e.getMessage()));
}
}
마지막으로 포스트맨으로 실제로 호출해보자.
현재 데이터베이스에는 1번 파티만 저장되어있기 때문에 2번 파티 정보 조회를 요청하는 경우 예외가 발생할 것이고, 우리가 지정한 “등록하지 않았거나 이미 해체된 파티입니다.”라는 메세지가 클라이언트로 전달될 것이다.
예외가 잘 처리되고, 우리가 지정한 메세지가 클라이언트로 잘 호출됨을 확인할 수 있다.
정리
사용자 정의 예외 클래스를 사용하는 것이 예외 메세지나 에러를 직관적으로 확인할 수 있다는 장점이 있지만 무분별하게 사용자 정의 예외 클래스를 만들어 사용하면 클래스가 많아져 관리하는 것이 어려워지고 유지보수 관점에서 별로 효율적이지 않을 것이다.
정답은 없지만 개인적으로 스프링이 제공하는 예외 내에서 예외를 최대한 처리하고, 필요 시에 사용자 예외 클래스를 만들어 사용하는 것이 좋을 것이라 판단된다.
'Backend > Spring Boot' 카테고리의 다른 글
RestControllerAdvice, ExceptionHandler를 이용한 전역 예외 처리 (0) | 2022.05.27 |
---|---|
Spring Cloud Gateway를 이용한 서비스 라우팅 및 JWT 토큰 검증 (2) | 2022.05.24 |
Request 스코프와 Proxy (0) | 2022.05.16 |
Spring Cloud Bus와 RabbitMQ를 이용해 설정 정보 한번에 최신화하기! (0) | 2022.05.15 |
싱글톤 스코프, 프로토타입 스코프 (0) | 2022.05.03 |