오류를 찾아 해결하는 것은 추리소설에서 탐정이 사건을 조사하는 것과 비슷하다. 현장을 꼼꼼이 관찰하듯이 코드를 잘 살펴보고, 오류 해결의 결정적 단서인 오류 메시지를 해석하고, 프로그램을 테스트하여 사건 현장을 재현하고, 로그 기록을 통해 사건을 추적하면 대부분의 오류를 해결할 수 있다.

버그와 디버그

프로그램에 내재한 오류를 버그(bug)라고도 부른다. 그리고 오류를 수정하는 작업을 디버그(debug)라고 한다. 이 용어를 우리말로 옮기면 ‘벌레’, ‘벌레 제거’라는 뜻이다. 오류를 벌레라고 부르게 된 계기는 과거 컴퓨터 회로에 날벌레가 끼어 오류가 발생한 일화 때문이라고 전해진다.

9.2.1 오류 메시지

코드를 자세히 살펴보는 것은 오류 해결의 기본이다. 다음 프로그램에서 무엇이 오류인지 한번 연습삼아 찾아보자.

코드 9-5 오류가 존재하는 프로그램 (debug1.py)

들짐승 = {'사자', '박쥐', '늑대', '곰'}
날짐승 = {'독수리', '매', '박쥐'}
육지생물 = 날짐승 + 들짐승
print('육지생물: ', 육지생물)

오류를 발견했다면 축하한다. 하지만 오류를 발견하지 못했더라도 괜찮다. 오류 메시지라는 결정적 단서를 아직 살펴보지 않았으니까. 위 프로그램을 실행하면 다음과 같은 오류 메시지가 출력된다. 이번에는 메시지를 읽어보고 무엇이 잘못인지 생각해보자.

실행 결과:

Traceback (most recent call last):
  File "debug1.py", line 3, in <module>
    육지생물 = 날짐승 + 들짐승
TypeError: unsupported operand type(s) for +: 'set' and 'set'

오류 메시지는 여러 행으로 되어 있다. 마지막 행을 제외한 행은 오류가 발생한 위치를 알려준다. 오류가 발생한 위치부터 알아보자. Traceback (most recent call last):이란 오류를 발생시킨 함수 호출을 역추적한 내용이라는 뜻이다. 그 아래에 오류를 일으킨 코드가 순서대로 나열된다. 이 경우에는 하나 뿐이다. File "debug1.py", line 3, in <module>debug1.py 파일(File)의 세번째 행(line)에 오류가 있음을 가리킨다. 육지생물 = 날짐승 + 들짐승이라고 오류가 발생한 코드도 출력되었다.

메시지의 마지막 행은 오류의 종류와 문제점을 설명한다. 마지막 행 앞부분의 TypeError는 발생한 오류의 종류로, 데이터 유형과 관련된 오류임을 나타낸다. 콜론(:) 뒤의 메시지는 무엇이 문제인지를 좀더 자세히 설명한다. unsupported operand type(s) for +: 'set' and 'set'라는 메시지를 해석해보면, “+는 피연산자 유형을 지원하지 않음: ‘set’, ‘set’“이라는 뜻이다. 즉, + 연산자로 집합과 집합을 연산할 수 없다는 뜻이다.

아하! 집합과 집합은 + 연산자로 합할 수 없다. 5.4 집합에서 알아본 것처럼, | 연산자나 union() 메서드를 사용해야 한다. 따라서, 다음과 같이 오류를 수정할 수 있다.

코드 9-6 오류를 수정하기

들짐승 = {'사자', '박쥐', '늑대', '곰'}
날짐승 = {'독수리', '매', '박쥐'}
육지생물 = 날짐승 | 들짐승
print('육지생물: ', 육지생물)

오류 메시지는 어디가 문제이고, 왜 문제인지를 알려준다. 이 메시지만 잘 해석해도 오류를 쉽게 해결할 수 있는 경우가 많다. 단, 오류 메시지가 출력되는 것은 구문 오류, 실행시간 오류에 한한다. 논리적 오류에는 오류 메시지가 없다.

초보자가 접하기 쉬운 오류 메시지

