본문 바로가기

개발

[Scala] 스칼라는 예외를 이용하지 않고 오류를 처리한다.

스칼라도 예외 처리가 없진 않다.

아래의 예시처럼 try catch 문도 존재한다. 

def main(args: Array[String]): Unit = {
    def falling(i:Int): Int = {
      val y: Int = throw new Exception("THREAD")
      try {
        val x = 42 + 5
        x + y
      } catch {
        case e: Exception => 43
      }
    }

    falling(3)
    println("이러면 에러가 발생한다.")
  }

그런데 참조에 투명하지 않다.

 

참조 투명성을 가지려면 문맥에 의존하지 않아야하는데 위의 코드는 try 블록에 속해있는지, 속해있다면 어느 try 블럭에 속해있는지에 따라 결과가 달라진다. 즉, 치환모형의 간단한 추론이 불가능해지고, 형식에 안전하지도 않다. falling 이라는 함수가 Int => Int 형식을 갖췄지만, 이 표현 형식으로부터 "이 함수가 예외를 던진다."는 걸 추론할 수는 없다. 

 

그 결과 우리는  예외를 던지는 대신  예외적인 조건이 발생했음을 의미하는 값을 돌려주는 방법을 선택했다.

 

'예외적인 조건이 발생했다.' 라는 걸 알려주는 값을 알려주는 방법은 여러가지가 있다.

 

1. 가짜 값 돌려주기 

에러가 발생하면 NaN 값을 돌려주는 방법이다. 

- 호출자가 오류 점검 구문을 누락시켜도 컴파일러가 경고하지 않는다. 오류가 발생한 함수가 아니라 그 함수를 호출한 다른 함수에서 오류가 검출될 수 있다.

- 진짜 값을 받은건지 검사하는 IF문이 주렁주렁 늘어난다. 

- 가짜 값을 돌려주기가 힘들 수도 있다. 특히 다형적 코드에서는 데이터 타입에 따라서 가짜 값을 돌려주기가 불가능한 상황이 생긴다.

def test[A,B](a:A)=>B 

이 함수는 B 형식의 값을 돌려줘야하는데 B형식에서 NAN에 해당하는 값이 없을 수 있다.

- 호출자가 특별한 규약을 준수해야한다. 그러면 고차함수로서 재사용성이 많이 떨어질 수 있다. 

 

2. 함수에서 오류가 발생했을 때 동작시킬 인수를 받아두기

def mean_1(xs: IndexedSeq[Double], onEmpty: Double): Double = 
  if (xs.isEmpty) onEmpty
  else xs.sum / xs.length

xs가 비어있으면 onEmpty라는 값을 돌려준다. 문제는 이 함수의 직접적인 호출자가 이 함수에서 정의되지 않은 값을 처리하는 방식을 알고 있어야 하고 이 반환값은 호출자가 변경할 수 없다는 점이다. 

 

3. Optional 

그래서 스칼라는 Optional을 사용한다. Optional은 함수가 항상 답을 내지는 못한다는 점을 반환 형식을 통해서 명시적으로 표현하는 것이다.

Option을 이용하면 오류 처리의 공통 패턴을 고차 함수들을 이용해서 추출함으로써 판에 박힌 예외처리 코드를 쓰지 않아도 된다. 

 

==============================

 

이 내용은 「스칼라로 배우는 함수형 프로그래밍」을 정리한 것입니다.