본문으로 건너뛰기 C++ 코드 리뷰 | '체크리스트' 20가지 [실무 필수]

C++ 코드 리뷰 | '체크리스트' 20가지 [실무 필수]

C++ 코드 리뷰 | '체크리스트' 20가지 [실무 필수]

이 글의 핵심

C++ 코드 리뷰 - "체크리스트" 20가지 [실무 필수]. C++ 코드 리뷰의 메모리 안전 (5개), 성능 (5개), 가독성 (5개)를 실전 코드와 함께 설명합니다.

메모리 안전 (5개)

C++는 가비지 컬렉터가 없는 언어이므로, 메모리 관리와 관련된 실수는 코드 리뷰에서 가장 먼저, 가장 꼼꼼하게 확인해야 할 항목입니다. 아래 다섯 가지는 리뷰 과정에서 실제로 자주 발견되는 메모리 관련 버그 패턴으로, 대부분 RAII와 스마트 포인터로 근본적으로 예방할 수 있습니다.

1. 메모리 누수 체크

new로 할당한 메모리를 짝이 되는 delete로 해제하지 않으면 그 메모리는 프로그램이 종료될 때까지 회수되지 않는 누수가 됩니다. 리뷰어는 코드에서 new가 등장할 때마다 반드시 대응하는 delete가 모든 실행 경로(정상 종료, 조기 반환, 예외 발생 포함)에서 호출되는지 확인해야 하며, 이 확인 자체가 번거롭고 실수하기 쉽다는 점에서 애초에 vector나 스마트 포인터 같은 RAII 컨테이너로 대체하는 것을 권장하는 것이 훨씬 근본적인 해결책입니다.

// ❌ 누수
void bad() {
    int* ptr = new int[100];
    // delete[] 없음!
}

// ✅ RAII
void good() {
    vector<int> v(100);  // 자동 해제
}

2. 댕글링 포인터

함수가 지역 변수의 주소를 반환하는 코드는 리뷰에서 가장 눈에 띄게 찾아야 할 위험 신호 중 하나입니다. 함수가 종료되면 지역 변수는 스택과 함께 소멸되므로 반환된 포인터는 더 이상 유효하지 않은 메모리를 가리키게 되고, 이를 역참조하면 미정의 동작으로 이어집니다. 리뷰 시에는 포인터나 참조를 반환하는 함수를 볼 때마다 “이 값이 가리키는 대상이 함수 종료 후에도 살아있는가”를 항상 확인하는 습관이 필요합니다.

// ❌ 위험
int* bad() {
    int x = 10;
    return &x;  // 지역 변수 주소 반환!
}

// ✅ 안전
int good() {
    int x = 10;
    return x;  // 값 반환
}

3. double delete

같은 포인터에 delete를 두 번 호출하는 것은 힙 메모리 관리자의 내부 상태를 손상시켜 크래시나 예측 불가능한 동작으로 이어지는 심각한 버그입니다. 이런 실수는 코드가 복잡해지고 소유권 관계가 불명확해질수록, 특히 같은 포인터가 여러 경로로 전달되는 코드에서 자주 발생합니다. 해제 직후 포인터를 nullptr로 초기화해 두면 실수로 다시 delete를 호출하더라도 nullptr에 대한 delete는 안전하게 아무 일도 하지 않으므로, 이 습관 하나만으로 상당수의 double delete를 예방할 수 있습니다.

// ❌ 크래시
int* ptr = new int(10);
delete ptr;
delete ptr;  // double delete!

// ✅ nullptr 설정
int* ptr = new int(10);
delete ptr;
ptr = nullptr;
delete ptr;  // 안전

4. 배열 범위 초과

배열이나 operator[]로 접근하는 컨테이너는 범위를 벗어난 인덱스로 접근해도 즉시 에러를 내지 않고 조용히 잘못된 메모리를 읽거나 써버리므로, 코드 리뷰에서 인덱스 계산 로직을 특히 주의 깊게 살펴봐야 합니다. 리뷰어는 반복문의 종료 조건이 <인지 <=인지, 인덱스가 사용자 입력이나 외부 데이터에서 온 값이라면 범위 검증이 이루어지는지를 확인해야 하며, 범위를 벗어나면 예외를 던지는 at()을 사용하도록 제안하는 것도 좋은 대안입니다.

// ❌ 위험
int arr[10];
arr[10] = 5;  // 범위 초과!

