들어가는 말


바로 아래  에 이어서 초보 개발자를 위한 올바른 디버깅 방법에 대해 이야기해볼까 합니다.

개인적으로 안타깝게 생각하는 부분은, 수 많은 정부 지원 학원에서 초급 개발자를 양산하면서도 개발자로서 가장 중요하고 기본이 될만한 소양들을 전혀 가르치지 않는다는 점입니다.

아마도 수박 겉핥기로 문법과 객체지향 개념 정도 가르치고 바로 스프링이나 마이바티스 같은 프레임워크를 응용하는 내용으로 넘어가는 것 같은데, 이런 과정을 거쳐 실무를 맡게 되는 개발자들이 문제가 발생했을 때 효율적으로 해결할 수 있기를 바라는 것은 어렵습니다.

개발자로 일하다 보면 디버깅을 위해 소비하는 시간은 생각 보다 많습니다. 그리고 디버깅의 접근 방식에 따라 단 30초면 원인을 좁힐 수 있는 문제를 쓸데없는 검색과 요행을 바라면서 이것 저것 고쳐보는 식의 대응으로 몇 시간씩 낭비하는 경우가 흔합니다.

그래서 저는 입문 단계의 개발자라면 프레임워크 사용법 등을 익히기 전에 반드시 올바른 디버깅 습관을 기르도록 노력할 것을 권해드리고 싶습니다.

본론으로 들어가서, 제가 생각하는 올바른 디버깅의 절차는 아래와 같습니다:

(재현 가능성 확보) -> 단서 수집 -> 단서 분석 -> 가설 수립 -> 가설 검증

이 때, 검증 과정은 전 단계에서 수립한 가설을 모두 소진하거나 문제가 해결될 때까지 반복하며, 전자의 경우 필요한 경우 전 단계로 돌아가서 잘못된 전제나 빠뜨린 가설이 있는지 확인해야합니다.

각 단계에 대한 자세한 설명은 아래와 같습니다.

바람직한 디버깅 절차


1. 재현 가능성 확보 (복잡한 이슈의 경우)


어쩌다가 한 번씩 발생하는 문제 만큼 원인을 찾기 어려운 문제도 드물 것입니다.

이 단계에서 가장 중요한 것은, 우선 최대한 곁가지를 덜어내고 오류의 핵심적 현상만을 분리하고, 가능하면 최소한의 오버헤드로 현상을 반복 재현할 수 있도록 하는 것입니다.

보통 디버깅 과정에서는 여러 차례 현상을 재현해보게되고, 해당 과정에 시간을 많이 소비할 수록 효율은 급격하게 떨어집니다.

단순한 오류의 경우라면 이 과정을 생략할 수 있지만, 복잡한 오류일수록 이 과정에 공을 들이는 것이 좋습니다. 어떤 경우에는 해당 오류를 재현하는 테스트 케이스를 작성하거나 임시 페이지 등을 만드는 것이 필요하기도 합니다.

2. 단서의 확보


디버깅을 시작하기 위해선 무엇보다 문제의 원인에 접근할 수 있도록 돕는 단서를 최대한 상세하게 많이 확보하는 것이 필요합니다.

이 단계에서 중요한 것은 내용을 빠뜨리지 않고 수집하는 것입니다.

실무를 하다보면, 탐캣 로그는 확인했지만 브라우저 콘솔은 보지 않았다던지, 아니면 초보들이 자주하는 실수대로 try-catch로 예외를 잡아서 스택트레이스를 보존하지 않고 커스텀 메시지만 뿌린다던지, 혹은 로그 레벨을 변경하지 않는 식으로 중요한 단서를 누락해서 디버깅에 애를 먹는 경우를 정말 흔하게 볼 수 있습니다.

올바른 방법으로 단서를 수집하는 것도 매우 중요합니다. 예컨대 메모리 에러가 발생가거나 성능 문제를 겪는다면 로그를 찍는 것 보다는 프로파일 데이터를 확보하는 것이 최우선이 되어야 합니다.

3. 단서의 분석


