엄범

 

Error와 Exception의 차이

  • 에러 : 애초에 예상이 불가능한 것.
  • 예외 : 발생을 예상할 수 있는 것. 그리고 예상할 수 있기 때문에 그에 대한 대비로 try-catch가 있는 것.
    • checked exception
      • 컴파일 타임에 경고를 해주는 예외. 예외 처리가 안되어 있으면 컴파일이 안된다.
      • 컴파일 타임에 발생하는 Exception이라고 말하기는 좀 그렇다.
      • 정확히는 Exception은 Runtime에 발생하며 컴파일 타임에 경고를 해주는거지.
      • 대표적인게 `` IOException, SQLException``
    • unchecked exception
      • 컴파일 타임에 경고를 안해주는 예외. 예외 처리가 안되어 있어도 컴파일이 된다.
      • 대표적인 것이 `` NPE, IndexOutOfBoundsException``
    • 자바에서는 `` RuntimeException``과 그 하위 예외들이 unchecked exception, 나머지는 checked exception이다.

 

예외를 어떻게 처리할지 생각하기 전에, 예외를 마주쳤다는 것은 무엇을 의미하는가?

1. Exception은 조건문이 아니다. Exception을 if처럼 쓰지 마라.

예외는 조건의 의미가 들어있는게 아니다.

의미 상 조건에 맞게 처리해야 하는 경우는 if, 예상치 못한 상황이 왔을 때 어떻게 행동할 것인가는 try - except로 처리한다.

 

조건문은 예상된 flow대로 흘러가는 반면 예외는 try 안의 코드를 실행하다 언제 실행 흐름이 except로 넘어갈지 모른다는 점도 생각해 보면 둘을 구분하는데 도움이 된다.

 

예를 들어서 argv의 개수가 적을 때라던가, 이런 예상할 수 있는 상황은 그냥 일단 argv[0]에 접근하고 안되면 except로 받는 것 보다, ``c if``로 length check를 하는게 더 나아보인다.

 

하지만 그럼에도 if와 try-catch가 애매한 순간들이 존재한다.
  • 주로 클라이언트의 입력 값을 검증해야 하는 상황일 때.
    • 예를 들어 클라이언트로 부터 입력받는 값의 합계가 100이 되어야 하는 상황이라고 생각하자.
    • 대부분의 클라이언트는 합계 100으로 맞춰서 요청하겠지만, 간혹 실수로 또는 일부러 합계가 100이 안되도록 요청할 수 있고, 나는 그걸 알 고 있다.
      • 이런 경우, 클라이언트가 이상하게 요청할 것이라는 것을 내가 알고 있으니까 "이런 경우 바로 리턴하면 돼."라는 조건으로 바라봐야 할지?
      • 아니면 원래는 이렇게 요청하라고 되어 있는 프로그램이 아닌데, 이렇게 요청을 했으니 이걸 예외로 바라봐야 할지?
    • 예외의 장점은 리턴 타입에 구애받지 않고, CustomException에 정형화된 파라미터(예외 메시지, code)를 넣어서 던질 수 있다는 점.

 

로그인 실패 같은 유저 코드에서 발생하는 예외, 401로 처리? 200으로 처리?
  • 로그인 실패 같은 유저 코드에서 발생하는 예외를 401로 처리해야 하느냐, 200으로 처리해야 하느냐 고민이 될 수 있는데,
    • 구글 같은 경우 로그인 실패해도 200이 내려온다.
    • 즉, HTTP code는 말 그대로 예상치 못한, 시스템에서 발생한 예외를 나타내는데 쓰고 있고,
    • 개발자가 캐치하고 의도적으로 Exception을 던지는 예외는 200으로 처리하고 있는 듯.
      • 물론 내부에서 로그인 실패 로직을 if로 처리할지 throw로 처리할지 알 수는 없지만... 아무튼 내려줄 때 200으로 내려준다.

 

2. API에서 발생하는 예외는 "이렇게 쓰지 마라."를 의미할 수도 있다.

예를 들면 ``java JdbcTemplate.queryForObject()``는 DB에서 가져온 데이터가 없으면(null) 예외를 발생시킨다.

근데 이 메서드는 애초에, 꼭 이 데이터를 null 없이 받아야 한다라고 설계되어 있는 메서드다.

그래서 이렇게 없는 데이터를 가져올 수 있는 케이스가 예상이 된다? 라면 예외가 발생하지 않는 다른 메서드를 사용하고 `` if``로 처리하는 것이 자연스럽다.

 

Global Exception Handling은 좋은 패턴이지만 모든 예외를 Global Exception Handler에서만 처리하는 것은 좋지 않다.

예를 들어 위 2번과 같은 상황에서, "어떤 입력에 대해서는 `` queryForObject()``에서 `` RuntimeException``이 발생할 것임을 알지만 그 것을 감안하고서라도 이 메서드를 꼭 사용해야 한다."와 같은 케이스를 생각해보자.

