CodeGym /행동 /C++ SELF /Watches: 변수, 표현식, 컨테이너 관찰

Watches: 변수, 표현식, 컨테이너 관찰

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

1. Watches의 기초: 왜 필요한가, 무엇이며 값의 홍수에 빠지지 않는 법

프로그램이 잘못 동작할 때 초보자가 가장 먼저 하고 싶은 일은 수십 개의 std::cout << "여기 살아있음" << '\n'; 을 찍고 버그가 도망가길 바라는 것입니다. 가끔은 효과가 있지만 대부분은 콘솔의 “스팸”과 어디서 값이 잘못됐는지에 대한 무지로 끝납니다. Watches는 바로 이 문제를 해결합니다: 코드 수정이나 출력물 오염 없이 각 멈춤 지점에서 중요한 값을 “눈앞”에 둘 수 있습니다.

중요한 생각은 단순합니다: 디버거가 좋은 것은 “모든 변수를 다 보여주기” 때문이 아니라 가설을 검증하는 데 도움을 주기 때문입니다. 여러분은 “인덱스가 범위를 벗어나는 것 같다” 또는 “합계가 어째서인지 0이 된다” 또는 “이 함수에 빈 문자열이 전달되었다” 같은 문장을 가질 수 있어야 합니다. Watches는 이런 가설을 빠르게 검증 가능하게 만드는 도구입니다.

예제로는 간단한 콘솔 학습용 애플리케이션 MiniBudget을 사용합니다 — 가장 단순한 “지출 기록”: 지출을 std::vector에 저장하고 항목을 추가하고 합계를 계산하고 카테고리로 검색합니다. 클래스나 OOP 같은 마법은 없고, 이미 알고 있는 것들만 사용합니다: struct, 함수, vector, string, 반복문.

watch란 무엇인가

Watch (관찰)는 디버거가 각 중단 시 계산하여 값을 보여주려고 하는 표현식입니다. 핵심 단어는 “표현식”이지 단순한 “변수”가 아닙니다. Watch는 sum일 수도 있고, i < v.size()일 수도 있고, v[i](하지만 주의점이 있음)일 수도 있으며, items.size()일 수도 있습니다.

이것은 Locals 창과 다릅니다. Locals는 보통 현재 scope에서 “발견된 것”만 보여줍니다: 함수 매개변수, 지역 변수 등. 반면 Watch는 여러분이 직접 선택하며 더 오래 유지됩니다: 다른 위치에서 중단해도 watch는 남아 있지만 (표현식이 현재 범위에 없으면 not available가 될 수 있습니다).

유용한 비유: Locals는 “현재 직원 책상 위에 있는 것”이고, Watch는 여러분이 관심 있는 곳에 설치한 “감시 카메라”입니다. 물론 카메라를 너무 많이 달면 흐름에 파묻힐 수 있습니다.

핵심 규칙: 하나의 가설 → 여러 관찰 → 결론

디버깅이 혼돈으로 변할 때 거의 항상 원인은 사람이 25개의 watches를 “혹시 몰라서” 추가했기 때문입니다. 디버거는 수많은 숫자를 보여주고 뇌는 꺼집니다. 따라서 다음 규율을 기억합시다: 하나의 가설 → 여러 관찰 → 결론.

보통 3–8개의 표현식이면 충분합니다. 예를 들어 vector에 대한 반복문을 디버깅할 때 전형적인 최소 세트는 다음과 같습니다:

의심 항목 Watch에 넣을 것 이유
범위 초과
i, v.size(), i < v.size()
조건이 “깨지는” 순간을 빠르게 확인
잘못된 합계
i, sum, added
어떤 단계에서 합계가 틀려졌는지 이해
if 분기가 잘못된 경우 cond, 조건의 부분 표현식들 어떤 부분 표현식이 false를 만드는지 보기

지금은 “자명해 보일”지 모르지만 실무에서는 반대로 하는 경우가 많습니다: v[i]만 watch에 넣고 i와 v.size()는 넣지 않습니다. 결과는 갑작스런 cannot evaluate나 크래시이고, 다시 “도구가 작동하지 않는다”는 느낌을 받게 됩니다.

2. 미니 예제: 반복문, std::vector, std::string

범위 초과 및 합계의 off-by-one 버그 디버깅

반복문에서 합계를 계산할 때 전형적인 실수는 경계 조건을 i < n 로 쓴 대신에 i <= n 로 써야 했거나 그 반대인 경우입니다. 코드만 보면 괜찮아 보이지만 결과는 약간 달라집니다. 이런 경우에 watches는 이상적입니다: 실제로 i가 어디까지 갔는지와 합계가 어떻게 변했는지 보고 싶습니다.

