SMP 캐시 일관성과 폴스 셰어링 (Cache Coherence & False Sharing)

핵심 인사이트 (3줄 요약)

  1. 본질: 대칭형 다중 처리 (SMP, Symmetric Multiprocessing) 아키텍처에서 각 CPU 코어는 자신만의 독립적인 캐시(L1/L2)를 갖는다. 여러 코어가 동일한 메모리를 바라볼 때 발생하는 **캐시 불일치 문제를 해결하는 하드웨어 규칙이 캐시 일관성(Cache Coherence)**이며, 이 규칙 때문에 발생하는 **소프트웨어적 성능 저하의 함정이 폴스 셰어링(False Sharing)**이다.
  2. 가치: 캐시 일관성(MESI 프로토콜) 덕분에 프로그래머는 멀티코어 환경에서도 메모리가 꼬이는 것을 덜 걱정하며 개발할 수 있지만, 캐시 라인(64 Byte)이라는 최소 전송 단위 때문에 의도치 않은 '가짜 공유(False Sharing)' 병목이 터진다.
  3. 융합: 이는 컴퓨터 구조의 캐시 하드웨어 매커니즘과 운영체제의 스레드 스케줄링, 그리고 고성능 C/C++ 멀티스레딩 프로그래밍(메모리 정렬, 패딩)이 완벽하게 맞물려 폭발하는 극한의 트러블슈팅 주제다.

Ⅰ. 개요 및 필요성 (Context & Necessity)

  • 개념:

    • 캐시 일관성 (Cache Coherence): 여러 CPU 코어가 각각 자신의 L1 캐시를 가지고 있을 때, 코어 1이 변수 A를 수정하면, 코어 2의 캐시에 들어있는 옛날 변수 A의 값이 '쓰레기(Invalid)'가 됨을 즉시 알려주어 데이터의 무결성을 유지하는 하드웨어 동기화 프로토콜(예: MESI).
    • 폴스 셰어링 (False Sharing): 변수 AB는 아무런 논리적 관련이 없는데, 우연히 물리적으로 너무 가깝게 붙어있어서 **하나의 캐시 라인(Cache Line, 64바이트 묶음)**에 동거하게 될 때 발생한다. 코어 1이 A만 고치고 코어 2가 B만 고쳐도, 하드웨어는 "같은 묶음(캐시 라인)이 변했다!"고 착각하여 서로의 캐시를 핑퐁처럼 무효화시키며 엄청난 성능 폭락(Stall)을 유발하는 현상.
  • 필요성(문제의식):

    • "멀티코어 CPU를 샀는데, 스레드 4개를 돌리니까 오히려 스레드 1개 돌릴 때보다 속도가 느려져요!"
    • 초보 개발자는 스레드 간 락(Mutex)을 안 쓰려고 변수 배열 int count[4]를 만들어서 코어마다 count[0], count[1]을 따로 쓰게 했다. 논리적으로는 완벽한 동시성이지만, 물리적으로 count[0]부터 count[3]까지가 하나의 64바이트 캐시 라인에 뭉쳐 들어가 버린다.
    • 코어 1이 0번을 고칠 때마다 코어 2의 캐시가 다 날아가고, 코어 2가 1번을 고치면 코어 1의 캐시가 다 날아가는 상호 파괴(Ping-pong) 늪에 빠졌다.
  • 💡 비유:

    • 두 학생(코어)이 도서관에서 각자 1번 문제(변수 A)와 2번 문제(변수 B)를 풀기로 했다. 문제는 두 문제가 **하나의 시험지 종이(캐시 라인)**에 앞뒤로 적혀 있다는 점이다.
    • 1번 학생이 시험지에 답을 쓰고 지우개를 쓸 때마다, 2번 학생은 종이를 뺏겨서 자기 문제를 풀지 못하고 기다려야 한다. 둘은 서로 다른 문제를 풀고 있지만(False), 종이 한 장을 공유(Sharing)하고 있기 때문에 멱살잡이가 일어나는 것이다.
  • 등장 배경:

    • 과거 싱글 코어 시절에는 캐시 일관성 문제가 없었다. 2000년대 후반 멀티코어(SMP) 시대가 열리고 L1/L2 캐시 구조가 복잡해지면서, 고성능 서버 아키텍처(게임 서버, 금융 트레이딩)에서 가장 잡기 어려운 극악의 지연(Latency) 원인으로 대두되었다.
  ┌─────────────────────────────────────────────────────────────┐
  │                 폴스 셰어링(False Sharing) 발생 메커니즘 시각화       │
  ├─────────────────────────────────────────────────────────────┤
  │                                                             │
  │  [ 메인 메모리 (RAM) ]                                        │
  │   ┌─────────────────────────────────────────────────────┐   │
  │   │ 캐시 라인 1 (64 Byte 묶음)                              │   │
  │   │ [ 변수 A (4B) ] [ 변수 B (4B) ] [ 나머지 빈공간 56B ]        │   │
  │   └─────────────────────────────────────────────────────┘   │
  │         ▲ 복사 됨                                 ▲ 복사 됨    │
  │         │                                        │          │
  │  ┌───────────────┐                        ┌───────────────┐ │
  │  │ Core 1 L1 캐시 │                        │ Core 2 L1 캐시 │ │
  │  │ [ A ] [ B ]   │                        │ [ A ] [ B ]   │ │
  │  └───────────────┘                        └───────────────┘ │
  │         │                                        │          │
  │  1. Core 1이 변수 A를 `A=99`로 변경.                          │
  │     => 하드웨어는 "이 캐시 라인 전체가 수정됨"으로 인지!               │
  │  2. 캐시 일관성(MESI) 프로토콜 발동: Core 2의 캐시 라인을 '무효화(I)'함! │
  │  3. Core 2가 변수 B를 읽으려 함. (캐시 미스 발생)                  │
  │     => 메모리나 Core 1에서 비싼 비용을 치르고 다시 라인 전체를 퍼와야 함. │
  │  4. 이번엔 Core 2가 B를 변경하면 Core 1의 캐시가 또 폭파됨 (무한 핑퐁)  │
  └─────────────────────────────────────────────────────────────┘

