Step 61. malloc과 free — 힙에서 빌리고 반납하라

Step 61. malloc과 free — 힙에서 빌리고 반납하라

Level 1 — 프로그래밍과 컴퓨터 내부 | 난이도 ★★★★☆ | 예상 소요 시간 4시간

전제: Step 56~60을 마쳤다. 메모리 네 구역과 힙의 존재를 안다. WSL 우분투의 bash와 gcc를 쓸 수 있다.

  • 준비물: WSL 우분투 터미널, gcc. 실측 환경은 Ubuntu 24.04, gcc 13.3.0, x86-64입니다.
  • 주의: 오늘 실습은 100% 안전합니다. 일부러 일으키는 사고(누수, 댕글링, 더블 프리)도 전부 여러분이 직접 쓴 프로그램 안에서만 일어나며, 프로그램이 종료되면 운영체제가 메모리를 전부 회수합니다.

int arr[100];은 만들 때 크기가 정해집니다. 그런데 실제 프로그램은 "실행해 보기 전에는 몇 개가 필요한지 모르는" 경우가 많습니다. 사용자가 입력하는 만큼, 파일에 들어 있는 만큼. 그래서 C에는 실행 중에 "이만큼 빌려 주세요" 하고 힙에서 메모리를 빌리는 방법이 있습니다. 빌렸으면 반납해야 하는데, C는 반납을 자동으로 해 주지 않습니다. 이 자유가 C의 힘이고, 이 귀찮음이 수많은 유명 취약점의 고향입니다. 오늘은 빌리기, 쓰기, 반납하기의 전 과정과 두 가지 대표 사고를 전부 손으로 겪어 봅니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • malloc으로 실행 중에 필요한 만큼 메모리를 빌리고, sizeof와 함께 쓴다
  • free로 다 쓴 메모리를 반납하고, NULL 초기화 관례를 지킨다
  • 메모리 누수(memory leak)댕글링 포인터(dangling pointer)를 설명하고 실험으로 재현한다
  • 입력 크기만큼 동적 배열을 만들어 쓰고 반납하는 프로그램을 완성한다
  • gcc가 use-after-free를 경고해 준다는 사실을 실측으로 확인한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 C 언어, WSL 우분투 bash, gcc 13.x (실측: 13.3.0, x86-64)
오늘의 함수·명령어 malloc(빌리기), free(반납), calloc(0으로 채워 빌리기), gcc -Wall(경고 켜고 컴파일), /usr/bin/time -v(메모리 사용량 측정)
필요한 개념 힙(heap), 동적 할당, NULL 검사, 메모리 누수, 댕글링 포인터, use-after-free, double free

2-1. malloc — 힙에서 땅 빌리기

malloc(memory allocation, 메모리 할당)은 "힙에서 이 크기만큼 빌려 줘"라는 함수입니다. malloc(40)이면 40바이트를 빌려, 그 땅의 시작 주소를 돌려줍니다. 빌린 땅은 내가 반납하기 전까지 내 것입니다.

배열과의 결정적 차이는 "몇 칸이 필요한지 실행 중에 정할 수 있다"는 것입니다. 그래서 이 방식을 동적 할당(dynamic allocation) — 실행 중에 움직이는 할당 — 이라고 부릅니다.

2-2. sizeof와의 합작

malloc은 바이트 단위로만 받습니다. "int 열 칸"을 빌리려면 바이트로 환산해야 하는데, sizeof(int)가 한 칸의 크기(4바이트, 환경에 따라 다를 수 있음)를 알려 줍니다. 그래서 관례가 malloc(sizeof(int) * 10)입니다. 이 형태를 통째로 외워 두세요. 숫자를 직접 적으면(malloc(40)) 환경이 바뀔 때 깨지는 코드가 됩니다.

2-3. free — 반납하기

free(빌린 주소)는 "이 땅 다 썼습니다"라는 반납입니다. 반납하지 않으면 프로그램이 사는 동안 그 땅은 계속 "사용 중"으로 묶여 있습니다. 중요한 사실 하나 — 반납해도 포인터 변수 자체는 그 주소를 계속 기억하고 있습니다. 이것이 오늘의 두 번째 사고의 씨앗입니다.

