디버깅 소개
소프트웨어 개발 과정에서 오류는 필연적으로 발생합니다. 이러한 오류를 찾아 수정하는 과정을 '디버깅(Debugging)'이라고 합니다. 프로그램 실행 중 오류가 발생하면, 파이썬 인터프리터는 흔히 다음과 같은 형태의 메시지를 출력합니다.
Traceback (most recent call last):
File "<stdin>", line 3, in <module>
result = perform_operation(10)
File "<stdin>", line 2, in perform_operation
return value + " is a string"
TypeError: unsupported operand type(s) for +: 'int' and 'str'
이것을 '트레이스백(Traceback)' 메시지라고 합니다. 이 메시지는 오류가 발생하기까지의 함수 호출 순서를 역순으로 보여주며, 가장 아래에 최신 호출이 위치합니다. 이 체인을 따라가며 문제의 원인을 파악할 수 있습니다.
트레이스백 메시지 이해하기
트레이스백의 각 줄은 정보를 짝으로 제공합니다. 첫 번째 줄은 다음과 같은 형식입니다:
File "<파일 이름>", line <줄 번호>, in <함수 이름>
이 줄은 다음 정보를 알려줍니다:
- 파일 이름: 오류를 포함하는 파일의 이름.
- 줄 번호: 문제가 발생했거나 다음 호출 함수가 포함된 코드 줄 번호.
- 함수 이름: 해당 줄에서 찾을 수 있는 함수의 이름.
이 짝에서 두 번째 줄(더 들여쓰기 됨)은 다음 함수가 호출되도록 만든 실제 코드 줄을 보여줍니다. 이를 통해 함수에 전달된 값과 사용된 컨텍스트를 빠르게 파악할 수 있습니다. 트레이스백은 항상 가장 최근에 발생한 오류와 그 오류가 발생하기까지의 호출 스택을 보여준다는 점을 기억하세요.
오류 메시지 분석
트레이스백 메시지의 마지막 줄은 '오류 구문(Error Statement)'입니다. 이 구문은 다음과 같은 형식입니다:
<오류 유형>: <오류 메시지>
이 줄은 두 가지 정보를 제공합니다:
- 오류 유형: 발생한 오류의 종류 (예:
SyntaxError,TypeError). 이 정보는 오류의 원인을 좁히는 데 매우 유용합니다. - 오류 메시지: 오류의 정확한 원인을 더 자세히 설명합니다. 오류 유형에 따라 다양한 메시지가 출력됩니다.
주요 디버깅 기법
문서 테스트 (Doctest) 활용
파이썬은 코드에 대한 테스트를 빠르게 작성하는 훌륭한 방법을 제공합니다. 이를 '문서 테스트(Doctest)'라고 하며, 다음과 같이 작성됩니다:
def calculate_rectangle_area(width, height):
"""
주어진 너비와 높이로 직사각형의 면적을 계산합니다.
>>> calculate_rectangle_area(5, 4)
20
>>> calculate_rectangle_area(10, 2)
20
>>> calculate_rectangle_area(0, 7)
0
"""
return width * height
문서 문자열(docstring) 내에서 파이썬 인터프리터 출력처럼 보이는 줄이 문서 테스트입니다. 이를 실행하려면 터미널에서 다음 명령어를 입력합니다:
python3 -m doctest your_file.py
이 명령은 your_file.py를 파이썬 인터프리터에 로드하고 각 doctest 입력이 지정된 출력을 생성하는지 확인합니다. 만약 불일치가 발생하면, 어떤 테스트가 실패했는지 알려주는 메시지가 표시됩니다. -v (verbose) 옵션을 사용하면 통과한 테스트도 함께 확인할 수 있습니다:
python3 -m doctest your_file.py -v
초기에 테스트를 작성하는 데 시간을 투자하는 것은 나중에 많은 디버깅 시간을 절약할 수 있습니다.
자체 테스트 작성
문서 테스트 외에도 독립적인 테스트 함수를 작성할 수 있습니다. 다음은 테스트 작성에 대한 몇 가지 조언입니다:
- 코드 작성 전 테스트 작성: '테스트 주도 개발(TDD)'이라고 불리는 이 방법은 함수가 어떻게 동작해야 하는지에 대한 기대를 먼저 작성하여 실제 코드 작성의 가이드라인을 제공합니다.
- 코드 작성 후 추가 테스트 작성: 초기 테스트를 통과한 후에는 극단적인 경우(edge cases)나 특이한 상황을 고려하여 더 많은 테스트를 작성합니다.
- 예외적인 상황 테스트: 코드가 모든 특수 케이스에서 올바르게 작동하는지 확인하세요.
print() 문 활용
테스트를 통해 오류 위치를 파악했다면, 무엇이 잘못되었는지 알아내야 합니다. 트레이스백 메시지가 있다면 오류 유형을 통해 검색 범위를 좁히는 데 도움을 받으세요. 프로그래밍을 처음 배울 때 코드의 버그를 찾는 것은 어려울 수 있습니다. 일반적인 방법 중 하나는 print() 문을 추가하는 것입니다.
예를 들어, 다음 함수가 계속해서 예상치 못한 결과를 반환한다고 가정해 봅시다:
def calculate_final_value(initial_data):
intermediate_result = process_data(initial_data)
final_output = transform_result(intermediate_result)
return final_output / 2
process_data가 반환하는 값을 확인하기 위해 print() 문을 추가할 수 있습니다:
def calculate_final_value(initial_data):
intermediate_result = process_data(initial_data)
print(f"DEBUG: intermediate_result의 값: {intermediate_result}") # 디버그 메시지 추가
final_output = transform_result(intermediate_result)
return final_output / 2
만약 intermediate_result가 예상과 다르다면, process_data 함수 내부를 조사합니다. 그렇지 않다면, transform_result가 반환하는 값을 확인하기 위해 print() 문을 추가해야 할 수도 있습니다.
print() 문 사용 시 몇 가지 조언:
- 변수 값만 출력하지 말고, 어떤 변수의 값인지 명확한 메시지를 함께 출력하세요. (예:
print(f"DEBUG: 현재 루프 카운터: {i}")). - 함수 호출 후 그 결과를 확인하기 위해
print()문을 사용하세요. - 반복문의 끝에
print()문을 사용하여 각 반복 후 카운터 변수의 상태를 확인하세요.
영구적인 디버깅 코드 관리
일반적으로 오류를 해결한 후에는 모든 print() 디버깅 문을 제거해야 합니다. 그러나 파일 테스트 시 정기적으로 디버깅 코드를 유지하고 싶을 때도 있습니다. 이때는 전역 변수를 활용할 수 있습니다:
DEBUG_ENABLED = False # 전역 디버그 플래그
def process_complex_data(data_list):
processed_count = 0
for item in data_list:
# 데이터 처리 로직
processed_count += 1
if DEBUG_ENABLED:
print(f"디버그: 항목 '{item}' 처리 완료. 현재 개수: {processed_count}")
return processed_count
이제 디버깅이 필요할 때 DEBUG_ENABLED를 True로 설정하고, 필요 없을 때는 False로 설정하면 됩니다.
인터랙티브 디버깅
많은 개발자들은 파이썬 REPL(Read-Eval-Print Loop)을 사용하여 코드를 탐색합니다. 터미널에서 직접 함수를 실행하고 출력을 확인할 수 있습니다. 일반적으로 다음 명령어로 파일을 로드하여 파이썬 세션을 시작합니다:
python -i your_file.py
이렇게 하면 your_file.py의 모든 정의가 실행된 파이썬 세션에 접속하여 코드의 특정 부분을 대화식으로 테스트해 볼 수 있습니다.
assert 문 사용
파이썬의 assert 문은 조건이 참인지 테스트하고, 거짓일 경우 한 줄로 오류를 출력하는 기능입니다. 특정 지점에서 특정 조건이 반드시 참이어야 한다고 확신할 때 매우 유용합니다.
예를 들어, 가격과 할인율을 받아 최종 가격을 계산하는 함수를 작성한다고 가정해 봅시다. 할인율은 0과 1 사이여야 합니다. 이때 assert를 사용할 수 있습니다:
def calculate_discount_price(original_price, discount_rate):
assert 0 <= discount_rate <= 1, "할인율은 0과 1 사이의 값이어야 합니다."
final_price = original_price * (1 - discount_rate)
return final_price
assert 문의 주요 장점은 단순히 디버깅 도구가 아니라 코드에 영구적으로 남겨둘 수 있다는 점입니다. 소프트웨어 개발의 핵심 원칙 중 하나는 "코드가 잘못된 결과를 생성하는 것보다 충돌하는 것이 낫다"는 것입니다. assert를 사용하면 오류 발생 시 코드가 더 분명하게 실패하도록 만들 수 있습니다.
일반적인 오류 유형
SyntaxError
- 원인: 코드의 구문이 잘못되었을 때 발생합니다.
- 예시:
File "<stdin>", line 1 def greet_user(name) ^ SyntaxError: invalid syntax - 해결책:
^기호가 잘못된 구문을 가리킵니다. 오류 메시지는 무엇이 잘못되었는지 정확히 알려주지는 않지만, 어디에서 잘못되었는지는 알려줍니다. 파이썬은 실행 전에 모든 코드의 구문을 검사하므로, 다른 런타임 오류와는 다르게 실행 전에 발생합니다.
IndentationError
- 원인: 파이썬의 코드 블록 들여쓰기가 올바르지 않을 때 발생합니다. 파이썬은 들여쓰기에 매우 민감합니다.
- 예시:
File "<stdin>", line 2 print("잘못된 들여쓰기") IndentationError: expected an indented block - 해결책: 잘못 들여쓰기 된 줄을 수정하여 올바른 들여쓰기 규칙을 따르도록 합니다. 탭과 공백을 혼용할 경우에도 이 오류가 발생할 수 있으니, 일관된 공백 사용을 권장합니다.
TypeError
- 원인 1: 기본 연산자의 피연산자 유형이 호환되지 않을 때 (예: 숫자와 문자열을 직접 더할 때).
- 예시:
TypeError: unsupported operand type(s) for +: 'int' and 'str' - 원인 2: 함수가 아닌 객체를 함수처럼 호출하려고 할 때.
- 예시:
>>> my_variable = 10 >>> my_variable() TypeError: 'int' object is not callable - 원인 3: 함수에 전달된 인수의 개수가 올바르지 않을 때.
- 예시:
>>> def add_numbers(a, b): return a + b >>> add_numbers(5) TypeError: add_numbers() missing 1 required positional argument: 'b'
NameError
- 원인: 변수나 함수가 정의되지 않았거나 유효한 범위 내에 없을 때 발생합니다.
- 예시:
File "<stdin>", line 1 result = undefined_variable + 5 NameError: name 'undefined_variable' is not defined - 해결책: 변수를 사용하기 전에 반드시 초기화(값을 할당)했는지 확인하세요. 파이썬은 지역 스코프부터 시작하여 전역 스코프까지 변수를 검색하며, 찾지 못하면 이 오류를 발생시킵니다.
IndexError
- 원인: 시퀀스(예: 리스트, 튜플, 문자열)의 범위를 벗어난 인덱스로 접근하려고 할 때 발생합니다.
- 예시:
File "<stdin>", line 1 my_list = [1, 2, 3] print(my_list[3]) IndexError: list index out of range - 해결책: 인덱스가 시퀀스의 유효한 범위(0부터 길이-1까지) 내에 있는지 확인하세요.
흔히 발생하는 실수
오타 (Spelling)
파이썬은 대소문자를 구분합니다. 이는 종종 NameError로 나타나지만, 때로는 오타가 난 변수가 실제로 정의되지 않은 것처럼 보여 찾기 어려울 수 있습니다. (예: user_name 대신 userName으로 입력).
괄호 누락
닫는 괄호(), ], })를 빠뜨리는 것은 흔한 실수입니다. 이는 종종 SyntaxError로 나타나지만, 파이썬이 오류를 보고하는 줄은 실제 괄호가 누락된 줄의 다음 줄인 경우가 많습니다.
def complex_calculation():
value = (10 + 5 * (3 - 1) # 닫는 괄호 하나 누락
return value
이 경우 파이썬은 complex_calculation() 호출 줄이나 그 이후에 SyntaxError를 보고할 수 있습니다. SyntaxError가 코드 상으로는 문제가 없어 보이는 줄을 가리킨다면, 괄호 누락을 의심해볼 수 있습니다.
따옴표 누락
닫는 따옴표(' 또는 ")를 빠뜨리는 것은 다음과 같은 SyntaxError를 발생시킵니다:
File "<stdin>", line 1
message = "Hello, world! # 닫는 따옴표 누락
^
SyntaxError: EOL while scanning string literal
EOL은 "End Of Line"을 의미하며, 문자열 리터럴을 스캔하다가 줄의 끝에 도달했음을 나타냅니다.
=와 == 혼동
단일 등호(=)는 변수에 값을 '할당'하는 데 사용되며, 이중 등호(==)는 두 값이 '같은지' 비교하는 데 사용됩니다. 가장 흔한 실수는 조건문에서 할당 연산자를 사용하는 것입니다:
if user_status = "active": # 할당 연산자 사용 (잘못됨)
print("사용자 활성화됨")
올바른 코드는 다음과 같습니다:
if user_status == "active": # 비교 연산자 사용 (올바름)
print("사용자 활성화됨")
무한 루프
무한 루프는 루프의 종료 조건이 절대로 변경되지 않을 때 발생합니다. 예를 들어:
counter = 0
while counter < 5:
print(counter)
# counter 증가시키는 부분이 누락됨
이 경우 counter는 항상 0이므로 루프는 영원히 계속됩니다. 때로는 잘못된 카운터를 증가시켜 무한 루프가 발생하기도 합니다.
Off-by-one 오류 (하나 차이 오류)
반복문이나 재귀 함수가 한 번 덜 또는 더 실행될 때 발생합니다. 예를 들어, 리스트의 길이를 기준으로 루프를 돌릴 때 인덱스 범위를 잘못 설정하는 경우입니다:
my_data = [10, 20, 30]
for i in range(len(my_data)): # i는 0, 1, 2
# 마지막 반복에서 my_data[i+1]에 접근하면 IndexError 발생
print(my_data[i] + my_data[i+1])
이런 오류가 발생하면 루프가 시작하거나 멈추는 지점의 숫자를 면밀히 검토해야 합니다.
개발자의 핵심 역량: 문제 해결 방법론
문제 해결 능력은 개발자에게 가장 중요한 '메타 스킬'입니다. 우리는 크고 작은 문제에 끊임없이 직면하지만, 문제에 접근하는 방식이 체계적이지 않을 때가 많습니다.
체계적인 방법 없이 문제를 해결하려고 하면, 저처럼 코딩 초보 시절에 다음과 같은 과정을 겪게 될 것입니다:
- 하나의 해결책을 시도한다.
- 작동하지 않으면 다른 해결책을 시도한다.
- 여전히 작동하지 않으면 해결될 때까지 2단계를 반복한다.
운이 좋으면 이렇게 문제가 해결될 수도 있지만, 이는 가장 비효율적인 문제 해결 방법이며 엄청난 시간 낭비입니다. 가장 좋은 문제 해결 방법은 다음과 같습니다:
- 프레임워크를 갖춘다.
- 프레임워크를 실천한다.
Hacker Rank의 '2018 개발자 기술 보고서'에 따르면, 거의 모든 고용주가 문제 해결 능력을 최우선으로 꼽습니다. 이는 단순히 프로그래밍 언어 숙련도나 디버깅 능력, 시스템 설계 능력 이상으로 중요한 자격 요건으로 간주됩니다. 복잡한 문제를 분해하는 컴퓨팅 사고력을 보여주는 것은 기본 기술만큼이나 가치 있습니다.
"초보 프로그래머가 저지르는 가장 큰 실수는 문법 학습에 집중하고 문제 해결 방법을 배우지 않는 것입니다."
새로운 문제에 직면했을 때 해야 할 일
다음은 문제 해결을 위한 단계입니다:
1. 문제 이해 (Understand)
문제를 정확히 이해하는 것이 중요합니다. 많은 어려운 문제들이 어려운 이유는 우리가 그것을 제대로 이해하지 못하기 때문입니다. 문제가 무엇인지 어떻게 판단할까요? 바로 당신의 말로 설명할 수 있을 때입니다.
어려운 문제에 갇혀 있다가 다른 사람에게 문제를 설명하기 시작하자마자 이전에 놓쳤던 논리적 결함을 발견한 경험이 있으신가요? 많은 프로그래머들이 이 감정을 알고 있습니다.
따라서 문제에 직면하면, 문제를 기록하거나, 그림을 그리거나, 다른 사람에게 설명해보세요.
"만약 당신이 어떤 것을 간단한 단어로 다른 사람에게 설명할 수 없다면, 당신은 그것을 아직 충분히 이해하지 못한 것이다." – 리처드 파인만
2. 계획 수립 (Plan)
계획 없이 문제 해결에 뛰어들지 마세요. 무작정 시도하다 보면 우연히 해결될 수도 있지만, 장기적으로는 비효율적입니다. 프로그래밍의 맥락에서 이는 바로 코딩을 시작하지 않는 것을 의미합니다. 문제를 분석하고 관련 정보를 처리할 시간이 필요합니다.
훌륭한 계획을 세우려면 다음 질문에 답해보세요:
"입력 X가 주어졌을 때, 출력 Y를 얻기 위해 어떤 필수 단계들을 거쳐야 하는가?"
팁: 주석(comments)은 계획을 기록하는 훌륭한 도구입니다!
3. 분할 정복 (Divide and Conquer)
이것은 가장 중요한 단계입니다. 크고 복잡한 문제를 한 번에 해결하려고 하지 마세요. 그렇게 하면 지치기 쉽습니다.
대신, 큰 문제를 더 작고 해결하기 쉬운 여러 하위 문제로 나누세요. 그리고 가장 쉬운 하위 문제부터 하나씩 해결해나가세요. 가장 쉬운 하위 문제란, 다른 하위 문제 해결에 의존하지 않고 즉시 답을 얻거나 쉽게 답을 얻을 수 있는 문제를 의미합니다.
모든 하위 문제를 해결한 후에는, 그것들을 합쳐서 원래 문제의 해결책을 완성합니다. 이 단계는 문제 해결의 핵심이므로 반드시 기억하세요.
"만약 내가 모든 초보자에게 단 하나의 문제 해결 기술을 가르칠 수 있다면, 그것은 문제를 단순화하는 방법일 것입니다."
예를 들어, 초보 개발자로서 10개의 숫자 입력에서 세 번째로 큰 숫자를 찾는 프로그램을 작성하라는 요청을 받았다고 가정해 봅시다. 이 문제는 기본 문법 지식만 필요하지만, 완전한 초보자에게는 벅찬 작업일 수 있습니다.
막혔다면 문제를 단순화하는 방법을 생각해보세요. 세 번째로 큰 숫자를 찾는 것이 목표라면, 먼저 가장 큰 숫자를 찾는 방법이나 두 숫자를 비교하는 방법부터 시작할 수 있습니다.
"문제를 해결할 수 있는 수준까지 단순화하고 그에 대한 해결책을 작성하세요. 그리고 문제를 약간 확장하여 다시 해결책을 작성하세요. 이 과정을 원래 문제로 돌아올 때까지 반복하세요."
4. 막혔을 때 (Stuck?)
혹시 지금 "이봐, 리차드... 이거 멋지긴 한데, 하위 문제조차 해결할 수 없을 때 어떻게 해야 해?"라고 생각하고 계신가요?
첫째, 심호흡하세요. 둘째, 타당한 질문입니다. 친구여, 걱정 마세요. 이건 누구에게나 일어나는 일입니다! 하지만 최고의 개발자/문제 해결자는 오류에 대한 호기심이 좌절감보다 더 큽니다.
실제로 혼란스러울 때 시도할 수 있는 세 가지가 있습니다:
- 디버깅 (Debugging): 해결책을 단계별로 추적하여 어디서 잘못되었는지 찾습니다. 프로그래머들은 이것을 '디버깅'이라고 부릅니다.
- 재평가 (Re-evaluate): 한 발 물러서서 문제를 다른 관점에서 보세요. 더 일반적인 접근 방식으로 추상화할 수 있는 것이 있을까요? 때로는 문제의 세부 사항에 너무 몰두하여 더 넓은 차원에서 문제를 해결할 일반 원칙을 간과할 때가 있습니다. (예: 1부터 n까지의 정수 합을 n(n+1)/2로 푸는 가우스의 일화). 재평가하는 또 다른 방법은 처음부터 다시 시작하는 것입니다. 모든 것을 지우고 다시 시작하세요. 놀랍도록 효과적입니다.
- 연구 (Research): 구글 검색은 좋은 친구입니다. 당신이 겪는 어떤 문제든 누군가는 이미 해결했을 가능성이 높습니다. 그 사람이나 해결책을 찾아보세요. 심지어 문제를 해결한 후에도 이렇게 하세요! 다른 사람의 해결책에서 많은 것을 배울 수 있습니다.
경고: 큰 문제에 대한 해결책을 찾지 마세요. 오직 하위 문제에 대한 해결책만 찾으세요. 왜냐하면 당신이 스스로 조금이라도 씨름하지 않으면 아무것도 배우지 못할 것이기 때문입니다. 배우지 못한다면 시간을 낭비하는 것입니다.
실천 (Practice)
일주일 만에 훌륭해질 것이라고 기대하지 마세요. 좋은 문제 해결사가 되고 싶다면 수많은 문제를 해결해야 합니다!
연습하고, 또 연습하세요. '이 개념을 사용하면 이 문제를 쉽게 해결할 수 있겠네.'라고 깨닫는 것은 시간 문제입니다.
어떻게 연습하나요? 수많은 선택지가 있습니다! 체스, 수학 문제, 스도쿠, 바둑, 보드게임, 비디오 게임 등. 사실, 성공한 사람들에게 흔한 습관 중 하나는 '미시적인 문제 해결'을 연습하는 것입니다. 이는 매일 작은 문제들을 해결하는 습관을 의미합니다.
코딩 분야에서는 Coderbyte와 같은 플랫폼에서 매일 한 문제 이상을 풀어보는 것이 좋습니다. 모든 문제는 유사한 패턴을 가지고 있습니다.
"하나의 장애물을 성공적으로 극복했다고 생각하는 순간, 또 다른 장애물이 나타날 것입니다. 그러나 이것이 삶을 흥미롭게 만드는 이유입니다. 삶은 이러한 장애물을 극복하는 과정이며, 우리가 뚫고 나가야 할 일련의 방어선입니다. 매번 당신은 무언가를 배울 것입니다. 매번 당신은 힘, 지혜, 그리고 선견지명을 얻을 것입니다. 매번, 더 많은 방해꾼들이 사라지고 당신만 남을 것입니다: 최고의 당신." – 라이언 홀리데이