// ✅ 체크
if (index < 10) {
    arr[index] = 5;
}

// ✅ vector 사용
vector<int> v(10);
v.at(10) = 5;  // 예외 발생

5. 스마트 포인터 사용

새로 작성된 코드에 원시 포인터로 자원을 직접 관리하는 new/delete 쌍이 등장한다면, 리뷰어는 왜 스마트 포인터를 쓰지 않았는지 반드시 물어봐야 합니다. 아래 예시처럼 newdelete 사이에 복잡한 로직이 끼어들수록 중간에 예외가 발생하거나 조기 반환 경로가 추가될 때 delete를 빠뜨리기 쉬워지지만, std::unique_ptrstd::shared_ptr로 바꾸면 이런 실수 가능성 자체가 컴파일러 수준에서 사라집니다.

// ❌ raw 포인터
int* ptr = new int(10);
// ... 복잡한 로직 ...
delete ptr;  // 깜빡할 수 있음

// ✅ 스마트 포인터
auto ptr = make_unique<int>(10);
// 자동 해제

성능 (5개)

성능 문제는 메모리 안전성 문제와 달리 당장 프로그램을 망가뜨리지는 않지만, 방치하면 사용자 트래픽이 늘어날수록 병목으로 누적되는 특성이 있습니다. 아래 다섯 가지는 리뷰 중 무심코 지나치기 쉽지만, 짚어주면 즉시 개선할 수 있는 대표적인 성능 패턴입니다.

6. 불필요한 복사

함수 매개변수를 값으로 받으면 호출할 때마다 인자 전체가 복사되는데, 이 비용은 인자가 커질수록(큰 벡터, 문자열, 사용자 정의 클래스 등) 무시할 수 없는 수준이 됩니다. 리뷰어는 함수가 인자를 읽기만 하고 수정하지 않는다면 값 대신 const 참조로 받도록 권장해야 하며, 이 작은 변경만으로도 불필요한 메모리 할당과 복사 비용을 완전히 없앨 수 있습니다.

// ❌ 복사
void process(vector<int> data) {  // 복사!
    // ...
}

// ✅ 참조
void process(const vector<int>& data) {
    // ...
}

7. reserve 사용

std::vector는 용량이 부족해질 때마다 더 큰 메모리를 새로 할당하고 기존 원소를 옮긴 뒤 이전 메모리를 해제하는 재할당을 반복하는데, 최종 크기를 미리 알 수 있는 상황에서는 이런 재할당 자체가 낭비입니다. 반복문 전에 예상되는 최종 크기로 reserve()를 호출해 두면 벡터가 처음부터 충분한 공간을 확보하므로, 반복적인 push_back 과정에서 재할당이 전혀 일어나지 않아 성능이 크게 개선됩니다.

// ❌ 재할당 여러 번
vector<int> v;
for (int i = 0; i < 1000; i++) {
    v.push_back(i);
}

// ✅ 미리 할당
vector<int> v;
v.reserve(1000);
for (int i = 0; i < 1000; i++) {
    v.push_back(i);
}

8. 적절한 자료구조

std::vector에서 특정 값을 찾으려고 find를 반복 호출하는 코드는 매번 처음부터 끝까지 선형 탐색을 해야 해서 원소 수가 많아질수록 눈에 띄게 느려집니다. 검색이 빈번하게 일어나는 데이터라면 정렬된 트리 구조인 std::set이나 해시 기반의 std::unordered_set으로 바꾸면 각각 O(log n), 평균 O(1)로 검색 성능을 크게 개선할 수 있으며, 리뷰어는 반복적인 find 호출을 볼 때마다 자료구조 선택이 최선인지 되짚어 볼 필요가 있습니다.

// ❌ 느림 (O(n) 검색)
vector<int> v;
find(v.begin(), v.end(), x);

// ✅ 빠름 (O(log n) 또는 O(1))
set<int> s;
s.find(x);

unordered_set<int> us;
us.find(x);

9. 문자열 연결

std::string+=으로 반복해서 문자열을 이어붙이면 매번 내부 버퍼가 부족해질 때마다 재할당이 일어날 수 있어, 반복 횟수가 많아질수록 성능이 급격히 나빠집니다. std::ostringstream을 사용하면 스트림 내부에서 버퍼를 효율적으로 관리하며 여러 조각을 이어붙인 뒤 마지막에 한 번만 문자열로 변환하므로, 대량의 문자열을 조합해야 하는 코드에서는 이 패턴을 우선적으로 고려해야 합니다.