[다이어그램 해설] 이 그림은 락(Lock)이 전혀 없는 코드에서도 왜 멀티코어 성능이 추락하는지 명확히 보여준다. 코어 1과 코어 2는 서로 다른 변수(A, B)를 조작하므로 논리적인 경쟁 조건(Race Condition)이 없다. 하지만 하드웨어 캐시는 바이트 단위로 움직이지 않고 무식하게 64바이트 단위(캐시 라인)의 블록으로만 움직인다. 두 변수가 한 블록 안에 입주해 있기 때문에, 한 코어의 수정 행위가 다른 코어의 캐시 블록 전체를 날려버리는(Invalidate) 치명적인 폭발 반경을 갖게 된다. 이것을 캐시 라인 바운싱(Ping-pong)이라고 부른다.

  • 📢 섹션 요약 비유: 각자 자기 방(캐시)에서 조용히 일기(변수)를 쓰고 싶은데, 두 사람의 일기장이 하나의 두꺼운 스프링 노트(캐시 라인)로 묶여 있어서, 한 명이 글을 쓸 때마다 다른 한 명은 노트를 뺏긴 채 멍하니 기다려야만 하는 바보 같은 상황입니다.

Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)

MESI 캐시 일관성 프로토콜의 4가지 상태

하드웨어는 스누핑(Snooping)이라는 기법을 통해 시스템 버스를 항상 엿듣고 있으며, 각 캐시 라인의 상태를 4가지(M, E, S, I) 라벨로 관리하여 코어 간의 데이터 불일치를 막는다.

상태 (State)의미설명 (동작)
M (Modified)수정됨이 캐시 라인은 나(코어)만 독점하고 있으며, 내가 데이터를 고쳤다. (즉, 메인 메모리의 옛날 값과 다르다).
E (Exclusive)독점 중이 캐시 라인은 나만 가지고 있지만, 아직 내용은 안 고치고 원본(메모리)과 똑같은 깨끗한 상태다.
S (Shared)공유됨이 캐시 라인은 나와 다른 코어가 사이좋게 나눠서 읽기 전용으로 가지고 있다. 내용도 원본과 일치한다.
I (Invalid)무효화됨누군가 이 캐시 라인의 데이터를 고쳤기 때문에, 내가 들고 있는 이 데이터는 쓰레기다! (다음번에 접근하면 캐시 미스 발생)

폴스 셰어링에 의한 MESI 상태 전이 파이프라인 (Ping-pong)

