2021년 2월 16일 12시 7분
아래 내용은 인사이트 출판사의 제안으로 작성 중인 책의 초고입니다. 실제 출판 시에는 내용이 달라질 수 있습니다. 많은 의견 부탁드립니다.
지금까지는 프로그램의 실행이 “잘못될” 가능성에 대해서는 전혀 고려하지 않았다. 하지만 프로그램의 실행이 잘못되기는 매우 쉽다. 덧셈의 의미를 다시 보자. “e1과 e2를 차례로 계산한다. e1의 계산 결과가 n1이고 e2의 계산 결과가 n2이면 n1과 n2의 합이 최종 결과이다.” 여기서 “e1의 계산 결과가 n1이고 e2의 계산 결과가 n2”라는 말은 e1과 e2의 계산 결과가 모두 정수여야 함을 뜻한다. e1과 e2는 임의의 식이므로 e1과 e2의 결과가 정수라는 보장은 어디에도 없다. e1의 결과가 불이나 유닛 같은, 정수가 아닌 값일 수도 있는 것이다. 예를 들어 true + 1이라는 식을 계산하려 했을 때 true의 결과는 정수가 아니라 불이다. 이런 경우에는 어떻게 될까? “e1의 계산 결과가 n1이고 e2의 계산 결과가 n2”라는 가정이 만족되지 않았으므로 계산을 더 이상 이어 나갈 수 없다. 더는 계산할 수 없다는 것은 프로그램이 아무런 결과도 내지 못한 채로 즉시 종료되어야 한다는 것이다. 이처럼 프로그램을 실행하던 중 반드시 만족되어야 할 조건이 지켜지지 않아 프로그램을 비정상적으로 종료하는 것을 실행 시간 오류(runtime error)라고 부른다. 말 그대로 프로그램을 실행하던 중 발생한 오류라는 뜻이다.
다양한 종류의 식이 존재하는 만큼 다양한 종류의 실행 시간 오류가 발생할 수 있다. 위에서 본 덧셈을 해야 하는데 정수가 아닌 값이 나온 경우는 그중 하나에 불과하다. 변수를 사용했는데 그 변수가 정의된 적이 없는 경우. 함수 호출을 하려고 했는데 함수 위치의 식을 계산하니 함수가 나오지 않은 경우. 호출하는 함수의 매개변수 개수와 주어진 인자 개수가 다른 경우. 리스트의 원소에 접근하려고 했는데 주어진 값이 리스트가 아닌 경우. 레코드의 필드에 접근하려고 했는데 그 필드가 레코드에 존재하지 않는 경우. this를 통해 객체에 접근했지만 지금 계산 중인 식이 메서드의 몸통 안에 있지 않은 경우. 이렇게 다양한 실행 시간 오류가 존재한다. 물론 지금 나열한 것이 전부일리 없다. 책에서 지금까지 소개한 식들만 고려해도 실행 시간 오류의 예시를 쉽게 더 찾을 수 있다.
프로그래머에게 실행 시간 오류는 언제나 피하고 싶은 대상이다. 프로그램을 만드는 이유는 그 프로그램이 의미 있는 일을 함으로써 어떤 결과를 내게 하려는 것이다. 결과를 내지 못한 채로 프로그램이 중간에 끝나 버린다면 그 프로그램은 존재할 가치가 없다. 사용자의 편의를 위한 간단한 프로그램에서는 실행 시간 오류가 발생해도 조금 불편한 정도이다. 하지만 자동차나 비행기 같은 기계 장치를 제어하는 데 사용되는 프로그램이 실행 시간 오류로 인해 갑자기 멈춰 버린다면 기계가 제어되지 않아 큰 사고로 이어질 수도 있는 노릇이다. 따라서 어떻게 실행 시간 오류를 줄일 것이냐는 문제는 프로그램을 만드는 데 있어 가장 중요한 문제들 중 하나이다. 실행 시간 오류를 줄이는 방법에 어떤 것들이 있는지, 또 그 방법이 이 책의 주제와는 무슨 상관이 있는지는 3장에서 보게 될 것이다.
한편, 실행 시간 오류와 비슷하면서도 조금 다른 개념인 예외(exception)가 있다. 둘 다 프로그램을 실행하다가 무언가 원하지 않는 일이 일어났을 때 발생한다는 점이 비슷하다. 두 개념이 차이를 보이는 부분은 발생 원인과 의도이다. 실행 시간 오류는 언어 자체가 규정한 조건에 위배되는 일이 생기면 발생하며 프로그래머의 의도와는 상관없다. 계산을 계속할 수 없기 때문에 어쩔 수 없이 일어난 일로, 언제나 안 좋은 일이다. 한번 일어나면 프로그램이 무조건 바로 종료되므로 돌이킬 방법도 없다. 반대로, 예외는 프로그래머가 의도적으로 발생시키며, 언제 발생시킬지 프로그래머가 정한다. 언어는 프로그래머에게 예외를 발생시킬 능력을 제공할 뿐이다. 예외가 발생했다고 해도 언어에 내재된 조건에 위배되는 일이 일어난 것은 아니므로 계산을 계속하는 데 문제가 없다. 프로그래머는 예외 처리 코드를 작성함으로써 발생한 예외를 원하는 방식으로 처리할 수 있다.
예외에 관련된 식에는 예외를 발생시키는 식과 발생한 예외를 처리하는 식이 있다.
throw e
try e1 x e2
try (1 + 1) n (n + 10)의 결과는 2이다. 1 + 1을 계산하면 예외가 발생하지 않으므로 1 + 1의 결과인 2가 최종 결과가 된다. 예외 처리 코드인 n + 10은 전혀 계산되지 않는다. 한편 try (1 + throw 1) n (n + 10)의 결과는 11이다. throw 1이 예외를 발생시키며 그 예외가 가지고 있는 값은 1이다. 1 + throw 1을 계산하던 중에 예외가 발생했으므로 1 + throw 1의 계산을 끝마치지 못하고 바로 예외 처리 코드로 이동한다. 예외 처리 코드에서는 n의 값이 1인 상태로 n + 10을 계산하게 되고, 최종 결과로 11이 나온다.
예외에 대해 생각할 때는 예외를 발생시키는 식이 아무런 결과도 내지 않는다는 점에 유의해야 한다. 이는 ()을 결과로 내는 것과는 다르다. 결과가 ()이라는 것은 결과에 별다른 정보가 없다는 뜻이지, 결과가 없는 것은 아니다. 반대로, throw e는 정말로 아무런 결과가 없다. 식은 프로그램의 다양한 곳에 등장하며, 식의 결과는 변수에 저장된다거나 함수 호출 시 인자로 사용된다거나 하는 등 여러 용도로 사용된다. 그렇기에 식이 아무런 결과도 내지 않는다는 것은 이상한 일이다. 이것이 가능한 이유는 throw e가 프로그램의 흐름을 바꾸는 매우 특별한 식이기 때문이다. let a = ()는 a라는 변수를 정의하며 그 변수가 나타내는 값이 ()이다. 한편, let a = throw 1은 throw 1을 계산하는 중에 예외가 발생하여 예외 처리 코드가 실행되기에 변수 a가 아예 정의되지 않는다. 즉, throw e가 계산되는 순간 그 식의 결과가 필요 없어지므로 throw e가 아무런 결과도 내지 않는 것이다.
만약 발생한 예외가 처리되지 않으면 결국에는 실행 시간 오류가 일어난다. 예를 들어 전체 프로그램이 throw 1이라면 throw 1 자체가 실행 시간 오류를 일으킨 것은 아니지만, throw 1이 발생시킨 예외를 처리할 식이 없기 때문에 결국은 실행 시간 오류로 이어진다.
현실에는 실행 시간 오류와 예외의 경계가 명확하지 않은 경우도 많다. 일부 언어에서는 계산을 위해 꼭 지켜져야 하는 조건이 만족되지 않은 경우에 바로 계산을 끝내기보다는 예외만 발생시킨다. 정수가 아닌 값을 덧셈에 사용하는 경우나 정의하지 않은 변수를 사용하는 경우에 예외가 발생하는 것이다. 이런 경우 비록 프로그래머가 명시적으로 예외를 발생시킨 것은 아니지만 프로그래머가 발생시킨 예외와 마찬가지로 예외 처리가 가능하다.
비록 실행 시간 오류와 예외를 구분하지 않는 현실 세계 언어가 있다고 해도, 개념적으로 그 둘은 확실한 차이가 있다. 실행 시간 오류는 언어가 계산을 하기 위해 요구하는 조건이 만족되지 않은 상황을 뜻하며, 프로그래머 입장에서는 반드시 피해야 한다. 예외는 프로그래머가 원하지 않는 상황이 발생했음을 나타내기 위해 사용되며 프로그래머가 원하는 방식으로 처리할 수 있으므로, 적절하게 사용한다면 프로그래밍에 도움이 된다. 이 책에서는 실행 시간 오류와 예외를 분명하게 구분되는 서로 다른 개념으로 바라보고 이야기를 진행한다.
이제 드디어 준비가 끝났다. 이 정도면 앞으로의 내용을 이해하는 데 필요한 배경 지식으로서 충분하다. 3장은 정적 타입 언어의 장점을 설명한다. 실행 시간 오류를 줄이는 방법에는 무엇이 있는지, 정적 타입 언어가 어떻게 실행 시간 오류를 줄여 주는지, 타입이 무엇이며 왜 필요한지, 이런 근본적인 질문들에 대한 답이 모두 다음 장에 있다. 그러니 어서 다음 장으로 넘어가 보자.