한 문장 요약
C 함수는 전부 같은 모양으로 쓴다.
가드로 시작 → 자원 잡기 → 로직 → goto CLEANUP → 잡은 역순으로 놓기 → 리턴.
이 모양을 벗어나는 순간 메모리가 새기 시작한다.
0. 왜 “모양을 통일”하는가
C에는 예외(exception)도, 소멸자(destructor)도, GC도 없다.
그래서 에러가 날 때마다 내가 직접 뒷정리를 해야 한다.
초보가 쓰는 코드는 보통 이렇게 생겼다.
/* ✗ 이렇게 쓰면 반드시 샌다 */int do_something(const char *name){ char *buf = malloc(1024); FILE *fp = fopen(name, "r"); if (fp == NULL) { return -1; /* ✗ buf 가 샌다 */ } if (fread(buf, 1, 1024, fp) == 0) { fclose(fp); return -1; /* ✗ buf 가 또 샌다 */ } ... free(buf); fclose(fp); return 0;}
에러 경로가 3개면 뒷정리 코드도 3벌이다. 그중 하나만 빠져도 샌다.
그리고 에러 경로는 시험에서 거의 안 밟힌다. 그래서 운영 나가서 터진다.
/* ✓ 단일 출구: 뒷정리가 딱 한 벌 */int do_something(const char *name){ int ret = RET_FAIL; /* ① 실패를 기본값으로 */ char *buf = NULL; /* ② 포인터는 전부 NULL 로 선언 */ FILE *fp = NULL; CHK_PTR(name); /* ③ 가드 */ buf = malloc(1024); CHK_ALLOC_GOTO(buf, ret, CLEANUP); /* ④ 실패하면 CLEANUP 으로 (할당 실패는 NO_MEM) */ fp = fopen(name, "r"); CHK_COND_GOTO(fp != NULL, RET_IO_ERROR, ret, CLEANUP); if (fread(buf, 1, 1024, fp) == 0) goto CLEANUP; ... ret = RET_OK; /* ⑤ 끝까지 왔을 때만 성공 */CLEANUP: /* ⑥ 뒷정리는 여기 한 곳뿐 */ if (fp) { fclose(fp); fp = NULL; } SAFE_FREE(buf); return ret;}
이 여섯 줄이 전부다
① ret 초기값은 실패. ② 포인터는 전부 NULL 초기화.
③ 가드로 인자 검사. ④ 실패하면 goto CLEANUP. ⑤ 성공은 맨 마지막 한 줄.
⑥ 해제는 잡은 역순으로 한 곳에서.
이 골격은 사내 프레임워크 코드에서도 똑같이 쓴다 →
실무 3. 네이밍·에러·로그·자원해제 규약
1. 리턴코드 체계
1-1. 세 가지 방식과 선택 기준
방식
모양
언제 쓰나
단점
0 / -1
return 0; / return -1;
실패 이유가 하나뿐일 때
”왜 실패했는지” 못 알려줌
enum 에러코드
return E_INVALID_ARG;
실패 이유가 여러 개일 때
상수를 관리해야 함
출력 파라미터
int f(in, out, int *err)
값과 에러를 둘 다 줘야 할 때
호출이 번거로움
기본은 enum 으로 시작하라
0 / -1 로 시작하면 나중에 “이 실패가 입력 문제인지 시스템 문제인지” 를 구분해야 할 때
모든 호출부를 고쳐야 한다. 처음부터 enum 으로 두면 값만 추가하면 된다.
1-2. 복붙용 리턴코드 정의
/* ══════════════ common.h — 프로젝트 하나당 한 벌 ══════════════ */#ifndef COMMON_H#define COMMON_H#include <stdio.h>#include <stdlib.h>#include <string.h>#include <stdint.h>#include <errno.h>/* ── 리턴코드 ───────────────────────────────────────── * 성공은 0 하나. 실패는 전부 음수. * "if (ret != RET_OK)" 한 줄로 모든 실패를 잡을 수 있다. * ──────────────────────────────────────────────────── */typedef enum { RET_OK = 0, /* 성공 */ RET_FAIL = -1, /* 분류 안 된 일반 실패 */ RET_INVALID_ARG = -2, /* 인자가 잘못됨 (NULL, 범위 밖) */ RET_NO_MEM = -3, /* malloc 실패 */ RET_NOT_FOUND = -4, /* 찾는 대상이 없음 (에러 아닐 수도) */ RET_TOO_SMALL = -5, /* 버퍼가 모자람 */ RET_BAD_FORMAT = -6, /* 파싱 실패, 규격 위반 */ RET_TIMEOUT = -7, /* 시간 초과 */ RET_IO_ERROR = -8, /* read/write/socket 실패 */ RET_NOT_SUPPORTED= -9, /* 아직 구현 안 함 / 규격상 미지원 */} ret_t;/* 에러코드를 사람이 읽는 문자열로. 로그 찍을 때 필수 */static inline const char *ret_str(int r){ switch (r) { case RET_OK: return "OK"; case RET_FAIL: return "FAIL"; case RET_INVALID_ARG: return "INVALID_ARG"; case RET_NO_MEM: return "NO_MEM"; case RET_NOT_FOUND: return "NOT_FOUND"; case RET_TOO_SMALL: return "TOO_SMALL"; case RET_BAD_FORMAT: return "BAD_FORMAT"; case RET_TIMEOUT: return "TIMEOUT"; case RET_IO_ERROR: return "IO_ERROR"; case RET_NOT_SUPPORTED: return "NOT_SUPPORTED"; default: return "UNKNOWN"; }}#endif /* COMMON_H */
RET_NOT_FOUND 는 에러가 아닐 수 있다
“조회했는데 없다” 는 정상적인 결과인 경우가 많다 (HTTP 404).
그래서 if (ret < 0) 로 뭉뚱그리면 안 되고,
if (ret == RET_NOT_FOUND) 를 따로 처리해야 하는 경우가 생긴다.
실제 상용 코드에서도 DB 조회는 실패 / 0건 / 성공세 갈래로 나눈다.
1-3. 호출부에서 쓰는 모양
int ret = 0;ret = do_something(name);if (ret != RET_OK) { LOG_ERR("do_something(%s) failed: %s(%d)", name, ret_str(ret), ret); return ret; /* ★ 에러코드를 그대로 위로 전달 */}
에러코드는 바꾸지 말고 그대로 올린다
중간 함수에서 return RET_FAIL; 로 덮어쓰면, 맨 위에서는
“뭔가 실패했다” 밖에 알 수 없다. 원래 코드를 그대로 전달해야
맨 위 로그만 보고도 “아, 메모리가 모자랐구나” 를 안다.
2. 가드 매크로 — 함수 첫 줄에 놓는 검사
2-1. 복붙용 가드 매크로 전문
/* ══════════════ guard.h ══════════════ */#ifndef GUARD_H#define GUARD_H#include "common.h"#include "log.h"#include "safe.h" /* SAFE_* 를 같이 쓰게 한다 *//* ── 즉시 return 하는 가드 (자원을 아직 안 잡았을 때) ── */#define CHK_PTR(_p) \ do { \ if ((_p) == NULL) { \ LOG_ERR("%s is NULL (%s/%d)", #_p, __func__, __LINE__); \ return RET_INVALID_ARG; \ } \ } while (0)/* 할당 결과 검사: 인자 오류(INVALID_ARG)가 아니라 NO_MEM 으로 구분한다 */#define CHK_ALLOC(_p) \ do { \ if ((_p) == NULL) { \ LOG_ERR("alloc %s failed (%s/%d)", #_p, __func__, __LINE__);\ return RET_NO_MEM; \ } \ } while (0)#define CHK_RANGE(_v, _min, _max) \ do { \ if ((_v) < (_min) || (_v) > (_max)) { \ LOG_ERR("%s(%d) out of range [%d,%d] (%s/%d)", \ #_v, (int)(_v), (int)(_min), (int)(_max), \ __func__, __LINE__); \ return RET_INVALID_ARG; \ } \ } while (0)#define CHK_STR(_s) \ do { \ if ((_s) == NULL || (_s)[0] == '\0') { \ LOG_ERR("%s is empty (%s/%d)", #_s, __func__, __LINE__); \ return RET_INVALID_ARG; \ } \ } while (0)#define CHK_RET(_expr) \ do { \ int _r = (_expr); \ if (_r != RET_OK) { \ LOG_ERR("%s failed: %s(%d) (%s/%d)", \ #_expr, ret_str(_r), _r, __func__, __LINE__); \ return _r; \ } \ } while (0)/* ── goto 로 빠지는 가드 (자원을 이미 잡았을 때) ── */#define CHK_PTR_GOTO(_p, _rv, _label) \ do { \ if ((_p) == NULL) { \ LOG_ERR("%s is NULL (%s/%d)", #_p, __func__, __LINE__); \ (_rv) = RET_INVALID_ARG; \ goto _label; \ } \ } while (0)#define CHK_ALLOC_GOTO(_p, _rv, _label) \ do { \ if ((_p) == NULL) { \ LOG_ERR("alloc %s failed (%s/%d)", #_p, __func__, __LINE__);\ (_rv) = RET_NO_MEM; \ goto _label; \ } \ } while (0)#define CHK_RANGE_GOTO(_v, _min, _max, _rv, _label) \ do { \ if ((_v) < (_min) || (_v) > (_max)) { \ LOG_ERR("%s(%d) out of range [%d,%d] (%s/%d)", \ #_v, (int)(_v), (int)(_min), (int)(_max), \ __func__, __LINE__); \ (_rv) = RET_INVALID_ARG; \ goto _label; \ } \ } while (0)#define CHK_STR_GOTO(_s, _rv, _label) \ do { \ if ((_s) == NULL || (_s)[0] == '\0') { \ LOG_ERR("%s is empty (%s/%d)", #_s, __func__, __LINE__); \ (_rv) = RET_INVALID_ARG; \ goto _label; \ } \ } while (0)#define CHK_RET_GOTO(_expr, _rv, _label) \ do { \ (_rv) = (_expr); \ if ((_rv) != RET_OK) { \ LOG_ERR("%s failed: %s(%d) (%s/%d)", \ #_expr, ret_str(_rv), (_rv), __func__, __LINE__); \ goto _label; \ } \ } while (0)#define CHK_COND_GOTO(_cond, _code, _rv, _label) \ do { \ if (!(_cond)) { \ LOG_ERR("cond(%s) failed (%s/%d)", #_cond, __func__, __LINE__); \ (_rv) = (_code); \ goto _label; \ } \ } while (0)#endif /* GUARD_H */
2-2. 매크로 해독 — 왜 이 모양인가
부분
왜 필요한가
do { ... } while (0)
if (x) CHK_PTR(p); else ... 에서 문법이 깨지지 않게. → do-while(0) 노트
#_p (stringify)
매크로 인자를 문자열로 만든다. CHK_PTR(name) → 로그에 "name is NULL"
(_p) 괄호
CHK_RANGE(a+b, 0, 10) 같은 식이 들어와도 우선순위가 안 깨지게
__func__, __LINE__
로그만 보고 어느 함수 몇 번째 줄인지 바로 찾게
int _r 지역변수
_expr 을 두 번 평가하지 않기 위해. 함수 호출이면 두 번 불린다!
_goto 판의 _rv
결과를 담을 변수를 인자로 명시한다 (아래 참고)
표 항목 디테일
do { ... } while (0)
if (ptr == NULL) CHK_PTR(ptr); else foo();
매크로가 단순 { ... } 였다면 세미콜론 때문에 if-else 가 깨진다(if(...){...};else... 는 문법 에러). do{...}while(0) 으로 감싸면 CHK_PTR(ptr); 전체가 세미콜론 하나로 끝나는 단일 문(statement) 처럼 동작해서 if/else, 중첩 매크로 어디에 넣어도 안전하다. while(0) 이라 실제로 한 번만 돈다.
#_p (stringize)
전처리기 연산자. CHK_PTR(name) 이라고 호출하면 매크로 인자로 넘어온 토큰 텍스트name 을 그대로 큰따옴표로 감싸 "name" 이라는 문자열 리터럴로 바꾼다. 값이 아니라 소스코드에 적힌 글자 그대로라는 게 핵심 (2-2-2 참고).
(_p) 괄호
매크로는 텍스트 치환이라 괄호가 없으면 인자로 들어온 표현식이 다른 연산자와 잘못 결합된다.
#define CHK_RANGE(_v, ...) ... if ((_v) < ...) // ✓CHK_RANGE(a + b, 0, 10)→ if ((a + b) < ...) // ✓ 괄호 있으면 안전→ if (a + b < ...) // ✗ 괄호 없으면 뒤 연산자와 우선순위가 깨질 수 있음
함수 인자는 호출 시점에 값으로 먼저 평가되니 이런 문제가 없지만, 매크로는 텍스트 그대로 붙여넣기라서 방어적으로 항상 괄호를 친다.
__func__, __LINE__ — 관례가 아니라 표준 메커니즘, 근데 둘이 다르게 동작함
정체
언제 치환되나
표준 근거
__LINE__
전처리기 매크로
전처리 단계에서 그 줄의 줄번호 정수 리터럴로 텍스트 치환
C 표준 6.10.8 (Predefined macro names)
__func__
암묵적 지역 변수 (매크로 아님!)
컴파일러가 각 함수 진입부에 static const char __func__[] = "함수이름"; 을 자동 삽입한 것처럼 동작 → 런타임에 그 배열을 읽음
C99 §6.4.2.2 (Predefined identifiers)
#include 없이 그냥 쓸 수 있는 이유가 이 둘 다 컴파일러/전처리기 내장이기 때문. __LINE__ 은 # 으로 문자열화할 수 없고(정수 리터럴이라 %d 로 찍음), __func__ 는 이미 문자열이라 %s 로 바로 찍는다 — LOG_ERR(..., __func__, __LINE__) 의 포맷이 %s/%d 로 짝지어진 이유.
이렇게 짜면 _e 가 텍스트로 세 곳(if, LOG_ERR, return)에 복붙되어 send_packet(fd) 가 세 번 실행된다. 그래서
int _r = (_expr); // 딱 한 번만 실행하고 결과를 변수에 저장if (_r != RET_OK) { LOG_ERR(..., _r, ...); return _r; }
로 한 번 평가한 값을 재사용한다. 함수 호출뿐 아니라 i++ 같은 부작용 있는 표현식에서도 중요.
_goto 판의 _rv — 결과 변수를 인자로 받는다
#define CHK_PTR_GOTO(_p, _rv, _label) do { if ((_p)==NULL) { ...; (_rv) = RET_INVALID_ARG; goto _label; } } while(0)CHK_PTR_GOTO(buf, ret, CLEANUP); /* 호출부에 'ret 이 바뀐다' 가 그대로 보인다 */
매크로가 호출 함수의 ret 이라는 이름을 몰래 캡처하지 않고, 결과를 담을 변수를 _rv 인자로 받는다. 이름 약속에 기대지 않으니 변수 이름이 ret 이 아니어도 되고, 바깥 스코프에 같은 이름의 다른 변수가 있어도 엉뚱한 변수를 덮어쓰지 않는다. CHK_COND_GOTO 는 값(_code)과 저장할 변수(_rv)가 둘 다 인자다. 인자는 (_rv) 로 괄호를 쳐서 쓴다.
할당 실패는 CHK_ALLOC* 로 — CHK_PTR* 와 에러코드를 구분한다
malloc/calloc 이 NULL 이면 원인은 인자가 아니라 메모리 부족이다.
CHK_PTR* (RET_INVALID_ARG) 를 쓰면 로그에서 두 원인이 섞이므로, 할당 결과는 CHK_ALLOC* (RET_NO_MEM) 로 검사한다.
fopen 처럼 실패 원인이 I/O 인 경우는 CHK_COND_GOTO(fp != NULL, RET_IO_ERROR, ...) 를 쓴다.
과거에는 ret 을 암묵적으로 썼다
예전 버전은 CHK_*_GOTO(_p, _label) 로 호출하고 매크로 안에서 ret = ... 를 직접 썼다.
짧긴 했지만 호출부에 ret 이 보이지 않았고 CHK_COND_GOTO 는 값만 인자로 받는 반쪽 명시였다.
지금은 모두 _rv 를 명시한다. 함수에는 여전히 int ret = RET_FAIL; 이 있어야 넘길 변수가 생긴다.
CHK_RET(send_packet(fd)) 라고 쓰면 패킷이 세 번 전송된다.
반드시 int _r = (_expr); 로 한 번만 평가해서 지역변수에 담는다.
2-2-1. #_v 는 “이름”, (_v) 는 “값” — 전처리 vs 런타임
CHK_RANGE 하나에 #_v 와 (int)(_v) 가 같은 인자를 서로 다른 방식으로 두 번 씁니다. 헷갈리기 쉬운 지점이라 따로 정리.
CHK_RANGE(v, 0, 100); /* v 는 지금 999 */
/* 매크로 정의 */LOG_ERR("%s(%d) out of range [%d,%d] (%s/%d)", #_v, (int)(_v), (int)(_min), (int)(_max), __func__, __LINE__);
자리
치환 결과
언제 결정되나
#_v (%s)
"v"
전처리 단계. 호출부에 적힌 토큰(글자) v 를 그대로 문자열화. v 가 몇 값을 담고 있는지는 전혀 모름
(int)(_v) (%d)
999
런타임._v 자리에 v 라는 코드가 그대로 삽입되어 실행되고, 그 순간의 변수값이 평가됨
출력: v(999) out of range [0,100] (take_ptr/14)
→ %s 가 “v”를 먹고, 바로 다음 %d 가 “999”를 먹은 것. #_v 혼자서 이름+값을 같이 주는 게 아니라, 포맷 문자열에 %s(%d) 두 지시자를 나란히 놓고 인자도 두 개(#_v, (int)(_v)) 나란히 줘서 “이름(값)” 모양을 만든 것.
매크로 인자로 리터럴을 넘기면 결과가 달라진다
CHK_RANGE(v, 0, 100) 처럼 변수를 넘기면 #_v → "v".
반대로 CHK_RANGE(999, 0, 100) 처럼 리터럴을 직접 넘기면 #_v → "999" 가 되어 999(999) 로 찍힌다.
# 연산자는 값이 아니라 호출부 소스코드에 적힌 텍스트 그대로를 문자열화하기 때문.
2-3. 어디까지 검사할 것인가
flowchart TD
Q{이 함수를 누가 부르나?}
Q -->|외부 입력<br/>네트워크·파일·사용자| A["전부 검사<br/>NULL, 범위, 길이, 포맷"]
Q -->|같은 모듈 내부<br/>static 함수| B["최소한만<br/>NULL 정도"]
Q -->|다른 모듈의 공개 API| C["NULL + 범위<br/>(계약 위반 잡기)"]
경계에서만 빡세게 검사한다
모든 함수에서 모든 인자를 검사하면 코드의 절반이 검사문이 된다.
외부에서 들어온 값은 한 번 철저히 검사하고, 그 뒤로는 믿는다.
단, static 함수라도 포인터 NULL 검사는 남겨두는 게 디버깅에 유리하다.
3. 로그 — 가드 매크로가 쓸 최소한의 로그
프레임워크가 없으면 로그도 직접 만들어야 한다. 최소 구성은 이것이면 충분하다.
/* ══════════════ log.h ══════════════ */#ifndef LOG_H#define LOG_H#include <stdio.h>#include <string.h>#include <errno.h>#include <time.h> /* localtime_r: -std=c11 이면 -D_POSIX_C_SOURCE=200809L 필요 */typedef enum { LOG_LV_ERR = 0, LOG_LV_WRN = 1, LOG_LV_INF = 2, LOG_LV_DBG = 3,} log_level_t;extern log_level_t g_log_level; /* 런타임에 바꿀 수 있게 전역 */#define LOG_PRINT(_lv, _tag, _fmt, ...) \ do { \ if ((_lv) <= g_log_level) { \ time_t _t = time(NULL); \ struct tm _tm; \ char _ts[20]; \ localtime_r(&_t, &_tm); \ strftime(_ts, sizeof(_ts), "%Y-%m-%d %H:%M:%S", &_tm); \ fprintf(stderr, "[%s][%s] " _fmt "\n", _ts, _tag, \ ##__VA_ARGS__); \ } \ } while (0)#define LOG_ERR(_fmt, ...) LOG_PRINT(LOG_LV_ERR, "ERR", _fmt, ##__VA_ARGS__)#define LOG_WRN(_fmt, ...) LOG_PRINT(LOG_LV_WRN, "WRN", _fmt, ##__VA_ARGS__)#define LOG_INF(_fmt, ...) LOG_PRINT(LOG_LV_INF, "INF", _fmt, ##__VA_ARGS__)#define LOG_DBG(_fmt, ...) LOG_PRINT(LOG_LV_DBG, "DBG", _fmt, ##__VA_ARGS__)/* errno 를 먼저 저장한다 — LOG_PRINT 안의 time/localtime_r 이 errno 를 바꿀 수 있다 */#define LOG_ERRNO(_fmt, ...) \ do { \ int _e = errno; \ LOG_ERR(_fmt ": %s(%d)", ##__VA_ARGS__, strerror(_e), _e); \ } while (0)#endif /* LOG_H */
부분
설명
##__VA_ARGS__
GCC 확장. 가변인자가 비었을 때 앞의 쉼표를 지워준다. 없으면 LOG_ERR("hello") 가 컴파일 에러
(_lv) <= g_log_level
레벨 검사를 매크로 안에서 하면, 꺼진 로그는 인자 조립 비용조차 안 든다
localtime_r
localtime 은 스레드 안전하지 않다. 멀티스레드면 반드시 _r 판
fprintf(stderr, ...)
버퍼링 없이 바로 나간다. 죽기 직전 로그가 안 날아간다
로그 포맷을 프로젝트 시작할 때 정하라
나중에 바꾸면 이미 쌓인 로그를 파싱하던 스크립트가 전부 깨진다.
최소한 [시각][레벨] 메시지 (함수/줄) 형태는 지키는 게 좋다.
4. 메모리 누수 방어
4-1. 해제 매크로 — 두 번 free 를 막는다
/* ══════════════ safe.h ══════════════ */#ifndef SAFE_H#define SAFE_H#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include "common.h"#define SAFE_FREE(_p) \ do { \ if ((_p) != NULL) { \ free(_p); \ (_p) = NULL; /* ★ 이 한 줄이 double free 를 막는다 */ \ } \ } while (0)#define SAFE_FCLOSE(_fp) \ do { if ((_fp) != NULL) { fclose(_fp); (_fp) = NULL; } } while (0)#define SAFE_CLOSE(_fd) \ do { if ((_fd) >= 0) { close(_fd); (_fd) = -1; } } while (0)/* 쓰기용 파일: 쓰기 오류는 fclose 에서 드러날 수 있다. * 이미 실패한 _rv 는 덮어쓰지 않는다 (첫 실패 원인을 보존). */#define SAFE_FCLOSE_RET(_fp, _rv) \ do { \ if ((_fp) != NULL) { \ if (fclose(_fp) != 0 && (_rv) == RET_OK) { \ (_rv) = RET_IO_ERROR; \ } \ (_fp) = NULL; \ } \ } while (0)#endif /* SAFE_H (SAFE_CPY / IS_ARRAY 는 아래 4-2) */
읽기 파일은 SAFE_FCLOSE, 쓰기 파일은 SAFE_FCLOSE_RET
버퍼에 남은 데이터는 fclose 시점에 디스크로 나가므로, 디스크 가득 참 같은 오류가 거기서 드러난다.
반환값을 버리는 SAFE_FCLOSE 로 쓰기 파일을 닫으면 저장 실패를 성공으로 보고할 수 있다.
= NULL 을 빼먹으면 언젠가 죽는다
free(buf); /* ✗ buf 는 여전히 옛 주소를 가리킨다 (dangling) */...if (buf) free(buf); /* ✗ NULL 이 아니므로 통과 → double free → 즉사 */
free(NULL) 은 합법이고 아무 일도 안 한다. 그러니 NULL 로 만들어두면 안전하다.
매크로로 감싸면 이걸 빼먹는 게 불가능해진다. 그게 매크로를 쓰는 이유다.
예전 버전은 포인터에 쓰면 sizeof 가 8 이라 8바이트만 복사되고 조용히 잘렸다. 크래시도 안 났다.
포인터에는 크기를 명시하는 snprintf(p, 64, "%s", src) 를 쓰고, 잘림 여부가 필요하면 str_copy() (종합본 §3-3) 를 쓴다.
또, 리터럴이 dst 보다 긴 게 확실하면 -Werror=format-truncation 이 여기서 터지는데 정상 동작이다.
4-3. 할당 실패도 반드시 검사
buf = malloc(len);if (buf == NULL) { LOG_ERRNO("malloc(%zu) failed", len); /* errno 를 먼저 저장하고 출력 */ ret = RET_NO_MEM; goto CLEANUP;}memset(buf, 0x00, len); /* ★ malloc 은 초기화를 안 해준다 */
위 4줄은 CHK_ALLOC_GOTO(buf, ret, CLEANUP); 한 줄과 같다 (로그는 변수 이름과 위치만 남긴다).