우리 MiniBudget의 첫 n개의 지출을 합산하는 함수 예제는 다음과 같습니다:

#include <cstddef>
#include <vector>

int sum_first_n(const std::vector<int>& amounts, std::size_t n) {
    int sum = 0;
    for (std::size_t i = 0; i < n; ++i) { // 버그: n이 amounts.size()보다 큰 경우
        sum += amounts[i];
    }
    return sum;
}

이상한 결과나 크래시가 보일 때 watches에 무엇을 넣을까요:

i, n, amounts.size(), i < amounts.size(), sum

여기서 매우 중요한 습관이 생깁니다: 값뿐만 아니라 조건의 “논리적 건강”을 보세요. Watch i < amounts.size() 는 작은 불변성 센서입니다. 이것이 true인 동안에는 비교적 안심할 수 있습니다. 한번이라도 false가 되면 프로그램이 위험한 상태로 들어가는 지점을 찾은 것입니다.

우리는 바로 코드를 고칠 필요는 없습니다. 먼저 사실을 고정합니다: n이 컨테이너 크기보다 컸고, 반복문이 “과도한” 인덱스에 도달했으며 그 이후의 결과는 예측 가능한 것입니다.

std::vector를 위한 Watches: 요소를 안전하게 보는 방법

컨테이너에는 좋은 보너스가 있습니다: 많은 IDE가 std::vector를 예쁘게 펼쳐 보여줍니다(요소 목록처럼). IDE가 그리 똑똑하지 않아도 필요한 부분을 직접 관찰할 수 있습니다. 핵심은 디버깅 도중에 또 다른 버그를 만들지 않는 것입니다.

나쁜 습관: v[i]를 watch에 넣는 것, i가 때때로 “나쁜 값”을 가질 때입니다. 그러면 디버거가 그 표현식을 계산하려다가 계산 오류를 내거나 UB 영역으로 들어갈 수 있습니다(디버거 구현에 따라 다름).

좋은 습관: 먼저 “안전망”이 되는 watches를 넣고 나서 요소를 확인하세요.

예를 들어 지출 목록이 있다고 합시다:

#include <string>

struct Expense {
    std::string category;
    int amount;
};

카테고리로 검색하는 함수:

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

int find_first_by_category(const std::vector<Expense>& items, const std::string& cat) {
    for (std::size_t i = 0; i < items.size(); ++i) {
        if (items[i].category == cat) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

검색이 “가끔은 못 찾는데 찾아야 하는 경우”라면 실제로 도움이 되는 watches는 다음과 같습니다:

i, items.size(), cat

추가로 유용한 것은: items[i].category (단, i < items.size()일 때만), 그리고 별도의 표현식으로 items[i].category == cat 을 넣는 것입니다.

여기서 흔히 보는 상황은, 예컨대 데이터에 카테고리가 "Food "(끝에 공백이 있음)으로 들어있고 여러분은 "Food"를 찾는 경우입니다. 보통 이런 상황에서 사람들은 모니터와 씨름하는데, watch는 5초 만에 진실을 보여줍니다.

std::string을 위한 Watches: 길이, 인덱스, 비교가 깨진 지점

문자열은 초보자들을 자주 괴롭힙니다. 시각적으로 "abc"와 "abc "는 거의 같아 보이기 때문입니다 (특히 끝의 공백이 있을 때). Watches는 비교 시점에 이 차이를 바로 보여줄 수 있습니다.

예를 들어 카테고리를 “정규화”하는 함수가 있다고 합시다: 앞뒤 공백을 제거하고 싶습니다(간단한 방법으로):

#include <string>

std::string trim_one_space_right(std::string s) {
    if (!s.empty() && s.back() == ' ') {
        s.pop_back();
    }
    return s;
}

정규화 후에도 뭔가 일치하지 않는다면, watches는 다음과 같을 수 있습니다:

s, s.size(), !s.empty()

또한 s.back() 는 (주의: !s.empty()일 때만) 유용합니다.

문자열 디버깅에서 흔한 장면은 watch가 s.size() == 0을 보여주는데 여러분은 여전히 s[0] 또는 s.back()를 보려고 합니다. 이 순간 디버거가 “문자열을 건드리지 마세요, 비어 있습니다”라고 조용히 알려주는 것과 같습니다. 그 말을 듣는 것이 좋습니다.

3. 뉘앙스: scope, stack frames 및 논리적 watches

Watch와 가시 범위(scope)

watch를 추가하면 가끔 회색/빨간색/not available로 바뀔 수 있습니다. 초보자는 보통 “디버거가 고장났나”라고 생각합니다. 실제로는 대부분의 경우 원인은 단순합니다: 변수가 scope를 벗어났기 때문입니다.

예: 변수 temp는 if 내부에서만 살아 있습니다.

#include <iostream>

int main() {
    int x = 10;

    if (x > 0) {
        int temp = x * 2;
        std::cout << temp << '\n'; // 20
    }

    return 0;
}

temp에 watch를 걸면 해당 블록 { ... } 안에 있을 때만 사용할 수 있습니다. 블록을 벗어나면 변수가 없어지고 watch가 이를 솔직하게 보여줍니다.

이것은 버그가 아니라 유용한 알림입니다: 지역 변수는 선언된 블록에서만 실제로 “삽니다”. 이 지식을 알면 일부 오류가 “가끔 나타났다 사라지는” 이유를 이해하는 데 큰 도움이 됩니다.

실용적인 결론: 값을 더 오래 관찰할 필요가 있으면(디버깅을 위해) 변수를 잠시 위쪽 레벨로 올리는 방법이 있습니다. 단, 이렇게 할 때는 주의하세요: 코드를 “혹시 몰라서 함수 시작부에 모든 변수를 둔다” 식으로 만들면 안 됩니다.

Watches와 stack frames

call stack에 대한 강의 후에는 한 가지 중요한 뉘앙스가 더 명확해집니다: watch 표현식은 현재 선택된 frame의 컨텍스트에서 계산된다는 점입니다. 다른 frame으로 전환하면 같은 이름이 다른 것을 가리킬 수 있습니다.

예를 들어 add_expense()가 문자열을 읽고 숫자를 파싱하기 위해 parse_amount()를 호출하고 어떤 문제가 생겼다고 합시다:

#include <string>

int parse_amount(const std::string& s) {
    int x = 0;
    // 어딘가에 파싱 버그가 있다고 가정
    return x;
}

void add_expense(const std::string& line) {
    int amount = parse_amount(line);
    (void)amount;
}

parse_amount 내부에서 중단했을 때 watch s는 parse_amount의 매개변수입니다. 그러나 위쪽 frame(add_expense)으로 전환하면 watch line는 다른 매개변수를 가리킵니다. 두 함수에 동일한 이름의 변수 x가 있으면 watch x는 frame에 따라 ‘복권’처럼 결과가 달라집니다.

따라서 작은 규율: watches를 신뢰하기 전에 올바른 stack frame을 보고 있는지 확인하세요.

논리적 watches: 불변식 관찰

문제는 특정 값 자체가 아니라 규칙 위반인 경우가 많습니다. 예: “인덱스는 항상 size()보다 작아야 한다”, “합계는 음수가 되어서는 안 된다”, “카테고리는 빈 문자열이면 안 된다”.

이를 위해 원시 데이터 대신 불리언 표현식을 watch로 설정하는 것이 편리합니다. 이렇게 하면 디버깅은 규칙이 언제 false가 되는지를 찾는 과정이 됩니다.

불변식 “amount는 > 0이어야 한다”의 미니 예제:

#include <vector>

int total_positive(const std::vector<int>& a) {
    int sum = 0;
    for (int x : a) {
        sum += x; // x가 음수인 경우 의심스러움
    }
    return sum;
}

유용한 watches는 x, sum, 그리고 표현식 x > 0입니다. 만약 x > 0가 갑자기 false가 되면, 질문은 “왜 합계가 틀렸나”가 아니라 “왜 컨테이너에 음수 지출이 들어왔나”가 됩니다.

이렇게 하면 점점 “값”이 아니라 “계약(contract)”을 디버깅하게 됩니다. 초보자라도 매우 성숙한 습관입니다.

4. 시나리오: Watches로 MiniBudget 디버깅하기

상황을 가정해봅시다: 사용자가 지출을 입력했는데 최종 합계가 예상보다 작습니다. 사용자와 언쟁을 벌이지 않고(그것도 기술이긴 하죠) 개발자가 하는 일을 합시다: 가설을 세우고 검증합니다.

합계를 계산하는 함수가 있다고 합시다:

#include <vector>

int total(const std::vector<int>& a) {
    int sum = 0;
    for (std::size_t i = 0; i < a.size(); ++i) {
        if (a[i] > 0) {
            sum += a[i];
        }
    }
    return sum;
}

때때로 지출이 음수로 입력되는 경우가 있습니다(예: 환불). 사용자는 절댓값으로도 합산되길 기대할 수 있습니다. 프로그램 로직이 요구사항과 다를 수 있지만 여기서는 디버깅 메커닉이 더 중요합니다.

합계에 대한 breakpoint를 sum += a[i]; 라인에 두고 watches에 다음을 추가합니다:

i, a.size(), a[i], a[i] > 0, sum

이후 첫 중단까지 Continue를 하고 보면: a[i] > 0가 자주 false입니다. 즉 이 분기문이 일부 값을 건너뛰고 있고, 그게 “작게 나온 합계”의 원인입니다.

핵심은 관찰된 사실에 따라 결론에 도달했다는 것입니다. “아마도 반복문이 잘못된 것 같다”는 추측이 아니라 관찰 가능한 증거로 결론을 내린 것입니다. 이것이 바로 watch 창의 존재 이유입니다.

간단한 프로세스 도식(과도하지 않게, 사람 친화적으로):

flowchart TD
    A[가설: 반복문에서 합계가 '사라진다'] --> B[의심되는 위치에 breakpoint 설정]
    B --> C[3-8개의 watches 추가: i, size, 요소, 조건, sum]
    C --> D[Continue/Step으로 변화 순간 관찰]
    D --> E[사실 고정: 어떤 분기나 값이 기대를 깨트리는지 확인]

5. Watches 사용 시 흔한 실수

실수 #1: watch 창을 아무것도 없는 쓰레기장으로 만드는 것.
20–30개의 표현식을 추가하면 프로그램이 아니라 잡음에 집중하게 됩니다. 결과적으로 어디서 값이 변경되었는지, 어떤 규칙이 깨졌는지를 놓치게 됩니다. 하나의 가설에 맞는 하나의 watch 세트를 유지하고, 끝나면 정리하고 새로 시작하세요.

실수 #2: 경계 확인 없이 v[i]를 watch에 넣는 것.
범위 초과를 의심하며 제일 먼저 v[i]를 넣는 경우가 많습니다. 하지만 이미 i가 잘못되었다면 그 표현식은 위험하고 쓸모가 없습니다. 먼저 i, v.size()와 i < v.size()를 관찰하고, 불변식이 유지될 때만 v[i]를 보세요.

실수 #3: 범위를 잊어버리는 것.
변수가 더 이상 존재하지 않아 watch가 “사라질” 수 있습니다: 블록 {}을 벗어났거나 stack frame을 바꾼 경우입니다. 이것은 디버거의 문제가 아니라 언어 모델의 정직한 반영입니다: 지역 변수는 선언된 위치에서만 살아 있습니다. watch가 사용 불가능해지면 먼저 현재 위치와 선택된 frame을 확인하세요.

실수 #4: 관찰을 단계와 라인에 연결하지 않고 결론을 내리는 것.
가끔 학생들은 sum = 42를 보고 곧바로 “여기 오류가 있다”라고 단정합니다. 그러나 특정 라인과 시점에 값이 어떻게 바뀌었는지를 모르면 그 숫자는 단지 숫자일 뿐입니다. 좋은 습관은 어떤 단계와 어떤 라인 이후에 값이 달라졌는지 주목하는 것입니다. Watches는 각 중단에서의 동적 변화를 보여주지만, 결론은 항상 실행 위치에 연결되어야 합니다.

실수 #5: 가설 없이 디버깅을 시도하는 것.
온도계를 매 5초마다 확인하는 데 비유할 수 있습니다. 무엇을 검사하려는지 모르면 watch는 숫자만 보여줄 뿐 의미를 주지 못합니다. 먼저 “인덱스가 범위를 벗어나지 않아야 한다” 또는 “조건이 이 반복에서 true가 되어야 한다”와 같은 검증 가능한 가설을 세우고, 그 다음 관찰을 선택하세요.

1
과제
C++ SELF, 레벨 34, 레슨 3
잠금
경로의 합
경로의 합
1
과제
C++ SELF, 레벨 34, 레슨 3
잠금
안전한 장바구니
안전한 장바구니
1
과제
C++ SELF, 레벨 34, 레슨 3
잠금
사라진 문자열
사라진 문자열
1
과제
C++ SELF, 레벨 34, 레슨 3
잠금
할인이 사라짐
할인이 사라짐
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION