CodeGym /행동 /C++ SELF /경고 정책: “warnings as errors”

경고 정책: “warnings as errors”

C++ SELF
레벨 30, 레슨 3
사용 가능

1. 경고 정책이 필요한 이유

컴파일러의 경고를 단순한 잔소리로 보면 확실히 짜증날 수 있습니다: 코드가 컴파일되고, 프로그램이 뭔가 출력하기도 하니까요 — 그럼 뭐가 문제일까요? 하지만 warnings를 고장에 대한 조기 경고로 보면, 그것들은 좋은 동료가 됩니다. 컴파일러는 피곤해지지 않고, 잊지 않으며, «작성자가 어떻게 하려 했는지 어렴풋이 기억한다» 같은 인간적 결함이 없으므로, 코드가 애매하거나 위험한 지점을 정직하게 알려줍니다.

신호등을 떠올려 보세요: 컴파일 오류는 빨간불로, 더 이상 진행할 수 없습니다. Warning은 노란불입니다. 형식적으로는 진행할 수 있지만, 항상 노란불을 무시하면 곧바로 “범퍼 수리비”를 내게 될지도 모릅니다. 경고 정책은 이 노란불을 어떻게 다룰지에 대한 프로젝트 내 합의입니다: 속도를 줄여서 살펴볼지, 아니면 그대로 밟을지를 결정하는 규칙이죠.

재미있는 점: 큰 기술 문서에서도 개발자들이 경고를 고치기 위해 진짜 노력하는 경우가 있습니다 — 문서 빌드에서 발생하는 “overfull hbox warnings” 같은 레이아웃 경고도 포함됩니다. 이는 심리적 기준점으로 작용합니다: 경고는 단순한 사소한 문제가 아니라 품질 신호이며, 관리하는 것이 바람직합니다.

실무에서의 “warnings as errors” 의미

“Warnings as errors”는 간단한 의미입니다: 프로젝트가 컴파일러 경고를 빌드 실패로 간주하기로 합의한다는 뜻입니다. 즉, 컴파일러가 «수상하다»고 하면 빌드는 멈추고, 코드의 의도를 명확히 할 때까지 진행하지 않습니다. 이는 컴파일러가 치기 어린 것이 아니라, 앞으로의 자신(과 동료)의 시간을 절약하기 위한 방식입니다.

개념을 혼동하지 않도록 다음 표를 기억하면 편합니다:

컴파일러 신호 프로그램이 빌드되나? 보통 무슨 의미인가 대응 방법
Error 아니오 코드가 확실히 유효하지 않다 반드시 고친다
Warning 예 코드는 유효하지만 의도/안전성이 의심된다 원인을 파악하고 의도를 명확히 한다
Nothing 예 컴파일러 관점에서 문제가 없어 보인다 좋다, 계속 진행

중요한 점: “warnings as errors”가 모든 warning = 버그라고 말하는 것은 아닙니다. 대신 이렇게 말합니다: «각 warning = 의사결정이 필요한 지점». 즉, 실제 문제를 고치거나, 아니면 의도가 명확하다는 것을 명시적으로 보여줘야 합니다.

핵심 원칙: 코드의 의도를 명확히 하자

초보자가 자주 하는 실수는 «어떻게든 warning을 없애자»는 접근입니다. C++에서는 특히 위험한데, 왜냐하면 «어떻게든»의 흔한 결과가 아무 곳에나 하는 static_cast이기 때문입니다. 코드는 컴파일되지만, 의미는 무엇인가요? 때로는 단순히 ‘Check engine’ 표시등에 임시로 테이프를 붙인 것과 같습니다.

올바른 생각은 이렇습니다: 컴파일러가 경고를 발생시키는 곳은 당신의 의도가 명확하지 않은 곳입니다. 따라서 적절한 타입으로 수정하거나, 코드 구조를 바꾸거나, 경계 검사를 추가하거나, 제한된 곳에서 의도적인 캐스트를 사용해 의도를 명확히 해야 합니다.