// ❌ 느림
string result;
for (const auto& s : strings) {
    result += s;  // 매번 재할당
}

// ✅ 빠름
ostringstream oss;
for (const auto& s : strings) {
    oss << s;
}
string result = oss.str();

10. move 시맨틱스

더 이상 사용하지 않을 객체를 다른 변수에 대입할 때 std::move를 쓰지 않으면 컴파일러는 안전하게 복사를 선택하며, 이는 큰 컨테이너나 문자열에서 불필요한 메모리 할당과 복사 비용으로 이어집니다. 리뷰어는 원본 변수를 대입 이후 다시 사용하지 않는다는 것이 확실한 지점을 발견하면 std::move로 자원 이동을 명시적으로 유도하도록 제안해, 복사 대신 포인터 이전만으로 대입이 끝나도록 개선할 수 있습니다.

// ❌ 복사
vector<int> v1 = getData();
vector<int> v2 = v1;  // 복사

// ✅ move
vector<int> v1 = getData();
vector<int> v2 = move(v1);  // move

가독성 (5개)

가독성은 코드가 당장 동작하느냐와는 별개로, 몇 달 뒤 다른 사람(또는 미래의 자신)이 그 코드를 이해하고 안전하게 수정할 수 있느냐를 좌우합니다. 아래 다섯 가지는 가독성을 해치는 대표적인 습관들로, 대부분 사소해 보이지만 누적되면 코드베이스 전체의 유지보수 비용을 크게 늘립니다.

11. 명확한 변수명

dtmp처럼 의미를 알 수 없는 짧은 변수명은 코드를 작성한 시점에는 문맥이 뻔해 보이지만, 나중에 코드를 다시 읽을 때는 그 의미를 다시 추론해야 하는 인지적 부담을 만듭니다. 리뷰어는 변수명만 보고 그 역할을 짐작할 수 없다면 daysUntilExpiry, userCount처럼 값이 무엇을 나타내는지 드러나는 이름으로 바꾸도록 요청해야 하며, 이는 코드 자체가 최소한의 문서 역할을 하게 만드는 가장 저렴한 투자입니다.

// ❌ 불명확
int d;  // 뭘 의미?
int tmp;

// ✅ 명확
int daysUntilExpiry;
int userCount;

12. 함수 길이

한 함수가 100줄이 넘어가면 그 함수가 정확히 무엇을 하는지 한눈에 파악하기 어려워지고, 함수 안의 지역 변수와 조건 분기가 서로 얽히면서 수정할 때 부작용을 예측하기도 힘들어집니다. 리뷰어는 함수가 여러 단계의 서로 다른 책임(검증, 변환, 저장 등)을 한꺼번에 처리하고 있다면 각 단계를 이름이 명확한 작은 함수로 분리하도록 제안해야 하며, 이렇게 나누면 각 함수를 개별적으로 테스트하고 재사용하기도 훨씬 쉬워집니다.

// ❌ 너무 긴 함수 (100줄+)
void processData() {
    // ... 100줄 ...
}

// ✅ 작은 함수로 분리
void validateData() { /* ... */ }
void transformData() { /* ... */ }
void saveData() { /* ... */ }

void processData() {
    validateData();
    transformData();
    saveData();
}

13. 매직 넘버 제거

코드 안에 2100 같은 숫자가 그대로 등장하면, 그 숫자가 왜 그 값이어야 하는지 코드만 봐서는 알 수 없어 나중에 값을 바꿔야 할 때 어디를 고쳐야 할지 찾기도 어렵습니다. 리뷰어는 이런 매직 넘버를 발견하면 이름 붙은 상수나 enum class로 바꾸도록 요청해야 하며, 이렇게 하면 값의 의미가 이름으로 드러날 뿐 아니라 같은 값을 여러 곳에서 참조할 때 한 곳만 수정하면 되는 유지보수 이점도 함께 얻을 수 있습니다.

// ❌ 매직 넘버
if (status == 2) {  // 2가 뭐지?
    // ...
}

// ✅ 상수
const int STATUS_ACTIVE = 2;
if (status == STATUS_ACTIVE) {
    // ...
}

// ✅ enum
enum class Status { Inactive, Pending, Active };
if (status == Status::Active) {
    // ...
}