변수 A와 B가 같은 캐시 라인에 있을 때 코어 1(A 수정)과 코어 2(B 수정) 사이에서 일어나는 MESI 상태 변화를 추적해 보면 하드웨어 병목이 어떻게 발생하는지 알 수 있다.

  ┌───────────────────────────────────────────────────────────────────┐
  │                 False Sharing으로 인한 MESI 핑퐁 (Ping-pong) 효과     │
  ├───────────────────────────────────────────────────────────────────┤
  │                                                                   │
  │   [ 초기 상태 ]                                                      │
  │   Core 1 캐시 라인: (상태 S) [ A=0, B=0 ]                            │
  │   Core 2 캐시 라인: (상태 S) [ A=0, B=0 ]                            │
  │                                                                   │
  │   [ T1: Core 1이 변수 A에 1을 씀 ]                                    │
  │   1. Core 1: 버스에 "내가 이 라인 고친다!"(Invalidate) 신호 발송           │
  │   2. Core 2: 신호 듣고 내 라인을 (상태 I) 쓰레기로 강등시킴               │
  │   3. Core 1: (상태 M) [ A=1, B=0 ] ◀ 혼자 최신 데이터 독점 (Modified)    │
  │                                                                   │
  │   [ T2: Core 2가 변수 B에 2를 씀 ]                                    │
  │   1. Core 2: 변수 B를 쓸려는데 자기 라인이 (I)라서 캐시 미스(Cache Miss) 발생!│
  │   2. Core 2: 버스에 "그 라인 가진 사람 최신본 좀 뱉어봐" 요청              │
  │   3. Core 1: 자기가 가진 M 상태 라인을 메모리에 Flush 후 (상태 I)로 강등      │
  │   4. Core 2: 메모리에서 라인 퍼온 뒤 (상태 M) [ A=1, B=2 ] 로 수정        │
  │                                                                   │
  │   [결과: 대참사]                                                     │
  │   서로 다른 변수를 건드렸을 뿐인데, 각 T1, T2마다 수백 사이클짜리 메모리 동기화  │
  │   (Flush & Miss)가 강제로 발생함. L1 캐시(1ns)가 아니라 메인메모리(100ns) │
  │   속도로 추락해버림!                                                  │
  └───────────────────────────────────────────────────────────────────┘

[다이어그램 해설] MESI 프로토콜은 데이터의 무결성을 지키는 수호신이지만, 64바이트라는 통짜 블록 묶음 때문에 "오지랖 넓은 경찰"이 되어버린다. 코어 1은 A만 바꿨는데 캐시 일관성 경찰은 "이 블록(A+B 묶음) 전체가 오염됐어!"라며 코어 2의 블록을 빼앗아 버린다(I 상태로 만듦). 코어 2가 B를 수정하려 할 때 텅 빈 캐시를 마주하고, 다시 메인 메모리에 수백 클럭의 비용을 지불하며 블록을 통째로 당겨온다. 이 무의미한 M(Modified) $\rightarrow$ I(Invalid) 상태 변화가 초당 수백만 번 반복되는 현상이 멀티코어 최악의 함정이다.

  • 📢 섹션 요약 비유: 식탁 위 반찬 그릇(캐시 라인)에 김치(A)와 시금치(B)가 같이 담겨 있는데, 철수(코어1)가 김치를 집어먹었다고 해서 식당 아줌마(MESI 프로토콜)가 찝찝하다며 밥 먹는 영희(코어2) 앞에서 시금치까지 포함된 그릇 통째로 뺏어가서 새 그릇으로 바꿔오는 삽질의 반복입니다.

Ⅲ. 융합 비교 및 다각도 분석

진성 공유 (True Sharing) vs 폴스 셰어링 (False Sharing)

두 현상 모두 MESI 프로토콜을 자극하여 캐시 미스를 유발하지만, 원인과 해결책이 완전히 다르다.

비교 항목진성 공유 (True Sharing)폴스 셰어링 (False Sharing)
정의두 코어가 **'정말 똑같은 변수 1개'**를 놓고 서로 쓰고 읽으며 싸우는 현상두 코어가 **'서로 다른 변수 2개'**를 쓰는데, 하필 같은 캐시 라인에 묶여서 싸우는 현상
코드의 오류프로그래머가 멀티스레드 락(Lock)을 잘못 걸거나 경합이 심한 디자인을 한 것 (소프트웨어 설계 결함)멀티스레드 설계는 완벽하지만, 메모리 레이아웃(배열 간격)을 신경 안 써서 생긴 것 (하드웨어/컴파일러 지식 부재)
해결책락(Mutex) 크기 줄이기, 읽기 위주로 로직 변경, Thread-local 변수로 분리각 변수 사이에 강제로 **빈공간(Padding)**을 채워 넣어 변수들을 서로 다른 캐시 라인으로 찢어놓기