우리는 이 접근을 이미 강의에서 본 짧은 상황들로 연습할 것입니다: 사용하지 않는 값들, size()와 인덱스, 암시적 타입 변환 등. 네, 방 치우기 같을 수 있습니다: 처음엔 귀찮지만 나중에 잃어버린 충전기를 찾게 될 수도 있습니다.

2. 초보자가 자주 만드는 세 가지 경고 원인과 적절한 해결

사용되지 않는 변수: 수정 후 남은 '꼬리'

사용되지 않는 변수는 초보자가 접하기 좋은 경고 중 하나입니다. 보통 논리는 이렇습니다: 로직을 덜 작성했거나 실험 뒤에 찌꺼기를 남겼거나 변수 이름을 혼동한 경우가 많습니다. 이런 건 “어딘가에서 계산은 했으니 사용하고 있을 것”이라는 착각을 유발하기도 합니다.

작은 예를 보겠습니다:

#include <iostream>

int main() {
    int answer = 42;               // warning: unused variable 'answer'
    std::cout << "Hello!\n";       // Hello!
}

변수가 실제로 필요 없다면 가장 깔끔한 해결은 삭제하는 것입니다. 하지만 중간 단계로, 잠시 후에 필요할 변수가 있고 지금은 경고 정책 때문에 문제되는 경우가 있습니다. 그럴 때는 컴파일러에게 «여기서 일부러 사용하지 않습니다»라고 명시할 수 있습니다. 가장 간단한 트릭(어트리뷰트나 전체 경고 비활성화 없이)은 void로 캐스트하는 것입니다:

#include <iostream>

int main() {
    int answer = 42;
    static_cast<void>(answer);     // 명시적: 지금은 사용하지 않음
    std::cout << "Hello!\n";       // Hello!
}

이건 마법이나 임시방편이 아니라, 해당 지점이 «의도적»이라는 작은 표시입니다. 다만 이런 표시가 잦아지면 보통 리팩터링 신호이거나 개발 브랜치에 너무 많은 초안 코드가 남아 있음을 의미합니다.

컨테이너 순회에서의 signed/unsigned 문제

벡터는 크기를 std::size_t로 반환합니다(무부호 타입). 초보자는 인덱스를 int로 쓰는 경우가 많습니다. 그리고 i < v.size() 같은 비교에서 signed와 unsigned를 비교한다는 경고가 생길 수 있고, 경계에서 놀라운 동작이 발생할 수 있습니다.

간단한 예:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    for (int i = 0; i < v.size(); ++i) {     // warning: signed/unsigned compare
        std::cout << v[i] << '\n';
    }
}

의미상 올바른 수정은 인덱스가 컨테이너 관련 타입이어야 한다는 것입니다. 가장 간단한 선택은 std::size_t를 사용하는 것입니다:

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    for (std::size_t i = 0; i < v.size(); ++i) {
        std::cout << v[i] << '\n';
    }
}

여기서 중요한 점은: 우리가 단순히 warning을 '없앤' 게 아니라 코드를 더 논리적으로 만든 것입니다. 이제 타입만 봐도 i가 인덱스임을 알 수 있고, 비교가 적절합니다.

한 가지 주의: 때로는 인덱스를 뒤로 감소시키는 루프(예: size() - 1 에서 0 까지)에서 std::size_t가 함정이 될 수 있습니다. 무부호이기 때문에 «i >= 0» 같은 조건은 항상 참이 됩니다. 이런 경우는 루프 구조를 바꾸하는 것이 낫습니다(예: 다른 순회 패턴 사용). 지금 우리의 목표는 «타입이 의도를 표현한다»는 원칙을 이해하는 것입니다.

암시적 변환과 데이터 손실

세 번째 경고 생산지는 암시적 변환, 특히 값의 일부가 손실될 수 있는 경우입니다. 전형적인 예: 가격을 double로 표현하고 센트를 int로 얻으려 할 때, 컴파일러는 반올림을 깜빡했을 수 있다고 경고합니다.