이미 Global RuntimeException Handler가 등록 되어 있다고 하더라도, 굳이 이 Exception을 그리로 보내야 할 이유가 있는가? 

오히려 여기서 잡아서 pass하고 null을 리턴하는게 맞을지도 모른다.

이런 경우 해당 메서드 내에서 try-catch로 예외에 대한 적절한 처리를 하는게 맞을 수 있다.

 

즉, Global Exception Handling은 한 가지의 선택지가 될 수 있는 것이지 모든 처리를 반드시 여기서 하라는 얘기는 아니다.

 

Global Exception Handler를 사용하면 좋은 케이스는 다음과 같다.
  • `` RuntimeException``에 대한 DefaultHandler를 지정할 때. (handleDefaultRuntimeException)
    NPE같은 일일히 예상할 수 없는 unchecked exception에 대해 DefaultHandler를 지정하게 되면 일관된 응답을 기대할 수 있다.
    로깅도 용이하고 갑자기 프로그램이 뻗는 상황도 방지할 수 있을 것이다.
  • 여러 곳에서 비슷한 예외가 발생하고 이에 대해 일관된 처리가 필요하거나, 일관된 응답이 필요할 때.
    • 최상위 unchecked인 RuntimeException에 대한 핸들러가 이미 존재한다고 해도, 그 자식인 다른 unchecked에 대한 핸들러를 지정해서 조금 더 구체적으로 예외 상황을 처리하는 것이 필요할 때가 많다.
    • 물론 checked에 대해서도 가능함.
  • checked exception과 unchecked exception에 대한 처리 로직을 한 곳으로 모으고 싶을 때.
  • try-catch를 하는 부분이 지저분하거나, throws를 해서 메서드 시그니처가 지저분해지는 것을 막을 때

 

checked exception도 Global Exception Handler에서 처리할 수 있다.

checked exception은 반드시 try-catch나 상위로 throws 둘 중 하나를 해야 하기 때문에, checked exception을 처리하는 척 하면서 unchecked exception으로 래핑해서 다시 던지면 Global Handler로 들어가게 할 수 있다.

 

RuntimeException을 상속 받은 RuntimeIOException 클래스를 하나 만들고,

try-catch로 감싼 다음 RuntimeIOException에 checked exception을 담아서 던지도록 한다. (stack trace에 남는다.)

```java

try {

    // IOException 발생 ( checked exception )

} catch (Exception e) {

    throw new RuntimeIOException(e)       // this is unchecked exception!!!

}

```

```java

/* global exception handler */

@ExceptionHandler(RuntimeIOException.class)

public void handleRuntimeIOException(final RuntimeIOException runtimeIOException)

```

 

* assert는 내부적인 정확성을 보장하는데 사용한다.

``py assert``는 내부적인 정확성을 보장하는 데 사용된다.

반드시 어떤 값을 입력 해야만 한다는 느낌으로 정확한 사용법을 강요할 때 쓰는게 아니다.

마찬가지로 예상치 못한 값이 들어왔으니까 exception을 발생시킨다!! 이런 느낌으로 쓰는게 아니다. (이런 경우는 ``py if - raise``를 사용한다.)

따라서 argument로 알맞은 값이 들어왔는지 검증할 때는 사용하지 않는다.

 

CommonMessageException

Enum에 OK("1000", "성공") 이런 식으로 정의해놓는건... 분명 좋은 방식이긴 하지만,

```java

throw new DirectBillingException(dr.getServiceName() + "/" + dr.getCorpName());

```

 

이런 케이스가 커버가 안됨. 특정 메시지나 데이터를 클라이언트 쪽에 내려주고 싶을 때.

다건 업데이트 같은 작업 도중 실패가 발생한 경우, 어느 항목에서 실패가 발생했는지를 클라이언트 쪽에 내려주어야 하는 경우들이 있음.

해결책은? 이런 경우에 사용할 일반적인 Exception을 하나 정의하는 것이 낫다.

 

```java

public class CommonMessageException extends RuntimeException {
    private String externalMessage;

    public boolean isIgnorable() { return this.getCause() == null; }

    ...

```

  • 이런 식으로 외부 메시지를 별도로 받아서 내려주도록 구성하면, 내부에 찍는 RuntimeException.message와 externalMessage를 구분할 수 있음.
  • 정말 예외 상황이라 로그를 찍어야 하는 경우, cause에 담긴 것이 있느냐 없느냐로 구분할 수 있음.
  • 여기서는 Message필드만 있고 Code 필드는 따로 안썼는데, 필요한 경우에는
    • Enum을 사용하여 Code, Message를 묶고, 생성자 오버로딩으로 Enum을 받도록 구성할 수 있음.
      • 생성자에서 이 Enum을 풀어서 code, message 변수에 할당.
    • 내부에서는 String code, String message로 가지고 있으므로, business layer에서 특정 문자열을 넘기도록 할 수 있음.
      • Enum만 사용하면 dr.getServiceName() 이런 문자열을 business layer에서 넘겨 받는 것이 불가능하니까... 내부 타입은 String이어야함.