2-4. 두 가지 대표 사고

메모리 누수(memory leak): 빌리기만 하고 반납을 잊는 것. 한 번이면 작지만, 반복문 안에서 계속 빌리면 프로그램이 살찌다 죽습니다. 오래 켜 두는 서버 프로그램일수록 치명적입니다.

댕글링 포인터(dangling pointer)use-after-free: 반납한 땅의 주소를 포인터가 계속 기억하고 있는 상태가 댕글링("매달린") 포인터이고, 그 주소로 다시 찾아가 읽거나 쓰는 것이 use-after-free입니다. 반납된 땅은 언제 다른 용도로 다시 쓰일지 모르니, 남의 집 열쇠로 계속 드나드는 것과 같습니다. 실제 취약점(CVE) 목록의 단골 원인입니다.


3. 따라 하기

3-1. 빌리고, 쓰고, 반납하기 — 기본 사이클

입력 (dyn1.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int) * 5);
    if (p == NULL) {
        printf("메모리를 빌리지 못했습니다.\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        p[i] = (i + 1) * 10;
    }
    for (int i = 0; i < 5; i++) {
        printf("p[%d] = %d (주소 %p)\n", i, p[i], (void *)&p[i]);
    }

    free(p);
    printf("반납 완료.\n");
    return 0;
}

컴파일과 실행

gcc -Wall dyn1.c -o dyn1
./dyn1
p[0] = 10 (주소 0x57f7e4a8c2a0)
p[1] = 20 (주소 0x57f7e4a8c2a4)
p[2] = 30 (주소 0x57f7e4a8c2a8)
p[3] = 40 (주소 0x57f7e4a8c2ac)
p[4] = 50 (주소 0x57f7e4a8c2b0)
반납 완료.

(2026-09-09 실측. 주소값은 실행할 때마다 다릅니다.)

출력 읽는 법: malloc이 돌려준 주소 p는 배열처럼 p[i]로 씁니다 — 주소면 인덱스로 접근할 수 있다는 Step 59의 진실 그대로입니다. 주소가 4씩 붙어 있는 것도 배열과 같습니다. 힙에서도 칸은 붙어 있습니다. NULL 검사는 "빌리기 실패(메모리 부족)에 대한 예의"로, 실무에서는 필수입니다. 마지막 free(p)가 반납입니다.

예측: malloc의 5를 3으로 바꾸고 p[4]까지 쓰면 어떻게 될까요? 경고가 뜰까요, 실행은 될까요? 일부러 실험해 보세요. (힌트: C는 검사하지 않습니다.)

: "빌리고(NULL 검사) → 쓰고 → 반납" 이 3박자가 힙 사용의 평생 리듬입니다.

3-2. 실행 중에 크기 정하기 — 배열이 못 하는 일

입력 (dyn2.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("몇 명의 점수를 입력할까요? ");
    scanf("%d", &n);

    int *scores = malloc(sizeof(int) * n);
    if (scores == NULL) {
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("%d번 점수: ", i + 1);
        scanf("%d", &scores[i]);
    }

    int total = 0;
    for (int i = 0; i < n; i++) {
        total += scores[i];
    }
    printf("합계 %d, 평균 %d\n", total, total / n);

    free(scores);
    return 0;
}

컴파일과 실행

gcc -Wall dyn2.c -o dyn2
./dyn2
몇 명의 점수를 입력할까요? 3
1번 점수: 90
2번 점수: 80
3번 점수: 70
합계 240, 평균 80

(2026-09-09 실측. 직접 입력할 때의 화면입니다. 참고로 파이프로 입력을 넣으면(printf "3\n90\n80\n70\n" | ./dyn2) 프롬프트들이 한 줄에 붙어 보이지만 결과는 같습니다.)

출력 읽는 법: 실행하기 전에는 n을 모릅니다. 배열로는 못 만드는 이 프로그램이 malloc으로는 됩니다. 실행 중에 정해진 크기만큼 딱 맞게 빌렸습니다 — 이것이 "동적"이라는 이름의 이유입니다.

3-3. 누수 — 반납을 잊으면

