SW/Java

올바른 Java 예외 처리

얇은생각 2023. 12. 16. 07:30
반응형

마음의 평화를 위해 그리고 여러분의 동료들에게 이익을 주기 위해 예외를 올바르게 다루는 이 유용한 기사를 읽으세요. 여러분을 행복하게 해줄 뿐만 아니라 여러분의 동료들도 행복하게 해주세요.

일반적으로 처음에는 서비스 분석의 결함에 문제가 숨겨져 있습니다. 종종 오류를 어떻게 던져야 하는지에 대한 참조 측면에서 아무런 요구 사항이 없습니다. 일반적으로 이런 일이 발생하는 이유는 두 가지입니다. 첫째는 새로운 서비스를 개발하려는 러시이고, 둘째는 분석가가 개발자의 경험을 신뢰한다는 것입니다

이제 사례로 넘어가겠습니다. 개발 과정에서 이 접근 방식의 결과에 대해 알아보겠습니다. 하지 말아야 할 첫 번째 작업은 RuntimeException을 실행하는 것입니다:

@ControllerAdvice
public class ApiExceptionHandler {

  @ExceptionHandler(value = {RuntimeException.class})
  public ResponseEntity<Object> handler(RuntimeException e) {
    HttpStatus badRequest = HttpStatus.INTERNAL_SERVER_ERROR;
    return new ResponseEntity<>(e.getMessage(), badRequest);
  }
  ...

 

 

 

올바른 Java 예외 처리

 

 

호출 서비스나 클라이언트는 500개의 오류를 수신할 것이고, 요청에 무엇이 문제인지 이해하지 못할 것입니다. 그런 경우에 문제를 찾는 데 얼마나 걸릴 것 같습니까? 코드의 양에 비례하여 증가할 것입니다. 그리고 메소드 콜 체인을 가지고 있다면, 서비스 내부의 메소드에서 그런 메시지를 처리하는 것이 더 어려워집니다.

어떻게 개선할 수 있을까요? 우선 오류 처리기를 만들어 봅시다. 거의 모든 프레임워크는 기본적으로 오류를 처리합니다. 예를 들어 스프링 프레임워크 처리기를 사용하겠습니다.

@ControllerAdvice
public class ApiExceptionHandler {

  @ExceptionHandler(value = {RuntimeException.class})
  public ResponseEntity<Object> handler(RuntimeException e) {
    HttpStatus badRequest = HttpStatus.INTERNAL_SERVER_ERROR;
    return new ResponseEntity<>(e.getMessage(), badRequest);
  }
  ...

 

 

이제 메시지가 보이지만 메시지의 형식은 문자열이지 JSON이 아닙니다. 곧 알게 될 것입니다.

이상적으로 프로젝트의 모든 서비스는 동일한 오류 메시지 형식을 가져야 합니다. 적어도 두 개의 필드는 메시지 자체와 내부 정수 오류 코드입니다. 메시지 텍스트가 부족한 이유와 문자열을 처리하는 데 필요한 리소스 수를 말할 필요가 없다고 생각합니다.

public class ExampleApiError {

  private String message;
  private Integer code;
  private LocalDateTime dateTime;
  ...

 

 

이 클래스를 에러 핸들러에 채울 것입니다. 그러나 내가 말했듯이 RuntimeException을 던지는 것은 나쁜 관행이므로 당신은 에러를 던지기 위해 당신만의 클래스를 만들어야 합니다. 그 컨스트럭터에서 우리는 메시지와 에러 코드를 전달할 것입니다.

public class ApiException extends RuntimeException {

  private final int code;

