포스트

(Day 30) 에러, 예외처리에 대해서

30일차

복습

날짜 관련 클래스

  • java.util.Date 클래스 사용할 줄 아는가? (날짜 및 시간 정보의 추출)
  • java.util.Calendar 클래스 사용할 줄 아는가?
  • java.sql.Date 클래스

    예외처리 문법의 목적과 구동 방식을 설명할 수 있나?

    문법오류가 아니고 실행 중 문제가 발생하면 그것은 대부분 예외이다. (오류도 있다..) 예외는 Exception 클래스를 상속한 예외 클래스를 반환한다. 이걸 받아주려면,

  • 어떤 부분에서 받을지를 try 문으로 감싸서 결정하고
  • catch 문에서 어떤 예외를 받을지, 그리고 받으면 어떻게 할지를 작성한다.
  • 하나의 try 블럭에 다수의 catch 블럭이 연결될 수 있다. 즉, 예외별로 처리를 다르게 할 수 있다.
  • 예외를 받아주는 catch 문이 없으면 호출한 메서드로 넘긴다. 거기서도 없으면 그것을 호출한 메서드로 올린다. 올라가고 올라가다보면 JVM으로 도달한다. JVM이 예외를 받는 경우 프로그램이 강제 종료된다.
  • 보통 메서드별로 예외를 처리한다. 코드를 재사용하기 위해 OOP를 적용한 만큼, 범용적인 예외는 추상클래스 등 상위 클래스에서 처리하고, 개별 클래스 단위에서 처리해야 할 예외는 하위 클래스에서 처리한다.

학습

예외처리 문법의 등장

메서드를 호출한 다음에, 그 메서드에서 어떤 예외가 발생했는지를 알 수 있는 방법은 return value를 사용하는 방법이 떠오를 것이다. 이런 return value를 이용하여 예외를 검출하는 대표적인 예시가 ArrayList 클래스의 indexOf() 메서드이다. 값을 받아서 그 값이 존재하는 인덱스를 반환하는데, 검색할 때 그 값이 없으면 -1을 리턴한다. 예외처리 문법을 사용하지 않는다.

그런데 return 값으로만 예외를 표현할 수 없는 경우가 생긴다. 이러면 다른 인스턴스를 이용해서 예외가 발생했음을 알려주는 방법을 사용해야 한다. 그런데 이게 자주 사용되다보니 아예 문법으로 만들어버렸다.

대표적으로는 ArrayList 클래스의 get() 메서드가 있다. 이 메서드는 인덱스를 파라미터로 받는다. 그리고 해당 인덱스의 값을 반환한다. 인덱스가 아닌 값을 반환하는 메서드다보니, -1도 반환할 수 있다. 또한 null 도 반환할 수 있다. (해당 인덱스에 null 이 저장된 경우) 리스트에 들어갈 수 있는 모든 값을 반환할 수 있는 메서드다보니, 유효하지 않은 인덱스 라서 예외가 발생했음을 return 값으로 알려줄 수 없다. 그래서 get() 메서드는 예외처리 문법을 사용한다.

위 예시를 보면, 어떤 메서드는 리턴값으로 예외를 알려주고, 어떤 메서드는 예외처리 문법을 이용해서 예외를 알려준다.

위 이야기를 똑똑한 추상화로 다시 말해보면..

예외 상황호출자에게 알리는 방법return value로 알려주는 방법이 가장 먼저 등장했다. 그러나 사용하지 않는 리턴 값이 없는 경우에는 이 방식을 사용할 수 없다. 그래서 예외 정보throw하는 문법이 생긴다. 왜 throw인가? return이 아니라서 구별하기 위해 던진다고 표현하는 것이다.

java에서 예외처리를 위한 클래스들은 이름을 참 잘 지었다. 모든 클래스의 슈퍼클래스인 Object 클래스를 상속한 Throwable 클래스가 있고, 이 Throwable 클래스를 상속하는 Exception 클래스와 Errror클래스가 있다. 던질 것들은 예외 아니면 오류라는 것이 아주 직관적이다.