#include <iostream>

int main() {
    double price = 3.99;
    int euros = price;            // warning: implicit conversion, loss of data
    std::cout << euros << '\n';   // 3
}

만약 소수 부분을 버리는 것이 의도라면(재무 계산에는 권장되지 않지만 예제로는 괜찮습니다), 이를 명시적으로 하세요:

#include <iostream>

int main() {
    double price = 3.99;
    int euros = static_cast<int>(price);    // 의도적으로 소수 부분을 버림
    std::cout << euros << '\n';             // 3
}

요점: static_cast는 모든 warning의 만병통치약이 아닙니다. 이는 «내가 무엇을 하는지 이해하고 있다»는 표시입니다. 이해 없이 남발하면 단지 더 위험한 코드를 자신감 있게 쓰는 법을 배우는 것뿐입니다.

경고 정책을 삶을 미워하지 않게 도입하는 방법

이미 경고가 수십 개 있는 프로젝트에서 “warnings as errors”를 바로 켜면, 다락을 정리할 때 고생하는 느낌과 비슷합니다. 형식적으로는 옳은 일이지만, 심리적으로는 모든 걸 끄고 숲으로 도망가고 싶어질 수 있습니다.

실용적인 전략은 보통 이렇습니다: 먼저 현재 설정에서 경고가 없는 조용한 빌드를 달성한 다음, 경고를 에러로 취급하는 정책을 도입합니다. 이유는 소음 감소입니다: 경고가 많으면 무감각해집니다. 경고가 0이면 새로운 경고는 이벤트가 되어, 변경한 바로 그 순간에 알아차리게 됩니다.

또한 좋은 습관은 경고를 고칠 때 코드가 더 읽기 쉬워지도록 고치는 것입니다. 경고를 고친 후 형변환 세 개, 수수께끼 같은 상수 두 개와 «건드리지 마세요, 작동합니다»라는 주석이 생겼다면, 아마도 증상을 치료한 것이지 원인을 고치지 않은 것입니다.

5. MiniTracker와 경고를 품질 검증으로 이용하기

이 시점까지 코스에서 작은 콘솔 애플리케이션 MiniTracker가 있다고 가정합니다. 이 애플리케이션은 메모리에 간단한 '할 일'들을 저장하고 출력합니다. 지금은 아키텍처의 우아함보다, 예제가 살아있고 한 컨텍스트를 발전시키는 것이 중요합니다.

먼저 경고가 쉽게 날 수 있는 아주 간단한 '할 일 목록 출력' 스텁을 봅시다.

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"Read", "Code", "Sleep"};

    for (int i = 0; i < tasks.size(); ++i) {     // 잠재적 경고
        std::cout << i << ": " << tasks[i] << '\n';
    }
}

프로젝트에 엄격한 경고 정책이 적용되어 있다면 이 루프는 빌드를 통과하지 못할 수 있습니다. 우리는 컴파일러가 까다롭게 구는 것이 아니라 루프 자체가 더 깔끔하게 작성될 수 있음을 보여주기 위해 이를 고칩니다.

#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> tasks{"Read", "Code", "Sleep"};

    for (std::size_t i = 0; i < tasks.size(); ++i) {
        std::cout << i << ": " << tasks[i] << '\n';
    }
}

이제 MiniTracker에 아직 사용하지 않는 '초안' 함수를 추가해 보겠습니다. 이는 실제로 자주 있는 상황입니다: API를 잡았지만 아직 연결하지 않았을 때입니다.

#include <string>

int countDone(const std::string& task) {
    // TODO: 나중에 "done"이 무엇인지 결정
    return 0;                       // warning: task is unused (아마도)
}

만약 사용하지 않는 매개변수 때문에 빌드가 실패한다면, 선택지는 명확합니다: 매개변수가 필요 없다면 제거하거나, 아직 사용하지 않지만 계약은 유지하고 있음을 명시적으로 보여주어야 합니다. 현재 범위에서는(어트리뷰트에 들어가지 않고) 이렇게 쓸 수 있습니다:

