006. 코틀린에서의 예외처리

코틀린에서의 예외처리

Java와 다른 많은 언어들과 같이 코틀린에서의 예외 처리도 비슷한 방식으로 처리된다.

예제를 통해 코틀린의 예외 처리 방식을 알아보자.

1
2
3
if (percentage !in 0..100) {
throw IllegalArgumentException("A percentage value must be between 0 and 100: $percentage")
}

위의 예제는 percentage 변수가 0~100 사이가 아니면 지정된 메시지와 함께 IllegalArgumentException 예외를 던지게 되어있다.

여기까지만 보면 Java와 완전 동일하지만, 코틀린은 프로퍼티나 변수의 표현을 위해 예외 처리 문법을 사용할 수 있다.

1
2
3
4
5
val percentage =
if (number in 0..100)
number
else
throw IllegalArgumentException("A percentage value must be between 0 and 100: $number")

위의 예제는 percentage 변수가 0~100 사이면 해당 값인 numberpercentage를 초기화하고 아니면 IllegalArgumentException를 던지면서 초기화도 실패하게 된다.

try-catch-finally

코틀린의 try-catch-finally도 Java와 동일하게 사용하면 된다.

아래 예제를 통해 파악해보자.

readNumber() 함수에서 오류가 발생하던지 안하던지 BufferedReader 객체의 close() 메소드를 호출함을알 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun readNumber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberFormatException) {
return null
}
finally {
reader.close()
}
}

/* 출력 결과 */
>>> val reader = BufferedReader(StringReader("239"))
>>> println(readNumber(reader))
239

try-catch에서 코틀린이 Java와 다른 점은 throws가 없다는 것이다.

만약 readNumber()를 Java에서 작성했다면 아래와 같이 throws IOException("...")가 빠지지않고 작성되었을 것이다.

1
2
3
4
5
6
7
8
9
10
11
public int readNumber(BufferedReader reader) throws IOException {
try {
String line = reader.readLine();
return Integer.parseInt(line);
} catch (NumberFormatException e) {
// return null; 컴파일 오류 발생
} finally {
reader.close(); // IOException을 처리해주어야 한다.
}
// 컴파일 오류 발생
}

코틀린은 함수에서 던지는 예외를 지정할 필요가 없다.

좀 더 상세히 말해 검증해야하는 예외와 검증할 필요가 없는 예외를 구분하지 않는다는 것이다.

검증해야 하는 예외
Exception 클래스를 상속받는 하위 클래스 중 RuntimeException을 제외한 모든 예외
즉 컴파일타임에서 무조건 검증해야 한다.
Ex) IOException, SQLException

검증할 필요가 없는 예외
RuntimeException 클래스를 상속받는 하위 클래스
Ex) NullPointerException, IllegalArgumentException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
        +-----------+
| Throwable |
+-----------+
/ \
/ \
+-------+ +-----------+
| Error | | Exception |
+-------+ +-----------+
/ | \ / | \
\________/ \______/ \
unchecked checked +------------------+
| RuntimeException |
+------------------+
/ | | \
\_________________/
unchecked

구조도로 보면 위와 같다.

다시 리마인드해서 코틀린은 함수에서 던지는 예외를 지정할 필요가 없는 이유는 무조건 검증해야하는 예외들로 인해 무의미한 코드가 많이 작성되며,

수많은 런타임 에러들을 겪어본 사람이라면 알 수 있듯, 작성된 코드들이 모든 예외를 막아주지도 못한다.

try 구문을 표현식으로 사용하기

readNumber()를 좀 더 개선해서 try-catch를 표현식으로 사용해보자.

1
2
3
4
5
6
7
8
9
10
11
12
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
return
}
println(number)
}

/* 출력 결과 */
>>> val reader = BufferedReader(StringReader("not a number"))
>>> readNumber(reader)

number 프로퍼티에 대한 초기화에 try-catch를 이용한 것을 알 수 있다.

단, if문을 사용할 때완 다르게 무조건 {}를 통해 코드 블록을 구별해주어야 한다.

위의 예제에서 아무런 출력이 발생하지 않는 이유는, catch 블록에서 return을 수행하기때문에 함수의 하단부 즉, catch 블록 이후의 코드는 무시되기 때문이다.

catch 이후 코드도 실행하려면 catch 블록에도 그게 설사 null일지언정 값이 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch (e: NumberFormatException) {
null
}
println(number)
}

/* 출력 결과 */
>>> val reader = BufferedReader(StringReader("not a number"))
>>> readNumber(reader)
null

catch 블록에 null을 작성하여 이후 코드도 수행된 것을 확인할 수 있다.