보통 예외처리라고 하면 try, catch만 떠오르는데 (내가 그랬다) finally라는 키워드도 있다. finally 키워드는 블럭을 붙여서, 해당 블럭 안에는 ‘try 블럭 안에서 예외가 발생하건 발생하지 않건 실행할 코드’를 작성하면 된다.

try문에서 예외가 발생하면, 마치 break 문장을 만난 것처럼 try 문을 예외가 발생한 코드에서 나오고, 해당하는 예외가 있는 catch 문으로 간 다음 그 블럭을 실행하고, finally 키워드의 블럭을 실행하고 다음 코드들을 실행한다.

에러와 예외, Error and Exception

예외는 catch 문으로 처리해 줄 수 있지만, 에러는 처리가 불가능하다. 무조건 JVM이 종료된다. 프로세스가 종료된다. 대표적인 예시로는 재귀함수를 다루다가 만나볼 수 있는 StackOverFlow 에러가 있다.

에러는 발생하지 않게 하는 것이 중요하고, 예외는 프로그래머가 처리해주는 것이 중요하다. 대부분의 예외는 적절한 처리를 통해 프로세스를 (적절하게) 계속 실행할 수 있다. 이를 Recoverable 가능하다고 표현한다. 에러는 Unrecoverable 하다. https://docs.oracle.com/javase/tutorial/essential/exceptions/index.html

에러는

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// throw 명령어를 사용하여 예외 정보를 호출자에게 던진다.
// => throw [java.lang.Throwable 타입의 객체];
// java.lang.Throwable
// => Throwable에는 두 부류의 서브 클래스가 있다.

// 1) java.lang.Error (시스템 오류)

// => JVM에서 발생된 오류이다.

// => 개발자가 사용하는 클래스가 아니다.

// => 이 오류가 발생하면 현재의 시스템 상태를 즉시 백업하고, 실행을 멈춰야 한다.

// => JVM에서 오류가 발생한 경우에는 계속 실행해봐야 소용이 없다.

// 근본적으로 문제를 해결할 수 없다.

// => 오류의 예:

// 스택 오버 플로우 오류, VM 관련 오류, AWT 윈도우 관련 오류, 스레드 종료 오류 등

//

// 2) java.lang.Exception (애플리케이션 오류)

// => 애플리케이션에서 발생시킨 오류이다.

// => 개발자가 사용하는 클래스이다.

// => 적절한 조치를 취한 후 계속 시스템을 실행하게 만들 수 있다.

// => 오류의 예:

// 배열의 인덱스가 무효한 오류, I/O 오류, SQL 오류, Parse 오류, 데이터 포맷 오류 등

에러는 Application 외부에서 발생하는 문제다. 예외는 Application 내부에서 발생하는 문제다. Error occrus at the external section of application. Exception occurs at the internal section of application. So error is unrecoverable, though exception can be recovered.

throw, throws

클래스가 어떤 것을 던지는지를 표현할 때는 동사로 쓰니 throws 이고 예외를 던지는 블럭은 명령문의 동사로 쓰니 throw 를 쓴다.

여러 종류를 던질 수 있으니 throws 키워드의 목적어는 여러 개가 올 수 있다. 즉 던질 수 있는 Throwable 서브 클래스의 여러개가 , 로 구분되어 여러 개를 적을 수 있다.

Error 의 처리

Error또한 처리 할 수는 있다. 처리하고 나면 JVM이 무조건 종료된다.

예외를 던지는 메서드의 선언부

에러를 던지는 경우는 메서드 선언부에 throws 키워드로 어떤 에러를 던질지 타입을 선언하지 않아도 문제가 발생하지는 않는다. 예외를 던지는 경우는 메서드 선언부에 반드시 throws 키워드로 어떤 예외를 던질지 타입을 선언해야 한다. 친절하게 컴파일러가 검사해준다.

RuntimeException

