프로그래밍 실습 도중 오류가 발생하면 학습에 방해가 될 뿐더러 어떻게 해결해야 할지, 원인은 무엇인지 몰라 당황스럽다. 책에서 시키는대로 똑바로 했는데… 왜 오류가 발생하는 걸까?

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

오류는 프로그램이 올바르게 동작하지 않는 현상이다. 프로그램이 오동작하는 원인은 여러 가지가 있다. 프로그래머가 작성한 코드가 프로그래밍 언어의 규칙에 맞지 않으면 프로그램이 동작하지 않을 것이다. 문법에 맞게 잘 작성한 프로그램이더라도 문제를 푸는 과정이 틀렸을 수도 있다. 또, 프로그램은 어떤 환경에서 실행하는지에 따라서도 오동작할 수 있다.

오류는 발생 시점과 원인에 따라서 구문 오류(syntax error), 실행시간 오류(runtime error), 논리 오류(logical error)로 분류할 수 있다. (표 9-1)

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

9.1.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

비록 프로그래밍 언어를 새로 배울 때는 구문 오류를 많이 경험하겠지만, 문법에 익숙해진 뒤에는 구문 오류는 별로 까다로운 문제가 아니다. 코드가 문법에 맞는지를 컴퓨터 프로그램으로 검사할 수 있기 때문이다. 예를 들어, 파이참을 사용해 코드를 작성하면 구문 오류를 실시간으로 확인할 수 있다.

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

“소주잔에 육수 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'

코드 9-2는 어떤 조건에서 실행하더라도 항상 발생한다. 항상 발생하는 실행시간 오류는 프로그래밍 언어에 익숙해지면 해결하기가 쉽다. 파이참 같은 개발 도구의 경고를 받을 수도 있다. 따라서 구문 오류와 마찬가지로 대개는 까다로운 문제거리가 되지 않는다.

하지만 실행시간 오류 중에는 ‘상황에 따라 발생하는 것’도 있다. 이런 오류는 ‘상황에 따라’ 큰 문제가 될 수 있다.

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

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

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

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

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

>>> 1 / int(input())   # 정상 동작: 사용자가 2를 입력
2
0.5

>>> 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: '여덟'

이처럼 같은 코드라도 입력된 데이터에 따라 정상 동작하기도 하고 실행시간 오류를 일으키기도 한다. 따라서 함수나 프로그램이 외부 환경에서 데이터를 입력받을 때는 어떤 데이터를 허용할 것인지 잘 판단하고 확인해야 한다.

실행시간 오류는 입력 데이터뿐 아니라 프로그램의 실행 환경에 의해서도 발생할 수 있다. 몇 가지 예를 들어 보자.

  • 인터넷으로 데이터를 교환하는 코드: 사용자의 컴퓨터가 인터넷에 연결되어 있지 않다.
  • 인쇄기에 글자를 출력하는 코드: 인쇄 재료가 다 떨어졌다.
  • 큰 파일을 메모리로 읽어들이는 코드: 시스템에 가용 메모리가 부족하다.

이런 문제는 실행 환경이 적절하다면 일어나지 않는 문제이기도 하다. 프로그래머가 운이 좋아서(사실은 운이 나빠서) 프로그램이 잘 실행되는 환경에서만 프로그램을 테스트했다면 이런 오류를 미리 방지하기가 어려울 것이다.

상황에 따라 발생하는 실행시간 오류는 비교적 대응하기가 어렵다. 오류가 일어날 것을 미리 예상하거나 다양한 경우를 테스트하여 오류를 발견해야 한다. 일단 오류가 일어날 조건을 예견했다면, 이에 맞는 대응책을 프로그램에 제시해야 한다. 그 방법은 9.3 예외 처리에서 알아볼 것이다.

9.1.4 논리 오류

“냄비에 육수 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 외의 짝수를 검사해보기 전에는 함수가 잘못됐다는 사실조차 모를 수 있다.

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

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

개념 정리

  • 구문 오류: 프로그램의 문법이 잘못되어 기계어로 번역할 수 없을 때 발생하는 오류.
  • 실행시간 오류: 프로그램의 문법에는 문제가 없으나, 명령을 실행할 수 없어 발생하는 오류.
  • 특정 상황에서만 발생하는 실행시간 오류는 발견하고 대응하기가 어렵다.
  • 논리 오류: 프로그램의 실행에는 문제가 없으나, 프로그램이 올바르게 동작하지 않는 오류. 발견하기가 어렵고 위험하다.

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