입력 (leak.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    for (int i = 0; i < 1000000; i++) {
        int *p = malloc(sizeof(int) * 1000);
        /* free(p); 를 일부러 뺐다! */
        if (p == NULL) {
            printf("%d번째에서 빌리기 실패\n", i);
            return 1;
        }
    }
    printf("끝까지 성공 (누수 상태로 종료)\n");
    return 0;
}

컴파일과 실행

gcc -Wall leak.c -o leak
/usr/bin/time -v ./leak
끝까지 성공 (누수 상태로 종료)
        ...
        Maximum resident set size (kbytes): 3923236

(2026-09-09 실측. WSL 메모리 7.6GB 환경에서 2.6초 만에 끝까지 돌았고, 이 프로그램이 실제로 잡아먹은 최대 메모리는 약 3.9GB였습니다.)

출력 읽는 법: 반복마다 4,000바이트씩 빌리고 반납하지 않았습니다. 백만 번이면 약 4GB. /usr/bin/time -v의 "Maximum resident set size"가 그 증거입니다. 메모리가 더 작은 환경에서는 중간에 "N번째에서 빌리기 실패"가 뜹니다. 그리고 종료되자마자 운영체제가 땅을 전부 회수했기에 여러분의 컴퓨터는 멀쩡합니다 — 누수가 무서운 것은 "끝나지 않는 프로그램", 즉 서버에서입니다.

3-4. 댕글링 포인터 — 반납 후에도 찾아가기

입력 (dangle.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    printf("반납 전: %d (주소 %p)\n", *p, (void *)p);

    free(p);
    printf("반납 후에도 p는 같은 주소를 기억: %p\n", (void *)p);
    printf("반납 후 읽기 시도: %d\n", *p);

    p = NULL;
    printf("NULL로 초기화 완료. 이제 실수로 쓰면 즉시 죽어서 오히려 안전.\n");
    return 0;
}

컴파일과 실행

gcc -Wall dangle.c -o dangle
./dangle
dangle.c:11:5: warning: pointer 'p' used after 'free' [-Wuse-after-free]
   11 |     printf("반납 후 읽기 시도: %d\n", *p);
      |     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
dangle.c:9:5: note: call to 'free' here
    9 |     free(p);
      |     ^~~~~~~
반납 전: 42 (주소 0x628bb3a5a2a0)
반납 후에도 p는 같은 주소를 기억: 0x628bb3a5a2a0
반납 후 읽기 시도: 683358810
NULL로 초기화 완료. 이제 실수로 쓰면 즉시 죽어서 오히려 안전.

(2026-09-09 실측. 주소와 마지막 값은 실행할 때마다 다릅니다.)

출력 읽는 법: 세 가지를 봐 주세요.

  1. 컴파일러가 경고합니다: 최신 gcc는 -Wuse-after-free 경고로 "free한 포인터를 또 쓰고 있습니다"라고 알려 줍니다. 경고를 무시하지 않는 것이 첫 번째 방어입니다.
  2. 반납 후에도 p는 같은 주소를 기억합니다: free는 땅을 반납할 뿐, 포인터 변수를 비우지 않습니다.
  3. 읽기 시도에서 42가 아니라 683358810이 나왔습니다: 반납된 땅을 힙 관리자가 내부 장부 용도로 벌써 다시 쓴 것입니다. 옛 주인의 값은 사라졌습니다. ("42가 그대로" 나오는 환경도 있습니다 — 그것은 안전이 아니라 유예입니다.)

반납 후에는 p = NULL로 "빈 손"으로 만드는 것이 수습의 관례입니다. NULL 포인터를 쓰면 즉시 세그폴트로 죽는데, 그것이 오히려 버그를 숨기지 않고 드러내 주어 안전합니다.

3-5. 반납은 한 번만 — double free

입력 (dfree.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 7;
    free(p);
    free(p);  /* 두 번 반납? */
    printf("이 줄이 보이면 운이 좋은 것\n");
    return 0;
}

컴파일과 실행

gcc -Wall dfree.c -o dfree
./dfree
free(): double free detected in tcache 2
Aborted (core dumped)

(2026-09-09 실측. 컴파일 때도 -Wuse-after-free 경고가 떴고, 실행은 두 번째 free에서 중단됐습니다. 한글 로캘 환경에서는 "중단됨 (core dumped)"처럼 보일 수 있습니다.)