Exception의 서브 클래스임에도 불구하고 RuntimeException 객체를 던질 경우, 메서드 선언부에 예외를 던진다고 표시하지 않아도 된다. => “Unchecked Exception”이라 부른다. 즉, 해당 메서드가 예외를 던지는지 검사하지 않는다는 뜻이다. => 보통 스텔스 모드(비유!)로 예외를 전달할 때 사용한다.

Exception 계열의 예외를 던지는 메서드를 호출할 때

  1. try catch 로 예외를 처리하거나
  2. 예외를 처리하지 않고 상위 호출자에게 예외 처리를 위임하거나 (보고)

둘 중에 하나는 반드시 해야 한다. 했는지 안했는지를 친절한 컴파일러가 검사해준다! 상위 호출자에게 예외 처리를 위임하려면, 호출된 메서드는 메서드 시그니처에서 throw 키워드로 뭘 던질지를 알려줘야 한다. 메서드 시그니처에 이것이 명시되어 있지 않으면 컴파일러가 문법 오류라고 알려준다.

어떤 예외인지 알려줄 때… 다형적 변수를 사용하는 것처럼 예외 발생할거라고, 슈퍼 클래스인 Exception 타입을 던질 것이라고 선언해도 된다.

예외 처리의 순서

예외 처리 순서는 단순히 효율성 문제가 아니라도 다형적 변수 사용에 따라 중요하다. 특히 슈퍼클래스인 Exception 타입에 대한 catch block이 상단에 있으면, 그 아래의 서브클래스 예외의 catch block들은 unrecahble 하다. 실행될 수가 없다.

논리연산자

1
2
3
catch (OOOException | OOOException | OOOException e) {
	// 예외를 처리
}

이게 가능하다. 예외 타입을 묶어서 받을 수 있다. 메서드 파라미터에서는 당연히 이런 문법이 지원되지 않는다.

Catch에서 Throwable 타입 안 쓰는 이유

JVM의 오류도 받을 수 있다.

finally 키워드를 사용하는 상황

호출한 메서드가 뭔가 던졌거나 던지지 않았거나 반드시 실행해야 하는 문장인데, 일반적으로 자원해제 코드가 들어간다.

RuntimeException 및 그 서브클래스들은 메서드 시그니처에 throws 선언 안해도 되는 이유?

매번 메서드 시그니처에 선언해주기 귀찮아서…. 컴파일러의 친절함을 줄이고 조금 더 편하게 트레이드 오프 한 것이다.


RuntimeException 및 그 서브클래스들은 “unchecked exception”에 속하며, 컴파일러가 체크를 강제하지 않습니다. 따라서 이러한 예외를 발생시키는 메서드에서는 throws 선언을 할 필요가 없습니다. 여기에는 몇 가지 이유가 있습니다:

  1. Unchecked Exception의 특성: RuntimeException 및 그 하위 클래스들은 주로 프로그램의 오류나 버그, 또는 더 이상 정상적인 실행이 불가능한 상황을 나타냅니다. 이러한 예외들은 일반적으로 개발자의 코드나 로직에 의해 발생하며, 컴파일 시에 미리 예측하기 어렵습니다.

  2. 코드 가독성 향상: Checked Exception은 메서드 시그니처에 throws 선언을 강제함으로써, 이를 처리할 수 있는 코드를 개발자가 작성하도록 유도합니다. 반면에 Unchecked Exception은 강제성이 없으므로, 코드가 더 간결하고 가독성이 향상됩니다.

  3. 프로그래머 실수와 예외 처리의 간소화: Unchecked Exception은 주로 프로그래머의 실수에 의해 발생하는데, 이러한 예외에 대해 강제적인 예외 처리를 요구한다면, 프로그래머는 더 많은 예외 처리 코드를 작성해야 하므로 실수할 가능성이 높아집니다. 따라서 이러한 예외들은 예외 처리를 단순화하고 프로그래머의 부담을 줄이기 위해 unchecked로 분류되었습니다.

그러나 이러한 예외들도 무작정 무시해서는 안 되며, 개발자는 프로그램이 예외 상황에 적절하게 대처할 수 있도록 신중하게 코드를 작성해야 합니다.