과목 융합 관점

  • 운영체제 스케줄러 (CPU 친화성, Affinity): 폴스 셰어링을 막는 또 다른 우회 기법은 스레드가 이리저리 코어를 옮겨 다니지 못하게 하는 것이다. OS 스케줄러 설정(예: sched_setaffinity)을 통해 특정 스레드를 특정 CPU 코어에만 강력하게 못 박아(Pinning) 두면, 캐시 라인 이주(Migration)에 따른 핑퐁을 원천적으로 막을 수 있는 아키텍처적 방어막이 된다.

  • 프로그래밍 (C/C++ 메모리 정렬): C++11부터는 alignas(64)라는 키워드가 등장했다. 이 키워드를 구조체에 붙이면 컴파일러가 알아서 "이 구조체는 무조건 64바이트(캐시 라인 크기) 단위로 시작하게 메모리 주소를 밀어줘!"라고 OS에 지시하여, 컴파일 타임에 우연한 셰어링을 찢어버린다.

  • 📢 섹션 요약 비유: 한 장난감을 두고 두 아이가 진짜로 뺏으려 싸우는 것(True Sharing)은 부모가 규칙(Lock)을 정해줘야 해결되고, 각자 자기 장난감을 갖고 놀고 있는데 서로의 팔꿈치가 부딪혀서 싸우는 것(False Sharing)은 단순히 아이들 사이의 간격(Padding)만 벌려주면 조용해집니다.


Ⅳ. 실무 적용 및 기술사적 판단

실무 시나리오 및 최적화 아키텍처

  1. 시나리오 — 멀티스레드 병렬 처리 서버의 성능 추락: 16코어 서버에서 이미지 픽셀 16등분 병렬 처리 필터링 알고리즘을 짰다. 각 스레드가 자기가 처리한 픽셀 개수를 전역 배열 int result[16];에 자기 번호 인덱스(result[thread_id]++)로 기록하게 했다. 코어를 늘릴수록 속도가 반토막이 났다.

    • 원인 분석: 락(Lock)을 안 썼기 때문에 논리적 충돌은 없었다. 하지만 int는 4바이트이므로 result[0]부터 result[15]까지 총 64바이트가 정확히 하나의 캐시 라인에 모조리 구겨 들어갔다. 16개의 코어가 1개의 캐시 라인 덩어리를 향해 맹렬하게 핑퐁 폭격을 가하는 최악의 폴스 셰어링 핫스팟(Hotspot)이 만들어졌다.
    • 아키텍트 판단 (Padding 삽입 기법): 가장 고전적이고 확실한 해결책은 데이터 구조를 부풀리는 것이다. 배열의 각 요소 뒤에 의미 없는 빈 쓰레기 변수(Padding) 60바이트를 강제로 끼워 넣는다. struct ThreadResult { int count; char padding[60]; }; 이렇게 만들고 ThreadResult result[16];을 선언하면, 각 스레드의 카운터 변수는 무조건 서로 다른 캐시 라인에 뚝뚝 떨어져 배치되므로 하드웨어의 MESI 간섭이 영구적으로 소멸하며 성능이 16배로 선형 증가(Linear Scaling)한다.
  2. 시나리오 — 스레드 로컬 스토리지 (TLS) 활용: 고성능 로그 서버에서 수십 개의 스레드가 중앙의 로그 카운터(통계)를 올리다 보니, 패딩을 넣는 짓조차 코드가 지저분해져서 관리하기 싫어짐.

    • 아키텍트 판단 (아키텍처 분리): 아예 배열 자체를 없앤다. 각 스레드가 전역 배열에 접근하게 하지 말고, OS가 제공하는 TLS (Thread Local Storage) 변수나 스레드 함수 내부의 지역 변수(Stack 메모리 영역)에 독립적으로 카운트를 올리게 한다. 이 경우 각 스레드의 스택 영역은 물리적으로 수 MB씩 멀리 떨어져 있으므로 폴스 셰어링이 원천 불가하다. 작업이 다 끝난 맨 마지막에 스레드가 죽기 직전 딱 한 번만 중앙 변수에 값을 더하게(합산) 만들면 병목이 사라진다.
  ┌───────────────────────────────────────────────────────────────────┐
  │                 폴스 셰어링의 소프트웨어적 해결 구조 (Padding 기법)         │
  ├───────────────────────────────────────────────────────────────────┤
  │                                                                   │
  │   [ 해결 전: 빽빽하게 붙은 배열 (False Sharing 발생) ]                │
  │   메모리 주소: 0x00      0x04      0x08                            │
  │   캐시 라인1: [ count[0] ][ count[1] ][ count[2] ] ... (한 공간에 옹기종기)│
  │                                                                   │
  │   [ 해결 후: 패딩(Padding)을 통한 강제 이격 ]                        │
  │                                                                   │
  │   메모리 주소: 0x00           (빈공간)               0x40 (64바이트 뒤)  │
  │   캐시 라인1: [ count[0] ][ padding 60 Byte... ]                   │
  │   캐시 라인2: [ count[1] ][ padding 60 Byte... ]                   │
  │   캐시 라인3: [ count[2] ][ padding 60 Byte... ]                   │
  │                                                                   │
  │   결과: Core 1이 라인1의 count[0]을 미친 듯이 수정해도,                  │
  │        Core 2가 바라보는 라인2에는 전혀 영향을 주지 않음! (MESI 분리 성공)   │
  └───────────────────────────────────────────────────────────────────┘

