ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 10. 버그 사냥하기
    Technique/Programmer 2017. 2. 8. 00:59
    반응형

    로그래머가 코드를 작성한다. 하지만 프로그래머는 완벽하지 않고, 프로그래머의 코드 역시 완벽하지 않다. 

    즉 처음부터 완벽할 수 없기 때문에 버그기 생긴다. 만약 더 나은 프로그래머가 나타난다면 그 만큼 더 나은 버그를 키워낼 뿐이다.

    대부분의 버그는 많은 시간을 할애하여 추적하는 과정에서 머리카락이 듬성듬성 빠져버릴 정도로 끔찍하고 미묘한 문제거리가 된다. 그러한 버크들은 이상하고 놀라운 상호 작용이나 예상치 못한 알고리즘의 결과물 로서, 매우 간단해 보이는 소프트웨어의 비결정론적 형태라 할 수 있다.

    버그를 피하지 말자. 어차피 많은 디버깅을 하게 될 것이다. 점차 그에 익숙해질 것이고 또한 잘 해낼 것이다!


    1. 경제적 우려


    단지 개발자의 임금에 국한된 문제가 아니다. 버그가 많은 소프트웨어로 인해 비롯될 결과를 생각해보라. 출시 지연, 프로젝트 취소, 불안정한 소프트웨어의 인지도 하락. 출시한 소프트웨어의 버그 수정에 소모되는 비용 등이 발생할 것이다.



    2. 대비책


    버그가 생긴 뒤에 고치기 보다는 처음부터 버그가 생기지 않도록 적극적으로 예방하는 편이 훨씬 낫다. 유비무환 이다.

    디버깅에 천문학적인 비용이 소요된다면, 애초에 버그가 나타나지 않도록 하여 비용을 경감하는 쪽으로 방향을 잡아야 한다.


    항상 적절한 공학적 방법론을 채택 함으로서 불쾌하게 경악할 가능성을 최소화하는 것만으로도 충분하다.

    섬세한 설계, 코드 검토, 페어 프로그래밍, 그리고 심사숙고한 테스트 전략은 가장 중요한 방법론들이다. assert, 방어 프로그래밍, 코드 커버리지 도구와 같은 기법으로 오류를 놓칠 가능성을 최소화할 수 있을 것이다.


    버그를 피할 수 있는 가장 좋은 충고는 믿기 힘들 정도로 영리한 코드를 만들지 말라는 것이다.

    - 브라이언 커니핸

    " 디버깅이 코드 작성보다 두 배는 힘들다. 가능한 한도 내에서 최대로 영리한 코드를 작성할 경우 정의에 따르면 디버깅하기 위해서는 2배로 영리해야 한다. 그러니 그 코드를 디버그 할 만큼 똑똑 할 수는 없다. "

    - 마틴 파울러

    " 미련한 프로그래머는 컴퓨터가 이해할 수 있는 코드를 만들고, 좋은 프로그래머는 사람이 이해할 수 있는 코드를 만든다 "



    3. 버그 잡기


    코드 작성법과 상관없이, 유해한 버그중 일부는 항상 방어막을 뚫고 들어온다. 

    버그를 찾아 수정하는 것은 논리 퍼즐 풀기와 같다. 일반적으로 프로그램은 체계적으로 접근하기 그리 어렵지 않다. 버그의 대부분은 쉽게 찾아내고 금방 수정할 수 있다. 그러나 어떤 것들은 끔찍하며 오랜 시간을 잡아먹는다. 이런 버그의 갯수는 손에 꼽을 정도지만 오랜 시간을 소비하게 만든다.

    버그가 얼마나 고치기 어려운지를 나타내는 지표는 다음 두가지와 같다.

    • - 재현 가능성이 얼마나 되는가, 즉 항상 재현 가능한가. 재현하기 쉬운가 등
    • - 버그의 원인, 즉 나쁜 코드나 코드 통합 시의 잘못된 가설과 같은 소프트웨어 결함 이 코드에 포함된 시점과 이를 발견한 시점 사이의 간격이 얼마나 되는가? 가장 중요한 것은 버그를 체계적으로 조사하고 특징을 잡아내는 것이다. 그러기 위해서는 최고의 원료가 필요하다.
    • - 버그 재현 과정을 가능한 한 가장 단순하게 줄이라. 이는 필수 요소이다. 산만하기도 하고 도움이 되지 않는 모든 관련 없는 항목을 걸러내자.
    • - 단 하나의 문제에 집중할 수 있도록 한다. 서로 관련 된 두 버그를 하나로 착각한 채로 이를 깨닫지 못할 경우 코드는 금세 뒤죽박죽이 될 수 있다.
    • - 해당 문제를 얼마나 반복할 수 있는지 알아보자 재현 과정을 거쳐쓸 때 문제가 얼마나 자주 재현 되는가? 그것은 간단한 일련의 행위에 의존하는가? 소프트웨어 설정이나 실행된 기계 타입에 따라 발생하는가? 혹은 연결되는 주변 장치에 따라 차이가 있는가? 이 모든 것은 조사 작업의 중요한 요소다


    3-1. 함정 파기

    시스템 내의 불변 요소들을 검증하기 위한 assert나 테스트 코드를 추가하라. 진단용 출력 코드를 추가하여 코드의 상태를 살피면, 테스트가 어떻게 진행되고 있는지 확인할 수 있따. 이렇게 하면 코드를 더 잘 이해할 수 있게 되고, 해당 구조를 더 많이 추론할 수 있으며, 더 많은 assert를 추가하여 가정을 입증할 수 있다.

    진단 로그와 assert는 문제를 발견하고 해결한 이후 코드를 남겨놓는 확실한 방법이다. 다만 쓸모 없는 로그로 코드를 어질러놓지 않도록 주의해야 한다. 지금 무엇이 진행중인지를 가리고, 불필요한 디버깅 노이즈를 유발할 수 있기 때문이다.


    3-2. 이진 탐색 배우기

    코드를 한 줄씩 단순히 따라가기 보다는 일련의 사건의 시작과 끝을 확인하라. 그런 다음 문제공간을 두 개로 나누고, 가운데 시점에서 코드가 괜찮은지 확인하라. 이 정보를 기초로 문제 영역을 절반으로 좁힐 수 있다. 이 과정을 반복하다 보면 곧 문제가 되는 부분에 도달할 수 있다.


    3-3. 소프트웨어 고고학을 채택하라

    과거 이력을 바탕으로 버전 관리 시스템을 뒤져보는 방법에 대해 설명한다. 이는 문제 발견을 위한 완벽한 경로를 얻는 데 도움을 줄 수 있고, 종종 버그를 잡기 위한 매우 간단한 방법이 되기도 한다.

    문제가 되는 코드 변경을 찾아낼 수만 있다면, 그 실패의 원인은 대개 명백해지고 수정 방법도 자명해진다 ( 이것이 한꺼번에 많은 범위를 커밋하기보다는, 작고 빈번하게 극미한 체크인을 생성해야 하는 이유다. )


    3-4. 테스트하고 테스트하고 테스트하자

    소프트웨어를 개발했다면 단위 테스트를 작성하는 데 시간을 투자하자. 이는 처음에 작성한 코드를 개선하고 검증해나가는 데 도움이 될 분만 아니라 향후 변경에 대비한 훌륭한 조기 경보 장치로서의 역할을 해줄 것이다. 

    테스트를 통해 문제를 찾아내기가 더 복잡해지고, 수정에 많은 비용이 소모되기 훨씬 이전에 문제를 발견할 수 있다.


    이 테스트는 또한 디버깅 세션을 시작하기에 훌륭한 지점이 될 수 있다. 간단하고 재현 가능한 단위 테스트 케이스는 완전히 가동되는 프로그램보다 디버깅 하기 훨씬 수월한 발판 역할을 한다. 완전히 가동되는 프로그램으로 실패를 재현 하려면 우선 프로그램을 가동해야 하며 그후에 문제를 재현하기 위한 일련의 수작업을 거쳐야 한다.

    일련의 테스트 코드를 작성한 후에는 코드 커버리지 도구를 적용하여 실제 코드가테스트에 의해 얼마나 커버되는지 살펴보자 그 결과는 아마 놀라울 것이다 ,이때 유일하고 간단한 규칙은 만약 특정 코드가 테스트 범위에 들어가지 않는다면, 제대로 작동한다고 신뢰할 수 없다는 것이다. 지금 당장은 괜찮아 보일지라도 테스트라는 안정장치 없이는 추후 쉽게 무너질수 있다.


    3-5. 예리한 도구에 투자하자

    이들 도구는 최후의 수단으로 활용하기 보다 지금 당장 몸에 익히는 편이 좋다. 문제를 발견하기 전에 도구의 사용법을 안다면 훨씬 더 효과적일 것이다.

    코드를 한 단계식 아무 생각 없이 따라가다 보면 미시적 단계에 사로잡혀, 거시적 이고 전반적인 코드의 형태에 대해 생각하지 않게 된다. 하지만 이것이 취약 하다는 신호는 아니다. 때로는 대형 도구를 사용하는 편이 훨씬 더 쉽고 빠를 수 있다, 각 작업에 알맞은 도구를 사용하는 것을 두려워 하지 말자.


    3-6. 원인 분석 과정에서 제외하기 위해 코드를 제거하라

    실패 재현 시 문제가 되는 코드 부분을 해결하는데 기여하지 않는 모든 부분을 삭제할 것인지 판단하라. 연관되지 말아야 할 주제들은 서로 엮이지 않도록 하라. 서로 관련성이 없이 보이는 코드의 세부 항목들도 삭제하라.


    3-7. 청결은 감염을 막는다.

    버그가 소프트웨어에 필요 이상으로 오래 머물지 않도록 해야 한다. 이미 알려진 이슈처럼 사소한 문제를 놓치는 일이 없도록 하자. 이는 위험한 행위로 깨진 유리창 신드롬을 일으켜 점차 버그의 행동이 일상적이고 자연스러운 것처럼 느껴지게 만들 수 있다. 이처럼 버그가 오래 남아 있으면 추적 중인다른 버그를 가릴 수도 있다.


    3-8. 간접적 전략

    때로는 기가 막히는 문제에 맞서 몇 시간 동안 씨름하다가 결국 아무 것도 얻지 못하기도 한다.

    이럴 때는 간접적 접근 전략을 시도하여, 디버깅 구덩이에 빠져 있는 상황을 벗어나자.

    • 쉬어가기
      • 작업을 멈추고 코드에서 떨어져 있어야 할 때를 배우는 것은 중요하다. 휴식을 통해 새로운 관점을 얻을 수 있고, 이를 통해 더욱 신중하게 생각할 수 있기 때문이다. 무턱대고 코드에 달려들기 보다는 문제에 대한 서술과 코드 ㄱ조를 생각해볼 수 있도록 휴식 시간을 가져보자.
    • 다른 사람에게 설명해 보자
      • 문제를 다른 사람에게 설명해 보자. 종종 어떤 문제를 다른 사람에게 설명하다 보면, 동시에 자신에게도 설명하게 도고 문제를 해결하게 된다.

    3-9. 도망치지 말자.

    버그를 찾고 해결하려 할 때 마구잡이로 달려 들어서는 안 된다. 잠시 숨을 고르면서 해당 코드 구역에 숨어 있는 또 다른 관련된 문제들이 있는지 고려해보자. 아마 아미 해결한 문제는 코드의 다른 구역에서도 반복되는 패턴일 것이다. 습득한 지식을 바탕으로 해당 시스템을 강화하기 위해 해야할 일이 더 있지 않나 살펴보자.

    코드의 어느 부분에 더 많은 문제가 있는지 메모해두자. 어디에나 핫스팟이 있기 마련이다. 이 같은 핫스팟은 80%의 사용자가 실제로 쓰고 있는 코드의 20%이거나, 상태가 엉망이거나 품질이 낮은 소프트웨어라는 신호다. 이 메모를 잘 활용하여 후에 문제 해결에 시간을 할애하는 것이 좋다.



    4. 재현할 수 없는 버그


    때로는 재현 방법을 찾을 수 없는 버그를 발견하기도 한다. 버그가 논리와 추론을 거부하는 것이다.

    원인과 결과를 결정하는 것도 불가능하다. 이처럼 끔찍하면서도 간헐적으로 발생하는 버그는 사용자에 의한 어떤 직접적인 행위 라기보다는 정체를 할 수 없는 우주 방사선에 의해 발생하는 것처럼 느껴질 수 도 있다.

    • 실패를 유발하는 요소들을 기록해 두자. 이를 반복하다 보면, 어떤 패턴을 찾아내게 되어 공통된 원인들을 십결하는 데 도움이 될 것이다.
    • 더 많은 정보를 수집하다 보면 결론을 그릴 수 있게 된다. 아마도 기록해둘 만한 데이터 수집 지점을 더 많이 찾을 수 있다.
    • 베타 버전이나 출시 버전에 더 많은 로그와 assert를 추가하여, 실제 사용자의 사용 시 정보를 획득하는 방법을 검토하라.
    • 매우 힘든 문제일 경우 테스트 팜을 세팅하여 오랫동안 가동하는 스트레스 테스트를 실행할 수 있도록 하자. 만약 대표적인 방식으로 시스템을 자동화하여 가동할 수 있다면 버그 사냥 작업이 더 가속화 할 것이다.


    안정적이지 않은 버그 생성에 기여하는 것으로 알려저 있는 요소들이 있다.

    • 스레드를 사용하는 코드
      • 스레드는 비결정적이고 재현하기 어려운 방법으로 이리저리 휘감기며 상호 작용을 하는 만큼, 기이하면서도 간헐적인 실패를 종종 유발한다.
    • 네트워크 상호 작용
      • 네트워크는 그 구현 방식으로 인해 지연이 심하고, 때론 아예 끊기거나 먹통이 될 수도 있다. 대부분 의 코드는 로컬 저장소에 항상 접속할 수 있다고 가정된다. 하지만 이는 부주의한 가정이다. 접속 실패와 건헐적인 긴 로딩 타임이 일상적으로 발생하는 네트워크상의 저장소에는 항상 접속할 수 없다.
    • 저장 장치의 다양한 속도
      • 저장 장소의 속도가 천차만별인 이유는 단지 네트워크의 지연 때문만은 아니다. 느리고 회전하는 디스크나 데이터베이스 작동으로 인해 프로그램의 형태가 바뀔 수도 있다. 이는 특히 타임아웃의 범위 끝에서 아슬아슬하게 프로그램이 작동하도록 만든 경우 발생한다.
    • 메모리 손상
      • 비정상적 코드가 스택이나 힙의 일부를 덮어버리면, 재현할 수 없고 탐지 하기도 어려운 숱한 이상 현상이 나타난다. 소프트웨어 고고학은 종종 이러한 오류들을 진단하기 가장 쉬운 방법이 될 수도 있다.
    • 전역 변수 / 싱글 톤 
      • 스레드나 프로세스 혹은 모듈 간의 상호 작용을 위한 부분이 하드 코딩으로 구현되면, 예측하기 어려운 현상의 집합소가 될 수 있다. 누군가 어느 순간에 전역적 상태의 일부를 자신도 모르는 사이 고쳐버리면, 아무리 코드의 정확성을 확인한다 해도 버그의 원인을 찾을 수 없고, 어떤 일이 벌어질지 예상 할 수 없다.

    디버깅은 쉽지 않지만 우리 스스로의 실수에서 비롯된 것이다. 버그를 만들어내는 것은 우리 자신이다.

    효과적인 디버깅은 모든 프로그래머들의 필수적인 기술중 하나다.

    반응형

    'Technique > Programmer' 카테고리의 다른 글

    12. 복잡도 다루기  (0) 2017.03.16
    11. 테스트 하기  (0) 2017.02.22
    9. 예상하지 못한 것을 예상하기  (0) 2017.02.06
    8. 오류 무시하지 않기  (0) 2017.02.06
    7. 똥통에서 뒹굴기  (0) 2017.02.05

    댓글

Designed by Tistory.