Java Exception 의 계층 구조
Java의 예외에는 대표적인 클래스 4가지가 있습니다
이 4가지 클래스는 사실 직접 사용하는 것은 권장되지 않습니다. 대부분의 경우에 이를 상속받는 새로운 클래스를 만들어 사용하거나, 대표적인 예외들을 사용하도록 권장하고 있습니다( ex) IllegalArgumentException, NullPointerException )
1. Throwable
모든 예외에 관련된 부분의 가장 조상이 되는 클래스 입니다. 모든 예외가 공통으로 가지고 있는 메서드나, 상태를 가지고 있습니다. 실질적으로 에러와 관련된 거의 모든 로직들이 담겨있지만, 직접 사용되는 경우는 거의 없기에, 사실 그렇게까지 깊게 알 필요는 없습니다
이 클래스를 포함한 서브 클래스들 중 RuntimeException 에 서브 클래스가 아니라면 모두 컴파일타임에 해결해줘야 하는 Checked Exception 이 됩니다
이 클래스의 서브클래스만 throw 키워드를 통해서 던질 수 있습니다
2. Exception vs Error
Exception 은 프로그램 코드에서 처리를 하면 복구가 가능한 예외입니다
Error 은 개발자가 처리를 하면 안되고, 대부분은 복구가 불가능한 경우입니다.
OutOfMemoryError
같은 문제가 실제로 발생했을 때, 이 부분을 코드상에서 복구하기는 불가능하니까요
3. RuntimeException
Unchecked Exception 을 만들기 위해서 필수적으로 상속받아야 하는 클래스입니다
이 클래스의 서브클래스인 경우에만 컴파일 타임에 필수적으로 처리할 필요 없이 코드를 작성할 수 있습니다
Checked Exception, Unchecked Exception
이펙티브 자바 아이템 70
복구할 수 있는 상황에는 Checked Exception
호출하는 쪽에서 복구할 것으로 여겨지는 상황이라면 Checked Exception 을 사용하면 좋다고 하는데요
이 부분이 기준이라면, 우테코에서 예외를 내고, 이를 처리하는 방식은 별로 권장되지 않는 방향일 것 같은데요
try {
return Integer.parseInt(height);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format(NOT_NUMBER_MESSAGE, height));
}
잘못된 데이터를 받아서 이를 다시 호출해줄테니 복구할 것을 기대하니, Checked Exception 을 만들어서 처리하도록 하는 방향이 더 이펙티브자바 아이템에 맞는 상황이기도 합니다.
프로그래밍 오류에는 Runtime Exception
복구가 불가능하거나, 복구할 것을 기대하지 않는 상황이라면 비검사 예외를 던지라고 적혀있는데요. 위의 메서드만을 통해서 이 예외가 복구될 것인지, 아닌지를 알 수 없다는 점이 문제입니다
이럴 때 선택의 기준은 대부분의 경우 Runtime Exception 을 사용하는 쪽이 좋다고 합니다
Checked Exception 의 단점
1. stream 에서 사용 불가능
Stream 에서는 Checked Exception 이 있는 메서드는 직접 사용할 수 없기에 사용성이 많이 떨어집니다
사용을 진짜 하고 싶다면, 내부에서 try{catch} 를 통해서 감싸 주거나
위처럼 익명 클래스로 감싸주어야 합니다
2. 대부분의 인터페이스와 호환되지 않음
대부분의 함수형 인터페이스는 throws 키워드를 붙히지 않았습니다. 그렇기에, throws가 있는 경우에는 함수형 인터페이스로 감쌀 수 없고, 직접 커스텀 인터페이스를 만들어 주어야 합니다
당연하지만 정말 귀찮은 작업이 될 가능성이 높고요
Custom Exception
Custom Exception 즉 사용자가 직접 Exception 을 만드는 방식입니다
당연하지만, 커스텀 예외도 하나의 클래스이기에, 너무 많아진다면 관리가 어려워질 수도 있고,
적절한 표준 예외가 주는 것보다 더 많은 정보를 주기 위해서 네이밍, 각종 데이터에 대해서 신경을 써야 할 것입니다
실제 ArrayIndexOutOfboundException 보다 많은 데이터를 담고, 더 디테일한 메시지를 담고 있는 커스텀 에러 객체입니다
실제 ArrayIndexOutOfBoundException 같은 경우에는, index를 받아서 전달하기는 하지만, 실제로 어떤 데이터에 접근했는지를 알기 위해서는 getMessage()함수를 통해서 나온 메시지를 파싱하는 과정을 거쳐야 합니다
이런 과정을 대체해버리는 장점을 생각했을 때, 확실한 장점을 가질 수 있기 때문에, 만들어서 제대로 데이터를 주는 여유가 있다면 만드는 것도 괜찮다고 생각합니다
왜 예외는 Serializable 인터페이스를 구현해야 했을까요?
실제로 내부 구현을 보면 Throwable 이 Serializable 인터페이스를 구현하고 있습니다
Serializable 인터페이스란
자바 시스템 내부에서 사용되는 객체나 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태로 변환하고, 다시 객체 형태로 변환하는 기술을 의미하는데요
그러면 왜 자바의 Throwable 은 외부의 자바 시스템으로 갈 수 있도록 byte 형태로 변환할 수 있도록 만들었을까요?
하나의 JVM 에서 모든 로직이 다 돌아가면 물론 좋겠지만, 여러 JVM 에서 로직이 돌아가고 있을 때, 어느 한 곳에서 문제가 발생했고, 그 정보가 필요할 때, 당연하지만 하나의 데이터도 잃고 싶지 않을겁니다
그 데이터를 다른 jvm에서 필요로 할 때, 안정적으로 전송하기 위해서 Serializable 을 구현했다고 합니다
사실 다른 객체들을 위해서 구현한 것과 동일한 이유라고 하네요
에러가 발생한 이유 찾기
기존 대부분의 경우에 사용하고 있는 생성자는
위와 같은 2가지의 생성자일텐데요
기본 생성자를 사용하거나, 메시지만을 전달하는 방식을 많이 사용했을 텐데요
아래 2가지의 생성자를 보면, Throwable cause 가 있습니다
이 생성자는 보통 예외 전환을 이용할 때 사용되는 생성자입니다.
이 예외는 어떤 예외로부터 발생했고, 그 호출스택에 대한 정보까지 누락되지 않게 하고 싶을 때 사용합니다
예외가 발생했을 때, 추상화 수준에 맞지 않는 너무 구체적인 예외이거나, Checked Exception 일 경우에, 이를 감싸서 새롭게 예외를 발생시켜줍니다
출력해보면 이런 느낌의 결과가 나오게 됩니다
예외에 대한 정보는 계속 누적되지만, 추상화 수준에 맞는 예외로 변환되고 Unchecked Exception 이 되기에 직접 처리하지 않아도 되고, 메시지에 대한 정보는 계속 남길 수 있습니다
Cause가 null 인 경우
의도적으로 Cause 를 null 로 지정하는 경우가 있는데요
cause is nonexistent or unknown 일 때 일부러 null로 지정한다고 합니다
기본값은 this로 초기화 되어있기에, null 인 경우와 구분할 수 있습니다
Try-With-Resource 문과 Exception
대부분의 사람들이 처음보는 생성자가 Runtime Exception 에 존재하는데요
에러를 suppression 하는 것과, stackTrace를 기록할지 여부를 담고 있습니다
이 부분이 사용되는 경우는 대부분이 try-with-resource 문과 함께 사용되는데요
이 부분이 사다리 게임에서 사용되는 메인 함수입니다
내용은 전혀 중요하지 않지만, try-with-resource 문을 통해서 inputView 를 초기화 하고 있는 것을 볼 수 있습니다
이 부분을 intellij를 이용해서 .class파일을 디컴파일해보겠습니다
실제로 클래스 파일에서는 close 메서드를 호출하는 것을 볼 수 있습니다
이때 만약 close 과정에서 예외가 발생한다면? var5.addSuppress() 를 통해서 suppress 를 추가해주는 모습을 볼 수 있습니다
눌러진 예외는 평범하게 출력해보면
과 같이 평범한 예외와는 다르게 Suppressed: 라는 메시지가 앞에 붙는 것을 볼 수 있습니다
이는 제대로 보여지지 않기 때문에,
를 통해서 직접 검색하는 방식을 통해서 찾아볼 수 있습니다
만약 enableSuppression 을 true로 둔다면, 왜 이렇게 두었는지에 대한 문서를 작성할 필요가 있습니다
Cheap Exception
기본적으로 Exception 은 매번 새롭게 만들어지고, 그때의 stack 에 어떤 순서로 호출되었는지 같은 데이터들을 담고 전달해주는 역할을 합니다
이때 stack을 채우는 과정이 시간이 오래 걸립니다
만약 어디서 이 예외가 발생하는지 정말 명확하고, 스택을 저장할 이유가 단 하나도 없다면, stack에 대한 데이터를 저장하지 않고, 예외를 만들 수 있습니다
Throwable 의 서브 클래스에서 writableStackTrace 를 직접 false로 두면 되는데요
이렇게 했을 때, 예외를 생성하는 비용이 거의 들지 않기에 빠를 수는 있어도, 예외에 대한 모든 문맥을 잊어버리게 되기에 권장되지 않고, 만약 이렇게 해야한다면 왜 이 작업이 필요한지에 대해서 문서 작성이 요구됩니다
나라별로 다른 에러 메시지 던지기
상속받은 클래스에서 이 메서드를 오버라이딩 해버리면, 나라별로 다른 메시지를 줄 수 있다고 합니다
추후에 cause 주입하기
생성 당시에는 왜 생성되었는지 모르지만, 추후에 cause를 알게 되는 경우가 있을 수 있습니다
그렇다면 여기 있는 것처럼, 한번 cause를 넣어줄 수 있습니다
여러번 호출했을 경우에는 예외를 만들어냅니다. 딱 1번만 초기화가 가능합니다
참조
'Java' 카테고리의 다른 글
Stream collect 알아보기 (0) | 2023.03.13 |
---|---|
Java 멀티 쓰레드 아는체하기 (1) | 2023.03.05 |
Java 초기화 되지 않음을 표현하는 방법 (0) | 2023.02.27 |
Java Generic 딥 다이브 (6) | 2023.02.23 |
Generics 시작하기 (6) | 2023.02.23 |