낯선 C 코드베이스 분석 순서 (방법론)

NOTE

위 도구들(ctags/cscope/GDB)을 실전에서 어떤 순서로 조합해 쓰는지의 적용편. 디렉터리 구조 → Makefile → main() → 헤더 → 콜백 역추적 → 공통 모듈 → 외부 설정 의존성 → 기존 문서 대조 → 실측 검증 순으로 정리한다.

0. 목적

문서도 없고 IDE(LSP)도 없이, 콜백/이벤트 기반으로 짜인 레거시 C 코드베이스를 처음 열었을 때 어디서부터 어떤 순서로 읽어야 헤매지 않는지 정리한다. ctags·cscope 실전 설정과 활용이 다룬 개별 도구를 실전에서 어떤 순서로 조합해 쓰는지의 “적용편”이다. 특정 프로젝트 얘기가 아니라 일반화한 순서다.

1. 디렉터리 구조부터 훑기

코드 한 줄 읽기 전에 ls -la로 각 디렉터리를 전체 나열해서 모듈 경계와 파일 규모부터 그린다.

ls -la ./모듈디렉터리

⚠️ 함정: find . -iname "*health*"처럼 이름 패턴으로 찾으면, 이름에 그 패턴이 없는 파일(예: monitor.c, hc_context.h)을 놓친다. 패턴 검색은 “이미 뭘 찾는지 아는 상태”에서나 쓸모 있고, 처음 훑을 때는 디렉터리 전체 나열이 안전하다.

2. 빌드 설정(Makefile)부터 읽기

소스보다 Makefile을 먼저 연다. 소스 파일 목록 변수(SRCS 등)가 “이 실행파일에 실제로 컴파일되는 파일 목록” = 진입점 지도이고, 링크 라이브러리 목록 변수(LIBS/MLIBS 등)가 “이 모듈이 의존하는 외부 라이브러리” = 이 코드가 뭘 다루는 코드인지(IPC? DB? 암호화?)에 대한 첫 힌트다.

SRCS = app_main.c app_context.c app_request.c ...
LIBS = -lipc -ldb -lcrypto ...

이 두 줄만 봐도 “IPC와 DB를 쓰는구나, 암호화도 관련 있구나”까지 미리 짐작하고 들어갈 수 있다.

3. main()을 처음부터 끝까지 순서대로 읽기

진입 함수 하나를 찾아서, 그 안의 호출 순서를 그대로 따라간다. 이 순서 자체가 “이 프로세스가 살아있으려면 뭐가 먼저 준비돼야 하는가”라는 의존 관계 그래프다.

로그 init → context init → IPC init → 하트비트 스레드 생성
  → 통신 채널 open → 스택 init → 수신 스레드 생성 → 메인 루프 진입

그리고 실패 처리(goto cleanup)가 있으면, 정리 순서가 초기화의 정확한 역순인지 대조해서 본다. 역순이 아니면 리소스 누수 버그일 확률이 높다(플래그 없이 무조건 pthread_join을 부르는 것도 여기서 걸린다 — 생성 안 된 스레드를 join하면 undefined behavior).

4. .h를 .c보다 먼저 읽는다

.h의 함수 시그니처와 구조체 정의만 보고 “이 모듈이 뭘 하려는 건지” 먼저 가설을 세운 다음, .c에서 검증한다.

// handler.h
void on_request(msg_t *rcv_msg, void *user_data);
void on_response(msg_t *rcv_msg, void *user_data);

함수가 이 두 개뿐이면 “콜백 기반 구조, 요청/응답을 분리해서 처리하는구나”까지 .c를 열기 전에 짐작할 수 있다. ctags로 Ctrl+] 눌러 정의로 바로 점프하면서 이 과정을 반복한다.

5. 콜백 등록 지점을 앵커로 역추적

이벤트/콜백 기반 코드는 물리적으로 위→아래로 읽는 순서와 실제 실행 순서가 다르다. 이런 코드는 “함수 포인터가 어디서 등록되는가”를 찾은 다음, 거기서부터 “이게 언제·누구에게 불리는가”를 거꾸로 추적하는 게 핵심이다.

receive_once(ds, msg, mode, on_request, on_response, ctx);
//                    ^^^^^^^^^^ ^^^^^^^^^^^ 여기가 앵커

이 앵커를 찾았으면 cscope로 역방향 탐색한다.

:cs find c on_request   " on_request를 호출하는 곳

이 방식으로 “A 핸들러가 성공하면 그 안에서 다시 B라는 역방향 호출을 추가로 한다” 같은, 코드를 순서대로만 읽어서는 못 보는 2차 호출 체인을 찾아낼 수 있다.

