cyphen156

Chapter7 : 디버깅과 소프트웨어 설계 본문

프로그래밍/C언어

Chapter7 : 디버깅과 소프트웨어 설계

cyphen156 2023. 8. 8. 15:35

뭔가 챕터 제목부터가 어려워보입니다.

근데 막상 해보면 그냥 이론적인 내용과 어떻게하면 내가 작성한 코드의 문제점을 찾고 수정하는지에 관한 안내 내용이므로 부담없이 그냥 천천히 읽어보면 됩니다.

디버그는 뭘까?

버그(Bug)즉 벌레를 없앤다는 것인데 프로그램의 오류를 버그라고 부르고, 이를 찾아내서 고치는 것을 디버그라고 합니다.

프로그램 오류를 버그라고 부르게 된 이유는 아주 옛날에  컴퓨터에 벌레가 한마리 들어가서 오작동한 적이 있었고, 이 사건 이후로 프로그램 오류를 버그라고 부르게 되었다고 합니다.

디버깅을 하는 가장 간단한 방법은 연필로 끄적여보는것 입니다. 차근차근 프로그램의 동작 순서를 노트에 적어 가면서 변수가 어떻게 변하는지를 추적해서 프로그램 동작 논리에 모순이 없는지를 확인하고, 예상치 못한 오류가 어디서 발생하는지를 찾아보는 것입니다. 

또 다른 방법으론 통합개발환경(IDE)의 디버거의 도움을 받는 것 입니다. 디버거는 프로그래머들이 자주 하는 실수들을 미리 입력해놓고, 자동으로 프로그램 오류를 추적해주는 기능을 합니다. 또 어디서 오류가 발생했는지를 찾아서 표시해줍니다.

그런데 이런 디버거에서 찾아지지 않는 프로그램이 실행은 되는데 예측과는 전혀 다른 결과를 만들어내는 경우가 있습니다.

이럴 때 사용하는게 주로 터미널에 출력문(printf(C), console.log(JS), System.out.println(JAVA))을 사용해 변수의 변화를 추적하는 방법입니다. 

다음 코드는 서로 소를 찾는 코드인데 에러가 발생될 수 있는 곳마다 printf문을 통해 어디서 에려가 발생하는지를 추적합니다.

