기본 패턴
c
#include <stdlib.h>
#include <stdio.h>
int process(void) {
int *buf1 = NULL, *buf2 = NULL;
FILE *fp = NULL;
buf1 = malloc(100 * sizeof(int));
if (!buf1) goto cleanup;
buf2 = malloc(200 * sizeof(int));
if (!buf2) goto cleanup;
fp = fopen("data.bin", "rb");
if (!fp) goto cleanup;
// ... 정상 처리 ...
cleanup:
if (fp) fclose(fp);
free(buf2);
free(buf1); // 획득 역순으로 해제
return fp ? 0 : -1;
}설명
goto는 왜 나쁜가 — 그리고 왜 이 경우는 예외인가
goto는 코드 흐름을 임의로 점프시켜 스파게티 코드를 만들기 때문에 일반적으로 금지됩니다. 하지만 C에는 finally 블록이나 RAII가 없어서, 여러 자원을 획득한 함수에서 에러가 나면 이미 획득한 자원을 모두 해제해야 합니다. goto 없이 이것을 처리하면 중복 코드나 깊은 중첩이 발생합니다.
goto가 허용되는 규칙: 앞으로만(forward) 점프, 같은 함수 안에서만, 자원 정리 레이블로만.
goto 없이 처리하면 어떻게 되나
c
// ❌ goto 없이 — 중복 free 또는 깊은 중첩
int process_bad(void) {
int *buf1 = malloc(100 * sizeof(int));
if (!buf1) return -1;
int *buf2 = malloc(200 * sizeof(int));
if (!buf2) {
free(buf1); // 중복 시작
return -1;
}
FILE *fp = fopen("data.bin", "rb");
if (!fp) {
free(buf2); // 점점 늘어남
free(buf1);
return -1;
}
// 정상 종료
fclose(fp);
free(buf2);
free(buf1); // 또 중복
return 0;
}자원이 4개, 5개로 늘어나면 에러 처리마다 해제 목록이 계속 길어집니다. 하나 빠뜨리면 누수입니다.
레이블 설계 — 획득 역순
자원을 A → B → C 순서로 획득했으면 C → B → A 역순으로 해제합니다. NULL을 미리 초기화해 두면 획득 전에 free/fclose를 호출해도 안전합니다(free(NULL)은 표준 보장).
c
int open_and_process(const char *path) {
char *buf = NULL;
FILE *in = NULL;
FILE *out = NULL;
int ret = -1;
buf = malloc(4096);
if (!buf) goto done;
in = fopen(path, "r");
if (!in) goto done;
out = fopen("output.txt", "w");
if (!out) goto done;
// 처리
while (fgets(buf, 4096, in)) fputs(buf, out);
ret = 0;
done:
if (out) fclose(out); // 역순
if (in) fclose(in);
free(buf); // free(NULL)은 안전
return ret;
}빠른 정리
| 상황 | 적합한 선택 |
|---|---|
| 자원 1개, 에러 경로 단순 | if (err) { free(x); return -1; } |
| 자원 2개 이상, 에러 경로 복잡 | goto cleanup 패턴 |
| 레이블 위치 | 함수 끝, 역순 해제 |
| NULL 초기화 | 모든 포인터를 NULL로 선언하면 free(NULL) 안전 |
| C++이라면 | RAII (스마트 포인터, 소멸자) 사용 — goto 불필요 |
주의할 점
goto로 변수 선언을 건너뛰면 컴파일 오류 또는 정의되지 않은 동작입니다.
c
int func(void) {
goto skip;
int x = 10; // ❌ 초기화를 건너뜀 — C에서 오류 (C++에서는 컴파일 에러)
skip:
return 0; // x는 초기화되지 않은 상태
}
// ✅ 정리 레이블은 항상 함수 끝으로 — 선언을 건너뛰지 않도록
// 선언은 함수 상단에 모아두고, goto는 앞으로만 점프
int func_good(void) {
int x = 0; // 상단에서 선언
goto skip; // 이제 선언을 건너뛰지 않음
skip:
return x;
}goto로 루프 안으로 들어가거나 뒤로 점프하는 것은 항상 금지입니다.
참고 링크
1 sources