6. 공통 모듈은 따로 떼어서 한 번에 읽는다

여러 실행 파일이 공유하는 디렉터리(common/류)가 있으면, 개별 모듈을 읽을 때마다 반복해서 다시 이해하지 말고 먼저 통째로 읽어서 “공통 기반”으로 확보해둔다. 이후 각 모듈을 읽을 때는 “이 공통 함수를 어떻게 조합해서 쓰는지”에만 집중하면 되므로 훨씬 빠르다.

7. 코드에 박힌 “외부 파일 경로 문자열”을 전부 모은다

레거시 C 코드는 설정 파일 경로가 대부분 하드코딩돼 있다. grep으로 이런 문자열을 싹 모은 다음, 로컬에 실제로 있는지 확인한다.

grep -rn '"\(/[a-zA-Z0-9_./]*\.\(ut\|conf\|map\|ini\)\)"' .
find / -iname "해당파일명" 2>/dev/null
  • 로컬에 있으면 → 바로 읽고 반영
  • 없으면 → “이건 실행 환경(원격 서버)에만 있는 설정”으로 분류하고, 왜 필요한지(코드 참조 위치·역할)를 정리해서 요청 목록으로 남긴다. 짐작으로 채우지 않는다.

이런 이름 기반 키/설정 테이블을 다루는 예시는 이름-키 매핑 테이블 방식의 IPC 키 관리 참고.

8. 기존 문서/컨벤션부터 확인한다

코드를 다 읽기 전에, 이미 있는 관련 노트나 설계 문서를 먼저 훑어본다. 목적은 두 가지다.

  1. 중복 작업 방지 — 이미 정리된 내용을 또 만들지 않는다.
  2. 스타일 맞추기 — frontmatter, 표/다이어그램 형식, 용어를 기존 문서와 통일해서 나중에 합쳐 봐도 이질감이 없게 한다.

9. 가설은 “확정”이 아니라 “표시”만 해두고, 실측값이 오면 정정한다

코드만 보고 세운 가설 중에는 런타임 설정 값을 봐야만 확정되는 것이 반드시 있다. 이런 건 단정하지 말고 “확인 필요”라고 명시적으로 표시해둔 다음, 실제 값을 받으면 재검증한다.

실제로 겪은 사례: 두 프로세스가 코드상 같은 목적지 문자열(예: "127.0.0.1:포트")을 쓰는 걸 보고 “이 값은 실제 라우팅 분기에 안 쓰이는구나”라고 판단했는데, 실제 라우팅 설정 파일을 받아보니 그 문자열이 물리적 목적지를 결정하는 첫 단계였고, 두 프로세스 행선지가 갈리는 건 그다음 단계(요청 경로 기반 매칭)에서였다. 코드 두 곳만 대조하고 내린 결론이 성급했던 것 — “이 값이 왜 같은가”에 대한 설명이 하나 이상 가능하면, 그중 하나를 코드만으로 확정 짓지 않는다.

10. 단계 ↔ 도구 매핑

단계쓰는 도구/명령
1. 디렉터리 구조ls -la, (신중하게) find
2. 빌드 설정cat Makefile
3. 진입점 순서에디터로 main() 그대로 읽기
4. 인터페이스 먼저vi + ctags(Ctrl+])
5. 콜백 역추적cscope(:cs find c, :cs find s)
6. 공통 모듈grep -rl '#include "공통헤더.h"'로 사용처 목록화
7. 외부 의존성grep -rn으로 경로 문자열 수집 + find로 존재 확인
8. 기존 문서리포지토리/노트 폴더 먼저 훑기
9. 검증죽었을 때/막혔을 때는 GDB로 실측

핵심

구조 파악(ls) → 빌드 설정(Makefile) → 진입점(main 순서대로)
   → 인터페이스 먼저(.h, ctags) → 콜백 역추적(cscope)
   → 공통 모듈 통합 이해 → 외부 의존성 목록화(grep+find)
   → 기존 문서 대조 → 가설은 표시만, 실측값으로 검증

두 가지가 핵심이다.

  • “어디서 시작하는가”를 항상 먼저 찾는다 — main(), 진입 함수, 또는 Makefile의 소스 목록 첫 줄. 그다음부터는 코드가 실제로 실행되는 순서를 따라가는 것이지, 파일에 적힌 물리적 순서를 따라가는 게 아니다.
  • 콜백/이벤트 기반 코드는 “등록 지점”을 앵커로 삼는다 — 함수 포인터가 등록되는 곳을 찾고, 거기서부터 “누가 언제 부르는가”를 cscope로 역추적하면 실행 흐름이 잡힌다.

관련 문서