문서 없는 코드베이스 생존기 — '이 코드 짠 사람 퇴사했는데요'

문서 없는 코드베이스 생존기 — '이 코드 짠 사람 퇴사했는데요'

"인수인계서요? 아 그거 구두로 했는데... 그 사람 지금 안 계시고..."


모든 개발자가 커리어에서 최소 한 번은 겪는 공포의 순간이 있음. "이 프로젝트 네가 맡아." 프로젝트를 열어봄. README.md 없음. 주석 없음. 테스트 없음. Wiki 없음. 인수인계 문서 없음. 이 코드를 짠 사람은 이미 퇴사했음. 아니 퇴사한 사람이 넘겨받은 건데, 그 사람도 퇴사했음. 원작자는 3세대 전에 사라졌음.

이것은 과장이 아님. IT 업계에서 일상적으로 벌어지는 일임. 코드 자체가 유일한 문서이고, git log가 유일한 역사서인 프로젝트. 이 글은 그런 상황에서 살아남는 법을 다룸.

공포의 첫 날

프로젝트를 처음 받았을 때의 전형적인 경험:

bash
# 1단계: 프로젝트 구조 파악
ls -la
# .gitignore, package.json, pom.xml 등으로 기술 스택 추측

# 2단계: README 확인
cat README.md
# "TODO: Add readme" (2018년에 쓴 마지막 커밋)

# 3단계: 어떻게 실행하는지 모름
npm start  # 에러
npm run dev  # 에러
# 의존성 설치가 안 됨
# Node.js 버전이 안 맞음
# 환경 변수가 없음
# DB가 필요한데 설정이 없음

# 4단계: git log 확인
git log --oneline -20
# fix
# fix
# hotfix
# asdf
# WIP
# temp
# ㅋㅋ
# 금요일 퇴근
# .
# update
커밋 메시지의 현실

위의 git log는 실제로 흔히 볼 수 있는 패턴임. "fix", "update", "asdf", "." 같은 커밋 메시지는 미래의 자신과 동료에게 보내는 저주의 편지임. 1년 후 이 커밋이 왜 필요했는지 아무도 모르게 됨. "fix: 결제 시 쿠폰 중복 적용 버그 수정" 같은 메시지를 쓰는 데 30초밖에 안 걸리는데, 그 30초를 아끼면 나중에 3시간을 써야 함.

코드 고고학 — 유적 발굴 기법

문서가 없으면 코드에서 정보를 뽑아내야 함. 이것을 "코드 고고학"이라 부름:

기법 1: git blame — 누가, 언제, 왜

bash
# 특정 파일의 각 줄을 누가 마지막으로 수정했는지 확인
git blame src/services/payment.ts

# 결과 예시:
# a3b4c5d (김개발 2023-03-15) function processPayment(order) {
# a3b4c5d (김개발 2023-03-15)   if (order.amount <= 0) {
# f1e2d3c (이시니어 2023-06-01)     throw new Error('금액 오류');
# a3b4c5d (김개발 2023-03-15)   }
# 7a8b9c0 (박인턴 2023-09-20)   // 임시 주석: 왜인지 모르겠는데
# 7a8b9c0 (박인턴 2023-09-20)   // 이거 빼면 에러남
# 7a8b9c0 (박인턴 2023-09-20)   order.amount = Math.floor(order.amount);

# 특정 커밋의 상세 내용 확인
git show f1e2d3c
# commit f1e2d3c
# Author: 이시니어
# Date: 2023-06-01
# Message: fix
# (... 이게 끝임. 왜 수정했는지 모름)
git blame 심화

git blame -L 10,20 file.ts로 특정 줄 범위만 볼 수 있고, git blame -w로 공백 변경을 무시할 수 있음. git log -p -S "processPayment" 명령으로 특정 함수가 처음 도입된 커밋을 찾을 수도 있음. 이것은 코드 고고학에서 가장 강력한 도구임.

기법 2: git log — 역사 추적

bash
# 특정 파일의 변경 이력
git log --follow -p -- src/services/payment.ts

