[C언어 50강] 50강. 캡스톤: 텍스트 게임 또는 데이터 처리 도구 + 코드리뷰/리팩터링 체크리스트

[C언어 50강] 50강. 캡스톤: 텍스트 게임 또는 데이터 처리 도구 + 코드리뷰/리팩터링 체크리스트

C언어 50강의 마지막은 새 문법을 더 배우는 시간이 아니라, 지금까지 배운 문법을 제품처럼 조립하는 시간입니다. 오늘 주제는 텍스트 게임(예: 간단한 던전/퀴즈 게임) 또는 데이터 처리 도구(예: CSV 통계 계산기) 같은 캡스톤을 설계·구현하는 방법입니다. 핵심은 기능을 많이 넣는 것이 아니라, 작동 가능한 최소 버전(MVP)을 만들고, 코드리뷰와 리팩터링 체크리스트로 품질을 올리는 흐름을 익히는 것입니다.


핵심 개념

  • 캡스톤의 본질은 "문법 시연"이 아니라 "요구사항을 코드 구조로 변환"하는 과정이다.
  • 완성도는 기능 개수보다 입력 검증, 에러 처리, 데이터 구조 일관성, 재현 가능성에서 결정된다.
  • 코드리뷰 체크리스트와 작은 리팩터링 반복이, 한 번에 크게 고치는 것보다 안정성과 학습 효과가 높다.

개념 먼저 이해하기

캡스톤을 시작하면 많은 학습자가 바로 "뭘 만들까"에 몰입합니다. 물론 주제 선택은 중요하지만, 실제 실패 지점은 주제보다 설계 순서에서 발생합니다. 예를 들어 텍스트 게임을 만든다고 할 때, 적 체력·플레이어 인벤토리·맵 이동·세이브 기능을 한 번에 넣으려 하면 함수 길이가 길어지고 전역 변수가 늘어나며, 어느 순간 어떤 값이 왜 바뀌었는지 추적이 어려워집니다. 데이터 처리 도구도 비슷합니다. 처음에는 "CSV 읽고 평균만 계산"으로 시작했는데, 곧 필터·정렬·출력 포맷·에러 로깅을 한 함수에 붙이다가 유지보수 불가능 상태가 됩니다. 즉, 프로젝트 난이도는 주제보다 관심사 분리를 얼마나 빨리 적용했는지로 갈립니다.

캡스톤에서 가장 먼저 정해야 할 것은 기능 목록이 아니라 "불변식(invariant)"입니다. 게임이라면 "체력은 0 미만이 되지 않는다", "인벤토리 개수는 배열 크기를 넘지 않는다" 같은 규칙이 불변식입니다. 데이터 도구라면 "레코드 수 count는 항상 실제 유효 데이터 수와 같다", "파싱 실패한 줄은 처리 대상에서 제외된다" 같은 규칙이 해당됩니다. 이 불변식이 문서/주석/검증 코드로 명시되지 않으면, 버그가 생겼을 때 어디서 깨졌는지 알 수 없습니다. 반대로 불변식이 명확하면 디버깅은 "어느 함수가 규칙을 깨뜨렸나"를 찾는 문제로 단순화됩니다.

두 번째로 중요한 개념은 "상태 전이"입니다. 프로그램은 정지된 코드가 아니라 입력에 따라 상태가 바뀌는 시스템입니다. 게임에서 턴 진행은 입력 -> 유효성 검사 -> 상태 갱신 -> 출력 순환입니다. 데이터 도구에서 명령 실행도 파일 로딩 -> 파싱 -> 변환/집계 -> 결과 출력 순환입니다. 이 전이를 함수로 나누면 테스트 가능성이 생깁니다. 예를 들어 apply_command()render_state()를 분리하면, 렌더링 없이도 상태 갱신 로직만 단위 테스트할 수 있습니다. 이것이 실무에서 말하는 "비즈니스 로직과 I/O 분리"의 실체입니다.

세 번째는 "코드리뷰 기준을 사전에 정의"하는 습관입니다. 많은 초보자가 구현이 끝난 뒤 감으로 리뷰를 합니다. 그러면 리뷰 품질이 작성 당시 컨디션에 좌우됩니다. 대신 체크리스트를 고정하면 품질이 안정됩니다. 예: (1) 입력 경계값 검증 존재? (2) 반환값/오류코드 확인? (3) 메모리 소유권 명확? (4) 함수 하나의 책임만 수행? (5) 이름이 의도를 설명하는가? (6) 재현 가능한 테스트 절차 문서화? 이런 항목을 매번 통과시키면 코드 실력은 "센스"가 아니라 "재현 가능한 과정"으로 성장합니다.

