[C] goto 문

JM·2026년 10월 7일
post-thumbnail

malloc lab 최대 난관이었던 realloc함수를 구현하다가 사용하게 된 goto가 큰 도움이 되었다. 존재는 알고 있었지만 직접 써본 건 처음이었다. 그런데 한 줄 한 줄 쓸 때마다 장단점을 확 알겠던데...


👍장점 - 무슨 도움이 되었는데?

코드를 짜면서 체감됐던 것 + 조사하면서 알게 된 것들을 적어 볼 예정.

중첩 반복문 / 조건문 탈출

 goto문을 사용해야겠다고 결정하게 된 이유는 조건문에서 탈출하기 위함이었다. 여러 겹 중첩된 조건문 안에서 탈출해도 되는 조건을 만족하는 경우가 생기는데, 반복문이었다면 break와 같은 코드로 제어했겠지만 조건문은 그런 기능이 없었다.

  • 확장 요청인가?
    • 우측으로 충분한가? merge_right
      • 부족하다면 우측이 에필로그인가?
        + 그래도 부족하면 basecase
    • 좌측으로 충분한가?
    • 양 옆 모두 필요한가?
      • 부족하다면 우측이 에필로그인가?
        + 그래도 부족하면 basecase
    • 힙 영역 안에 fit한 공간이 있는가?
      등등

조건문 중첩으로 필요한 조건을 만족시키게 되면 실제 재할당 로직을 실행시키도록 하면 되는데, 너무 깊게 중첩된 상태에서는 빠져 나가기 어렵다.
가장 직관적인 방법은 탈출 플래그🚩를 설정해 매 번 검사하는 방식이 있을 것이다.

위 그림처럼 세 번 중첩된 조건문에서 탈출하려면 error플래그를 설정하고, 세 번의 조건문 각각에서 검사하고 탈출해야 한다.
혹은 조건을 만족했을 때의 조건문 블록 안에 실행 코드를 작성하면 되는데, 이 코드가 만약 다른 조건문에서 똑같은 로직이나 코드를 사용한다면? 중복된 코드를 조건을 만족할때 마다 작성해야 하는 큰 낭비가 생긴다.

코드 중복 작성 방지

realloc함수의 흐름 일부를 가져온 것.

왼쪽 그림에서는 merge_*코드를 두 군데에서 호출하고 있다.

  • 일반 병합 경로 조건 1 → merge_right
  • 힙 확장 경로 조건 2 → merge_right
  • 일반 병합 경로 조건 2 → merge_both
  • 힙 확장 경로 조건 1 → merge_both

이런 식으로 특정 코드를 실행해도 되는 조건에 도달하는 경로가 언제나 한 가지라고 할 순 없기 때문에 각 조건문 안에 똑같은 코드가 여러 번 들어가게 될 수도 있다.

goto문은 이런 문제를 해결해준다.

우측 그림은 공통 코드를 실행하게 했을 경우를 나타내는데, 같은 조건일 때 다른 코드(내용은 같지만)를 실행하던 왼쪽 경우와 달리 동일한 라인으로 점프해서 같은 코드를 읽게 하고 있다. merge_both는 30줄 정도로 구현되어 있으니 위와 같이 단편적인 예시로도 그 만큼 코드가 짧아지게 된다.


heap_extend 레이블에선 merge_both와 merge_right을 조건에 따라 호출하게 된다.
각각의 코드도 길이를 가지고 있기 때문에 잘못했다간 코드가 너무 길어질 것 같은데...
쉽게 생각해보면
1. 조건 1, 2에서 heap_extend를 호출한다.
2. heap_extend에서는 merge_both를 호출한다.
3. merge_both를 독립적으로 호출하는 조건 3, 4가 존재한다.
4. merge_both를 수행하는 코드는 결국 총 4번 입력되어야 한다.

조건 영역과 실행 코드 영역의 분리

goto문을 사용하지 않았다면 각 조건문 안에 실행 코드를 작성했을 것이다.

static void *realloc(...)
{
...
else{
	if (right_available)
    	/* 
         * 우측 병합을 위한 코드
         *
         */

	else if (left_available)
    	/*
         * 좌측 병합을 위한 코드
         *
         */

	else if (both_available)
    	/*
         * 양쪽 병합을 위한 코드
         *
         */

	else
    	/* 
         * 힙 확장 후 재할당을 위한 코드
         *
         */
     }
}

만약 각각의 코드들이 긴 길이를 가지고 있었다면 4개의 조건이 한 눈에 들어오지 않을 정도로 길어졌을 것이다.