[다이어그램 해설] 이 해결책은 "메모리 공간을 낭비해서 CPU 속도를 산다"는 컴퓨터 공학의 고전적 트레이드오프를 보여준다. 고작 4바이트짜리 숫자를 담기 위해 64바이트 덩어리를 통째로 소모하는 짓은 메모리 낭비다. 하지만 기가바이트급 RAM이 넘쳐나는 현대 서버 환경에서는 약간의 메모리 패딩 낭비를 내주고 수백만 번의 캐시 미스 스톨(Stall)을 제거하는 것이 수백 배 이득이다. 성능 최적화란 결국 아키텍처의 어느 자원(메모리 용량)을 희생해 어느 자원(CPU 캐시 대역폭)을 살릴 것인가의 결단이다.

안티패턴

  • 구조체 남용: C언어에서 스레드들이 관리하는 데이터를 묶기 편하다는 이유로 struct { int core1_var; int core2_var; } 식으로 하나의 거대한 구조체 안에 때려 박는 행위. 코드가 논리적으로 예뻐 보일진 몰라도, 런타임 하드웨어 레벨에서는 구조체 내 변수들이 연달아 배치되므로 지옥 같은 폴스 셰어링의 온상이 된다. 서로 다른 코어가 만지는 변수는 구조체로 묶지 말고 철저히 찢어놓아야 한다.

  • 📢 섹션 요약 비유: 코로나19 때 식당에서 감염(캐시 무효화)을 막기 위해 사람들이 다닥다닥 붙어 앉지 못하게 의자 사이에 커다란 투명 가림막(패딩)을 세워 공간을 강제로 띄워버린 사회적 거리 두기 조치와 완벽히 같습니다.


Ⅴ. 기대효과 및 결론

정량/정성 기대효과

구분False Sharing 방치 시Padding 및 TLS 최적화 적용 시개선 효과
정량 (확장성, Scaling)코어를 16개 늘려도 성능이 1개일 때보다 낮음코어 수에 정비례하여 성능 선형(Linear) 증가멀티코어 하드웨어 투자(ROI) 100% 회수
정량 (캐시 미스율)L1 캐시 미스율 80% 이상 폭증L1 캐시 히트율 99% 달성캐시 라인 핑퐁에 의한 메모리 버스 트래픽 증발
정성 (아키텍처 지식)락(Lock) 문제로만 오해해 쓸데없는 로직 수정하드웨어-소프트웨어 상호작용의 심연 이해시스템 아키텍트의 극저지연(Low-Latency) 설계 역량 증명

