예외 처리
자바 프로그램 실행 중, 에러가 발생하는 등의 상황을 다룰 때, 예외(Exception)를 씁니다. 코드 수행 중간에 예외가 발생하는 경우 해당 예외가 호출스택을 따라 전파되고, 전파된 예외는 try
- catch
구문으로 잡아서 처리합니다.
자바
try {
writeTextToFile(text);
} catch (IOException ioe) {
System.out.println(ioe.getMessage());
} catch (NumberFormatException nfe) {
System.out.println(nfe.getMessage());
} finally {
System.out.println("여기서 리소스 정리");
}
위 코드는 writeTextToFile
메서드에서 파일에 저장하다가 예외가 발생한 경우, 예외의 종류에 따라 IOException
일 때나, NumberFormatException
일 때를 잡아서 예외 메시지를 출력하도록 했습니다. 정상 처리된 경우나, 예외가 발생한 경우 모두에 반드시 마무리해야 할 작업은 finally
문 안에서 정리합니다.
스칼라
try {
writeTextToFile(text)
} catch {
case ioe: IOException =>
println(ioe.getMessage)
case nfe: NumberFormatException =>
println(nfe.getMessage)
} finally {
println("여기서 리소스 정리")
}
스칼라에서도 try
- catch
- finally
문이 똑같은 흐름으로 수행됩니다. catch
구문의 문법이 패턴 부합(pattern matching) 기능 형태의 문법으로 작성되는 점이 다릅니다.
검사 필수 예외 (Checked Exception)
자바
자바에서 예외는 크게 두 종류가 있고, (1) 그 중 검사 필수 예외(checked exception)는, 메서드 선언부에 해당 예외가 발생할 수 있다고 명시해야 합니다. (2) 하지만, RuntimeException
의 하위 클래스 예외인 경우에는 검사를 강제하지 않고, 메서드 선언부에 명시하지 않습니다.
스칼라
스칼라에서는 예외 검사를 강제하지 않습니다. 자바 코드를 사용하면서 발생하는 예외를 포함한 모든 예외에 대해 검사를 필요로 하지 않습니다. 즉, 스칼라에서는 검사 필수 예외가 따로 없습니다.
예외를 다른 방식으로 다루기
자바 8에 추가된 Optional<T>
를 이용해서, 타입 안전(type-safe)하게 "값이 없는 경우"를 다룰 수 있습니다.
자바
import java.util.Optional;
Optional<Integer> makeInt(String s) {
try {
return Optional.of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
위 예제에서는 주어진 문자열 s
를 정수로 변환할 수 있으면, Optional<Integer>
안에 변환에 성공한 정수값이 담기게 되고, 변환할 수 없는 경우에는, empty()
를 반환합니다. Optional<Integer>
안에는 Integer
로 담겨 있다는 것을 알기 때문에 Integer
타입에 대한 연산을 안전하게 수행할 수 있습니다.
스칼라
def makeInt(s: String): Option[Int] =
try {
Some(s.toInt)
} catch {
case e: NumberFormatException => None
}
스칼라에서도, 자바의 Optional<T>
와 비슷한 Option[T]
를 사용합니다.
makeInt("123") match {
case Some(i) => println(s"정수 i = $i")
case None => println("정수로 변환할 수 없어요")
}
Option[T]
를 사용하는 코드에서는 match
문을 이용해서 값 유무에 따른 처리를 합니다. 첫번째 case Some(i)
에서 패턴 부합 기능으로 i
값을 바로 꺼낼 수 있다는 점을 눈여겨 보세요.
(스칼라) 둘 중 하나의 값 - Either
Option[T]
는 어떤 값이 정상적으로 있는지 없는지를 다룰 때 유용하게 쓸 수 있습니다.
만약, 정상 값이 없을 때 에러 내용 따위를 알고 싶다면 스칼라의 Either[L, R]
를 쓸 수 있습니다. Either
로 둘 중 하나의 값을 택일해서 쓰는데, 하나는 실패 사유에 쓰고, 다른 하나는 정상값을 표현하는데 쓰곤 합니다.
def makeInt(s: String): Either[String, Int] =
try {
Right(s.toInt)
} catch {
case e: NumberFormatException => Left(e.getMessage)
}
makeInt("123") match {
case Left(e) => println("에러: " + e)
case Right(i) => println(s"정수 i = $i")
}
Either
값은 "왼쪽 값" 또는 "오른쪽 값"을 담을 수 있는데, 관례로 왼쪽(Left) 값을 예외적인 값으로 활용하고, 오른쪽(Right)을 정상값으로 씁니다. 아마도 Right
라는 단어가 오른쪽을 의미하기도 하지만, 올바른 값이라는 중의가 있기 때문인 것 같습니다.
(스칼라) 예외도 값으로 다루자 - Try
예외가 발생하면 기본적으로 메서드 호출 스택을 따라서 전파되게 됩니다. 예외가 발생한 가장 안쪽에서부터 바깥쪽으로 전파되는 과정이 기본입니다만, 이를 전파하지 않고 Try[T]
로 감싸서 예외를 값으로 다루는 방법도 있습니다.
Try[T]
는 둘 중 하나의 값이 되는데, 하나는 정상값 T
를 담고 있는 Success
이고, 다른 하나는 실패한 예외(Exception
)를 담고 있는 Failure
입니다.
def makeInt(s: String): Try[Int] = Try(s.toInt)
makeInt("1024") match {
case Success(n) => println(s"정수 변환: $n")
case Failure(e) => println(s"예외 발생: $e")
}
Try
로 감싼 코드가 정상적으로 실행되면, Success
로 결괏값이 담기고, 실행 중 예외가 발생하면 Failure
안에 발생한 예외가 담겨 전파되지 않은 채로 남아있게 됩니다.
성패 결과를 값으로 갖고 있다가, 나중에 원하는 시점에 별도로 처리하기 좋습니다.