출력 읽는 법: 같은 땅을 두 번 반납하자 힙 관리자가 "이미 반납된 땅인데요?" 하고 프로그램을 중단시켰습니다. tcache는 반납된 땅을 잠시 보관하는 힙 관리자의 캐시 창고입니다. 탐지되는 것은 다행인 경우입니다 — 더블 프리 역시 실제 취약점 목록의 단골입니다.

3-6. calloc — 0으로 채워서 빌리기

malloc의 사촌을 하나 더. calloc은 빌리면서 모든 칸을 0으로 채워 줍니다.

입력 (zero.c)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *a = malloc(sizeof(int) * 3);
    int *b = calloc(3, sizeof(int));
    printf("malloc 직후: %d %d %d (쓰레기 값일 수 있음)\n", a[0], a[1], a[2]);
    printf("calloc 직후: %d %d %d (항상 0)\n", b[0], b[1], b[2]);
    free(a);
    free(b);
    return 0;
}

컴파일과 실행

gcc -Wall zero.c -o zero
./zero
zero.c:7:5: warning: '*a' is used uninitialized [-Wuninitialized]
malloc 직후: 0 0 0 (쓰레기 값일 수 있음)
calloc 직후: 0 0 0 (항상 0)

(2026-09-09 실측. 이번 실행에서는 malloc 쪽도 우연히 0이었습니다. 컴파일러는 -Wuninitialized 경고로 "초기화 안 한 값을 읽고 있습니다"라고 알려 줬습니다.)

출력 읽는 법: malloc은 "땅만" 빌려줍니다. 이전 주인의 흔적(쓰레기 값)이 남아 있을 수 있습니다 — 이번엔 우연히 0이었을 뿐, 보장은 없습니다. calloc은 칸 수와 칸 크기를 따로 받아(calloc(3, sizeof(int))) 깨끗이 지우고 건네줍니다. 초깃값이 0이어야 할 때 편리하고 안전한 선택입니다.

생각해 보기: "이전 주인의 흔적이 남아 있다"는 말이 보안에서 의미하는 바는 무엇일까요? 민감한 데이터(비밀번호, 키)를 쓴 뒤 지우지 않고 반납하면, 다음에 그 땅을 빌린 코드가 흔적을 읽을 수 있습니다. 이것이 정보 유출의 한 경로입니다.


4. 미션과 연습문제

미션 — 동적 성적 관리 프로그램

dyn2.c를 확장해 아래 요구사항을 전부 만족하는 프로그램 grade.c를 만드세요.

  1. 실행 중에 입력받은 인원수만큼 malloc으로 점수 배열을 빌린다
  2. 점수 입력 후 합계·평균에 더해 최고점과 최저점을 구한다
  3. 최고점을 받은 사람의 번호(몇 번째 입력인가)를 출력한다
  4. free 전에 "빌린 땅의 시작 주소와 바이트 크기"를 출력해 기록한다
  5. free 후 포인터를 NULL로 초기화하는 관례를 지킨다

연습문제

문제 1. malloc(10)으로 int 열 칸을 만들 수 있나요? 없다면 올바른 코드를 쓰고, 그 이유를 한 문장으로 설명해 보세요.

문제 2. malloc 직후의 NULL 검사는 왜 필요하며, 검사 없이 p[i]에 쓰면 어떤 일이 일어나나요?

문제 3. free를 했는데도 포인터가 같은 주소를 기억하고 있었습니다(3-4 실측). 이 상태로 읽기를 계속하면 왜 위험한가요? 그리고 p = NULL이 이 사고를 어떻게 예방하나요?

문제 4. 3-3 실측에서 누수 프로그램이 약 3.9GB를 쓰고도 컴퓨터가 멀쩡했던 이유는 무엇인가요? 그런데도 서버 프로그램에서 누수가 치명적인 이유와 함께 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    printf("몇 명의 점수를 입력할까요? ");
    scanf("%d", &n);

    int *scores = malloc(sizeof(int) * n);
    if (scores == NULL) {
        return 1;
    }

    for (int i = 0; i < n; i++) {
        printf("%d번 점수: ", i + 1);
        scanf("%d", &scores[i]);
    }

    int total = 0, max_i = 0, min_i = 0;
    for (int i = 0; i < n; i++) {
        total += scores[i];
        if (scores[i] > scores[max_i]) max_i = i;
        if (scores[i] < scores[min_i]) min_i = i;
    }
    printf("합계 %d, 평균 %d\n", total, total / n);
    printf("최고점 %d (%d번), 최저점 %d (%d번)\n",
           scores[max_i], max_i + 1, scores[min_i], min_i + 1);
    printf("빌린 땅: 시작 %p, 크기 %zu바이트\n",
           (void *)scores, sizeof(int) * (size_t)n);

    free(scores);
    scores = NULL;
    return 0;
}

검증하는 법: ① gcc -Wall grade.c -o grade에서 경고가 하나도 없어야 합니다. ② 인원 3명, 점수 90/80/70을 넣으면 "최고점 90 (1번), 최저점 70 (3번)"이 나와야 합니다. ③ 마지막에 주소가 출력되고, 그 뒤 freeNULL 초기화가 코드에 있어야 합니다.

연습문제 해답

문제 1 해답. 안 됩니다. malloc의 숫자는 바이트 단위라서 malloc(10)은 10바이트, 즉 int 두 칸 반입니다. 올바른 코드는 malloc(sizeof(int) * 10)입니다. 자료형의 실제 크기는 환경에 따라 달라질 수 있으므로 항상 sizeof로 계산해야 합니다.

문제 2 해답. malloc은 메모리가 부족하면 실패하고 NULL을 돌려줍니다. NULL은 "아무 땅도 가리키지 않음"이라는 특별한 값인데, 여기에 p[i]로 쓰면 존재하지 않는 땅에 쓰는 셈이라 즉시 세그멘테이션 폴트로 죽습니다. 실무에서는 NULL 검사 없는 malloc을 버그로 취급합니다.

문제 3 해답. 반납된 땅은 언제든 힙 관리자나 다른 코드가 재사용할 수 있습니다. 실측에서도 읽기 값이 42에서 683358810으로 바뀌어 있었습니다 — 운 좋게 옛 값이 보여도 그것은 유예일 뿐입니다. p = NULL로 초기화해 두면, 실수로 다시 쓰는 순간 즉시 세그폴트로 죽어 버그가 숨지 않고 드러납니다. 조용히 오염되는 것보다 즉시 죽는 쪽이 안전합니다.

문제 4 해답. 프로그램이 종료되면 운영체제가 그 프로세스의 메모리를 전부 회수하기 때문입니다. 그래서 "돌리고 끝나는" 프로그램의 누수는 종료와 함께 사라집니다. 반면 서버 프로그램은 종료하지 않고 계속 살아 있으므로, 누수가 시간과 함께 쌓여 결국 메모리 부족으로 죽거나 시스템 전체를 느리게 만듭니다.

완료 기준 체크리스트

  • [ ] malloc(sizeof(자료형) * 개수) 형태로 실행 중에 크기를 정해 메모리를 빌릴 수 있다
  • [ ] malloc 직후 NULL 검사를 빼지 않고 쓴다
  • [ ] free로 반납하고 NULL로 초기화하는 관례를 코드에서 지킬 수 있다
  • [ ] 메모리 누수와 댕글링 포인터(use-after-free)의 차이를 한 문장씩 설명할 수 있다
  • [ ] 더블 프리가 무엇인지, 실측 오류 메시지와 함께 설명할 수 있다
  • [ ] calloc과 malloc의 차이(초기화 유무)를 설명할 수 있다
  • [ ] 미션: 동적 성적 관리 프로그램을 완성하고 경고 없이 컴파일했다

6. 흔한 실수와 해결

벽 1. NULL 검사를 빼먹는다

증상: 메모리가 부족한 상황에서 프로그램이 이유 없이 죽습니다.
원인: malloc은 실패할 수 있고, 실패하면 NULL을 돌려줍니다. NULL에 p[i]로 쓰면 즉시 사고입니다.
해결: 빌린 직후 if (p == NULL) 검사를 무조건 붙이세요. 오늘의 모든 예시가 그렇게 되어 있습니다.