미래 전망

  • 캐시 일관성 디렉토리 방식 부상: 기존의 버스 전체에 소리를 지르는 스누핑(Snooping) 방식은 코어가 64개, 128개로 늘어나면 방송 자체가 병목이 된다. 미래의 매니코어(Many-core) 서버 칩은 어느 코어가 어느 캐시 라인을 가졌는지 중앙 명부(Directory)에 기록하여 필요한 코어에게만 콕 집어 무효화 신호를 쏘는 디렉토리 기반 MESI 프로토콜로 빠르게 넘어가고 있다.
  • 하드웨어의 자동 탐지 (Intel TSX): 하드웨어 트랜잭셔널 메모리(HTM) 기술이 고도화되면서, CPU 내부의 감시 유닛이 "어? 얘네 둘 폴스 셰어링 중인데?"를 실시간으로 탐지하고, 런타임에 일시적으로 캐시 충돌을 무시(Elision)해주는 하드웨어 레벨의 자가 치유 기술도 연구되고 있다.

참고 표준

  • C11 / C++11 Memory Model: 하드웨어의 캐시 동작을 추상화하여, std::atomicalignas 등을 통해 언어 차원에서 멀티코어 캐시 가시성(Visibility)과 폴스 셰어링을 통제하게 만든 국제 표준.
  • perf & Valgrind (DRD/Helgrind): 리눅스 환경에서 캐시 미스와 폴스 셰어링 지점을 L1 D-Cache 로드/스토어 이벤트 레벨에서 잡아내는 표준 프로파일링 툴셋.

SMP 캐시 일관성과 폴스 셰어링은 "소프트웨어 코드가 하드웨어 물리 공간에 어떻게 안착하는가?"를 묻는 뼈아픈 질문이다. 알고리즘 시간 복잡도(O(N))가 아무리 완벽해도, 하드웨어의 64바이트 룰을 모르면 코딩은 탁상공론에 불과해진다. 진정한 엔지니어는 0과 1의 논리 세계를 넘어, 전자가 실리콘 칩의 캐시를 돌아다니는 물리적 마찰과 병목까지 상상하며 코드를 배열하는 장인이어야 한다.

  • 📢 섹션 요약 비유: 우주선 궤도(소프트웨어 논리)를 아무리 컴퓨터로 완벽하게 계산했어도, 대기권을 돌파할 때 발생하는 엄청난 공기 마찰열(하드웨어 캐시 충돌)을 고려해 내열 타일(패딩)을 붙이지 않으면 결국 우주선은 타버린다는 치열한 현실 공학의 교훈입니다.

📌 관련 개념 맵 (Knowledge Graph)

개념 명칭관계 및 시너지 설명
캐시 라인 (Cache Line)메모리와 CPU 캐시가 데이터를 주고받는 최소 배송 단위(보통 64바이트)로, 이 뭉텅이 철학이 폴스 셰어링의 근본 원흉이다.
MESI 프로토콜캐시 블록의 상태를 Modified, Exclusive, Shared, Invalid로 나누어 코어 간의 멱살잡이를 정리하는 하드웨어 규칙이다.
진성 공유 (True Sharing)코어들이 정말로 같은 변수 1개를 놓고 싸우는 진짜 병목 현상으로, 뮤텍스(Mutex) 락 경합이나 Atomic 연산의 한계를 부른다.
스레드 로컬 저장소 (TLS)각 스레드만의 독립된 전역 변수 공간을 스택 쪽에 멀찍이 떨어뜨려 만들어주어, 셰어링 자체를 물리적으로 원천 차단하는 도구다.
NUMA 아키텍처코어 그룹별로 로컬 메모리를 쪼개놓은 구조로, 원격 NUMA 노드의 캐시 라인을 당겨오다 폴스 셰어링이 터지면 지연 시간이 수천 배로 악화된다.

👶 어린이를 위한 3줄 비유 설명

  1. 코어 형제들이 도서관에서 각자 자기 공부(계산)를 아주 열심히 하고 있었어요.
  2. 그런데 하필 둘이 쓰는 공책이 찰싹 붙어있는 '스프링 수첩(캐시 라인)'이라서, 형이 지우개질을 할 때마다 동생의 글씨까지 같이 지워져(캐시 무효화) 버리는 바람에 매번 다시 써야 했어요.
  3. 그래서 똑똑한 선생님이 빈 종이 여러 장(패딩)을 수첩 중간에 끼워 넣어서 형과 동생이 쓰는 페이지를 멀찍이 떨어뜨려 줬더니, 서로 싸우지 않고 엄청 빨리 공부를 끝냈답니다!