#include <string>

int countDone(const std::string& task) {
    static_cast<void>(task);        // 네, 아직 사용하지 않음
    return 0;
}

전체 경고를 끄는 것보다 이 방법이 더 깔끔합니다. 전체 경고를 끄는 것은 경보음을 꺼버린 것과 같아서 문제를 숨길 뿐입니다.

미니 도식: 빌드에서 품질 필터로서의 warnings

아이디어를 고정시키기 위해, “warnings as errors”를 빌드 파이프라인의 작은 게이트로 상상하면 좋습니다. 프로그램은 여전히 전처리 → 컴파일 → 링크 단계를 거치지만, 컴파일 단계와 성공 사이에 추가 조건이 생깁니다: «경고가 없어야 한다».

flowchart TD
    A[소스 .cpp/.hpp] --> B[Preprocessing]
    B --> C[Compile: object files]
    C --> D{경고가 있나?}
    D -->|예| E[빌드 실패
코드 수정] D -->|아니오| F[Link: 실행 파일]

심리적으로 이 방식은 강력합니다: 노란불에 익숙해지지 않게 되고, 새로운 경고가 생기면 변경한 바로 그 순간에 즉시 인지할 수 있습니다.

6. “warnings as errors” 도입 시 흔한 실수

실수 1: 이미 경고가 많은 프로젝트에서 엄격한 정책을 즉시 켜고 고통받기.
경고가 수십 개 쌓여 있는 상태에서 “warnings as errors”를 활성화하면 개발이 마비될 수 있습니다. 사람들은 ‘빌드만 되게’ 수리하는 식으로 임시방편의 캐스트를 남발하게 되고, 코드가 이상해질 가능성이 큽니다. 더 건강한 접근은 먼저 경고를 0으로 줄인 다음 규칙을 강화하는 것입니다.

실수 2: 경고를 끄는 것으로 문제를 치료하려 하기.
경고 끄기는 이해하기 쉽지만 원인을 잃어버립니다. 진단을 넓게 끄면 현재 위치뿐 아니라 미래의 다른 위치에서 중요한 신호를 잃게 됩니다. 체크엔진에 테이프를 붙이는 것과 같습니다.

실수 3: static_cast을 만능 해결책으로 생각하기.
명시적 캐스트는 «왜 안전한가?»라는 내부적 질문에 대한 답과 함께 사용돼야 합니다. 답이 없다면 문제를 숨기는 것뿐입니다. 특히 unsigned와 signed를 섞어 조용히 해결하려는 시도는 경계와 오버플로우를 고려하지 않은 위험한 방법입니다.

실수 4: 경고를 고친 결과로 의도가 덜 명확해지는 경우.
경고를 없애는 방법은 여러 가지일 수 있습니다. 옳은 방법은 코드를 더 투명하게 만드는 방법입니다. 만약 경고를 없앤 뒤 가독성이 나빠졌다면, 아마도 잘못된 해결책을 선택했거나 함수에 적합한 계약이 없다는 신호입니다.

실수 5: '사소한 곳'의 경고를 무시하는 것.
컴파일러는 당신이 ‘명확하다’고 느끼는 것을 모릅니다. 코드만 봅니다. 종종 경고는 당신이 분명하다고 생각하는 곳에서 발생하고, 입력 데이터나 컨테이너 크기 또는 타입이 바뀌었을 때 문제가 드러납니다. “warnings as errors” 규율은 이런 곳들을 방치하지 않도록 합니다.

1
과제
C++ SELF, 레벨 30, 레슨 3
잠금
깨끗한 빌드
깨끗한 빌드
1
과제
C++ SELF, 레벨 30, 레슨 3
잠금
미래를 위한 매개변수
미래를 위한 매개변수
1
과제
C++ SELF, 레벨 30, 레슨 3
잠금
안전한 인덱스
안전한 인덱스
1
과제
C++ SELF, 레벨 30, 레슨 3
잠금
센트 손실 없이
센트 손실 없이
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION