프로그래밍 실습 도중 오류가 발생하면 학습에 방해가 될 뿐더러 어떻게 해결해야 할지, 원인은 무엇인지 몰라 당황스럽다. 책에서 시키는대로 똑바로 했는데… 왜 오류가 발생하는 걸까? 컴퓨터는 이유 없는 오류는 내뿜지 않는다. 오류는 무엇인지 왜 발생하는지 그 정체부터 차근차근 알아 보자.

9.1.1 오류를 어떻게 대할 것인가

그림 9-1 오류 메시지 (준비중)

프로그래밍 세계에서 오류란 프로그램이 올바르게 동작하지 않는 현상이다. 오류와 마주하는 것은 프로그래머의 숙명이다. 저숙련자는 자신의 실수에 따른 오류를 경험하고, 고숙련자는 더 복잡한 프로그램에서 더 까다로운 오류와 씨름한다. 프로그램을 완성한 후에도 시간이 흐름에 따라 새로운 오류를 발견할 것이다. 이런 관점에서 프로그래밍은 계속해서 오류를 고쳐나가는 활동이라고도 볼 수 있다.

오류는 대개 프로그래머의 잘못으로 발생하지만, 언제나 그런 것은 아니다. 문법이 틀렸거나 논리적으로 잘못된 프로그램의 오류는 프로그래머의 책임이다. 하지만 프로그램은 실행 환경의 영향으로 오작동하기도 한다. 만약 무제한의 시간과 노력을 들인다면 특정 시점의 제한된 환경에서 모든 경우에 대비할 수 있을지도 모른다. 그러나 현실에서는 프로그램에 투자할 수 있는 자원이 무제한이 아니며, 시간이 흐르고, 환경이 변화한다. 프로그램은 만들어진 이후에 실행되며, 사람은 미래를 예견할 수 없다. 따라서 프로그램의 오류는 필연이다.

그러나 앞으로 오류가 발생할 것이라는 것을 받아들이더라도, “지금” 프로그램을 사용자에게 전달할만큼 “완성”하려면 적어도 프로그래밍 도중에 발견한 오류를 모두 제거하고 예견 가능한 오류에 대비할 필요가 있다. 그리고 이미 출시한 프로그램도 꾸준히 유지보수하여 수명을 유지해야 한다.

9.1.2 오류의 종류

오류에도 여러가지가 있다. 눈 감고도 고칠 수 있는 간단한 것이 있는가하면, 프로그램을 완전히 새로 짜는 게 나을 법한 복잡한 오류도 있다. 메시지가 조금 틀리는 미미한 것이 있는가하면, 핵발전소 사고를 유발할 지도 모르는 재앙적인 오류도 있다. 효율적인 프로그래밍이라는 목적을 위해서는 오류가 일어나는 시점과 원인을 기준으로 오류를 분류하는 것이 좋다. 이 분류법에 따라 오류를 구문 오류(syntax error), 실행시간 오류(runtime error), 논리적 오류(logical error)로 나눌 수 있다. (표 9-1)

오류의 종류 발생 시점 원인
구문 오류 번역중 프로그램이 문법적으로 잘못되었다
실행시간 오류 실행중 프로그램의 지시를 실행할 수 없다
논리적 오류 실행중 프로그램이 논리적으로 잘못되었다

표 9-1 오류의 종류

이들을 하나씩 자세히 알아보자.

구문 오류

“냄비 활활활 육수 촬촬촬”

레시피에 이런 지시가 있다면 따를 수 있을까? 이 표현은 우리말 문법에 맞지 않아 무슨 의미인지 정확히 알 수 없다. 너그러운 사람이라면 의미를 대략적으로 추측해서 알아들을지도 모른다. 하지만 컴퓨터는 깐깐한 성격이다.

컴파일러 또는 인터프리터는 프로그래밍 언어에 의해 작성된 프로그램을 실행 가능한 기계어 프로그램으로 번역한다. 그런데 번역 과정에서 프로그램에 문법과 맞지 않는 표현이 발견되었다면? 더이상 번역을 할 수 없으므로 번역 과정이 중단된다. 구문 오류는 이 때 일어나는 오류다.

(+ 1 2 3 4)라는 표현을 파이썬에 대화식 셸에 입력하면 ‘SyntaxError: invalid syntax’라는 메시지와 함께 구문 오류가 발생한다. 파이썬 문법에 맞지 않고, 따라서 번역할 수 없기 때문이다.

코드 9-1 구문 오류의 예

>>> (+ 1 2 3 4)
  File "<stdin>", line 1
    (+ 1 2 3 4)
         ^
SyntaxError: invalid syntax