# 특정 기간의 변경 사항
git log --after="2023-06-01" --before="2023-07-01" --oneline

# 특정 함수가 언제 추가/수정됐는지
git log -p -S "processPayment" --all

# 누가 가장 많이 커밋했는지 (핵심 기여자 파악)
git shortlog -sn --no-merges

# 가장 자주 수정되는 파일 (핫스팟 = 문제의 근원)
git log --format=format: --name-only | sort | uniq -c | sort -rn | head -20
# 자주 수정되는 파일 = 버그가 많거나 설계가 잘못된 파일

기법 3: 프로젝트 구조에서 아키텍처 추론

bash
# 디렉토리 구조로 아키텍처 추론
tree -d -L 3
# src/
#   controllers/    → MVC 패턴의 Controller
#   models/         → 데이터 모델
#   services/       → 비즈니스 로직
#   repositories/   → DB 접근
#   middlewares/     → 미들웨어
#   utils/          → 유틸리티
#   config/         → 설정
#   routes/         → 라우팅

# 엔트리 포인트 찾기
# package.json의 "main" 또는 "scripts.start" 확인
# app.ts, index.ts, server.ts, main.ts 등

# 의존성 분석
cat package.json | jq '.dependencies'
# express → REST API 서버
# sequelize → ORM (DB 접근)
# jsonwebtoken → JWT 인증
# redis → 캐싱
# bull → 작업 큐

기법 4: 테스트 코드 읽기 — 실행 가능한 문서

typescript
// 테스트 코드가 있다면 이것이 최고의 문서임
// "이 함수가 어떻게 동작해야 하는지"를 정확히 알려줌

describe('PaymentService', () => {
  describe('processPayment', () => {
    it('정상 결제 시 영수증을 반환해야 함', async () => {
      const order = {
        id: 'ORD-001',
        amount: 50000,
        method: 'card',
      };

      const receipt = await paymentService.processPayment(order);

      expect(receipt.status).toBe('completed');
      expect(receipt.transactionId).toBeDefined();
    });

    it('금액이 0 이하면 에러를 던져야 함', async () => {
      const order = { id: 'ORD-002', amount: -1000 };

      await expect(
        paymentService.processPayment(order)
      ).rejects.toThrow('금액 오류');
    });

    it('재고 부족 시 결제를 거부해야 함', async () => {
      // 이 테스트를 통해 결제와 재고 관리가 연결되어 있음을 알 수 있음
      mockInventory.setStock('PROD-001', 0);

      await expect(
        paymentService.processPayment(outOfStockOrder)
      ).rejects.toThrow('재고 부족');
    });
  });
});
테스트 = 실행 가능한 문서

테스트 코드가 잘 작성되어 있으면 문서 없이도 시스템을 이해할 수 있음. 테스트를 실행하면 "이 시스템이 어떤 동작을 보장하는지" 즉시 알 수 있음. 반대로 테스트가 없으면? 코드를 한 줄 바꿀 때마다 "이거 바꿔도 되나?" 공포에 시달리게 됨. Michael Feathers가 "레거시 코드 = 테스트 없는 코드"라고 정의한 이유임.

리버스 엔지니어링으로 비즈니스 로직 파악하기

데이터베이스에서 도메인 모델 추론

sql
-- 테이블 구조를 보면 비즈니스 도메인이 보임
SHOW TABLES;

-- users, orders, products, payments, coupons,
-- shipping_addresses, reviews, cart_items...
-- → 이커머스 시스템이구나

-- 테이블 관계 파악
DESCRIBE orders;
-- id, user_id, total_amount, status, coupon_id,
-- shipping_address_id, created_at, updated_at

-- status 컬럼의 가능한 값 확인
SELECT DISTINCT status FROM orders;
-- pending, paid, preparing, shipping, delivered,
-- cancelled, refunded
-- → 주문 상태 머신이 있음

-- 외래 키 관계 확인
SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_NAME IS NOT NULL;

API 엔드포인트에서 기능 파악

bash
# Express 라우트 찾기
grep -rn "router\.\(get\|post\|put\|delete\|patch\)" src/routes/

# 결과에서 API 맵 만들기:
# GET    /api/users           → 사용자 목록
# POST   /api/users           → 사용자 생성
# GET    /api/users/:id       → 사용자 상세
# GET    /api/orders           → 주문 목록
# POST   /api/orders           → 주문 생성
# PUT    /api/orders/:id/status → 주문 상태 변경
# POST   /api/payments         → 결제 처리
# POST   /api/payments/refund  → 환불 처리

환경 변수에서 외부 의존성 파악

bash
# .env.example 또는 코드에서 환경 변수 찾기
grep -rn "process\.env\." src/

# 결과 분석:
# DB_HOST, DB_PORT       → 데이터베이스
# REDIS_URL              → Redis 캐시/세션
# AWS_S3_BUCKET          → S3 파일 저장소
# SMTP_HOST              → 이메일 발송
# SLACK_WEBHOOK          → Slack 알림
# PG_API_KEY             → PG사 결제 연동
# GOOGLE_CLIENT_ID       → 구글 소셜 로그인
# SENTRY_DSN             → 에러 모니터링
.env 파일의 부재

환경 변수 설정 문서가 없으면 프로젝트를 실행할 수 없음. .env.example 파일이 없는 프로젝트는 코드에서 process.env를 grep해서 필요한 환경 변수 목록을 직접 만들어야 함. 이것만 해도 반나절은 걸림. .env.example 하나만 만들어두면 되는 건데...

코드에서 발견되는 고대 유물들

문서 없는 코드베이스에서 흔히 발견되는 패턴들:

주석이 거짓말하는 경우

typescript
// 가격을 계산한다
function calculateDiscount(user: User, items: CartItem[]) {
  // ↑ 함수명은 할인 계산인데 주석은 가격 계산이라고 함
  // 누군가 함수를 리팩토링하면서 주석을 안 바꿨음

  // 할인율은 10%이다
  const discountRate = 0.15;
  // ↑ 주석은 10%인데 실제 코드는 15%
  // 어느 게 맞는 거임?

  // 아래 코드는 사용하지 않음 (2022-03-15 김개발)
  // 하지만 삭제하면 안 됨
  if (user.level === 'VIP') {
    // VIP 할인 추가
    return items.reduce((sum, item) =>
      sum + item.price * 0.2, 0
    );
  }
  // ↑ "사용하지 않음"이라면서 실제로는 VIP 유저일 때 실행됨
  // 주석이 거짓말하고 있음

  return items.reduce((sum, item) =>
    sum + item.price * discountRate, 0
  );
}
거짓말하는 주석은 주석이 없는 것보다 위험함

잘못된 주석은 개발자를 잘못된 방향으로 유도함. "이 코드는 사용하지 않음"이라는 주석을 믿고 삭제했더니 VIP 할인이 사라져서 장애가 발생하는 시나리오가 현실에서 벌어짐. 코드는 거짓말을 안 하지만 주석은 거짓말함. 코드를 읽는 것이 주석을 읽는 것보다 신뢰성이 높음.

TODO의 무덤

typescript
// TODO: 나중에 리팩토링 (2019-05-10)
// TODO: 성능 개선 필요 (2020-01-15)
// TODO: 이 부분 보안 취약점 있음 (2020-06-30)
// FIXME: 동시성 문제 있음, 빨리 고쳐야 함 (2021-03-01)
// HACK: 임시 해결. 제대로 된 솔루션 필요 (2021-08-15)
// XXX: 왜 이렇게 동작하는지 모르겠음 (2022-01-20)
// TODO: 위의 모든 TODO를 해결한다 (2022-06-01)
// ... (2024년 현재, 아무것도 해결되지 않음)

function processOrder(order: Order) {
  // HACK: 특정 고객사에서 주문 금액이 음수로 들어오는 버그
  // 원인을 못 찾아서 일단 절대값으로 처리
  // 2021-04-10 박인턴
  order.amount = Math.abs(order.amount);

  // TODO: 왜 여기서 setTimeout을 쓰는지 모르겠음
  // 빼면 결제가 안 됨
  // 2020-12-01 김개발 (퇴사)
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(submitPayment(order));
    }, 100);
  });
}

