0%

20. 디버깅

글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist 를 바탕으로 작성된 글임을 알려드립니다.

이 포스트는 Debugging를 한글로 요약 정리한 글입니다.

디버깅 (Debugging)

디버깅을 할 때, 보다 신속하게 오류를 추적하려면 여러 종류의 오류를 구분할 수 있어야 한다.

  • 소스 코드가 바이트 코드로 변환할 때 인터프리터가 ‘문법 오류(Syntax errors )’을 발견하여 프로그램 구조에 문제가 있음을 나타낸다. 예시로 함수 블록 끝에 end 키워드를 생햑하면 아래과 같은 메시지가 생성된다.
    ERROR: LoadError: syntax: incomplete: function requires end.

  • 프로그램이 실행되는 동안 문제가 발생하면 인터프리터가 ‘런타임 오류 (Runtime errors)’를 생성한다. 대부분의 런타임 오류 메시지에는 오류가 발생한 위치 및 실행중인 함수에 대한 정보가 초함된다. 예시로 무한재귀로 인해 결국 런타임 오류가 발생하는 경우를 볼 수 있다. ERROR: StackOverflowError.

  • 의미 오류(Semantic errors)는 오류 메시지를 생성하지 않고 실행되지만 프로그램이 올바르게 작동하지 않는 문제이다. 예로는 표현식이 예상한 순서대로 평가되지 않아서 잘못된 결과가 발생하는 등이 있다.

디버깅의 첫 번째 단계는 처리중인 오류의 종류를 파악하는 것이다. 아래의 글은 오류 타입 별로 구성되어 있지만, 사실 몇몇 기술들은 둘 이상의 오류를 가진 경우도 있다.

문법 오류 (Syntax Errors)

문법 오류는 대부분 쉽게 찾아서 수정할 수 있다. 하지만 가끔씩 오류 메시지는 도움을 주지 않는다. 대부분의 일반적인 메시지는 ERROR: LoadError: syntax: incomplete: premature end of input 이나 ERROR: LoadError: syntax: unexpected "=", neither of which으로 어느 것도 유익한 정보를 주지 않는다.

반면, 메시지는 프로그램에서 문제가 발생한 위치를 알려준다. 줄리아는 문제를 발견한 위치를 전달하는데, 때때로 그 위치는 명확하지 않다. 이런 상황에서 실제 오류 위치는 대부분 오류 메시지가 알려준 위치보다 앞서 있는 경우가 많다.

프로그램을 점진적으로 개발하는 경우 오류의 위치를 잘 알고 있어야 한다. 이를 위해 추가되는 코드는 아래로 붙이는 것이 좋다.

책에서 코드를 복사하는 경우 본인의 코드를 책의 코드와 매우 신중하게 비교해야 한다. 되도록이면 모든 글자들을 전부 확인하는 것이 좋다. 동시에 책이 잘못되었을 수 있으므로 문법 오류처럼 보이는 것이 있다면 한번 더 확인해야 한다.