  public ApiException(String msg, int code) {
    super(msg);
    this.code = code;
  }
  ...

 

 

더 이상 모든 것이 명료해 보이지만 여기서도 문제가 시작됩니다. 모든 사람이 다른 방식으로 클래스 생성자에게 전달하기 위한 매개 변수를 만듭니다. 어떤 것은 메소드에서 직접 메시지와 오류 코드를 생성합니다.

public Object badExample() {
  throw new ApiException("Something wrong!", 10);
}

 

 

코드에서 그런 장소를 찾는 것은 여전히 어렵습니다. , 첫 번째로 생각나는 것은 상수의 클래스를 만드는 것인데, 하나는 메시지를 위한 클래스이고 두 번째는 메시지 코드를 위한 클래스입니다.

public static final String SOMETHING_WRONG = "Something wrong";
public static final int SOMETHING_WRONG_CODE = 10;

public Object badExample() {
  throw new ApiException(SOMETHING_WRONG, SOMETHING_WRONG_CODE);
}

 

 

이것은 오류 코드가 어디에 있는지 알 때 이미 훨씬 더 좋고, 더 읽기 쉽고, 쉽게 찾을 수 있습니다.

하지만 마이크로 서비스가 없다면 한 클래스에 모든 것을 저장하는 것은 좋지 않은 생각일 수 있습니다. 메시지가 점점 더 커지기 시작하기 때문에 일정한 클래스를 ProductExceptionConstant, PaymentExceptionConstant 등과 함께 사용할 메서드의 기능에 따라 구분하는 것이 좋습니다.

하지만 그게 전부가 아닙니다. 일부 사람들에게는 구식으로 보일 수 있지만 상수는 Enum 또는 Interface로 생성되어야 합니다. 인터페이스의 변수는 기본적으로 정적 변수, 공용 변수 및 최종 변수입니다. 저는 이 접근 방식에 반대하지 않습니다. 가장 중요한 것은 모든 것이 균일해야 한다는 것입니다. 인터페이스를 통해 시작했다면 계속 그렇게 해야 합니다. 혼합할 필요가 없습니다. 프로젝트 중 하나에서 동일한 팀이 상수와 함께 세 가지 다른 접근 방식을 사용하는 것을 보았습니다. 그렇게 할 필요는 없습니다

리뷰하는 동안 눈에 띄었던 실제 프로젝트에서 한 가지 사례를 더 보여드리겠습니다.

우선 개발자는 자신이 반환하는 모든 개체에 오류 메시지와 오류 코드가 포함되어 있고 상태는 항상 200이 된다고 결정했는데, 이는 제 생각에는 발신자에게 오해의 소지가 있습니다. 글쎄요, 상수의 예입니다.

public enum ErrorsEnum {
    DEFAULT_ERROR(ErrorsConstants.DEFAULT_ERROR, "400"),
    REFRESH_TOKEN_NOT_VALID(ErrorsConstants.REFRESH_TOKEN_NOT_VALID, "400.1"),
    USER_NOT_FOUND(ErrorsConstants.USER_NOT_FOUND, "400.2"),
...

 

보기에는 코드 93 \ 4가 누락된 것 같습니다. 그리고 기본적으로 이 경우에는 Pi라는 숫자를 사용할 수 있습니다.

가장 편리한 것은 모두에게 구속력 있는 계약이 될 인터페이스를 만드는 것입니다.

public interface ExceptionBase {

  String getMsg();

  Integer getCode();
}

  

 

그리고 예외 클래스의 컨스트럭터를 변경해야 합니다.

public ApiException(ExceptionBase e) {
  super(e.getMsg());
  this.code = e.getCode();
}

 

 

이제 예외를 던지려는 모든 사람들은 우리가 인터페이스를 구현하는 객체를 생성자에게 전달해야 한다는 것을 이해할 것입니다. 그리고 가장 적합한 옵션은 Enum입니다.

/**
 * This class is intended for declaring exceptions that occur during order processing. code range
 * 101-199.
 */
public enum OrderException implements ExceptionBase {
  ORDER_NOT_FOUND("Order not found.", 101),
  ORDER_CANNOT_BE_UPDATED("Order cannot be updated,", 102);

  OrderException(String msg, Integer code) {
    this.msg = msg;
    this.code = code;
  }

  private final String msg;
  private final Integer code;

  @Override
  public String getMsg() {
    return msg;
  }

  @Override
  public Integer getCode() {
    return code;
  }
}

 

 

사용 예:

public Object getProduct(String id) {
  if (id.equals("-1")) {
    throw new ApiException(OrderException.ORDER_NOT_FOUND);
  }
...

 

결과적으로 다음과 같은 예외 패키지 모델이 있습니다.

보시다시피 복잡하지 않고 논리만 쉽게 읽을 수 있습니다. 이 예제에서는 APIException 클래스에 문자열을 가져오는 컨스트럭터를 남겼는데, 신뢰성을 위해서는 문자열을 제거하는 것이 좋습니다.

일반적으로 코드의 대부분의 불일치는 코드 검사의 부족 또는 약한 검사 때문입니다. 가장 흔한 핑계는 "이것은 일시적인 해결책이다. 나중에 해결하겠다."이지만, 아니, 그렇게 되지 않습니다. 아무도 아무도 어디에 일시적인 해결책이 있는지, 어디에 영구적인 해결책이 있는지 찾지 않을 것입니다. 그리고 "일시적인 것은 영구적이다."라고 밝혀졌습니다

서로 소통하는 서비스가 많다면 하나의 오류 메시지 형식을 만들어 클라이언트 라이브러리 작성 작업을 크게 간소화합니다. 예를 들어, Retrofit을 사용할 때 일단 핸들러 코어를 작성하면 인터페이스의 메서드와 수신되는 개체만 변경하면 됩니다.

 

 

결론

오류 처리는 코드에서 매우 중요한 부분입니다. 코드에서 문제 영역을 쉽게 찾을 수 있고 외부 클라이언트가 엔드포인트를 사용할 때 무엇을 잘못하고 있는지 이해할 수 있으므로 프로젝트 작성의 첫 단계부터 주의해야 합니다.

반응형