마지막으로 리팩터링의 단위를 작게 가져가야 합니다. 큰 리팩터링은 멋져 보이지만, 한 번에 여러 함수를 건드리면 원인 추적이 어려워집니다. 캡스톤에서는 한 번에 한 가지 규칙만 적용하세요. 예를 들어 1차는 "중복 로직 함수화", 2차는 "전역 상태 구조체로 캡슐화", 3차는 "에러 코드 enum 통일"처럼 나눕니다. 매 단계마다 컴파일·실행·회귀 테스트를 통과시키면 프로젝트를 망가뜨리지 않고 품질을 올릴 수 있습니다. 결론적으로 캡스톤은 결과물이 아니라 문제를 다루는 태도를 훈련하는 과정이며, 그 태도가 이후 어떤 언어를 배우든 그대로 자산이 됩니다.

기본 사용

예제 1) 캡스톤의 최소 골격

#include <stdio.h>
#include <string.h>

#define MAX_CMD 64

typedef struct {
    int hp;
    int gold;
    int running;
} GameState;

void init_game(GameState* g) {
    g->hp = 100;
    g->gold = 0;
    g->running = 1;
}

int apply_command(GameState* g, const char* cmd) {
    if (strcmp(cmd, "hunt") == 0) {
        g->gold += 10;
        g->hp -= 5;
        if (g->hp < 0) g->hp = 0;
        return 1;
    }
    if (strcmp(cmd, "rest") == 0) {
        g->hp += 8;
        if (g->hp > 100) g->hp = 100;
        return 1;
    }
    if (strcmp(cmd, "quit") == 0) {
        g->running = 0;
        return 1;
    }
    return 0;
}

void render(const GameState* g) {
    printf("[STATE] HP=%d, GOLD=%d\n", g->hp, g->gold);
}

설명:

  • 상태 구조체를 명시해 "어떤 값이 프로그램 상태인지"를 분리했습니다.
  • 명령 처리(apply_command)와 출력(render)을 나눠 테스트 지점을 만들었습니다.
  • hp 경계값을 즉시 보정해 불변식(0~100)을 유지합니다.

예제 2) 데이터 처리 도구의 파이프라인 분리

#include <stdio.h>
#include <stdlib.h>

#define MAX_ROWS 1024

typedef struct {
    int values[MAX_ROWS];
    int count;
} Dataset;

int parse_line_to_int(const char* line, int* out) {
    char* endptr = NULL;
    long v = strtol(line, &endptr, 10);
    if (line == endptr) return 0;
    *out = (int)v;
    return 1;
}

double average(const Dataset* ds) {
    if (ds->count == 0) return 0.0;
    long long sum = 0;
    for (int i = 0; i < ds->count; i++) sum += ds->values[i];
    return (double)sum / ds->count;
}

int load_numbers(const char* path, Dataset* ds) {
    FILE* fp = fopen(path, "r");
    if (!fp) return 0;

    ds->count = 0;
    char buf[128];
    while (fgets(buf, sizeof(buf), fp)) {
        int v;
        if (parse_line_to_int(buf, &v) && ds->count < MAX_ROWS) {
            ds->values[ds->count++] = v;
        }
    }
    fclose(fp);
    return 1;
}

설명:

  • 파싱, 저장, 집계를 함수로 분해해 실패 원인을 좁힐 수 있습니다.
  • 파싱 실패 줄을 건너뛰는 정책을 코드에 반영해 견고성을 올렸습니다.
  • count를 유효 데이터 수로만 증가시켜 자료구조 일관성을 유지합니다.

예제 3) 코드리뷰를 코드로 강제하는 체크 함수

#include <assert.h>

int validate_game_state(const GameState* g) {
    if (g->hp < 0 || g->hp > 100) return 0;
    if (g->gold < 0) return 0;
    if (!(g->running == 0 || g->running == 1)) return 0;
    return 1;
}

void debug_guard(const GameState* g) {
    /* 디버그 빌드에서 불변식 파괴 즉시 발견 */
    assert(validate_game_state(g) && "GameState invariant broken");
}

설명:

  • 코드리뷰 항목(경계값/상태 무결성)을 런타임 검사로 전환했습니다.
  • "눈으로만 리뷰"보다 빠르게 회귀 버그를 발견할 수 있습니다.
  • 리팩터링 이후에도 불변식이 유지되는지 자동 확인하는 장치가 됩니다.

