실무 함수의 80%가 이 모양이다.
malloc 도 파일도 없다.
그래서 제일 먼저 체화한다.
이 드릴의 문서 구성 (D1~D9 공통)
① 요구사항서 — 무엇을 만드나, 표로 확정한다.
② pseudo code 템플릿 — 보고 치는 뼈대이고, 회차마다 가린다.
③ 테스트 코드 — 이게 통과하면 끝이다.
④ 자주 하는 실수
⑤ 모범답안 — 맨 뒤에 접어 둔다.
변형 A/B/C 는 문제가 바뀌는 게 아니라 같은 함수 모양을 다른 소재로 다시 치는 것이다.
각 변형마다 ①②③이 모두 준비되어 있다.
0. 이 드릴로 체화할 것
int f(const in_t *in, out_t *out){ CHK_PTR(in); /* ① 인자 검사 = 첫 줄 */ CHK_PTR(out); CHK_RANGE(in->x, MIN, MAX); memset(out, 0x00, sizeof(*out)); /* ② 출력은 항상 초기화부터 */ /* ③ 여기서만 생각한다 */ return RET_OK; /* ④ 성공은 맨 끝 한 곳 */}
이 네 줄의 순서가 흔들리지 않는 게 목표다.
로직이 틀린 건 괜찮다.
순서가 틀리면 실패다.
회차별 진행
회차
변형
보는 것
순서
1회차
A MSISDN 정규화
① 요구사항서 ② pseudo code 전체
1. pseudo 를 한 줄씩 코드로 바꾼다. 2. ③ 테스트를 실행한다.
2회차
B IMSI 분해
① 요구사항서 ② pseudo code 의 번호와 제목만 (Level 2)
1. 번호만 보고 쓴다. 2. ③ 테스트를 실행한다.
3회차
C 타임스탬프 검증
① 요구사항서만
1. ③ 테스트를 먼저 읽는다. 2. 백지에서 친다.
1회차에 막히면 같은 변형을 한 번 더 친다.
3회차를 통과하면 A 를 백지로 다시 친다.
1. 요구사항서
1-A. 변형 A — MSISDN 정규화
가입자가 입력한 전화번호 문자열을 E.164 숫자열로 정규화한다.
함수: int msisdn_normalize(const msisdn_req_t *in, msisdn_rsp_t *out);
입력 msisdn_req_t
필드
타입
필수
범위 / 형식
비고
in_raw
char[32]
O
최대 31자, 허용 문자 0-9-+
널 종단을 믿지 않는다
def_cc
uint16_t
O
1 ~ 999
기본 국가코드
출력 msisdn_rsp_t
필드
타입
항상 채워짐
범위 / 형식
비고
e164
char[16]
성공 시만
숫자만, + 없음, 최대 15자
+1 필수
nsn
char[16]
성공 시만
국내번호
cc
uint16_t
성공 시만
def_cc 와 같다
len
uint8_t
성공 시만
e164 길이
규칙
- 와 공백은 무시한다.
+ 는 맨 앞에만 올 수 있다.
중간에 있으면 RET_BAD_FORMAT 이다.
+ 로 시작하면 뒤의 숫자가 def_cc 로 시작해야 한다.
다르면 미지원이다 (단일 국가 서비스).
+ 없이 0 으로 시작하면 선행 0 을 떼고 국가코드를 붙인다.
예) "010-1234-5678", def_cc=82 → "821012345678"
결과 숫자열이 15자를 넘으면 규격 위반이다.
실패하면 out 은 전부 0 이다.
쓰레기를 남기지 않는다.
에러
상황
리턴코드
로그
in / out 이 NULL, def_cc 범위 밖
RET_INVALID_ARG
가드가 남긴다
허용되지 않는 문자, + 가 중간, 숫자 없음, 15자 초과
RET_BAD_FORMAT
O
+ 뒤 국가번호가 def_cc 와 다름
RET_NOT_SUPPORTED
O
1-B. 변형 B — IMSI 분해
15자리 숫자 IMSI 를 MCC(3) + MNC(2 또는 3) + MSIN 으로 분해한다.
함수: int imsi_split(const imsi_req_t *in, imsi_rsp_t *out);
형식 검사가 먼저, 범위 검사가 나중이다.
2100-02-29 는 존재하지 않는 날짜이므로 형식 위반이다.
유효한 날짜라도 epoch 가 음수(1970 이전)이거나 2^31 이상(2038-01-19 03:14:08 UTC 부터)이면 미지원이다.
실패하면 out 은 전부 0 이다.
에러
상황
리턴코드
로그
in / out 이 NULL
RET_INVALID_ARG
가드가 남긴다
길이 ≠ 14, 숫자 외 문자, 존재하지 않는 날짜·시각
RET_BAD_FORMAT
O
유효하지만 epoch 가 범위 밖
RET_NOT_SUPPORTED
O
2. pseudo code 템플릿 — 보고 쳐서 익힌다
사용법
변형별로 아래 pseudo code 를 d1_pure/src/*.c 에 주석만 복사한다.
코드는 주석을 보고 내가 쓴다.
다 쓰면 [ ] 를 x 로 바꾼다.
막히면 §5 모범답안(A)의 해당 단계만 본다.
2-A. msisdn_normalize — Level 1 (1회차, 전부 본다)
int msisdn_normalize(const msisdn_req_t *in, msisdn_rsp_t *out){ /* [ ] 1. 지역변수 (전부 선언부에서 초기화) * digits[64], cc_str[8], n / i / cc_len = 0, has_plus = 0, nsn = NULL */ /* [ ] 2. 가드 (이 순서 그대로) * CHK_PTR(in) * CHK_PTR(out) * CHK_RANGE(in->def_cc, 1, 999) */ /* [ ] 3. out 과 digits 를 0 으로 초기화 */ /* [ ] 4. in_raw 를 한 글자씩 본다 * 루프 상한: i < sizeof(in->in_raw) && in_raw[i] != '\0' ← 널 종단을 믿지 않는다 * * - 허용 문자(0-9 - 공백 +)가 아니면 * → 로그, RET_BAD_FORMAT * - '+' 이면 * i != 0 이면 → 로그, RET_BAD_FORMAT * 아니면 has_plus = 1, 다음 글자 * - '-' 또는 공백이면 * → 건너뛴다 * - 숫자이면 * digits[n++] = c (digits 가 가득 차면 RET_BAD_FORMAT) * * 루프 뒤 n == 0 이면 → 로그, RET_BAD_FORMAT */ /* [ ] 5. cc_str = def_cc 를 문자열로 (snprintf) * cc_len = 그 길이 */ /* [ ] 6. nsn 시작 위치를 정한다 * * has_plus 이면 * digits 가 cc_str 로 시작하지 않으면 → 로그, RET_NOT_SUPPORTED * 시작하면 nsn = digits + cc_len * digits[0] == '0' 이면 * nsn = digits + 1 (선행 0 제거) * 이미 cc 로 시작하면 * nsn = digits + cc_len * 그 외 * nsn = digits * * nsn 이 빈 문자열이면 → 로그, RET_BAD_FORMAT */ /* [ ] 7. cc_len + strlen(nsn) > 15 이면 → 로그, RET_BAD_FORMAT */ /* [ ] 8. out 채우기: cc, len, nsn, e164 (cc_str + nsn) * 길이를 검증한 뒤 memcpy 로 복사한다 (strcpy 금지) */ /* [ ] 9. return RET_OK ← 성공은 맨 끝 한 곳 */}
2-B. imsi_split — Level 2 (2회차, 번호와 제목만 본다)
int imsi_split(const imsi_req_t *in, imsi_rsp_t *out){ /* [ ] 1. 지역변수 선언·초기화 */ /* [ ] 2. 가드 */ /* [ ] 3. out 0 초기화 */ /* [ ] 4. 길이 15 검사 + 전부 숫자인지 검사 (루프 상한에 sizeof 를 같이 건다) */ /* [ ] 5. MCC 3자리를 떼어 MNC 자릿수 표에서 찾기 (없으면 미지원) */ /* [ ] 6. MNC / MSIN 자르기 */ /* [ ] 7. out 채우기 (snprintf) */ /* [ ] 8. return RET_OK */}
Level 1 로 보고 싶으면 2-A 의 4 · 6 번 칸을 이렇게 바꿔 읽는다.
4. 입력 검증 길이 != 15 → RET_BAD_FORMAT 숫자가 아닌 문자 있음 → RET_BAD_FORMAT6. MNC 자릿수 표 { "450":2, "440":2, "310":3, "404":3 } MCC 가 표에 없으면 → RET_NOT_SUPPORTED mnc_len = 표 값 msin 길이 = 15 - 3 - mnc_len
2-C. ts_parse — Level 3 (3회차, 백지)
int ts_parse(const ts_req_t *in, ts_rsp_t *out){ /* 아무것도 안 본다. * §1-C 요구사항서만 보고 §0 의 네 줄 모양으로 친다. * * 막히면 세 가지만 찾아본다. * - 윤년 판정 * - 월별 일수 배열 * - days_from_civil */}
막혔을 때 찾는 순서
컴파일 에러가 나면 에러 문구를 본다.
unknown type name / implicit declaration 이면 include 누락이다.
man 3 <함수> 를 연다.
SYNOPSIS 맨 위의 #include 를 확인한다.
로직이 막히면 §3 테스트의 기대값을 먼저 읽는다.
요구사항이 애매한 부분은 테스트가 답이다.
그래도 안 되면 모범답안의 해당 단계 하나만 본다.
3. 테스트 코드 — 이게 통과하면 끝
d1_pure/src/msisdn.h imsi.h ts.h 구조체 2개 + 함수 선언 1개d1_pure/src/msisdn.c imsi.c ts.c 함수 구현d1_pure/test/test_msisdn.c 변형 A 시험 (3-0 매크로 + 3-A + main)d1_pure/test/test_imsi.c 변형 B 시험 (3-0 매크로 + 3-B + main)d1_pure/test/test_ts.c 변형 C 시험 (3-0 매크로 + 3-C + main)
시험 3개의 형태는 고정이다 (D1~D9 공통)
정상은 대표 입력 1개다.
경계는 최대·최소·한 칸 넘김, 그리고 에러 표의 행마다 1개씩이다.
NULL은 인자를 하나씩 NULL 로 넣는다.
그리고 실패한 호출 뒤에는 out 이 0 인지 확인한다.
§0 의 ② 를 시험으로 고정하는 것이다.
3-0. 공통 매크로
#include <stdio.h>#include <string.h>#include "common.h"#include "log.h"log_level_t g_log_level = LOG_LV_ERR; /* log.h 의 extern 정의. 시험 파일마다 한 번 */static int g_fail = 0;#define T_EQ_INT(_got, _exp) \ do { \ long long _g = (long long)(_got), _e = (long long)(_exp); \ if (_g != _e) { g_fail++; \ printf(" X %s:%d got=%lld exp=%lld\n", \ __func__, __LINE__, _g, _e); } \ } while (0)#define T_EQ_STR(_got, _exp) \ do { \ if (strcmp((_got), (_exp)) != 0) { g_fail++; \ printf(" X %s:%d got=\"%s\" exp=\"%s\"\n", \ __func__, __LINE__, (_got), (_exp)); } \ } while (0)
3-A. 변형 A 시험 — msisdn_normalize
#include "msisdn.h"static void test_normal(void) /* 1) 정상: 대표 입력 1개 */{ msisdn_req_t in = { .in_raw = "010-1234-5678", .def_cc = 82 }; msisdn_rsp_t out; T_EQ_INT(msisdn_normalize(&in, &out), RET_OK); T_EQ_STR(out.e164, "821012345678"); T_EQ_STR(out.nsn, "1012345678"); T_EQ_INT(out.cc, 82); T_EQ_INT(out.len, 12);}static void test_edge(void) /* 2) 경계: 에러 표의 행마다 1개 */{ msisdn_req_t in = { .in_raw = "+8210123456789012", .def_cc = 82 }; msisdn_rsp_t out; memset(&out, 0xFF, sizeof(out)); T_EQ_INT(msisdn_normalize(&in, &out), RET_BAD_FORMAT); /* 15자 초과 */ T_EQ_INT(out.e164[0], 0); /* ★ 실패하면 out 은 0 */ T_EQ_INT(out.cc, 0); snprintf(in.in_raw, sizeof(in.in_raw), "01-0-1234-5678"); T_EQ_INT(msisdn_normalize(&in, &out), RET_OK); /* '-' 는 전부 무시 */ snprintf(in.in_raw, sizeof(in.in_raw), "010+12345678"); T_EQ_INT(msisdn_normalize(&in, &out), RET_BAD_FORMAT); /* '+' 가 중간 */ snprintf(in.in_raw, sizeof(in.in_raw), "010-12ab-5678"); T_EQ_INT(msisdn_normalize(&in, &out), RET_BAD_FORMAT); /* 허용되지 않는 문자 */ snprintf(in.in_raw, sizeof(in.in_raw), "---"); T_EQ_INT(msisdn_normalize(&in, &out), RET_BAD_FORMAT); /* 숫자 없음 */ snprintf(in.in_raw, sizeof(in.in_raw), "+8112345678"); T_EQ_INT(msisdn_normalize(&in, &out), RET_NOT_SUPPORTED); /* 다른 국가 */}static void test_null(void) /* 3) NULL: 인자 하나씩 + def_cc 범위 */{ msisdn_req_t in = { .in_raw = "01012345678", .def_cc = 82 }; msisdn_rsp_t out; T_EQ_INT(msisdn_normalize(NULL, &out), RET_INVALID_ARG); T_EQ_INT(msisdn_normalize(&in, NULL), RET_INVALID_ARG); T_EQ_INT(msisdn_normalize(NULL, NULL), RET_INVALID_ARG); in.def_cc = 0; T_EQ_INT(msisdn_normalize(&in, &out), RET_INVALID_ARG); in.def_cc = 1000; T_EQ_INT(msisdn_normalize(&in, &out), RET_INVALID_ARG);}int main(void){ test_normal(); test_edge(); test_null(); printf(g_fail ? "FAIL %d\n" : "PASS\n", g_fail); return g_fail ? 1 : 0;}