만약 goto문으로 조건 영역과 실행 코드 영역을 분리한다면 다음과 같을 것이다.

static void *realloc(...)
{
...
else{
/* =============== if - block =============== */
	if (right_available)
    	goto merge_right;

	else if (left_available)
    	goto merge_left;

	else if (both_available)
    	goto merge_both;

	else
    	goto heap_extend;
    }
/* ============== execute - block ============== */
merge_right:
	/*
     * CODE
     */
     
merge_left:
	/*
     * CODE
     */

merge_both:
	/*
     * CODE
     */

heap_extend:
	/*
     * CODE
     */
}

함축해서 적어 보았기 때문에 두 구조 사이에 큰 차이는 없어 보일 수 있지만, 실제로는 다르다.
실제 코드 구현에서는 한 레이블 (label: 처럼 생긴 것)에서 다른 레이블을 호출하기도 하니까 만약 조건문 블록 안에 코드를 다 담고자 했으면 전체 길이도 크게 길어지고 코드의 흐름을 파악하기도 어렵다.

조건 영역을 분리해 놓으니 조건문을 설계하고 수정할 때 편리했다. 우선 조건들이 한 화면 안에 담겨 있으니 구상했던 조건이 잘 담겼는지 확인하기 좋았다.
게다가 각 블록에서 실행하는 것이 통짜 코드가 아니라 의미를 담고 있는 레이블이어서 어떤 코드가 실행되려고 하는지 파악하는 것도 도움이 되었다.


흐름의 유지 (Fall-Through)

레이블은 점프할 수 있는 위치를 나타내는 것 뿐이지 함수처럼 블록을 제한하지는 않는다.

한 레이블로 점프한 뒤에 다른 분기 제어가 없으면 그 아래 있는 코드도 전부 실행한다는 뜻이다.

goto release_b;

release_c:
    release_c_resource();

release_b:
    release_b_resource();

release_a:
    release_a_resource();

out:
    return result;

이 코드의 실행 흐름은
release_b → release_a → out = return result가 된다.
그 아래 작성 된 코드 또한 활용할 수 있다는 것이다.
이 기능은 다음 작성할 Cleanup에 유용하게 사용된다.


오류 발생 시 원활한 Cleanup

C언어에는 자원을 자동으로 관리해주는 기능이 없다. 반환되지 않은 자원에 대한 제약도 없다. 프로그래머에게 권한을 더 주었다고는 하지만, 반대로는 큰 제약처럼 느껴지기도 한다.

 자원을 할당하고 사용하는 과정에서 오류가 발생해서 프로그램이 종료되어야 한다고 해 보자. 아무 조치 없이 프로그램을 끝낼 수도 있겠지만 그렇게 되면 잘 알고 있는 메모리 누수가 발생한다. 프로그래머는 그런 상황을 피하기 위해 할당한 자원을 회수하는 코드를 작성하게 된다.

예시만 간단하게 들어보고 넘어가자

// 코드 중복 예제
b = acquire_b();
/* ============= 개별 처리 ============= */
if (b == NULL)
{
    release_a(a);
    return -1;
}

c = acquire_c();

if (c == NULL)
{
    release_b(b);
    release_a(a);
    return -1;
}

/* ============== 하나의 레이블로 통일 ============= */
if (b == NULL)
    goto release_a;

if (c == NULL)
    goto release_b;

release_c:
    release_c_resource();

release_b:
    release_b_resource();

release_a:
    release_a_resource();

out:
    return result;

만약 자원을 a → b → c 순서로 획득하려고 했을 때, c를 얻는 과정에서 오류가 발생하면 b와 a를 회수해야 한다.
c == NULL일 때 release_b로 점프해서 그 아래 있는 release_a코드까지 실행하도록 되어 있다. Fall-Through의 쉬운 예시다.


🚫단점 - 조심해야 할 것

물론 너무 당연하게 단점 또한 명확하다. 세상에 완벽한 게 어디 있겠어...

라벨의 실행 선행 조건이 크게 드러나지 않는다.

함수는 매개변수와 반환형을 선언할 때 표기하니 어느 정도 약속을 하고 들어가는 느낌이다.

라벨은 (함수도 마찬가지지만)라벨 이름에밖에 표기할 방법이 없으니 잘못된 조건에서 호출할 수 있다.
realloc함수에서 merge_방향을 잘못 호출하면 double free문제가 발생할 수 있는 것처럼.

라벨 위에 선행 조건을 주석으로 남기는 습관이 필요하다.