매직 넘버의 숲

typescript
function calculateShipping(weight: number, distance: number) {
  if (weight <= 3) {
    return 3000;  // 3kg 이하? 3000원?
  } else if (weight <= 10) {
    return 5000;  // 10kg 이하? 5000원?
  } else if (weight <= 30) {
    return 8000;  // 왜 이 금액인지 아무도 모름
  }

  // 거리 기반 추가 요금
  if (distance > 100) {
    return 15000;  // 100km? 100miles?
  } else if (distance > 50) {
    return 12000;
  }

  // 이 37.5는 뭐임?
  const surcharge = weight * 37.5;
  return Math.ceil(surcharge / 100) * 100;  // 100원 단위 올림?
}

문서 없는 프로젝트에서 온보딩하는 현실적 방법

1단계: 실행부터 시키기 (1-2일)

bash
# 1. 기술 스택 확인
cat package.json  # Node.js
cat pom.xml       # Java/Maven
cat requirements.txt  # Python

# 2. 의존성 설치
npm install  # 에러 나면 Node.js 버전 확인

# 3. 환경 변수 설정
# .env.example이 없으면 코드에서 찾기
grep -rn "process.env" src/ | sort -u

# 4. DB 스키마 확인
# migration 파일이 있으면 실행
# 없으면 SQL 덤프 파일 찾기

# 5. 실행
npm run dev
# 에러 메시지를 하나씩 해결

2단계: 핵심 흐름 추적 (3-5일)

typescript
// 가장 중요한 비즈니스 흐름을 추적
// 이커머스라면: 회원가입 → 상품 조회 → 장바구니 → 결제

// 엔트리 포인트에서 시작
// POST /api/orders 요청이 들어오면 어떤 코드가 실행되는지
// 라우터 → 미들웨어 → 컨트롤러 → 서비스 → 리포지토리 순으로 추적

// 각 단계에서 console.log를 넣어서 실행 흐름 확인
console.log('=== OrderController.create 진입 ===');
console.log('요청 데이터:', JSON.stringify(req.body, null, 2));

3단계: 지도 만들기 (1주)

시스템 지도 (직접 만들어야 함):

[클라이언트]
    |
    v
[Nginx] → 정적 파일 서빙
    |
    v