C, 자바 등 컴파일 언어에서는 프로그램을 실행하기 전에 프로그램 전체를 미리 번역한다. 번역 과정에서 전체 구문이 검사되므로 프로그램이 구문 오류가 존재하는채로 사용자에게 전달하는 경우가 드물다. 하지만 파이썬은 실행할 코드만을 그때그때 실시간으로 번역하는 인터프리트 언어다. 따라서 프로그래머가 구문 오류가 존재하는데도 이를 깨닫지 못하고 프로그램을 사용자에게 전달할 수도 있다. 대화식 셸에 잘못된 표현을 입력했을 때는 대화식 셸이 표현을 즉시 평가하므로 문법이 틀렸다는 것을 바로 알 수 있지만, 파이썬 프로그램 파일 속에 잘못된 표현을 입력해 둔 경우에는 그 프로그램을 구석구석 실행해 보지 않으면 문법 오류의 존재를 알 수 없다.

이 문제는 구문 오류를 검사해주는 개발 도구를 이용함으로써 어느 정도 해결할 수 있다. 프로그램을 별도로 검사해주는 프로그램도 있고, 텍스트 편집기에 구문 오류 검사기가 내장된 것도 있다. 파이참도 뛰어난 구문 오류 검사 기능을 제공한다. 지금까지 실습을 해 오면서 파이참이 붉은 글씨와 밑줄로 코드 속의 오류를 여러 번 알려주었을 것이다.

구문 오류는 예방과 해결이 비교적 쉽다. 문법을 숙지하고, 좋은 개발 도구를 사용하고, 충분한 주의를 기울여 프로그램을 작성하면 예방할 수 있다. 개발 도구가 문법이 틀린 곳을 바로 알려주므로 오류의 해결이 간단하다.

항상 발생하는 실행시간 오류

“소주잔에 육수 1 리터를 담고 센 불로 끓인다.”

어이쿠! 육수가 넘쳐버렸네! 소주잔에 육수 1리터를 넣을 수는 없다. 문법적으로 올바르고 기계어로 번역도 가능하지만, 정작 그것을 실행하는 것은 불가능한 명령이 있다. 이런 명령은 자신이 실행되어야 할 시점에 오류를 일으키며 프로그램의 동작을 중지시킨다. 이 오류를 실행시간 오류라고 한다.

실행시간 오류의 예를 들어 보자. 1 / 0은 적법한 파이썬 코드다. 하지만 이 코드를 실행하면 오류가 발생한다. 0은 나누는 수가 될 수 없기 때문이다. 또, '붕어빵' - '붕어'은 뺄셈 연산자 좌우에 피연산자를 입력한 문법적으로는 올바른 표현이다. 하지만 문자열 유형에서 뺄셈을 수행할 수는 없어 오류가 발생한다.

코드 9-2 실행시간 오류의 예

>>> 1 / 0              # 실행시간 오류: 0은 나누는 수가 될 수 없다
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

>>> '붕어빵' - '붕어'  # 실행시간 오류: 문자열 뺄셈은 지원되지 않는다
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'str' and 'str'

실행시간 오류에는 항상 발생하는 것과 상황에 따라 발생하는 것이 있다. 위의 예는 항상 발생하는 실행시간 오류다. 이런 오류는 사용하는 프로그래밍 언어의 특성을 숙지하면 충분히 예방할 수 있고, 개발 도구가 어느 정도 사전 경고도 해준다. 따라서 예방하고 해결하기가 비교적 쉬운 편이다. 반면, 상황에 따라 발생하는 실행시간 오류는 더 까다로운 문제가 된다.

상황에 따라 발생하는 실행시간 오류

“냄비에 육수 1리터를 담고 센 불로 끓인다.” … 그런데 냄비가 없다면?

이 레시피는 문제가 없어 보인다. 그러나 냄비가 없다면 조리를 할 수 있을까?

문법적으로 올바르고, 실행도 잘 되는 코드가 특정 상황에서만 실행시간 오류를 일으키는 경우가 있다. 예를 들어, 사용자로부터 어떤 수를 입력받아 그 수로 다른 수를 나누는 코드 1 / int(input())은 많은 경우 정상 동작하겠지만, 사용자가 0을 입력하거나 숫자가 아닌 문자를 입력할 경우에는 오류를 일으킨다.

코드 9-3 상황에 따라 발생하는 실행시간 오류의 예

>>> 1 / int(input())   # 실행시간 오류: 사용자가 0을 입력
0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

>>> 1 / int(input())   # 실행시간 오류: 사용자가 숫자 외 문자를 입력
여덟
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: '여덟'