변수 초기화 영역이 모호해진다.

함수 작성하면서 많이 겪었던 문제였다. (사람마다 코딩 스타일이 다르겠지만) 나는 지역변수를 쓰이는 조건문 내에 선언하려고 하고 가급적이면 사용하는 라인이나 함수 근처에 작성하려고 한다. 교수님이 맨 위에 선언하라고 하셨던 기억이 있긴 한데
물론 되도록이면 스코프 초반에 모여 있게 선언하려고 하지만... 아무튼 goto문으로 라인을 뛰어 넘다 보면 변수 초기화도 건너 뛸 때가 있었다.
코드를 유지보수할 때도 취약점이 드러나는데, 리팩토링에 약하다고 표현한다.

next_bp may be used uninitialized

컴파일 과정에서 생긴 경고인데 next_bp변수가 초기화되지 않았을지도 모른다는 뜻이다.

초기화뿐 아니라 레이블 블록에 진입했을 때 사용할 변수에 저장되어 있어야 하는 값이 미리 계산되지 않았을 경우 결과에 영향을 줄 수 있다.
함수의 경우 매개변수로 전달받는데 goto의 경우엔 없기 때문에 변수의 계산을 확실히 해 주거나 진입 이후에 추가로 계산하는 식으로 확실히 해야 한다.

의도치 않은 Fall-Through

위에선 Fall-Through가 장점이라고 하지 않았는가? 항상 그렇지만도 않다.
코드를 병렬적으로 배치할 수 없기 때문에 레이블의 순서가 항상 생길텐데, 만약 위의 레이블이 모든 기능을 잘 수행해 냈다면 아래 레이블을 실행하는 것은 구조 상 문제일 수 있다.
다음 레이블의 기능을 실행할 필요가 없다면 Close레이블로 점프하게 하여 안전하게 종료하거나, return으로 즉시 종료시키는 방법을 택해야 한다.

함수의 경우에는 각 기능이 분리되어 있기 때문에 신경쓰지 않아도 될 부분인데 말이지

코드 흐름을 파악하기 어려워진다.

내가 구현한 realloc은 그 길이가 짧고 작동 방식이 단순해서, 그리고 goto문의 개수 자체도 적어서 흐름을 파악하기 쉬운 편이다.

그런데 길고 복잡한 코드에서 goto문을 과도하게 사용한다면?

A → C, B → A, C → E, D → B, E → C
이런 식으로 상호 호출하는 코드를 작성한다면 실행 순서를 머리속으로 추적하기 어려워진다.

goto문을 과도하게 사용하게 되면 실행은 되긴 되는데 구조를 프로그래머 본인도 몰라서 유지보수하기 불가능해지는 지경, 흔히 부르는 스파게티 코드가 되는 원인이 될 수 있다.

정리

다음과 같은 경우는 goto문의 사용을 삼가야 한다

  • 평범한 if/else, for, while로 더 명확하게 표현할 수 있을 때. 중첩되지 않은 조건 / 반복문은 빠져나오기 쉽다.
  • 라벨과 점프가 함수 전체에서 서로 교차할 때. 스파게티 코드가 되지 않도록 조심하자.
  • 목적지의 선행 조건이 복잡할 때.
  • 점프하면서 변수 초기화나 자원 획득을 건너뛸 때. 컴파일러가 경고해주긴 하지만 프로그램 흐름에 문제가 된다.
  • 라벨 코드가 독립된 함수로 자연스럽게 분리될 수 있을 때

나만의 생각

 goto문은 함수로 대체될 수 있는 것 아닌가? 하는 의문이 중간중간 생기긴 했다.
틀린 생각은 아니다. 레이블은 마치 함수처럼 독립적으로 작동하는 것 처럼 보이기는 한다. 가장 큰 차이점은 함수는 실행을 마치고 원래 위치로 돌아온다는 것이고 goto는 흐름 자체를 바꾼다는 것이겠다.

지금 realloc함수는 사용하는 변수의 수가 많아서 함수로 만든다면 매개변수 수가 너무 많아질 것 같기는 하다. 아마 구조체를 만들어 사용하면 해결될 것 같은데...
그렇다고 해도 지금 선언한 변수들이 원래 사용하던 변수들의 상호 계산 결과를 알아보기 쉽게 이름을 지어 준 것에 불과해서 또 다른 오버헤드가 발생할 것 같다.

profile
개발자 지망생

1개의 댓글

comment-user-thumbnail
2026년 10월 7일

글이 매번 goto 고트하네요

답글 달기