• logo

      SeolMyeongTang

  • 디버깅은 왜 쓰나요?

    2021년 11월 07일

    필자는 언어를 처음 공부하거나 어떠한 서비스를 만들 때 무조건 하는 작업이 있습니다. 바로 디버깅 환경 구축입니다. Go 언어를 처음 배우거나, Flutter 프레임워크로 어플을 만들거나, nodeJS로 웹 서버를 만들거나, OS 공부를 하거나 기본적인 셋팅 완료 후 디버깅 환경을 구축합니다. 이번 글은 여러분한테 디버깅의 '파워'를 보여드리는 시간을 갖도록 하겠습니다.

    컴공생이라면 '디버깅이 중요하다'라는 말은 주위에서 들어봤을 겁니다. 디버깅(Debugging)은 쉽게 말하면 오류를 찾고 고치는 작업입니다. 어떤 오류가 나타났을 때 이를 찾는 방법은 다양합니다. 필자의 경험을 빌리자면 오류가 의심이 되는 부분에 print() 와 같은 출력 함수를 찍으면서 확인했습니다. 허접한 방법이지만 나름 직관적이고 쉬운 방법입니다. 툴을 이용한 디버깅 방법을 알고는 있었지만 이의 필요성을 느끼지 못했습니다. 귀찮은 디버깅 환경 설정하고 배우느니 그냥 출력함수로 확인하고 고치면 그만인데 말이죠. 하지만 피부로 디버깅의 유용함을 알게된 계기가 있었습니다. 바로 flutter 앱과 node 서버 사이에서 통신을 하던 도중 사건이 일어났습니다.

    node의 서버 내부 오류는 500 status로 위의 메시지 처럼 표시하도록 구성 했습니다. 오류 로그를 보거나 postman, curl로 확인해서 오류를 잡을 수 있지만 이 마저도 감이 안 오는 오류가 있습니다. 어떤 오류인지 고민하고 있는데 옆의 팀원이 와서 'F11'번을 누르면서 한줄 한줄 코드를 보고 오류를 고치는 모습을 보았습니다. 제가 몇 시간 동안 끙끙댔던 문제를 디버깅 몇 분 만에 오류를 찾은 모습을 보고 충격을 받았습니다. '디버깅의 파워를 쉽게 보면 안되는 구나!' 디버깅 환경을 구성하고 디버깅을 통해 한줄 한줄 살펴보는 과정이 답답하고 느리고 귀찮은 작업으로 보일지라도 무지성으로 print()를 통해 찾는 것보다 명확하고 정확하게 오류를 찾을 수 있다는 확신이 든 경험이였습니다.

    간단하게 디버깅의 순서를 살펴봅시다. 디버깅은 크게 3가지 순서로 진행합니다.

    1. 오류가 의심되는 코드 줄과 그 전 코드에 breakpoint 찍기
    2. 디버깅 실행(F5)
    3. Step Over(F10) 혹은 Step Into(F11)으로 확인하기

    디버깅의 핵심이라 할 수 있는 breakpoint가 있습니다. 프로그램을 실행 했을 때 breakpoint에 걸린 코드에 멈춰라는 의미입니다. 저희는 breakkpoint에서 멈출 때 마다 변수 값들의 변화를 관찰할 수 있습니다. Step Over는 한 줄씩 실행하라는 의미로 함수가 있어도 다음 한 줄을 실행합니다. Step Into는 함수를 만나면 함수 내부로 들어가서 한 줄씩 실행하라는 의미입니다. 말만 들어선 감이 잘 안 오실 겁니다. 다음의 예시를 살펴봅시다.

    개인적으로 '멀티코어 OS 만들기' 프로젝트를 진행하고 있습니다. OS의 기능을 하나씩 추가할수록 덩치가 커지고 있으며 현재 약 200개 C파일이 있습니다. 초기에는 어디에서 오류가 발생했는지 예상이 갔지만 현재는 규모가 커져 디버깅이 절실했습니다. 다음은 문제를 발견하고 임시방편으로 해결한 방법을 소개해드리겠습니다. 참고로 환경은 작성한 OS를(C언어) qemu에 띄워 디버깅을 진행하였습니다. 디버깅 환경 구축 방법은 github readme를 참고해주세요[1].

    https://seolmyeongtang.s3.ap-northeast-2.amazonaws.com/211107/1.png

    램 디스크를 구현하던 중 구현한 램 디스크로 부팅이 되는지 확인하는데 하드디스크 초기화 부분에서 넘어가지 않는 문제를 만났습니다. 어디에서 문제가 일어났는지 위해 디버깅을 꺼내 들고 하나씩 파헤쳐보았습니다.

    • 디버깅 순서

      FileSystem.c

      FileSystem.c

      맨 처음의 breakpoint는 하드디스크를 초기화 하는 함수에 걸었습니다.

      HardDisk.c

      HardDisk.c

      다음은 하드디스크의 정보를 읽어오는 함수로 진행 되었습니다.

      HardDisk.c

      HardDisk.c

      다음은 하드디스크가 작동할 준비가 되었는지 확인을 합니다.

      HardDisk.c

      HardDisk.c

      하드디스크가 작동할 준비가 안 되었다면 1 millisecond를 쉬는 동작을 합니다.

      Utility.c

      Utility.c

      1 millisecond를 쉬는 동안 스케줄링이 발생하여 다른 처리를 하도록 합니다.

    디버깅 순서대로 보면, kInitializeHDD() > kReadHDDInformation() > kWaitForHDDReady() > kSleep(1) 으로 진행됩니다. 디버깅을 진행하다보니 이상한 문제점을 찾을 수 있었습니다. kSleep(1)의 실행으로 분명 1 millisecond를 쉬고 다시 돌아와 하드디스크가 준비 상태를(kWaitForHDDReady()) 확인해야하는데 설정한 시간 이상으로 작동하여 처리가 늦어지는 문제였습니다. 따라서 필자는 kSleep() 함수 이상으로 보고 kWaitForHDDReady() 함수안의 kSleep(1)를 주석처리 하여 해결하였습니다.

    현재 개인 OS 프로젝트의 코드 수는 약 10만줄 입니다. 위의 오류를 찾기 위해 일일이 printf()와 같은 출력 함수를 찍어보는건 정말... 상상도 하기 싫습니다. 이 글을 통해 조금이나마 디버깅의 파워를 느껴보셨으면 합니다. 언어 설치한 김에 디버깅 하나정돈... 괜찮잖아요?

    [1] https://github.com/redundant4u/DeodeokOS#디버깅