같은 코드라도 입력된 데이터에 따라 정상 동작하기도 하고 실행시간 오류를 일으키기도 한다. 위 코드는 오류를 일으켰지만 사용자가 입력한 값이 0 외의 수였다면 오류가 발생하지 않았을 것이다. 이런 오류를 방지하기 위해서는 함수나 프로그램에 입력될 데이터의 허용범위를 잘 판단해야 한다.

실행시간 오류는 입력 데이터뿐 아니라 프로그램의 실행 환경에 의해서도 발생할 수 있다. 예를 들어, 인터넷으로 데이터를 다운로드하는 코드는 사용자의 컴퓨터가 인터넷에 연결되어 있지 않거나, 접속하려는 웹페이지가 점검 중이라면 실행시간 오류를 일으킬 것이다. 인쇄기에 문자를 출력하는 코드는 인쇄기가 꺼져 있거나 인쇄 재료가 없을 때 실행시간 오류를 일으킬 것이다. 심지어 단순히 컴퓨터 가용 메모리가 부족해도 프로그램이 실행되지 못할 수 있다.

상황에 따라 발생하는 실행시간 오류는 발견하기가 어려운 편이다. 오류가 일어날 것을 미리 예상하거나 다양한 경우를 테스트하여 오류를 발견해야 한다. 컴퓨터가 이에 대처할 수 있으려면 상황에 알맞는 대응책을 프로그램에 제시해 주어야 한다. 그 방법은 9.3 예외 처리에서 알아볼 것이다.

논리적 오류

“냄비에 육수 1리터를 담고 일주일 동안 상온에 방치한다.”

이 레시피를 따르는 데는 문제가 없으나, 결과물로는 상한 육수밖에 얻지 못할 것이다.

어떤 프로그램이 문법 오류도 발생하지 않고 실행시간 오류도 발생하지 않는다면, 완성된 프로그램일까? 프로그램은 잘 실행되는 것뿐 아니라 목적을 정확히 수행할 수 있어야 한다. 다른 오류가 없더라도 프로그램을 실행한 결과가 잘못되는 경우, 그 프로그램은 논리적 오류를 일으키는 것이다.

논리적 오류는 프로그램의 근본 설계가 잘못되어 발생할 수도 있지만, 연산자, 수, 변수 이름을 잘못 쓰는 등 사소한 코딩 실수로도 쉽게 발생할 수 있다. 입력받은 수가 홀수인지 검사하는 함수를 다음과 같이 잘못 작성했다고 해 보자.

코드 9-4 논리적 오류의 예

>>> def is_odd(n):
...     """n이 홀수인지 검사한다."""
...     return n / 2 != 0    # % 연산자를 / 연산자로 잘못 표기했다
... 
>>> is_odd(0)  # 0은 홀수가 아니다
False

>>> is_odd(1)  # 1은 홀수다
True

>>> is_odd(2)  # 2는 홀수가 아닌데...?
True

위 코드에서 한 실수는 단순히 %/로 잘못 표기한 것 뿐이다. 이런 사소한 실수로도 쉽게 발견하기 힘든 논리적 오류를 일으킬 수 있다. 심지어 이 함수로 0, 1을 검사했을 때는 올바른 결과가 나오기까지 한다. 0 외의 짝수를 검사해보기 전에는 함수가 잘못됐다는 사실조차 모를 수 있다.

논리적 오류는 구문 오류나 실행시간 오류와 달리 실행이 방해받지 않기 때문에 미리 발견하기가 어렵다. 복잡한 프로그램 속에 조용히 숨어있다가 결정적인 상황에서 큰 피해를 입힐 수 있기 때문에 다른 오류보다도 더 위험하다.

실수를 적게 유발하는 코딩 스타일을 익히고 작성한 함수가 다양한 입력에 대해 정상적인 결과를 내는지 테스트하는 것이 논리적 오류를 방지하는 데 큰 도움이 된다. 좀더 장기적으로는 논리적 사고력과 프로그램 설계 능력을 갖춰야 한다. 논리적 사고력은 컴퓨터 과학과 이산수학을 학습하면 향상될 수 있다. 프로그램 설계 능력은 프로그래밍 경험과 함께 성장한다.

정리해보자.

  • 구문 오류: 프로그램의 문법이 잘못되어 기계어로 번역할 수 없을 때 발생하는 오류.
  • 실행시간 오류: 프로그램의 문법에는 문제가 없으나, 명령을 실행할 수 없어 발생하는 오류. 프로그램을 구석구석 테스트해봐야 발견할 수 있다. 특히, 상황에 따라 발생하는 오류를 발견하기가 더 까다롭다.
  • 논리적 오류: 프로그램의 실행에는 문제가 없으나, 프로그램이 올바르게 동작하지 않는 오류. 발견하기가 어렵고 위험하다.

다음 절에서는 오류를 발견한 경우 수정하는 방법을 알아본다.