벽 2. 바이트와 칸을 혼동한다

증상: malloc(10)으로 int 열 칸을 만들었다고 생각합니다.
원인: malloc의 숫자는 바이트입니다. int 열 칸은 40바이트(환경에 따라 다를 수 있음)입니다.
해결: 언제나 malloc(sizeof(자료형) * 개수) 형태를 쓰세요. 숫자를 직접 적는 것은 환경이 바뀌면 깨지는 코드입니다.

벽 3. 반납하고 또 쓴다

증상: free 후에도 값이 보이길래 계속 쓰다가 어느 날 이상한 값이 나옵니다.
원인: 반납된 땅은 언제 재사용될지 모릅니다. 3-4 실측처럼 컴파일러가 경고를 줍니다:

warning: pointer 'p' used after 'free' [-Wuse-after-free]

해결: -Wall로 컴파일하고 이 경고를 절대 무시하지 마세요. 그리고 free 직후 p = NULL. 이 한 줄이 댕글링 사고의 대부분을 예방합니다.

벽 4. 빌린 곳과 반납하는 곳이 다르다

증상: malloc한 주소를 p에 받고, p++로 움직인 뒤 free(p)를 합니다.
원인: free는 "빌릴 때 받은 그 주소"를 요구합니다. 주소를 움직이면 반납처를 못 찾고 오류로 죽습니다.
해결: 빌린 시작 주소는 별도 변수에 보관하거나, 포인터를 움직이지 말고 인덱스(p[i])를 쓰세요.

벽 5. 초기화 안 한 값을 읽는다

증상: 컴파일 때 이런 경고가 뜹니다 (2026-09-09 실측):

warning: '*a' is used uninitialized [-Wuninitialized]

원인: malloc은 땅만 빌려주고 내용은 이전 주인의 흔적 그대로입니다.
해결: 0으로 시작해야 하는 용도라면 calloc을 쓰거나, 빌린 직후 값을 채우고 나서 읽으세요.


7. 정리

오늘의 개념

개념 한 줄 설명
힙(heap) 실행 중에 빌리는 메모리 구역
동적 할당 실행 중에 크기를 정해 빌리는 방식
메모리 누수 빌리고 반납하지 않아 메모리가 새는 사고
댕글링 포인터 반납된 땅을 계속 가리키는 포인터
use-after-free 반납한 땅을 다시 읽거나 쓰는 사고
double free 같은 땅을 두 번 반납하는 사고

오늘의 문법과 명령어

코드·명령 하는 일
malloc(sizeof(int) * n) 힙에서 int n칸 분량을 빌림
if (p == NULL) 빌리기 실패 검사 (필수)
free(p) 반납 (빌릴 때 받은 그 주소로)
p = NULL 반납 후 빈 손으로 만드는 관례
calloc(n, sizeof(int)) 0으로 채워서 빌림
gcc -Wall 경고를 켜고 컴파일 (오늘의 경고들이 다 여기서 나옴)
/usr/bin/time -v ./프로그램 최대 메모리 사용량 측정

명령어보다 중요한 감각

"빌리고(NULL 검사) → 쓰고 → 반납(NULL 초기화)" 이 리듬을 몸에 익히는 것이 오늘의 핵심입니다. 그리고 오늘 일부러 일으킨 사고들 — 누수, 댕글링, 더블 프리 — 은 앞으로 유명 취약점을 공부할 때마다 "아, 그 사고" 하고 알아볼 얼굴들입니다.

하나 더 기억해 두세요. 파이썬에서 리스트를 만들 때 우리는 반납을 한 적이 없습니다. 파이썬이 "이제 아무도 안 쓰는 것"을 알아서 회수해 갔기 때문입니다(가비지 컬렉션, garbage collection). 언어의 편리함은 누군가가 대신 해 주는 노동이고, 그 노동을 아는 사람과 모르는 사람은 오류 메시지 앞에서 다른 태도를 갖습니다. 오늘부터 "이 메모리, 누가 언제 반납하지?"라는 질문이 코드를 읽을 때 자동으로 떠오르면, 오늘은 성공입니다.


전부 체크되면 Step 61 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.