오류 메시지가 매우 유용하긴 하지만, 아무래도 영어여서 해석하기가 부담될 수 있다. 그렇다 하더라도 오류 메시지를 무시해서는 안 된다. 해석하기가 귀찮다는 이유로 다잉 메시지를 무시하는 탐정이 있을까? 사전을 동원해 능력껏 해석해보고, 여의치 않으다 번역기 프로그램을 사용해서라도 읽어보아야 한다. 언어는 사용할수록 는다. 처음에는 힘들더라도 오류 메시지를 자주 읽으면 금세 익숙해 수 있다.

초보자가 접하기 쉬운 오류 메시지 몇 개를 해석해 두었으니 참고하자.

메시지 의미
SyntaxError: invalid syntax 구문 오류: 문법에 맞지 않다
IndentationError: expected an indented block 들여쓰기 오류: 들여쓰기를 해야 한다
NameError: name ‘a’ is not defined 이름 오류: 이름 ‘a’가 정의되지 않았다
TypeError: unsupported operand type(s) for +: ‘a’ and ‘b’ 유형 오류: 연산자 +가 유형 ‘a’와 ‘b’를 지원하지 않는다
TypeError: must be ‘a’, not ‘b’ 유형 오류: 유형이 ‘a’여야 하는데 ‘b’다
TypeError: ‘int’ object is not callable 유형 오류: ‘int’ 유형 객체는 호출할 수 없다
TypeError: f() takes 1 positional arguments but 2 were given 유형 오류: f() 함수는 인자 1개를 전달받는데 2개가 전달되었다
TypeError: ‘str’ object does not support item assignment 유형 오류: ‘str’ 유형 객체의 항목에 다른 값을 대입할 수 없다
ValueError: invalid literal for int() with base 10: ‘a’ 값 오류: ‘a’는 int() 함수의 10진법으로 해석할 수 없다
AttributeError: type object ‘int’ has no attribute ‘a’ 속성 오류: ‘int’ 유형 객체에는 ‘a’ 속성이 없다
ZeroDivisionError: division by zero 영 나눗셈 오류: 영(0)으로 나눌 수 없다
KeyError: ‘a’ 키 오류: (사전에) ‘a’ 키가 없다

표 9-2 초보자가 접하기 쉬운 오류 메시지

함수 호출 역추적

오류 메시지에서 마지막 행 앞의 행들은 오류가 발생한 위치를 알려준다. 이 정보는 몇 행에 불과할 때도 있고 매우 길 때도 있다. 이는 함수 호출이 연쇄적으로 일어난 경우 그 호출 과정을 알려주기 위해서다.

오류 메시지에 함수 호출 과정이 포함되어야 하는 이유는 무엇일까? 어떤 명령이 함수를 호출했는데, 그 호출된 함수 속에서 오류가 발생했다고 하자. 그러면 수정해야 할 것은 호출된 함수일까, 함수를 호출한 명령일까? 이는 컴퓨터가 판단할 수 없다. 간단한 예를 들어 보자.

코드 9-7 함수 연쇄 호출 속의 오류(debug2.py)

def a(x):
    return 8 / x      # x가 0인 경우 오류 발생

def b(y):
    return a(y - 1)   # y가 1인 경우 오류 발생

def c(z):
    return b(z - 2)   # z가 3인 경우 오류 발생

c(3)

위 코드의 함수 a()는 0을 입력받았을 때 0으로 나누는 오류를 일으킨다. 그리고 함수 b()a()를 호출함으로써 오류를 일으킬 가능성이 있고, 이는 b()를 호출하는 함수 c()도 마찬가지다. 결국 마지막 행에서 c(3)을 실행함으로써 오류가 발생하고 만다. 그렇다면 이 프로그램에서 오류를 일으키는 원인 코드는 무엇일까?

규모가 큰 프로그램에서는 여러 함수가 연쇄적으로 호출되기 마련이다. 이런 연쇄 호출 도중 오류가 발생한 경우, 어디를 고쳐야 할지는 프로그래머가 판단할 몫이다. 오류 메시지에는 함수 연쇄 호출 과정이 모두 보고된다. 이 프로그램을 실행하여 오류 메시지를 확인해 보자.

Traceback (most recent call last):
  File "debug2.py", line 10, in <module>
    c(3)
  File "debug2.py", line 8, in c
    return b(z - 2)   # z가 3인 경우 오류 발생
  File "debug2.py", line 5, in b
    return a(y - 1)   # y가 1인 경우 오류 발생
  File "debug2.py", line 2, in a
    return 8 / x      # x가 0인 경우 오류 발생
ZeroDivisionError: division by zero

오류 메시지의 첫번째 행은 “역추적 (최근에 호출된 것일수록 나중에 표시함):”이라는 뜻이다. 그리고 들여쓰기된 그 다음행부터 마지막 행 전까지의 행은 호출된 함수를 역추적하여 출력한 것이다. 이 경우 최초로 실행된 c(3) 명령부터 연쇄적으로 호출된 c(), b(), a() 함수에서 문제가 되는 부분이 각각 출력되었다. 자세히 살펴보고 오류 메시지의 의미를 완전히 이해하는 연습을 하자.

오류 메시지가 복잡해 보이는 것은 주로 함수 호출 역추적 정보탓이 크다. 하지만 컴퓨터의 입장에서는 오류 수정에 꼭 필요한 내용만을 간결하게 보고한 것이다. 역추적 정보에 따라 오류가 발생한 지점을 추적해가면 어디를 고쳐야할지 가늠할 수 있다.

연습문제

연습문제 9-1 오류 메시지로 오류 알아내기

다음은 중앙값(값들을 크기순으로 정렬했을 때 가운데 위치하는 값)을 구하는 프로그램이다.

def median(data):
    """데이터의 중앙값을 반환한다."""
    sorted_data = sorted(data)
    median_value = sorted_data[len(sorted_data) / 2]
    return median_value

print(median([10, 9, 4, 1, 5, 7]))

이 프로그램을 실행하면 다음과 같은 오류가 발생한다.

Traceback (most recent call last):
  File "exercise_9_1.py", line 7, in <module>
    print(median([10, 9, 4, 1, 5, 7]))
  File "exercise_9_1.py", line 4, in median
    median_value = sorted_data[len(sorted_data) / 2]
TypeError: list indices must be integers or slices, not float

이 오류 메시지를 읽고 문제점을 지적한 후, 올바르게 수정해 보아라.

9.2.2 로그

그리스 신화의 영웅 테세우스는 다이달로스의 미궁을 헤쳐나갈 때 실타래를 이용해 지나온 길을 표시했다고 한다. 프로그램의 실행 흐름이라는 미궁을 헤쳐나갈 때도 실타래가 있다면 좋을 것이다. 프로그래머들은 프로그램을 실행하는 도중에 화면, 파일, 데이터베이스 등에 메시지를 출력함으로써 실행 흐름을 나타내곤 한다. 이를 로그(log)라고 한다.

로그를 잘 활용하면 복잡한 프로그램 속에서도 실행 흐름과 상태를 파악할 수 있다. 오류 메시지는 일종의 로그다. 오류 메시지는 오류가 발생한 시점에 오류의 내용을 출력한다. 더 일반적인 로그는 임의의 시점에 임의의 내용을 출력하는 것이다. 언제 무엇을 출력할지는 프로그래머의 재량이다. 오류가 일어날 때 프로그램은 어디까지 실행되었는지, 변수는 어떤 값이었는지, 사용자가 입력한 값은 무엇인지 등 디버그에 도움되는 다양한 정보를 출력할 수 있다.

오류 메시지는 프로그래밍 언어에 의해 자동 출력되지만, 로그 출력은 여러분이 작성하는 프로그램 속에 직접 지시해야 한다. 로그를 남기는 가장 초보적인 방법은 print() 함수로 추적할 내용을 출력하는 것이다. 다음은 오류 메시지의 함수 호출 역추적 기능을 살펴보기 위해 예로 들었던 코드 9-7에 로그 출력을 추가한 것이다.

코드 9-8 함수 연쇄 호출 속의 오류(debug3.py)

print('프로그램 실행됨')

def a(x):
    print('함수 호출됨: a()  <= ', x)
    return 8 / x      # x가 0인 경우 오류 발생

def b(y):
    print('함수 호출됨: b()  <= ', y)
    return a(y - 1)   # y가 1인 경우 오류 발생

def c(z):
    print('함수 호출됨: c()  <= ', z)
    return b(z - 2)   # z가 3인 경우 오류 발생

print('명령 실행: c(3)')
c(3)

print('프로그램 종료됨')

위 코드의 print() 함수를 잘 보자. 프로그램의 시작과 종료, 함수 호출, 명령 실행 등의 시점에 필요한 정보(프로그램 진행상황, 함수에 전달된 인자 등)를 출력하고 있다. 이 프로그램을 실행하면 오류 메시지와 별도로 프로그램 실행 과정이 출력된다.

프로그램 실행됨
명령 실행: c(3)
함수 호출됨: c()  <=  3
함수 호출됨: b()  <=  1
함수 호출됨: a()  <=  0
Traceback (most recent call last):
  File "debug3.py", line 16, in <module>
    c(3)
  File "debug3.py", line 13, in c
    return b(z - 2)   # z가 3인 경우 오류 발생
  File "debug3.py", line 9, in b
    return a(y - 1)   # y가 1인 경우 오류 발생
  File "debug3.py", line 5, in a
    return 8 / x      # x가 0인 경우 오류 발생
ZeroDivisionError: division by zero

이 출력을 통해 명령과 함수가 실행된 과정과 함수에 전달된 데이터를 쉽게 파악할 수 있다. 함수 a()에 0이 입력되었다는 것도 분명히 확인할 수 있다. 사실 이 예는 간단해서 오류 메시지 만으로도 오류를 잡는데 어려움이 없다. 하지만 프로그램이 복잡하게 뒤엉켜서 흐름을 파악하기 힘들 때, 특정한 상태에서만 오류가 발생할 때, 논리적 오류가 발생해 오류 메시지의 도움을 받을 수 없을 때는 로그가 큰 도움이 될 수 있다.

본격적인 로그에는 기록 시각, 위험 수준, 보고 대상 담당자, 기록 수단 등의 부가 정보가 추가된다. 실무 프로젝트를 담당할 때는 로그를 작성하는 규범과 도구를 자세히 알아야 한다. 하지만 초보 프로그래머의 개인 프로젝트 수준에서는 print() 함수만 잘 사용해도 대부분의 문제를 해결하는 데 충분하다.

사용자에게 로그를 노출하지 않으려면

print() 함수로 로그를 출력하면, 일반 사용자에게도 로그가 노출된다는 점이 문제다. (사용자에게 로그를 보여주는 프로그램도 있지만) 로그는 기본적으로 프로그래머를 위한 것이다. 프로그래머에게는 노출해야 하지만 사용자에게는 노출하고 싶지 않은 정보가 있을 수 있다.

크게 두 가지 방법이 사용된다. 하나는 프로그램의 출력을 일반 출력과 로그 출력(오류 출력)으로 구별하여 사용자에게 일반 출력만을 노출하는 것이다. 다른 하나는 프로그램을 개발판과 발표판으로 구별하여 개발판에서만 로그의 출력을 활성화하는 것이다.

이를 실제로 적용하는 방법은 여러분이 차차 익혀나갈 과제로 남겨둔다.

9.2.3 테스트

테스트(test)란 다양한 상황에서 프로그램을 실행해보고 오류를 찾는 것이다. 오류가 간헐적으로 일어나는 경우, 테스트를 통해 오류가 어떤 조건에서 일어나는지를 파악할 수 있다. 또, 오류 메시지의 도움을 받을 수 없는 논리적 오류를 파악할 때도 이 방법이 필요하다. 하지만 프로그램 전체를 실행하는 것으로 오류를 파악하기란 말처럼 쉽지 않다. 프로그램은 여러 데이터와 함수가 복잡한 구조 속에서 유기적으로 동작하기 때문이다. 오류가 있다는 것은 알아도 그 오류가 어디서 왜 발생하는지 알기는 무척 어려울 것이다.

이럴 때 문제를 나누어 해결하는 방법이 빛을 발한다. 프로그램은 여러 함수로 이루어져 있고, 모든 함수가 올바르게 동작하다면, 전체 프로그램도 올바르게 동작할 것이다. 따라서 문제는 전체 프로그램 대신 개별 함수의 동작을 확인하는 것으로 경감된다. 함수의 동작은 전달된 인자에 달려 있으므로, 다양한 값을 인자로 전달해 테스트해보면 된다.

예를 들어, 코드 9-4의 잘못된 함수 is_odd()0, 1에 대해서는 잘 동작하지만 2를 입력하면 그렇지 않음을 확인할 수 있었다. 또 다른 예로, 월을 입력받아 대응하는 계절을 출력하는 다음 함수에서 오류가 무엇인지 찾아보자.

코드 9-9 잘못된 계절 함수

def season(month):
    """월(month)에 대응하는 계절을 반환한다."""
    if month < 3:
        return '겨울'
    if month < 6:
        return '봄'
    if month < 9:
        return '여름'
    if month < 12:
        return '가을'

코드를 읽는 것만으로 오류를 찾아내지 못했더라도, 함수의 처리 범위인 1월부터 12월까지를 모두 함수에 입력해보면 문제점을 바로 알 수 있다. 입력 월이 12월일 때 '겨울'이 아니라 None을 반환하므로, 논리적 오류다.

코드 9-10 함수의 입력 허용 범위 테스트하기

>>> for month in range(1, 13):
...     print(month, '월:', season(month))
... 
1 월: 겨울
2 월: 겨울
3 월: 봄
4 월: 봄
5 월: 봄
6 월: 여름
7 월: 여름
8 월: 여름
9 월: 가을
10 월: 가을
11 월: 가을
12 월: None

입력 데이터의 범위가 무제한일 때

앞에서 든 예는 함수가 입력받는 데이터의 범위가 열두 개에 불과하여, 입력할 수 있는 데이터를 모두 입력해보기가 쉬웠다. 하지만 abs(), len() 함수처럼, 입력받는 데이터의 범위가 무한한 것도 많다. 이런 함수는 모든 값을 다 입력해볼 수 없을 텐데, 어떻게 검사해야 할까?

예를 들어, 문자열을 입력받아 반반씩 나누는 함수를 생각해 보자.

코드 9-11 문자열을 반으로 나누는 함수

def half_and_half(s):
    """문자열 s를 입력받아, 반반으로 나누어 반환한다."""
    center = len(s) // 2  # 중간 위치
    return (s[:center], s[center:])

이 함수에 입력할 수 있는 문자열의 범위는 무한하므로, 모든 경우를 테스트할 수는 없다. 이런 경우에는 입력할 수 있는 데이터의 성격을 고려해야 한다. 함수가 입력받을 데이터에는 흔히 함수에 입력될 것으로 기대되는 데이터와 예외적인 데이터가 있다. 둘 다 테스트해보아야 하지만, 특히 예외적인 데이터를 빠트리지 않고 테스트하는 것이 중요하다.

이 함수의 경우, 일반적인 데이터는 어중간한 길이의 짝수로 된 문자열이 될 것이다. 일반적인 데이터에 대해 이 함수는 잘 동작한다.

코드 9-12 일반적인 데이터를 조건으로 테스트하기

>>> half_and_half('코드')
('코', '드')

>>> half_and_half('프로그램')
('프로', '그램')

>>> half_and_half('online')
('onl', 'ine')

하지만 예기치 못한 오류는 예외적인 데이터가 입력되었을 때 주로 발생한다. 이 함수의 경우에는 예외적인 데이터의 예로 빈 문자열, 홀수 길이 문자열을 들 수 있다.

코드 9-13 예외적인 데이터를 조건으로 테스트하기

>>> half_and_half('')        # 빈 문자열
('', '')

>>> half_and_half('?')       # 홀수 길이 문자열
('', '?')

>>> half_and_half('파이썬')  # 홀수 길이 문자열
('파', '이썬')

테스트를 함으로써 이 함수가 예외적인 데이터에 대해서도 실행 오류를 일으키지 않는다는 것을 확인했다. 물론, 실행 결과가 논리적으로 올바른지도 확인해야 할 것이다. 이는 함수의 목적과 용도에 따라 판단해야 할 텐데, 이 함수의 주석에는 빈 문자열이나 홀수 길이 문자열의 입력에 관한 설명이 없어 결과가 옳다 그르다 판단할 수 없다.

입력받는 데이터의 범위가 무제한적인 함수를 테스트할 때는 예외적인 입력 데이터에 주의를 기울여야 한다. 무엇이 예외적인 데이터인가는 함수의 논리와 데이터의 종류에 달려 있다. 몇 가지만 예를 들면, 일반적으로 문자열을 입력받는 함수는 빈 문자열, 매우 긴 문자열, 외국어, 특수문자, 홀수 또는 짝수 길이의 문자열에 주의해야 한다. 또, 정수를 입력받는 함수는 0, 음수, 매우 작거나 매우 큰 수, 0부터 세는가, 1부터 세는가 등에 신경을 써야 한다.

테스트 주도 개발

테스트는 오류를 발견할 때도 도움되지만, 여기서 한걸음 더 나아가면 함수를 작성하기 이전에 테스트를 먼저 작성하는 방법도 생각할 수 있다. 테스트를 먼저 작성한다는 것은 함수의 입력값과 출력값의 사례를 미리 만들어 두는 것을 의미한다. 그러면 오류를 미리 방지할 수 있고, 함수의 동작도 더 쉽게 파악할 수 있다.

이를 수행하는 가장 간단한 방법은 함수의 주석에 입력과 출력의 예를 작성해두는 것이다. 문자열과 문자를 입력받아, 문자가 문자열에 등장하는 빈도를 구하는 함수를 예로 들어 보자. 먼저, 함수를 작성하기 전에 이 함수가 처리할 데이터를 생각해 본다. 일반적인 데이터와 예외적인 데이터를 모두 고려해볼 필요가 있다.

  • ‘banana’, ‘a’: 0.5
  • ‘code’, ‘c’: 0.25
  • ‘파이썬’, ‘프’: 0.0
  • ‘파이썬’, ‘파이’: 오류. (두번째 값이 문자가 아닌 경우. 이 경우 None을 반환하기로 하자.)
  • ’’, ‘a’: 0 (0으로 나누는 문제가 생긴다. 이 경우 0 을 반환하기로 하자.)

그 후, 함수를 작성할 때 미리 작성해 둔 입출력 예를 주석에 포함시킨다. 함수의 본문을 작성할 때는 주석에 작성한 테스트를 염두에 두고 코드가 잘 동작할지 생각해본다. 테스트를 미리 작성한 덕분에 예상되는 오류를 피할 수 있다.

코드 9-14 함수의 주석에 테스트 제시하기

def frequency(s, c):
    """문자열 s와 문자 c를 입력받아, c가 s에 등장하는 빈도를 구한다.
    테스트:
        * 'banana', 'a'     => 0.5
        * 'code', 'c'       => 0.25
        * '파이썬', '프'    => 0
        * '파이썬', '파이'  => None
        * '', 'a'           => 0
    """
    if len(c) != 1:         # c가 문자가 아닌 경우
        return None
    
    if len(s) == 0:         # s가 빈 경우
        return 0
    
    count = s.count(c)      # c가 s에 등장하는 횟수
    return count / len(s)   # 빈도를 구해 반환

함수를 다 작성했으면 미리 작성한 테스트에 따라 실제로 테스트해 본다. 모두 올바른 결과가 나온다면 함수를 올바르게 작성한 것이다.

코드 9-15 작성한 함수 테스트하기

>>> frequency('banana', 'a') == 0.5
True

>>> frequency('code', 'c') == 0.25
True

>>> frequency('파이썬', '프') == 0
True

>>> frequency('파이썬', '파이') == None
True

>>> frequency('', 'a') == 0
True

테스트할 양이 많으면 아무래도 함수를 수정할 때마다 테스트하기가 번거롭다. 그래서 본격적인 프로젝트에서는 별도의 개발 도구를 활용해 테스트를 자동으로 수행하도록 하는 경우가 많다. 이는 입문서에서 다룰 내용은 아니다. 나중에 그런 도구를 이용하더라도 함수의 입력 샘플과 그에 대응하는 출력 샘픔을 미리 정의해둔다는 기본 개념은 변하지 않는다.

함수를 테스트하기 쉬운 것은 함수가 입력값에만 의존하고 외부 상태에는 독립적으로 수행되기 때문이다. 함수를 외부의 상황에 영향받도록 작성했다면 테스트하기가 까다로워진다. 따라서 그런 함수는 꼭 필요한 것만으로 최소화하는 것이 좋다.

연습문제

연습문제 9-2 함수 테스트하기

다음은 수 하나를 입력받아 그 수에 숫자 '3', '6', '9' 중 하나 이상이 있을 경우 '짝'을, 그렇지 않으면 입력받은 수에 대응하는 숫자 문자열을 반환하는 함수다.

def 삼육구(n):
    """n에 숫자 '3', '6', '9' 중 하나 이상이 있을 경우 '짝'을,
    그렇지 않으면 n에 대응하는 숫자를 반환한다."""
    characters = str(n)
    found_3 = characters.find('3') != -1
    found_6 = characters.find('6') != -1
    found_9 = characters.find('9') != -1
    if found_3 or found_6 or found_9:
        return '짝'
    else:
        return str(n)

이 함수가 올바르게 동작하는지 확인하려 한다. 열 개 이상의 입출력 쌍을 정의하고, 각각 함수에 대입하여 올바른 결과를 내는지 검사해 보아라.

9.2.4 선례 찾아보기

혼자서 아무리 머리를 싸매고 씨름해도 끝내 오류를 해결하지 못할 때도 있을 수 있다. 이럴 때는 다른 사람들의 도움을 받는 것도 한 방법이다. 주변의 동료 프로그래머와 상의할 수도 있곘지만, 인터넷을 활용하는 것도 좋다. 세계에는 수천만여 명의 프로그래머가 일하고 있다. 여러분이 겪는 오류는 이미 다른 누군가가 경험하였고 해결책까지 공개해 두었을 가능성이 크다. 인터넷 환경은 변하기 마련이지만, 이 책을 쓰는 시점을 기준으로 문제 해결에 가장 도움이 되는 웹사이트를 소개한다면 스택 오버플로(Stack Overflow)와 깃허브(GitHub)를 꼽을 수 있다.

  • 스택 오버플로(https://stackoverflow.com): 프로그래밍에서 발생하는 다양한 문제를 질문하고 답하는 웹사이트. 질문자와 답변자의 글을 다른 프로그래머가 수정할 수 있는 방식, 논쟁과 중재가 가능한 점, 많은 프로그래머들이 활동하고 있는 점 등이 특징이다. 1천만개 이상의 질문이 올라와 있으며, 그 중에는 질 높은 질문과 답변도 많다. 구글에서 프로그래밍 문제를 검색하면 상위 페이지로 랭크되는 경우가 많다.
  • 깃허브(https://github.com): 많은 프로그래머가 깃(Git)이라는 프로그램 이력 관리 도구를 사용하는데, 깃허브는 깃으로 관리하는 소스 코드의 사본을 인터넷 공간에 보관해주는 서비스 중 하나다. 수많은 라이브러리(다른 프로그램에 삽입하여 활용할 수 있도록 제작된 프로그램)가 깃허브에 업로드되어 있으며, 라이브러리의 문서, 오류 관련 정보, 토론 등도 함께 열람할 수 있다. 라이브러리 관련 문제가 발생한 경우 깃허브에서 검색하면 해결책을 구할 수 있는 경우가 많다.

이 웹사이트나 구글에서 오류 메시지를 검색어로 검색하면 해결 방법을 쉽게 찾을 수 있다. 다음은 구글에서 “ZeroDivisionError: division by zero”을 검색한 예다. 검색 결과에서 스택 오버플로의 질의가 최상위에 랭크되었다. 이 문서에는 오류의 예시와 해결 방법이 친절하게 서술되어 있다.

그림 9-2 구글에서 오류 메시지 검색

그림 9-2 구글에서 오류 메시지 검색

그림 9-3 스택 오버플로의 게시물

그림 9-3 스택 오버플로의 게시물

이 외에도 검색을 이용해 여러분이 사용하는 프로그래밍 언어와 라이브러리의 공식 자료를 쉽게 찾아 열람할 수 있다. 이들을 찾아 읽는 것도 문제 해결에 큰 도움이 될 수 있다.

인터넷에서 오류 해결 방법을 검색하는 것은 매우 유용하지만, 마지막 수단 정도로 생각하자. 인터넷의 정보는 검증되지 않은 것이며 틀릴 때도 많으며, 문제가 생길 때마다 검색부터 하는 습관을 들이면 문제 해결 능력을 발전시킬 기회도 빼앗긴다. 스스로 문제를 해결하려 노력하고, 책과 공식 문서를 통해 이론, 규약, 모범 사례를 학습하는 것이 장기적으로 더 좋은 오류 해결 방법이다.

이 절에서는 오류를 찾고 수정하는 기본 방법들을 알아보았다. 오류 메시지를 읽고, 로그를 남기고, 프로그램을 테스트하고, 선례를 찾아보는 방법으로 대부분의 오류를 해결할 수 있다. 추후에 이런 방법을 전문화한 디버그 도구와 테스트 도구들도 사용하게 될 것이다. 어떤 도구를 사용하더라도 여기서 배운 기본 원리는 동일하다.