포인트 사용 시 포인트가 부족하면 IllegalArgumentException(extends RuntimeException) 예외 발생
@Entity
public class Point {
private Integer amount;
...
public Integer usePoint(Integer amount) {
checkEnoughPoint(amount);
this.amount -= amount;
return this.amount;
}
private void checkEnoughPoint(Integer amount) {
if(this.amount - amount < 0)
throw new IllegalArgumentException("사용할 포인트가 부족합니다.");
}
}
- 표준 예외로 간단하게 쓰기 위해
- 남은 포인트보다 큰 포인트가 들어온 건 적절하지 못한 인자가 들어왔다고 생각했기 때문
검사 예외 사용
그러다 이펙티브 자바를 읽던 중에 아래 문장을 보게 되었다.
아이템 70 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라
검사 예외
- 호출하는 쪽에서 복구하리라 여겨지는 상황
- ex 결제 시 카드 잔고가 부족할 때
런타임 예외
- 프로그래밍 오류를 나타낼 때
- ex 배열의 인덱스보다 크거나 작은 숫자로 인덱스에 접근했을 때
포인트가 부족한 경우는 프로그래밍 오류라기보다는 카드 잔고가 부족한 경우와 비슷하잖아?
그럼 이걸 커스텀 검사 예외(checked exception)로 바꿔야겠다고 생각했다.
그런데 검사 예외에 문제가 생겼다.
1. 무한 예외 회피
@RestControllerAdvice와 @ExceptionHandler로 예외를 처리해야 하다 보니 Point 내부에서 처리하는 게 아니라 예외 회피를 해야 했다.
그 과정에서 메서드에 throws를 미친듯이 붙이게 됐다. (심지어 테스트 코드까지..)
2. 예외 전환
물론 위 방법을 해결하기 위해 Point 내부의 try/catch문에서 커스텀 검사 예외를 잡고 또 다른 RuntimeException을 던져 throws를 없애는 방법도 있다. 하지만 내 상황에선 의미 없는 방법이었다. 그럴 거면 처음부터 런타임 예외로 만드는 게 낫지 않을까?
런타임 예외 사용
<클린코드> 중
unchecked exception을 사용하라
논쟁은 끝났다. 여러 해 동안 자바 개발자들은 checked exception의 장단점을 놓고 논쟁을 벌여왔다.
처음으로 자바가 공개되었을 때 checked exception을 멋진 아이디어로 생각했다.
하지만 지금은 안정적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지 않다는 사실이 분명해졌다.
..
OCP(Open Closed Principle)을 위반한다.
메서드에서 checked exception을 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다.
하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.
모듈과 관련된 코드가 전혀 바뀌지 않았더라도 (선언부가 바뀌었으므로) 모듈을 다시 빌드한 다음에 배포해야 한다.
-> throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.
<토비 스프링> 중
런타임 예외의 보편화
일반적으로는 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템 장애나 프로그램상의 오류에 사용한다고 했다.
...
이렇게 예외처리를 강제하는 것은 예외가 발생할 가능성이 있는 API 메서드를 사용하는 개발자의 실수를 방지하기 위한 배려라고 볼 수도 있겠지만, 실제로는 예외를 제대로 다루고 싶지 않을 만큼 짜증나게 만드는 원인이 되기도 한다.
...
어디에서든 DuplicateUserldException을 잡아서 처리할 수 있다면 굳이 체크 예외로 만들지 않고 런타임 예외로 만드는 게 낫다.
대신 add() 메소드는 명시적으로 DuplicateUserldException를 던진다고 선언해야 한다.
그래야 add() 메소드를 사용하는 코드를 만드는 개발자에게 의미 있는 정보를 전달해 줄 수 있다.
나만 그런 게 아니었다..!
책에 적혀진 주장은 시대가 변하면 또 달라질 수도 있지만.. 그럼에도 검사 예외에 한계가 있음은 명확했다.
그래서 런타임 예외를 적용하여 다시 커스텀 예외를 만들었다.
@Getter
public class NotEnoughPointException extends RuntimeException {
private final ErrorEnum error;
public NotEnoughPointException(ErrorEnum ee) {
super(ee.getMessage());
this.error = ee;
}
}
...
@Entity
public class Point {
private Integer amount;
...
public Integer usePoint(Integer amount) {
checkEnoughPoint(amount);
this.amount -= amount;
return this.amount;
}
private void checkEnoughPoint(Integer amount) {
if(this.amount - amount < 0)
throw new NotEnoughPointException(ErrorEnum.REJECT_USE_POINT);
}
}
public enum ErrorEnum {
REJECT_USE_POINT(401, "포인트가 부족하여 결제에 실패했습니다.");
private final int code;
private final String message;
ErrorEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
테스트 코드
@Test
@DisplayName("포인트부족_예외처리")
void notEnoughPointException_Handler() throws Exception {
// given
// 가진 포인트보다 더 비싼 메뉴를 고름
// then
ResultActions ra = mvc.perform(post("/order")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(orderDto)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("errorCode").value(401))
.andExpect(jsonPath("errorMessage").value("포인트가 부족하여 결제에 실패했습니다."))
.andDo(print());
}
결과
결론
검사 예외, 런타임 예외의 특징을 알아두고
검사 예외를 써야하는 상황이 아니라면 런타임 예외를 사용하자
'Study > Java' 카테고리의 다른 글
@ExceptionHandler는 어떻게 예외를 처리할 수 있을까? (0) | 2023.07.20 |
---|---|
자바의 함수형 프로그래밍 전략, 메서드 참조 (0) | 2023.05.04 |
[Java] 제네릭 (0) | 2023.02.01 |
JVM 명세 - Run-Time Data Areas (0) | 2022.06.25 |
자바로 간단한 http 웹 서버 구현 (0) | 2022.06.11 |