14. 주석

좋은 주석은 코드가 “무엇을” 하는지가 아니라 “왜” 그렇게 했는지를 설명해야 하는데, 코드를 그대로 한국어로 옮긴 듯한 주석이나 몇 년째 방치된 TODO는 오히려 코드를 읽는 데 방해가 됩니다. 리뷰어는 주석이 코드만 봐도 알 수 있는 내용을 반복하고 있다면 삭제를 제안하고, 반대로 왜 특정 값이나 방식을 선택했는지에 대한 배경 설명이 빠져 있다면 그 의도를 주석으로 남기도록 요청하는 것이 바람직합니다.

// ❌ 불필요한 주석
int x = 10;  // x에 10을 할당

// ❌ 오래된 주석
// TODO: 나중에 수정 (2020년)

// ✅ 의도 설명
// 타임아웃을 10초로 설정 (서버 응답 시간 고려)
int timeout = 10;

15. const 정확성

함수가 매개변수를 읽기만 하고 수정하지 않는다면 그 사실을 const로 명시하는 것이 좋은데, 이는 단순한 스타일 문제가 아니라 함수를 호출하는 쪽과 유지보수하는 쪽 모두에게 “이 함수는 인자를 바꾸지 않는다”는 계약을 코드 자체로 알려주는 역할을 합니다. 리뷰어는 참조나 포인터 매개변수를 볼 때마다 함수 내부에서 실제로 값을 변경하는지 확인하고, 변경하지 않는다면 const를 붙이도록 요청해 실수로 값을 변경하는 버그를 컴파일 타임에 방지할 수 있게 해야 합니다.

// ❌ const 없음
void print(vector<int>& v) {
    for (int x : v) {
        cout << x << " ";
    }
}

// ✅ const 추가
void print(const vector<int>& v) {
    for (int x : v) {
        cout << x << " ";
    }
}

안전성 (5개)

앞서 다룬 메모리 안전, 성능, 가독성이 코드 품질의 세 축이라면, 마지막으로 안전성은 예상치 못한 입력이나 예외 상황에서도 프로그램이 견고하게 동작하는지를 다룹니다. 아래 다섯 가지는 특히 프로덕션 환경에서 장애로 직결되기 쉬운 항목들입니다.

16. 예외 안전성

new로 자원을 할당한 뒤 그 사이에 예외를 던질 수 있는 연산을 호출하면, 예외가 실제로 발생했을 때 뒤따르는 delete가 실행되지 못하고 건너뛰어져 자원이 누수됩니다. 리뷰어는 원시 자원 관리 코드 사이에 예외를 던질 가능성이 있는 함수 호출이 끼어 있는지 확인해야 하며, 이런 패턴을 발견하면 RAII 컨테이너로 바꿔 예외가 발생하든 정상 종료되든 자원이 항상 안전하게 정리되도록 개선을 제안해야 합니다.

// ❌ 예외 시 누수
void bad() {
    int* ptr = new int[100];
    riskyOperation();  // 예외 발생 가능
    delete[] ptr;  // 실행 안 됨!
}

// ✅ RAII
void good() {
    vector<int> v(100);
    riskyOperation();  // 예외 발생해도 안전
}

17. nullptr 체크

포인터를 매개변수로 받는 함수가 그 포인터를 검증 없이 곧바로 역참조하면, 호출하는 쪽에서 실수로 nullptr을 넘겼을 때 프로그램이 크래시나 미정의 동작으로 이어집니다. 리뷰어는 포인터 매개변수를 받는 함수마다 nullptr 가능성을 검토하고, 검증이 빠져 있다면 명시적인 체크를 추가하거나 애초에 nullptr이 될 수 없는 참조 타입으로 시그니처를 바꾸도록 제안해야 합니다.

// ❌ 체크 없음
void process(int* ptr) {
    *ptr = 10;  // ptr이 nullptr이면?
}

// ✅ 체크
void process(int* ptr) {
    if (!ptr) {
        throw invalid_argument("ptr is null");
    }
    *ptr = 10;
}

18. 정수 오버플로우

부호 있는 정수 연산이 표현 가능한 최댓값을 넘어서면 C++ 표준상 미정의 동작이 발생하며, 이는 사용자 입력이나 외부 데이터를 기반으로 계산하는 코드에서 특히 위험합니다. 리뷰어는 덧셈이나 곱셈 같은 연산의 입력값이 외부에서 올 수 있는 경우 오버플로 가능성을 항상 의심해야 하며, 연산 전에 범위를 검사하거나 더 넓은 정수 타입을 사용하도록 제안해 이런 위험을 사전에 차단해야 합니다.

// ❌ 오버플로우 가능
int a = INT_MAX;
int b = a + 1;  // 오버플로우!

// ✅ 체크
if (a > INT_MAX - 1) {
    throw overflow_error("overflow");
}
int b = a + 1;

19. 입력 검증

setter나 생성자가 전달받은 값을 아무런 검증 없이 그대로 저장하면, 논리적으로 불가능한 값(음수 나이, 범위를 벗어난 값 등)이 객체 상태로 들어와 나중에 이 값을 사용하는 다른 코드에서 예상치 못한 오류를 일으킬 수 있습니다. 리뷰어는 외부에서 들어오는 값을 저장하는 지점마다 유효 범위를 검증하는 코드가 있는지 확인하고, 없다면 유효하지 않은 값에 대해 예외를 던지거나 명확히 거부하도록 요청해 잘못된 상태가 객체 안에 스며들지 않도록 막아야 합니다.

// ❌ 검증 없음
void setAge(int age) {
    this->age = age;  // 음수도 가능?
}

// ✅ 검증
void setAge(int age) {
    if (age < 0 || age > 150) {
        throw invalid_argument("invalid age");
    }
    this->age = age;
}

20. 스레드 안전성

여러 스레드가 동기화 장치 없이 같은 전역 변수나 멤버 변수를 동시에 읽고 쓰면 데이터 레이스가 발생하며, 이는 타이밍에 따라 발생 여부가 달라져 일반적인 테스트로는 재현되지 않는 경우가 많습니다. 리뷰어는 코드가 멀티스레드 환경에서 실행될 가능성이 있다면 공유 상태에 대한 접근이 mutexatomic으로 보호되고 있는지 반드시 확인해야 하며, 이런 검토는 문제가 실제 운영 환경에서 드물게 재현되는 버그로 나타나기 전에 미리 잡아낼 수 있는 중요한 방어선입니다.

// ❌ 경쟁 조건
int counter = 0;

void increment() {
    counter++;  // 스레드 안전하지 않음!
}

// ✅ mutex
mutex mtx;
int counter = 0;

void increment() {
    lock_guard<mutex> lock(mtx);
    counter++;
}

코드 리뷰 프로세스

지금까지 다룬 20가지 체크리스트를 사람이 매번 눈으로 하나씩 확인하는 것은 비효율적이며 실수도 생기기 쉽습니다. 효과적인 코드 리뷰 프로세스는 기계가 잘하는 반복적인 검사는 자동화 도구에 맡기고, 사람은 의도와 설계처럼 맥락이 필요한 부분에 집중하는 방식으로 역할을 나눕니다.

1. 자동 체크

컴파일러 경고, 정적 분석, 코드 포맷팅처럼 규칙이 명확한 검사는 사람이 리뷰하기 전에 자동화 도구로 미리 걸러내는 것이 효율적입니다. -Wall -Wextra -Werror로 컴파일 경고를 에러로 승격시키고, cppcheckclang-tidy 같은 정적 분석 도구를 CI에 통합해 두면 앞서 다룬 메모리 누수나 정수 오버플로 같은 문제의 상당수를 사람이 코드를 읽기도 전에 자동으로 잡아낼 수 있습니다.

# 컴파일 경고
g++ -Wall -Wextra -Werror

# 정적 분석
cppcheck --enable=all .
clang-tidy *.cpp

# 포맷 체크
clang-format -i *.cpp

2. 수동 체크

자동화 도구가 잡아낼 수 없는 부분, 즉 코드가 실제로 요구사항을 만족하는지, 테스트가 실제 시나리오를 충분히 다루는지, 설계가 팀의 아키텍처 방향과 맞는지는 사람이 직접 판단해야 합니다. 아래와 같은 체크리스트를 PR 템플릿에 포함시켜 두면, 리뷰어가 매번 무엇을 확인해야 하는지 놓치지 않고 일관된 기준으로 리뷰할 수 있습니다.