자주 하는 실수

실수 1) 기능을 먼저 늘리고 설계를 나중에 붙이기

  • 원인: "일단 돌아가게"를 반복하며 함수 책임이 뒤섞인다.
  • 해결: 최소 기능 3개만 먼저 정의(MVP)하고, 기능 추가는 체크리스트 통과 후에만 진행한다.

실수 2) 전역 변수로 상태를 무제한 공유

  • 원인: 매개변수 전달이 귀찮아 전역으로 우회한다.
  • 해결: 상태를 구조체 하나로 묶고 함수에 명시적으로 전달해 변경 지점을 추적 가능하게 만든다.

실수 3) 오류 처리 없이 성공 경로만 구현

  • 원인: 데모 입력만 가정하고 비정상 입력/파일 실패를 무시한다.
  • 해결: 모든 I/O 함수 반환값 확인, 실패 시 사용자 메시지 + 복구 경로(재시도/종료)를 설계한다.

실수 4) 리팩터링과 기능 추가를 한 커밋에 섞기

  • 원인: "한 번에 정리"하려다 변경 영향이 커진다.
  • 해결: 리팩터링 커밋(동작 동일)과 기능 커밋(동작 변경)을 분리해 회귀 원인 추적성을 확보한다.

실무 패턴

  • MVP 우선: 30분 안에 끝나는 최소 기능을 먼저 완성하고, 그 위에 단계별 확장한다.
  • 체크리스트 기반 리뷰: 입력 검증, 메모리 안전, 에러 처리, 이름 품질, 재현 문서 5개를 고정 점검한다.
  • 작은 리팩터링 반복: 중복 제거 → 함수 분리 → 타입 명확화 순으로 한 단계씩 개선한다.
  • 관찰 가능성 확보: 로그 포맷을 통일하고, 실패 케이스를 재현 가능한 입력 파일로 보관한다.
  • 회귀 테스트 습관: 버그가 한 번 났던 입력은 반드시 테스트 세트에 추가해 재발을 막는다.

오늘의 결론

한 줄 요약: 캡스톤의 목적은 "큰 프로그램"이 아니라, 요구사항을 안전한 코드 구조로 바꾸고 체크리스트로 품질을 꾸준히 끌어올리는 개발 습관을 만드는 것이다.

연습문제

  1. 텍스트 게임을 선택했다면 shop, fight, save 3개 명령만 가진 MVP를 만들고, 각 명령의 실패 조건을 표로 정리해 보세요.
  2. 데이터 처리 도구를 선택했다면 "입력 파일 손상 줄 10% 포함" 상황에서도 프로그램이 종료되지 않도록 로딩 정책(건너뛰기/중단)을 설계해 보세요.
  3. 본인 프로젝트에 코드리뷰 체크리스트 8개를 작성하고, 체크리스트 위반 항목 2개를 실제 리팩터링으로 해결한 뒤 전/후 차이를 기록해 보세요.

이전 강의 정답

49강(관리 프로그램 + 파일 저장) 연습문제 해설 요약:

  • phone 필드 추가 시 하위 호환은 "3필드/4필드 모두 파싱" 전략으로 해결할 수 있습니다. 예: 먼저 4필드 파싱 시도, 실패하면 3필드 파싱으로 폴백.
  • updateStudentfindById로 대상 인덱스를 찾고, 점수 범위(0~100) 검증 실패 시 에러코드를 반환하는 방식이 안전합니다.
  • 백업 저장은 students.tmp 저장 성공 → 기존 students.csvstudents.bak로 이동 → tmpcsv로 교체 순서가 핵심이며, 각 단계 실패 시 롤백 정책을 분리해야 데이터 손실 가능성을 줄일 수 있습니다.

실습 환경/재현 정보

  • 컴파일러: Apple clang 17.x 이상 또는 gcc 13+
  • 컴파일 옵션: -std=c11 -Wall -Wextra -Wpedantic -O0 -g
  • 실행 환경: macOS 15+ / Linux (UTF-8)
  • 재현 체크:
    1. 캡스톤 MVP 기능 3개만 구현한 초기 버전 실행
    2. 비정상 입력(빈 문자열, 너무 긴 입력, 잘못된 숫자)으로 실패 경로 검증
    3. 체크리스트 기반 코드리뷰 1회 수행 후 리팩터링 적용
    4. 리팩터링 전/후 동일 입력으로 동작 동일성(회귀 없음) 확인
    5. 최종적으로 README에 실행 방법, 입력 예시, 알려진 제약 사항 기록