만약 mutually_prime함수를 실행하던 중 반복문 안의 분기문이 실행된다면 printf("Inside if.\n);문이 실행될 것이고 이것은 문제가 있음을 의미합니다.

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>

int mutually_prime(int a, int b) {
    int min, i, prime = 1;
    printf("Entering function, a = %d, b = %d.\n", a, b);
    min = (a < b) ? a : b;
    printf("Currently, min is %d.\n", min);
    for (i = 2; i <= min; i++) 
        if (a % i == 0 && b % i == 0) {
            printf("Inside if.\n");
            prime = 0;
            break;
        }
    return prime;
}

int main() {
    int first, second;
    printf("Enter two integers.\n");
    scanf("%d%d", &first, &second);
    mutually_prime(first, second) ? printf("They are mutually prime.\n")
        : printf("They are NOT mutually prime.\n");
    return 0;
}

또 다른 방법은 DEBUG라는 매크로 상수를 쓰거나 assert라는 매크로 함수를 사용하는 것 입니다.

매크로는 전처리기에게 프로그램 빌드시 해당되는 단어를 다른 단어로 모두 변경하라는 지시어로, 해당 단어를 변수로 만들어 메모리에 저장하는게 아닌 치환시켜 바로 적용시키기때문에 프로그램의 실행 속도를 빠르게 하고, 메모리 사용을 줄여 메모리 관리와 여러 곳에서 사용되는 같은 변수를 제어할 때 효과적이다. 가령 500이라는 최댓값 변수를 여러 곳에서 사용할 때 매크로를 사용하면 다음과 같이 표현할 수 있습니다.

#define MAX 500

#include <stdio.h>

int main() {
    printf("MAX is %d\n", MAX);
    printf("%d - %d = %d", MAX, MAX, MAX - MAX);
    return 0;
}

MAX라는 매크로를 사용하지 않고 변수에 500을 직접 넣을 수도 있지만 이것은 hardCoding이라고 해서 여러 변수를 직접 사용하는 것으로 지양하는것이 좋습니다.

하드코딩을 사용하면 나중에 MAX를 500이 아닌 다른 수 (EX 1000)로 바꿀때 500이라는 변수가 들어가있는 위치를 모두 찾아서 고쳐야하는데, 매크로를 사용하면 맨 위에 #define 된 MAX의 값만 1000으로 바꿔주면 해당 코드의 모든 위치의 500이 1000으로 바뀌어 코드를 유지보수하기 편해집니다.

장점만 있는것은 아닌데, 매크로를 사용하면 디버깅할 때 변수가 없기 때문에 에러가 발생한 위치를 찾기 어려워 진다는 단점이 있습니다. 또 다른 단점으로는 매크로는 타입을 확인하지 않기 때문에 전혀 예상치 못한 또 다른 오류를 일으킬 수 있습니다.

그래서 매크로는 적절하게 사용하는것이 중요합니다.

어? 디버깅을 위해서 매크로를 쓴다면서요??

맞다. 그래서 매크로 타입에 따라 코드를 다르게 실행시키는 조건부 매크로라는 것을 사용합니다.

#undef는 이미 정의된 매크로를 해제하여 #undef 이후의 코드로는 해당 매크로를 사용하지 않겟다는 의미입니다.

#define#undef를 통해 특정부분만 매크로를 적용한다던지 하는 방식으로 코드를 제어할 수도 있습니다.

#define DEBUG	// 매크로 상수 정의

main() {
	int num = 3;
    #ifdef = DEBUG	// 만약 정의되어있으면 코드를 실행해라
    	printf("Currently num is %d.\n", num);
    #endif			// 여기까지
    ...
}

그런데 이렇게 #ifdef와 #endif문을 쓰는게 상당히 번거롭기 때문에 개발된게 assert함수입니다.

#define _CRT_SECURE_NO_WARNINGS

//#define NDEBUG

#include <assert.h>
#include <stdio.h>

int factorial(int n) {
    int result = 1;
    assert(n >= 0);
    while (n)
        result *= n--;
    return result;
}

int main() {
    int first, second, larger, count, sum = 0;
    printf("Enter two integers.\n");
    scanf("%d%d", &first, &second);
    printf("Factorial of %d is %d.\n", first, factorial(first));

    assert(second != 0);
    printf("first/second is %d.\n", first / second);

    larger = (first > second);
    switch (larger) {
    case 1:
        printf("First is the larger.\n");
        break;
    case 0:
        printf("Second is the larger.\n");
        break;
    default:
        assert(0);
    }
    assert(first <= second);
    for (count = first; count <= second; count++) {
        sum += count;
        assert(count >= first && count <= second);
    }
    printf("Sum from first to second is %d.\n", sum);
    return 0;
}

assert()함수는 #define NDEBUG를 선언하느냐 여부에 따라 수행된다. 선언되어있다면 디버깅을 하지 않고, 선언되어있지 않다면 디버깅모드로 진입합니다.

assert()는 조건을 만족하면 코드를 실행시키지만 조건을 만족시키지 못하면 에러메세지를 출력하고 abort()함수를 호출해 프로그램을 비정상종료시킵니다.

first보다 작은 second를 입력했을때 발생하는 에러 assert(first <= second)위 조건을 만족하지 못한다.

exit()return

eixt()은 프로그램을 종료하라는 명령이고, return은 함수를 종료하고 호출자로 돌아가라는 소리입니다.

그럼 왜 int main()함수에서는 return을 쓸까? 

가장 처음 생각난건 main()함수는 프로그램 실행시 가장 처음 호출되는 함수이기 때문에 호출자 스택구조중 가장 최하단에 위치하고 있어 exit()과 동일한 효과를 낸다는 것이다. 상위에 main을 부른 다른 함수가 없으니 프로그램을 종료한다는것입니다.

두 함수는 명령하는 주체가 다릅니다. return은 프로그램 내에서 실행하는 함수지만, exit()은 운영체제가 수행하는 시스템 함수입니다. 또한 return문은 전달 인자에 따라서 함수가 정상종료되었는지 (return 0;) 또는 비정상종료되었는지(return -1;)를 알려주어 사용자가 알기 쉽게 해줍니다.

또 다른 차이점은 exit()이 프로그램을 종료할 때 atexit()으로 등록된 함수를 호출하고 열린 모든 파일을 닫는 등의 정리 작업을 수행한다는 것입니다. 반면 returnmain() 함수에서 호출되면 프로그램이 종료되기는 하지만, 이러한 추가적인 정리 작업을 자동으로 수행하지는 않습니다.

디버거 활용하기

아래의 코드는 소수인지 아닌지를 판정하는 코드입니다. 하지만 입력으로 4를 넣어주면 4를 소수라고 판정하는 오류가 발생합니다.

왜 이런 오류가 발생하는지를 추적하기 위한 도구가 디버거입니다.

#define _CRT_SECURE_NO_WARNINGS
#define true 1
#define false 0

#include <stdio.h>

int is_prime(int n) {
    int divider, remainder, prime = true;

    for (divider = 2; divider < n; divider++) {
        remainder = n % divider;
        if (remainder = 0) {
            prime = false;
            break;
        }
    }
    return prime;
}

int main() {
    int num;
    printf("Enter an integer.\n");
    scanf("%d", &num);
    if (is_prime(num))
        printf("It is a prime number.\n");
    else
        printf("It is NOT a prime number.\n");
    return 0;
}

보통 만든 소스코드를 실행해볼때 비주얼 코드에서 F5키를 입력하여 디버깅을 하실겁니다.

F10키는 함수 단위로 프로그램을 실행한다.

위의 코드에서는 총 5단계가 나타난다.

  1. main()함수를 실행하고
  2. printf()문을 통해 문자열을 출력한 뒤
  3. scanf()문으로 입력받고
  4. 조건문에서 is_prime(num)함수를 계산한 뒤
  5. 결과에 따라서 선택된 printf()문을 실행시킨뒤 return 0를 통해 main함수를 종료하고 프로그램을 종료시킨다.

프로그램을 한줄 단위로 디버깅하고 싶다면 F11을 입력하면 됩니다. 

또는 코드 왼쪽 줄 번호 옆에 중단점(F9)이라는 마크를 표시하여 해당 위치까지 프로그램을 실행한 뒤 잠깐 멈추게 하는 방법이 있습니다. 

디버깅중 노란색 위치에 탐색하고 싶은 변수의 주소를 입력하면 해당메모리에 실제로 어떤 값이 저장되어있는지를 볼 수 있고, 로컬, 조사식, 자동에서 함수, 또는 프로그램단위로 어떤 함수가 호출되었는지, 변수가 생성되어있고, 어떤 값을 갖고 있는지 또한 볼 수 있습니다. 

디버깅 작업은 프로그램의 오류를 찾아내서 고치는 과정이라 오래걸리고 지루할 수 있습니다. 하지만 예상대로 작동되지 않던 프로그램을 내가 원하는대로 작동시켰을 때의 기쁨이 있으니 여러분들도 디버깅과 친해졌으면 좋겠습니다.

위의 코드의 문제는 조건문에 대입연산자를 사용한것이 문제였습니다. 

if (remainder = 0)

이 코드를 실행하면 remainder라는 변수는 항상 0이라는 값을 갖게 되기 때문에 항상 prime은 1인 상태로 리턴되기 때문에 

        printf("It is a prime number.\n");

이 코드가 실행되었던 것입니다. 그래서 코드를

if (remainder == 0)

로 수정하면 정상적으로 프로그램이 작동됩니다.

#define _CRT_SECURE_NO_WARNINGS
#define true 1
#define false 0

#include <stdio.h>

int is_prime(int n) {
    int divider, remainder, prime = true;

    for (divider = 2; divider < n; divider++) {
        remainder = n % divider;
        if (remainder == 0) {
            prime = false;
            break;
        }
    }
    return prime;
}

int main() {
    int num;
    printf("Enter an integer.\n");
    scanf("%d", &num);
    if (is_prime(num))
        printf("It is a prime number.\n");
    else
        printf("It is NOT a prime number.\n");
    return 0;
}

정상작동하는 소수판별 프로그램

위에서 디버깅시 기본 단위로 함수단위로 실행된다고 얘기한 적이 있습니다.

단위라는 것이 중요한 키워드입니다. 

디버깅을 할 때 어느 부분까지를 정상작동하는 코드이고, 어디부터가 문제를 찾아내서 수정해야 할 코드인가나누어서 파악하는것전체 코드를 처음부터 확인하는 보다 디버깅 시간을 엄청나게 줄여줍니다. 이렇게 부분으로 나누어서 문제를 해결하는 방법을 분할정복기법이라고 하며, 이렇게 문제를 해결하는 과정을 알고리듬(Algorithm)이라고 합니다.

분할 정복과 알고리즘에 대한 자세한 내용은 알고리듬에서 정리하겠습니다. 

'컴퓨터공학/알고리듬' 카테고리의 글 목록 (tistory.com)

 

'컴퓨터공학/알고리듬' 카테고리의 글 목록

Cyphen의 개인 공부 블로그입니다. 프사는 아는 동생이 그려준 그림 주인장과 싱크로율 0%

cyphen156.tistory.com

그런데 소프트웨어의 품질 관리에는 디버깅만이 있는것이 아닙니다.

프로그램은 항상 오류에 노출되어있고, 노출된 오류를 찾아 수정하였더라도 아직 찾지 못한 오류는 항상 존재합니다. 

아직 찾지 못한 오류를 찾아내기 위해 다양한 경우의 상황을 만들어내어 실험하는 것소프트웨어 테스팅입니다.

보통 함수와 같이 작은 단위로 테스트하는것을 단위테스트, 연관된 코드들을 모아 통합적으로 테스팅해보는 통합테스트,

코드의 수정 후 이전과 같이 모든 기능이 제대로 작동하는지를 점검하는 회귀 테스트, 프로그램의 성능을 시험하는 성능테스트, 보안문제는 없는지, 데이터가 유실되지 않는지 등을 점검하는 보안 테스트 등이 있고, 좋은 프로그램을 개발하기 위해 항상 디버깅과 소스코드 제작과 병행되어 수행되는것이 테스팅입니다.

소프트웨어 설계

소프트웨어 설계에 대한 내용은 어떻게 하면 좋은 프로그램을 만들 수 있고, 지속적으로 안정된 서비스를 제공할 것인가,  최대한 발생가능한 문제들을 예방할 것인가?에 관한 내용인데, 이것은 따로 소프트웨어공학/프로젝트 설계로 빼서 자세히 설명하도록 하겠습니다.

'컴퓨터공학/소프트웨어 공학' 카테고리의 글 목록 (tistory.com)

 

'컴퓨터공학/소프트웨어 공학' 카테고리의 글 목록

Cyphen의 개인 공부 블로그입니다. 프사는 아는 동생이 그려준 그림 주인장과 싱크로율 0%

cyphen156.tistory.com

'컴퓨터공학/프로젝트 설계' 카테고리의 글 목록 (tistory.com)

 

'컴퓨터공학/프로젝트 설계' 카테고리의 글 목록

Cyphen의 개인 공부 블로그입니다. 프사는 아는 동생이 그려준 그림 주인장과 싱크로율 0%

cyphen156.tistory.com

 

//모든 예제 소스는 한빛 미디어홈페이지에서 찾으실 수 있습니다. 

IT CookBook, 전공자를 위한 C 언어 프로그래밍 (hanbit.co.kr)

또는 cyphen156/Work-space: Studying (github.com)에서 찾으실 수 있습니다.

 

 

 

 

'프로그래밍 > C언어' 카테고리의 다른 글

Chapter7 과제  (0) 2023.08.24
Chapter6 과제  (0) 2023.08.07
Chapter6 반복문 : for와 while  (0) 2023.07.12
Chapter5 과제  (0) 2023.06.21
Chapter5 조건문 : If와 Switch  (0) 2023.06.02