□ 코드가 요구사항을 만족하는가?
□ 테스트가 충분한가?
□ 에러 처리가 적절한가?
□ 성능 문제는 없는가?
□ 보안 취약점은 없는가?
□ 가독성이 좋은가?
□ 문서화가 되어 있는가?

3. 피드백 작성

같은 문제를 지적하더라도 피드백을 어떻게 전달하느냐에 따라 리뷰가 팀의 학습 기회가 될 수도, 감정적인 마찰이 될 수도 있습니다. 좋은 피드백은 구체적인 코드 위치와 근거를 함께 제시해 왜 그 변경이 필요한지 상대가 스스로 납득할 수 있게 하는 반면, 문제를 구체적으로 짚지 않고 코드 전체를 뭉뚱그려 평가하는 피드백은 받는 사람이 무엇을 어떻게 고쳐야 할지 알 수 없게 만듭니다.

✅ 좋은 피드백:
"line 42: vector를 const 참조로 받으면 복사를 피할 수 있습니다."

❌ 나쁜 피드백:
"이 코드는 엉망입니다."

실전 예시

지금까지 다룬 20가지 체크리스트가 실제 코드 하나에 얼마나 많이 겹쳐서 등장할 수 있는지, 리뷰 전과 리뷰 후 코드를 나란히 비교해 보면 명확하게 드러납니다.

예시 1: 리뷰 전 코드

아래 함수는 짧은 코드 안에 앞서 다룬 체크리스트 항목 중 네 가지 문제를 동시에 안고 있습니다. vector<int> data를 값으로 받아 불필요한 복사가 발생하고, 원시 포인터로 배열을 직접 관리하며, 반복문 종료 조건이 <=라서 배열 범위를 한 칸 초과하고, 마지막으로 배열을 new[]로 할당했으면서 delete[]가 아닌 delete로 해제해 정의되지 않은 동작을 일으킵니다.

void process(vector<int> data) {
    int* arr = new int[data.size()];
    
    for (int i = 0; i <= data.size(); i++) {
        arr[i] = data[i] * 2;
    }
    
    // ... 처리 ...
    
    delete arr;  // delete[] 아님!
}

문제점:

  1. 불필요한 복사 (vector)
  2. raw 포인터 사용
  3. 범위 초과 (i <= size)
  4. delete vs delete[]

예시 2: 리뷰 후 코드

리뷰 피드백을 반영한 아래 코드는 앞서 지적된 네 가지 문제를 모두 해결했습니다. 매개변수를 const 참조로 바꿔 복사를 없앴고, 원시 포인터 대신 vector로 결과를 담아 RAII의 이점을 얻었으며, 범위 기반 for문을 사용해 인덱스 계산 실수 자체를 원천적으로 방지했고, reserve()로 결과 벡터의 재할당 비용까지 함께 줄였습니다. 이렇게 하나의 짧은 함수를 고치는 과정만으로도 메모리 안전성, 성능, 가독성이라는 세 가지 체크리스트 카테고리를 동시에 개선할 수 있다는 것을 보여줍니다.

void process(const vector<int>& data) {
    vector<int> result;
    result.reserve(data.size());
    
    for (int value : data) {
        result.push_back(value * 2);
    }
    
    // ... 처리 ...
}

개선 사항:

  1. const 참조로 복사 제거
  2. vector 사용 (RAII)
  3. 범위 기반 for
  4. reserve로 성능 개선

FAQ

Q1: 코드 리뷰는 얼마나 자주?

A: 모든 PR/커밋마다 리뷰하는 것이 이상적입니다.

Q2: 리뷰어는 몇 명?

A: 최소 1명, 중요한 코드는 2명 이상 권장합니다.

Q3: 리뷰 시간은?

A: 200-400줄당 1시간 정도가 적절합니다.

Q4: 자동화 도구는?

A:

  • clang-tidy
  • cppcheck
  • SonarQube
  • Coverity

Q5: 코드 리뷰 문화는?

A:

  • 건설적인 피드백
  • 코드를 공격하지 말고 개선 제안
  • 배우는 기회로 활용

Q6: 리뷰 체크리스트를 만들려면?

A:

  1. 팀의 과거 버그 분석
  2. 자주 발생하는 실수 정리
  3. 코딩 스타일 가이드 포함

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글


이 글에서 다루는 키워드 (관련 검색어)

C++, 코드리뷰, 체크리스트, 코드품질, 실무 등으로 검색하시면 이 글이 도움이 됩니다.