finally 도 귀찮아! try-with-resources

매번 자원 해제를 finally에서 처리하도록 작성하는게 비효율적이라 느껴졌는지, 이를 더 간소화한 문법이 등장했다. 이는 try-with-resources 문법이라고 불린다. 이는 java.lang.AutoCloseable 인터페이스의 구현체만 쓸 수 있다.

1
2
3
4
5
6
7
try (   /*close()있는 객체만 가능*/ 
		Object1 obj1 = new Object1();
		Object2 obj2 = new Object2();
) {
	//try문이 수행할 코드 작성
}
// 자동으로 close()를 수행하여 자원을 반환한다. (컴파일러가 미리 변환함)

참고로 AutoCloseable 구현체들을 선언할 때, 마지막 문장은 세미콜론을 적지 않아도 된다. finally 로 close()를 수행하는 것과 동일하게, 예외가 발생하면 try-with-resources 문법에서도 close()를 먼저 실행한다.

throws, throw 키워드의 의미

영어에 익숙한 사람들이 아주 직관적으로 받아들일 수 있는 키워드다. 이 메서드는 이런 예외를 던질 수 있습니다 = 이런 예외가 발생할 수 있습니다.

김메서드 던진다 예외

1
2
3
methodKim throws Exception {
	...
}

던져라 예외객체

1
throw new Exception();

RuntimeException은 Unchecked Exception

예외가 발생했는데 지금까지 throws OOOException 을 메서드 시그너처에 넣지 않았었다면? 그건 RuntimeException 혹은 그의 서브클래스에 해당하는 예외 클래스를 반환하는 메서드들을 썼기 때문이다.

컴파일러가 오류를 말해주지 않는 것이 좋은 게 아니다! 컴파일러의 깐깐함 = 초보에 대한 친절함이다. 그 친절함을 편리를 위해 RuntimeException 에서 덜어낸 것이다!

무엇이 편리한가? RuntimeException 혹은 그의 서브클래스인 예외 클래스를 던지는 경우, 메서드 시그너처에 throw RuntimeException 선언을 하지 않아도 (생략해도) 된다. 굳이 코딩하지 않아도 된다는 것이다. 상위 호출자 (caller method)로 계속 던지고 싶을 때 상위 호출자인 메서드의 시그너처에 throw RuntimeException 작성하지 않아도 된다…

편리한 만큼 사용할 때 더 조심해야 하는 것이 RuntimeException 예외 클래스이다. 컴파일러가 검사하지 않는 Unchecked Exception 이기 때문이다. catch 하지 않은 RuntimeException 및 그의 서브 클래스들은 JVM까지 예외가 도달하게 되면, 콘솔에 해당 예외의 StackTrace를 출력하고 프로세스가 종료되어버리고 말 것이다.

그래서… 최후의 보루로 main()에서는 예외처리를 해 주는 것이 권장된다.

예외 클래스와 상속

예외 클래스는 일반 클래스의 상속과는 좀 다르다. //아닌가?..? 일반 클래스의 상속은 보통 메서드를 재정의하거나 변수를 추가하는데, 예외 클래스는 보통 상속을 기능 확장 목적이 아니라 기존 예외에 한번 포장만 다시 하는, 껍데기만 다른 클래스로 만들 목적으로 사용한다. 클래스의 이름만 달라져도 직관적으로 어떤 예외가 발생했는지 알기 쉽기 때문이다.

예외 클래스 까보면, 그냥 super(); 만 붙어있는 경우가 많다.

예외 클래스 계층도에서 서브 클래스의 목적은 기능 확장이 아니라 예외 식별을 용이하게 만들기 위함이다.

링크드리스트 Linked List

실습 위주로 진행한거고 필기하면서 공부했다. 따로 정리해서 포스팅해야 할 내용.. add(오버로딩), delete 손코딩 맛집이고, 취업준비를 떠나서 기술에 매력을 느끼는 사람으로서 당연히 알아야 할 내용!

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.