[Express 서버]
    ├── /api/auth/*     → AuthService → users 테이블
    ├── /api/products/* → ProductService → products 테이블
    ├── /api/orders/*   → OrderService
    │                      ├── products 테이블 (재고 확인)
    │                      ├── orders 테이블
    │                      ├── PaymentGateway (외부 PG)
    │                      └── NotificationService
    │                           ├── 이메일 (SMTP)
    │                           └── Slack (Webhook)
    └── /api/admin/*    → AdminService → 관리자 기능

외부 의존성:
- PostgreSQL (메인 DB)
- Redis (세션 + 캐시)
- AWS S3 (이미지 저장)
- 토스페이먼츠 (결제)
- SendGrid (이메일)
지도는 최고의 문서

이 지도를 만드는 데 1주일이 걸리지만, 한 번 만들면 새로 합류하는 팀원이 1일 만에 시스템을 파악할 수 있음. 이것이 바로 "문서의 가치"임. 과거의 개발자가 이걸 만들어놨으면 내가 1주일을 안 써도 됐을 텐데. 그래서 이 지도를 만드는 것이 "미래의 동료를 위한 투자"임.

비즈니스 로직 발굴의 기술

if문의 고고학

비즈니스 로직은 보통 if문에 숨어있음:

typescript
function processRefund(order: Order, reason: string) {
  // 이 if문들이 비즈니스 규칙임
  if (order.status !== 'delivered') {
    // 규칙 1: 배송 완료 상태에서만 환불 가능
    throw new Error('배송 완료 후에만 환불 가능');
  }

  const daysSinceDelivery = getDaysSince(order.deliveredAt);

  if (daysSinceDelivery > 7) {
    // 규칙 2: 배송 후 7일 이내만 환불 가능
    throw new Error('환불 기간 초과');
  }

  if (order.items.some(item => item.category === 'food')) {
    // 규칙 3: 식품은 환불 불가
    throw new Error('식품류는 환불 불가');
  }

  if (order.usedCoupon && order.items.length === 1) {
    // 규칙 4: 쿠폰 사용 주문의 마지막 상품은
    // 환불 시 쿠폰도 함께 회수
    revokeCoupon(order.couponId);
  }

  // 규칙 5: 부분 환불 시 배송비는 환불 안 됨
  const refundAmount = order.totalAmount - order.shippingFee;

  return createRefund(order.id, refundAmount, reason);
}
코드에서 비즈니스 규칙 추출하기

위 함수에서 5개의 비즈니스 규칙을 발견할 수 있음. 이걸 문서화하면: 1) 배송 완료 후에만 환불 가능 2) 7일 이내 3) 식품 제외 4) 쿠폰 회수 조건 5) 배송비 비환불. 이런 규칙은 코드에만 존재하고 어디에도 문서화되어 있지 않은 경우가 대부분임. 코드에서 이런 규칙을 추출해서 문서화하는 것이 레거시 코드 이해의 핵심임.

예외 케이스에서 비즈니스 로직 찾기

typescript
// catch 블록, 에러 핸들러, 유효성 검사에 숨겨진 비즈니스 규칙

function createOrder(cart: Cart, user: User) {
  // 유효성 검사 = 비즈니스 규칙
  if (cart.items.length === 0) {
    throw new Error('장바구니가 비어있습니다');
  }

  if (cart.totalAmount < 10000) {
    // 비즈니스 규칙: 최소 주문 금액 10,000원
    throw new Error('최소 주문 금액은 10,000원입니다');
  }

  if (user.point < cart.usedPoint) {
    // 비즈니스 규칙: 사용 포인트 > 보유 포인트 불가
    throw new Error('포인트가 부족합니다');
  }

  const maxItems = user.level === 'VIP' ? 50 : 20;
  if (cart.items.length > maxItems) {
    // 비즈니스 규칙: VIP는 50개, 일반은 20개까지
    throw new Error(`최대 ${maxItems}개까지 주문 가능합니다`);
  }

  // 재고 체크
  for (const item of cart.items) {
    const stock = getStock(item.productId);
    if (stock < item.quantity) {
      // 비즈니스 규칙: 재고 부족 시 주문 불가
      throw new Error(`${item.name} 재고 부족`);
    }
  }

  // ... 주문 생성 로직
}

문서화 문화 만들기

ADR (Architecture Decision Record)

markdown
# ADR-001: 결제 시스템을 토스페이먼츠로 변경

## 상태: 승인됨

## 맥락
- 기존 PG사(이니시스)의 API가 불안정하고 문서가 부실함
- 토스페이먼츠가 개발자 친화적인 API를 제공함
- 수수료 차이: 이니시스 3.3% vs 토스페이먼츠 3.0%

## 결정
- 신규 결제는 토스페이먼츠로 처리
- 기존 정기결제는 이니시스 유지 (마이그레이션 비용 높음)
- 3개월간 양쪽 병행 후 이니시스 완전 종료

## 결과
- 결제 실패율 5% → 1.2%로 개선
- 개발 시간 40% 단축 (API 문서 품질 차이)

코드 주석 가이드

typescript
// BAD: 코드가 하는 일을 반복하는 주석
// i를 1 증가시킨다
i++;

// BAD: 의미 없는 주석
// 생성자
constructor() {}

// GOOD: "왜"를 설명하는 주석
// 토스페이먼츠 API는 금액을 정수로만 받아서
// 소수점 이하를 버림 (반올림하면 금액 불일치 발생)
const amount = Math.floor(order.totalAmount);

// GOOD: 비즈니스 규칙을 설명하는 주석
// 정책: VIP 고객은 주문 취소 시 수수료 면제
// (2023-06-01 마케팅팀 요청, JIRA-1234)
if (user.level === 'VIP') {
  cancellationFee = 0;
}

// GOOD: 비직관적인 코드의 이유를 설명
// setTimeout이 없으면 PG사 응답이 누락되는 경우가 있음
// PG사 측에서 최소 100ms 딜레이를 권장 (2023-03-15 기술지원 답변)
await new Promise(resolve => setTimeout(resolve, 100));
주석보다 좋은 것

주석보다 더 좋은 것은 자기 설명적인 코드임. const x = a * 0.15보다 const discountAmount = price * DISCOUNT_RATE가 훨씬 명확함. 매직 넘버에 이름을 붙이고, 함수명을 명확하게 쓰고, 변수명을 의미있게 지으면 주석의 80%는 불필요해짐. 나머지 20%인 "왜(Why)"에 대한 주석만 작성하면 됨.

온보딩 문서 템플릿

새로 합류하는 사람을 위한 최소한의 문서:

markdown
# 프로젝트 온보딩 가이드

## 1. 환경 설정
- Node.js v20 이상
- PostgreSQL 15
- Redis 7

## 2. 실행 방법
npm install
cp .env.example .env  # 환경 변수 설정
npm run db:migrate     # DB 마이그레이션
npm run db:seed        # 테스트 데이터
npm run dev            # 개발 서버

## 3. 핵심 아키텍처
[간단한 다이어그램]

## 4. 주요 비즈니스 규칙
- 최소 주문 금액: 10,000원
- 환불 기한: 배송 후 7일
- VIP 할인율: 15%
- 식품류 환불 불가

## 5. 알려진 기술 부채
- PaymentService 리팩토링 필요
- 테스트 커버리지 30% (목표 70%)
- API 응답 형식 통일 필요

## 6. 핵심 연락처
- 결제 관련: 토스페이먼츠 기술지원 (xxx-xxxx)
- 인프라: DevOps팀 (Slack #infra)
이 문서를 만드는 데 2시간이면 충분함

위 문서를 만드는 데 2시간이면 충분함. 근데 이 2시간을 안 쓰면 새로 합류하는 사람이 1-2주를 허비함. 팀에 5명이 새로 들어오면 5-10주의 시간이 낭비되는 셈. 2시간 투자로 10주를 절약할 수 있다면 이건 투자가 아니라 의무임.

실전 체크리스트

문서 없는 프로젝트를 받았을 때 순서대로 하면 되는 것들:

  1. README.md 확인 — 없으면 만들 예정이니 넘어감
  2. 기술 스택 파악 — package.json, pom.xml 등
  3. 실행 시도 — 에러 메시지를 통해 의존성 파악
  4. git log로 역사 파악 — 최근 변경 사항, 핵심 기여자
  5. DB 스키마 분석 — 테이블 구조로 도메인 이해
  6. API 엔드포인트 목록 추출 — 라우터 파일에서 추출
  7. 핵심 비즈니스 흐름 추적 — 가장 중요한 기능 1개부터
  8. 테스트 실행 — 있으면 실행해서 동작 확인
  9. 환경 변수 목록 정리 — .env.example 생성
  10. 발견한 것을 문서화 — 미래의 동료를 위해
고고학자의 노트

문서 없는 코드베이스를 만나면 화가 나는 게 당연함. "왜 문서를 안 남겼지?" "인수인계를 왜 이따구로 했지?" 하지만 그 감정을 원인 분석에 쓰지 말고, 행동에 쓰자. 지금 내가 파악한 것을 문서로 남기면, 다음 사람은 같은 고통을 겪지 않아도 됨. 레거시 코드의 고리를 끊는 것은 "누군가"가 아니라 "지금 이 코드를 보고 있는 나"의 역할임. 1시간 코드를 분석했으면 10분만 써서 발견한 것을 기록하자. 그 10분이 미래의 누군가에게 1시간을 선물하는 것임.