이 단계는 앞서 수집한 단서를 분석해서 현상의 일차적인 원인을 찾는 단계이며, 가장 흔한 과정은 스택 트레이스의 내용을 파악하는 작업입니다.

어쩌면 이 부분이 초보 개발자 입장에서 가장 쉽게 익힐 수 있으면서도 가장 중요한 내용이 될 것 같습니다.

스택 트레이스를 읽기 위해서는 무엇이 핵심 예외이고 무엇이 파생되는 예외인지 구분하는 것이 중요합니다. 보통 서버측 자바 프로그램은 복잡한 계층 위에서 구동되기 때문에 예외가 몇 겹으로 포장(wrap)되어 있는 경우가 흔합니다.

이 경우 로그에 스택트레이스가 제법 길게 기록되어 초보 개발자들이 지레 겁을 먹는 경우도 있는데, 'Caused By' 문구나 순서를 근거로 따라가면 실제로 중요한 내용은 단 한 줄인 경우가 많습니다.

두 번째로, 스택 트레이스 자체를 읽을 수 있는 능력이 필요합니다. 예외에 대한 스택 트레이스는 말 그대로 오류가 난 시점의 '호출 스택의 기록'입니다.

이를 관련 소스 코드를 띄워 놓고 순서대로 따라가면서 대입해보는 연습을 하는 것이 중요합니다. 보통 스택트레이스의 첫 번째 줄 이하의 스택은 예외에 대한 어떤 '맥락'으로 이해할 수 있습니다. 당연한 이야기지만 맥락을 먼저 이해하면 본론 역시 이해하기가 그만큼 수월해집니다.

이 단계에서 가장 중요한 것은 예외가 발생한 정확한 위치와 예외의 성격을 확인 하는 것입니다. 최소한 자신의 소스 코드 몇 번째 줄에서 어떤 예외가 발생했는지 확인 후, 해당 예외의 내용을 파악하는 것이 기본입니다.

내용을 파악하는 데는 우선 메시지를 읽어보고, 필요한 경우 해당 예외 클래스의 정의를 API 문서를 통해 확인하는 것이 좋습니다.

초보 개발자들이 NullPointerException을 단지 '널값이 들어가면 나는 오류'라고만 이해해서 헤매거나 NoClassDefError/NoSuchMethodError/ClassNotFoundException 등과 같이 내용만 알면 10초면 원인을 알 수 있는 단순한 오류로 몇 시간씩 고생하는 건 정말 흔하게 볼 수 있는 일입니다.

최소한 한 번 본 예외는 정확하게 무엇을 의미하는 지 API를 찾아보고 이해하는 습관이 무엇보다 중요합니다.

4. 가설의 수립


단순한 문제의 경우 이미 전 단계에서 해답이 나오는 것이 일반적이지만 때때로 직접적인 원인은 알 수 있지만 보다 근본적인 문제를 알 수 없는 경우도 있습니다.

예컨대 이 변수가 널 값이 되어 NullPointerException이 나는 것은 알겠는데 애초에 왜 널 값이 들어가는 지 모르겠다는 식의 경우인데, 이 때는 보다 체계적인 접근이 필요합니다.

제가 추천하는 방법은 전 단계에서 확인한 직접적인 현상을 일으킬 수 있는 가능성에 대한 모든 경우의 수를 정리하는 것입니다.

위의 예를 활용하면, 해당 변수가 널 값이 될 수 있는 논리적인 모든 경우의 수 - 즉, 조건에 따라 할당 문을 건너 뛸 수 있는 가능성, 예외가 발생해서 할당이 이루어지지 않았을 가능성 등등을 빠짐없이 찾아서 가능성이 높은 순으로 정렬하는 것입니다.

5. 가설의 검증


일단 가설을 수립했다면 각 가설을 검증해야 합니다. 코드를 자세히 살펴보면 해당 가설의 성립을 활용할 수 있는 중단점을 설정할 수 있는 포인트나 반증할 수 있는 조건들이 보입니다.

단순하게 말하자면, 만일 검증하려는 가설이 앞서 예에서 '이 조건문이 성립하면 해당 변수가 널값일 수 있는데, 정말 조건문이 성립했을까?'에 대한 답을 찾는 과정입니다.

단순한 경우라면 중단점으로 조건문 실행을 감시하거나 로그를 추가해볼 수도 있고, 아니면 해당 조건문이 성립했다면 반드시 설정되는 다른 변수라던지 화면상의 변화 등을 통해 반증해볼 수도 있습니다.

많은 개발자들이 디버깅을 위해 중단점과 로그를 사용하지만 이를 무작위로 적용하는 경우와 이렇게 경우의 수를 좁히기 위해 체계적으로 접근하는 과정에서 사용하는 것은 효율성에서 정말 큰 차이를 보입니다.

따라서, 그런 식으로 테스트를 할 때엔 반드시 내가 지금 어떤 가설을 검증하기 위해 작업을 하고 있는지 잊어버리지 않도록 노력하는 것이 중요합니다.

6. 반복


정상적인 경우라면 이전 단계를 통해 근본 원인을 찾을 수 있어야 하지만, 때로는 상정한 모든 경우의 수를 소진했음에도 여전히 문제를 해결할 수 없는 경우가 있습니다.

이 때는 전 단계로 돌아가서 빠뜨린 경우의 수가 있는지, 혹은 그러한 가설들을 추출하기 위한 전제 조건에 오류가 없는지 (예컨대 이 조건문이 성립하면 해당 변수가 널이라고 생각했는데 그렇지 않은 경우) 등을 꼼꼼하게 확인해보아야 합니다.

마치며


꽤나 장황하고 복잡하게 들릴 수도 있지만, 개인적으로는 이러한 접근이 디버깅을 가장 효율적으로 할 수 있는 최상의 방벙이라고 생각하고, 초보 개발자라면 특히 시간을 써서 익숙해지도록 연습해야 한다고 봅니다.

이러한 '분석-가설-검증'의 절차가 익숙해지면 이전에는 검색으로 몇 시간을 고생하던 문제들도 나중에는 잠깐 눈으로 훑어보는 것 만으로 바로 정확한 원인을 찾아 해결하는 단계까지 발전할 수 있습니다.

그런 단계로 발전하기 위해 절대로 피해야할 것은 뚜렷한 이유 없이 무작위로 코드를 고쳐보고 이전 상태로 돌려보거나 단지 검색만을 위해 트레이스를 활용하는 등의 습관입니다.

물론 최악의 경우 검색으로 힌트를 얻거나 일종의 '소거법'으로 하나하나 연관이 없는 부분을 제거하는 방식으로 문제를 찾아야 하는 때도 있습니다.

하지만 이는 어디까지나 예외적인 경우고, 일반적인 문제는 항상 체계적으로 접근해서 원인을 찾는 습관을 갖는 것이 중요합니다.

아무리 SI 환경에서 개발자의 실력이 크게 중요하지 않다고 하지만, 누군가 하루를 소비할 문제를 십 분만에 풀 수 있다면 그 만큼 야근을 덜 할 수 있고 스트레스도 적게 일을 할 수 있습니다.

디버깅을 할 때 어떤 접근 방법을 택하는 지는 실제로 과정없이 십 분과 하루만큼의 극단적인 효율성 차이를 나타낼 만큼 중요한 문제입니다.

그리고 그런 시간 절약, 혹은 낭비가 십 년 쯤 쌓이면 결국 그 차이가 십 년후 자신의 커리어가 닿을 수 있는 한계를 결정짓는 것이라고 봅니다.


출처 : http://okky.kr/article/272227

'Java' 카테고리의 다른 글

Eclipse 디버거 사용법  (0) 2015.11.24
자바 I/O란  (0) 2015.11.24
Exception Handling  (0) 2015.11.02
초보 개발자를 위한 디버깅 방법 소개  (0) 2015.10.27
스택트레이스(stack trace)  (0) 2015.10.27
Java 인자 전달 방식: Call-by-{Value | Reference}?  (0) 2015.01.29
Posted by 양형

댓글을 달아 주세요