가장 일반적인 문법 오류들을 피하는 방법은 다음과 같다.

  • 변수 이름에 줄리아 키워드를 사용하지 않기

  • for문, while문, if문, 함수 블록을 포함하여 모든 복합 명령문 끝에 end 키워드가 있는지 확인하기

  • 코드에 일치하는 인용 부호가 있는지 확인하기

  • 삼중 따옴표(triple quotes)가 있는 문자열인 경우 문자열을 올바르게 종료했는지 확인해야 한다. 종료되지 않은 문자열은 프로그램 끝에서 유효하지 않은 토큰 오류를 발생시키거나 다음 문자열이 나올 때까지 이후 프로그램을 문자열로 인식한다. 두 번째의 경우에는 오류 메시지도 생성되지 않는다.

  • 닫히지 않은 (,{,[ 등의 연산자들은 줄리아가 현재 명령문의 일부로 다음 행을 진행하도록 한다. 일반적으로 다음 행에서 오류가 발생한다.

  • 조건부 내부에서 ==대신에 =를 사용했는지 확인하기

  • 코드에 ASCII 외의 다른 문자가 있는 경우 줄리아가 일반적으로 비 ASCII로 처리하지만 문제가 발생할 수 있다. 웹 페이지나 다른 소스에서 텍스트를 붙여 넣을 때는 주의해야 한다.

아무것도 작동하지 않는다면, 아래로 넘어가자.

코드를 계속 변경하지만 변하는 것이 없습니다

REPL에 오류가 있다고 표시되지만 오류가 보이지 않는다면, REPL과 해당 개발자가 다른 코드를 보고 있을 가능성이 있다. 이럴 때는 프로그래밍 환경을 확인하여 편집중인 프로그램이 줄리아가 실행하려는 프로그램이 맞는지 확인해야 한다.

확실하지 않은 경우 프로그램을 시작할 때 명확하고 의도적인 구문 오류를 시도하고 다시 실행해보자. REPL에서 새 오류를 찾지 못하면 새 코드를 실행하지 않은 것이다.

몇 가지 주요 원인은 아래와 같다.

  • 파일을 편집하고 다시 실행하기 전에 변경 사항을 저장하는 것을 잊었다. 일부 프로그래밍 환경에서는 이 작업을 수행하지만 그렇지 않은 환경도 있다.

  • 파일 이름을 변경했지만 여전히 이전 이름을 사용하여 실행중이다.

  • 개발 환경의 무언가가 잘못 구성되었다.

  • 모듈을 작성하고 사용하는 경우, 표준 줄리아 모듈 중 하나와 이름을 동일하게 지정하면 안된다.

  • using을 사용하여 모듈을 가져오는 경우, 모듈에서 코드를 수정할 때 REPL을 다시 시작해야 한다. 모듈을 다시 가져오면 아무 작업도 수행되지 않는다.

문제가 발생하여 진행 상황을 파악할 수 없는 경우 한 가지 방법은 “Hello, world”와 같은 새로운 프로그램으로 다시 시작하는 것이다. 이후 원래 프로그램의 조각을 새 프로그램에 점차적으로 추가하면 된다.

런타임 오류 (Runtime Errors)

프로그램이 문법상 올바른 경우 줄리아는 적어도 프로그램을 읽고 실행할 수 있다. 그렇다면 무엇이 더 잘못될 수 있을까?

내 프로그램은 아무것도 하지 않습니다

이 문제는 함수와 클래스로 구성된 파일이 실행을 시작하기 위해 함수를 호출하지 않는 경우가 가장 일반적이다. 만약 클래스와 함수를 제공하기 위한 모듈만 계획한다면 이것은 아마 의도적일 수 있다.

의도하지 않은 경우, 프로그램에 함수 호출이 있는지 확인하고 실행 흐름에 도달했는지 확인하라

내 프로그램이 멈췄습니다

프로그램이 멈추고 아무 것도 하지 않는 것 같다면 “매달려(hang)” 있는 것이다. 그것은 무한 루프 또는 무한 재귀에 걸리는 것을 의미한다.

  • 문제가 있다고 생각되는 특정 루프가 있는 경우 루프 바로 시작점에 “루프 입력”, 종료점에 “루프 종료”라고 인쇄문을 추가하고 프로그램을 실행하라. 첫 번째 메시지만 받으면 무한 루프이며, 아래의 무한 루프 파트를 참고하라

  • 대부분의 경우 무한 재귀로 인해 프로그램이 잠시 실행된 다음 ERROR: LoadError: StackOverflowError 오류가 발생한다. 이런 경우 아래의 무한 재귀를 참고하라.

  • 이들 단계 중 어느 것도 작동하지 않는다면 다른 루프, 기타 재귀 함수 및 메서드 테스트를 시작하라.

  • 그래도 문제가 해결되지 않는다면 프로그램의 실행 흐름을 이해 못한 것일 가능성이 높다. 아래의 실행 흐름 파트를 확인하라.

무한 루프

무한 루프를 가지고 있다고 생각하고 어떤 루프가 문제인건지 안다면 해당 루프 끝에 조건의 변수 값과 조건 값을 인쇄하는 출력문을 추가해보자.

예를 들면 아래의 코드와 같다.

1
2
3
4
5
6
while x > 0 && y < 0
# do something to x
# do something to y
@debug "variables" x y
@debug "condition" x > 0 && y < 0
end

이제 프로그램을 디버그 모드로 작동하면, 루프가 작동할 때마다 변수 값과 상태를 볼 수 있다. 루프의 마지막 작동에서 상태는 false가 나와야 한다. 만약 루프가 계속 작동된다면 xy의 값을 볼 수 있으며, 왜 그 값들이 제대로 업데이트되지 않는지 찾을 수 있을 것이다.

무한 재귀

대부분의 경우, 무한 재귀는 프로그램이 잠시 실행되었다가 ERROR: LoadError: StackOverflowError와 같은 오류가 바로 발생한다.

만약 함수가 무한 재귀를 일으킨다고 의심되면 조건 코드를 확인해봐야 한다. 재귀 호출을 하지 않고 함수가 반환되도록 하는 조건이 있어야 한다. 그렇지 않은 경우 알고리즘을 다시 생각하고 조건 코드를 작성해야 한다.

조건이 명확함에도 프로그램이 멈추지 않는 경우에는 함수의 시작 부분에 매개 변수를 출력하는 출력문을 추가하자. 이후 프로그램을 실행하면 함수가 호출될 때마다 매개 변수 값이 출력된다. 조건문으로 이동하지 않는 이유에 대한 아이디어를 얻을 수도 있다.

실행 흐름

프로그램에서 실행 흐름이 어떻게 움직이는지 확실하지 않다면, “함수 시작 (함수 이름)” 이런 문구를 함수 시작 부분에 출력문으로 추가하자.

그러면 프로그램이 작동할 때, 각 함수들의 이름이 출력될 것이다.

프로그램을 실행할 때 예외가 발생합니다

런타임동안 무언가 잘못되었다면 줄리아는 예외의 이름과 위치를 포함한 메시지를 출력한다.

stacktrace는 현재 실행중인 함수부터 호출되었던 함수들을 계속 식별한다. 즉, 각 호출이 발생한 파일의 줄 번호를 포함하여 현재 위치로 오는 과정의 함수 호출 순서를 추적하는 것이다.

첫 번째 단계는 오류가 발생한 프로그램의 위치를 확인하고 발생한 문제를 파악할 수 있는지 확인하는 것이다. 다음은 가장 일반적인 런타임 오류 중 일부이다.

  • ArgumentError
    함수 호출에 대한 인수 중 하나가 잘못된 경우

  • BoundsError
    배열 인덱싱 작업이 범위를 벗어난 요소에 엑세스하려는 경우

  • DomainError
    함수 또는 생성자에 대한 인수가 유효한 도메인 외부에 있는 경우

  • DivideError
    분모 값이 0인 정수 나누기를 시도한 경우

  • EOFError
    파일이나 스트림에서 더 이상 읽을 데이터가 없는 경우

  • InexactError
    정확하게 데이터 타입으로 변환할 수 없는 경우

  • KeyError
    AbstractDict(Dict) 또는 Set 등 인덱싱 작업이 불가능한 객체에 인덱스를 통해 요소에 접근하거나 삭제하려는 경우

  • MethodError
    주어진 함수에 필요한 메서드가 존재하지 않은 경우

  • OutOfMemoryError
    시스템이 올바르게 처리하기에는 너무 많은 메모리를 할당한 조작인 경우

  • OverflowError
    표현식의 결과가 지정된 데이터 타입에 비해 너무 커서 랩어라운드(wraparound)를 발생시키는 경우

  • StackOverflowError
    함수 호출이 호툴 스택의 크기를 넘어선 경우 (일반적으로 호출이 무한번 반복될 때 발생한다.)

  • StringIndexError
    유효하지 않은 인덱스로 문자열에 접근하는 경우

  • SystemError
    오류 코드와 함께 시스템 호툴에 실패한 경우

  • TypeError
    데이터 타입이 이상하거나 또는 잘못된 데이터 타입을 인수로 가진 함수를 호출한 경우

  • UndefVarError
    현재 범위에서의 심볼(symbol)이 정의되어 있지 않은 경우

프린트문을 너무 많이 추가해서 결과가 넘칩니다

디버깅에 프린트문을 사용할 때 발생하는 문제 중 하나는 출력에 묻힐 수도 있다는 점이다. 해결 방안으로는 출력을 단순화하거나 프로그램을 단순화하는 두 가지의 방법이 있다.

출력을 단순화하기 위해 도움이 되지 않는 인쇄 설명을 제거하거나 주석 처리하거나 이해하기 쉽도록 출력을 형식화 할 수 있다.

프로그램을 단순화하기 위해 할 수 있는 몇 가지가 있다. 먼저 프로그램이 진행중인 문제를 축소하라. 예를 들어 목록을 검색하는 경우에는 작은 목록을 검색하는 것이다. 프로그램이 사용자로부터 입력을 받는 경우 문제를 일으키는 가장 간단한 입력을 제공하면 된다.

둘째, 프로그램을 정리하라. 데드 코드를 제거하고 가능한 쉽게 읽을 수 있도록 프로그램을 재구성하라. 예를 들어 프로그램의 중첩된 부분에 문제가 있다고 생각된다면 간단한 구조로 해당 부분을 다시 작성하면 된다. 큰 함수가 의심되는 경우에는 작은 함수로 분할하여 별도로 테스트하면 된다.

작은 크기의 테스트 과정에서 종종 버그를 발견한다. 어떤 상황에서는 프로그램이 작동하지만 다른 상황에서는 작동하지 않는다는 것을 알게 되면 무슨 일이 일어나고 있는지에 대한 실마리를 얻을 수 있다.

마찬가지로 코드를 다시 작성하면 미묘한 버그를 찾는 데 도움이 될 수 있다. 프로그램에 영향을 미치지 않아야 한다고 생각되는 변화를 만들고 작동하면 버그를 찾을 수도 있다.

의미 오류 (Semantic Errors)

어떤 면에서 의미 오류는 인터프리터가 오류에 대한 정보를 제공하지 않기 때문에 디버그하기 가장 어렵다. 오직 개발자만이 어떻게 프로그램이 진행되어야 하는지 알고 있다.

첫 번째 단계는 프로그램 텍스트와 현재 보고 있는 동작을 연결하는 것이다. 이를 위해서는 프로그램이 실제로 작동하고 있는 것에 대한 가설이 필요하다. 하지만 가설을 확인하기에는 프로그램 작동이 너무 빠르게 진행된다.

그래서 개발자들은 종종 프로그램을 인간과 같은 속도로 늦출 수 있기를 바란다. 잘 배치된 프린트문을 삽입하는 것이 디버거(debugger)를 셋팅하거나 중단 포인트를 삽입하는 것보다 더 빠르다.

내 프로그램이 작동하지 않습니다

아래와 같은 질문을 스스로에게 해야한다.

  • 프로그램이 제대로 작동하지 않는 것 같은가? 해당 기능을 수행하는 코드 섹션을 찾아 필요할 때 실행되는지 확인하라.

  • 일어나면 안되는 것들이 발생했는가? 프로그램에서 해당 기능을 수행한느 코드를 찾고 실행하지 않아야 할 때 실행 중인지 확인하라.

  • 코드 섹션이 예상과 다른 결과를 도출하는가? 특히 줄리아 모듈의 함수 및 메서드와 관련된 코드인 경우 문제의 코드를 이해해야 한다. 호출한 함수에 대한 설명서를 읽은 후 간단한 테스트 사례를 작성하고 결과를 확인하여 시험해보라.

프로그래밍을 하려면 프로그램 작동 방식에 대한 추상적 모델이 필요하다. 프로그램이 예상대로 되지 않는 이유 중에 종종 추상적 모델 자체가 잘못된 경우가 있다.

추상적 모델을 수저어하는 가장 좋은 방법은 프로그램을 구성요소로 나누고 각 구성요소를 독립적으로 테스트하는 것이다. 현실과 추상적 모델의 차이가 발견되면 문제를 해결할 수 있다.

물론 프로그램을 개발할 때 구성요소를 작성하고 테스트해야 한다. 문제가 발생하면 정확하지 않은 소량의 새 코드가 문제여야 한다.

크고 거친 표현식을 사용해서 기대한 대로 작동하지 않습니다

복잡한 표현식을 작성하는 것은 읽기 쉬우면 괜찮지만 디버깅하기 어려울 수 있다. 복잡한 표현을 임시 변수에 대한 할당으로 나누는 것이 더 좋을 수 있다.

예시는 아래와 같다.

1
addcard(game.hands[i], popcard(game.hands[findneighbor(game, i)]))

위의 코드를 아래와 같이 다시 작성할 수 있다.

1
2
3
neighbor = findneighbor(game, i)
pickedcard = popcard(game.hands[neighbor])
addcard(game.hands[i], pickedcard)

변수 이름은 추가적인 설명으로 제공되기 때문에 보다 쉽게 읽을 수 있으며, 중간 변수의 데이터 타입을 확인하고 해당 값을 표시할 수 있으므로 디버그하기가 더 쉽다.

큰 표현식으로 인해 발생할 수 있는 또 다른 문제는 평가 순서가 예상과 다를 수 있다는 점이다. 예를 들어 $\frac{x}{2π}$을 줄리아에서 작성하는 경우는 아래와 같다.

1
y = x / 2 * π

곱셈과 나눗셈은 같은 순서라서 왼쪽에서 오른쪽으로 평가되기 때문에 위의 코드는 정확하지 않다. 이 표현식은 $\frac{xπ}{2}$로 계산된다.

디버그하기 위한 좋은 표현식은 괄호를 추가해서 평가 순서를 정확하게 정해주는 것이다.

1
y = x / (2 * π)

만약에 평가 순서를 확신할 수 없다면, 괄호를 사용하라. 프로그램 작동이 정확할뿐만 아니라 작업 순서를 외우지 않은 다른 사람들도 더 읽기 쉽다.

함수는 있는데 기대했던 결과를 반환하지 않습니다

복잡한 식의 반환문이 있는 경우 반환하기 전에 결과를 볼 기회가 없다. 다시 말하면, 임시 변수를 사용할수 있다는 것이다. 예를 들면 아래와 같다.

1
return removematches(game.hands[i])

위의 코드를 아래와 같이 바꿀 수 있다.

1
2
count = removematches(game.hands[i])
return count

이제는 반환하기 전에 변수 count를 확인해볼 수 있다.

디버깅하다가 막혀서 도움이 필요합니다

먼저 몇 분정도 컴퓨터에서 벗어나라. 컴퓨터를 사용하다보면 아래와 같은 증상이 나타날 수 있다.

  • 좌절과 분노
  • 미신적 신념과 마법적 사고
  • 랜덤 워크 프로그래밍

이런 증상으로 고통받고 있다면 일어나서 산책을 하는 것도 좋다. 침착할 때 프로그램에 대해 생각하라. 어떻게 작동하는가? 오류 발생하기 직전의 수정은 무엇이었나? 등등

때로는 버그를 찾는 데 시간이 걸린다. 컴퓨터에서 멀리 떨어져 있을 때 종종 버그를 찾게 된다. 버그를 찾는 가장 좋은 장소는 기차, 샤워시설, 잠들기 직전 등이 있다.

진지하게 정말 도움이 필요합니다

최고의 프로그래머조차도 때때로 막힌다. 또한 프로그램에서 작업하면서 오류를 보지 못하는 경우도 종종 있다. 그래서 신선한 눈이 필요하다.

다른 사람을 데려 오기 전에 미리 준비하라. 프로그램은 가능한 단순해야 하며 오류를 발생시키는 가장 작은 입력에 대해 작업해야 한다. 적절한 장소에 프린트문이 있어야 하며, 간결하게 설명할 수 있을 정도로 문제를 잘 이해해야 한다.

도움을 받기 위해 누군가를 데려올 때 필요한 정보를 제공하라.

  • 오류 메시지가 있는 경우 오류 메시지는 무엇이며 어떤 부분을 나타내는가?

  • 이 오류가 발생하기 전에 마지막으로 수행한 작업은 무엇이며, 마지막 코드 줄은 무엇인가? 테스트한 결과는 어떠한가?

  • 지금까지 무엇을 시도했으며, 어떤 결과를 얻었나?

버그를 발견하면 잠시 시간을 내어 버그를 더 빨리 찾을 수 있는 방법에 대해 생각하라. 다음에 비슷한 버그를 발견하면 더 빨리 해결할 수 있다.

목표는 단지 프로그램을 작동시키는 것이 아니라는 점을 기억하라. 목표는 프로그램 작동 방법을 배우는 것이다.