Ch.1 왜 CS를 알아야 하는가¶
CS를 왜 알아야 하는가. 에피소드 하나로 시작하겠다.
WORD size 에피소드¶
이 강의 작성자가 직접 겪은 이야기다.
2010년이었다. 군 복무 중이었고, 부대에서 예전 프로그램을 새 환경으로 마이그레이션하는 작업이 진행 중이었다. 원작자가 2004년에 짠 코드였고, Windows 98 기반 Win32 MFC 프로그램이었다. 임베디드 프로그램인데, 열화상 레이더 데이터를 입력받아 처리하고 있었다.
새로운 amd64(64비트) 환경으로 마이그레이션하는 중에 문제가 생겼다. 열화상 레이더 데이터가 제대로 읽히지 않는 거다. 데이터는 동일하게 들어오고 있고, 구형 프로그램을 가동하면 잘 작동한다. 뭐가 문제인지 모르겠다.
그래서 몇 명이 몇 주 동안 디버깅을 하겠다고 해안가를 왔다갔다, 컴퓨터를 바꾸고 포맷하고 있었다.
투입된 인원은 5명이었다:
- 40세, 코딩 20년차 수학과 출신
- 29세, 컴공 졸업 장교
- 26세, 컴공 졸업 장교
- 27세, 컴공 휴학 병장
- 23세, 컴공 휴학 일병
무려 5명이서 몇 주 동안 못 고치고 있었다. 결국 영내에 있는 다른 개발자에게 전화로 상황을 설명했다.
그때 빡빡이(23세, 상병)가 고민을 조금 하더니 말한다.
"아키텍처가 달라졌으면 WORD size부터 확인해봐야 하는 거 아니에요? 이건 amd64잖아요. 그거 원래 Win32 MFC로 개발한 거니까 레이더 장비 인풋 처리할 때 워드사이즈 안 맞을 것 같은데... 받아온 데이터 LOWORD랑 HIWORD부터 찍어봐요(32비트 값의 하위/상위 16비트를 따로 확인하는 거). 제대로 들어오는지."
WORD size
WORD size는 두 가지 의미로 쓰인다.
(1) 아키텍처 관점: CPU가 한 번에 처리하는 데이터의 기본 단위 크기다. 32비트 CPU는 4바이트, 64비트 CPU는 8바이트. CPU의 레지스터 크기, 메모리 주소 체계, 데이터 정렬 방식에 영향을 준다. WORD size가 달라지면 같은 데이터 구조라도 메모리에 배치되는 방식이 달라질 수 있다.
(2) Windows API 관점: WORD는 항상 16비트로 고정된 타입명이다. DWORD는 32비트, QWORD는 64비트. LOWORD/HIWORD 매크로는 32비트(DWORD) 값에서 하위 16비트와 상위 16비트를 추출한다.
에피소드에서 "LOWORD/HIWORD부터 찍어봐라"는 "데이터를 쪼개서 어디서 깨지는지 확인하라"는 디버깅 전략이다. 32비트 환경에서 만든 프로그램을 64비트로 옮기면, 포인터 크기(4바이트→8바이트)와 데이터 정렬이 달라져서 데이터를 잘못 읽게 될 수 있다.
그대로 코드를 고치자 프로그램이 기적같이 잘 돌아갔다. 일동 경악.
왜 이런 일이 벌어지는가¶
좀 더 파고들어 보겠다. Python만 써본 사람에게는 아래 코드가 낯설 수 있다. C 언어에서는 변수를 선언할 때 타입(char, int, short)과 크기를 직접 지정한다. Python의 x = 1과 달리, 데이터가 몇 바이트를 차지하는지가 프로그래머의 책임이다.
struct SensorData {
char type; // 1바이트: 문자 또는 작은 정수
int value; // 4바이트: 일반적인 정수
void* ptr; // 포인터: 32비트 환경 4바이트, 64비트 환경 8바이트 (여기가 핵심)
short status; // 2바이트: 작은 정수
};
구조체 정렬 (struct alignment/padding)
CPU가 메모리에서 데이터를 읽을 때, 특정 경계에 맞춰 정렬된 주소에서 읽는 것이 가장 효율적이다(혹은 그렇게만 읽을 수 있다). 그래서 컴파일러는 구조체 필드를 자연 정렬(natural alignment)에 맞춰 배치한다. 자연 정렬이란, N바이트 크기의 데이터를 N의 배수 주소에 배치하는 것이다.
char(1바이트): 아무 주소에나 배치 가능short(2바이트): 2의 배수 주소에 배치int(4바이트): 4의 배수 주소에 배치- 포인터 (4/8바이트): 해당 크기의 배수 주소에 배치
이 규칙을 맞추기 위해 컴파일러가 필드 사이에 자동으로 삽입하는 빈 공간이 padding이다.
아래 그림에서 offset은 구조체 시작 지점으로부터 몇 번째 바이트인지를 나타낸다. pad는 정렬을 맞추기 위해 컴파일러가 자동으로 넣는 빈 공간이다.
32비트 환경에서의 메모리 배치 (가장 큰 멤버가 4바이트이므로, 구조체 전체도 4의 배수로 맞춤):
offset 0: [type(1)] [pad(3)] -- int를 4의 배수 주소에 맞추기 위해 3바이트 빈칸
offset 4: [value(4)]
offset 8: [ptr(4)] -- 32비트에서 포인터는 4바이트
offset 12: [status(2)] [pad(2)] -- 구조체 전체를 4의 배수(가장 큰 멤버 기준)로 맞추기 위해
총 16바이트
64비트 환경에서의 메모리 배치 (가장 큰 멤버인 ptr이 8바이트이므로, 구조체 전체도 8의 배수로 맞춤):
offset 0: [type(1)] [pad(3)] -- int를 4의 배수 주소에 맞추기 위해 3바이트 빈칸
offset 4: [value(4)]
offset 8: [ptr(8)] -- 64비트에서 포인터는 8바이트
offset 16: [status(2)] [pad(6)] -- ptr 끝(offset 16)은 2의 배수이므로 status는 바로 배치
-- 구조체 전체를 8의 배수(가장 큰 멤버 기준)로 맞추기 위해 pad 6
총 24바이트
같은 구조체 정의인데, 32비트에서는 16바이트, 64비트에서는 24바이트다. 32비트 환경에서 16바이트로 레이더 데이터를 읽던 프로그램이 64비트에서 같은 방식으로 읽으면, 바이트 오프셋이 어긋나서 필드 값이 엉뚱한 위치에서 읽히게 된다.
5명이 몇 주를 뒤진 이유는 실력이 없어서가 아니다. "WORD size"라는 키워드를 몰랐기 때문이다. 이 키워드를 모르면 "아키텍처가 바뀌면 데이터 정렬도 바뀐다"는 연결 자체가 떠오르지 않는다. 떠오르지 않으면 검색할 수도 없고, 의심할 수도 없다.
(이 에피소드에서 WORD size의 기술적 세부사항까지 전부 이해할 필요는 없다. 실제로 어떻게 고쳤는지가 궁금하다면 - 컴파일러에게 패딩을 넣지 말라고 지시하는 #pragma pack 같은 방법이 있고, 데이터 구조를 명시적으로 재정의하는 방법도 있다. 크로스 플랫폼 환경에서는 WORD size 말고도 Endianness(바이트 순서) 같은 함정이 더 있다. 중요한 건 "키워드를 아느냐 모르느냐"가 디버깅 시간을 몇 주에서 하루로 바꿀 수 있다는 사실이다.)
"임베디드 이야기라 나한테는 해당 없는 것 아닌가?" 싶을 수 있다. 백엔드에서 WORD size 때문에 데이터가 깨지는 경우는 거의 없다. 하지만 같은 구조의 문제는 도처에 있다. Timezone을 모르면 시간 데이터가 몇 시간씩 밀리는 버그를 못 잡고, 문자 인코딩(UTF-8 vs EUC-KR)을 모르면 한글이 깨지는 원인을 못 찾는다. 코드 로직에는 문제가 없는데 데이터가 이상한 경우, 원인은 대부분 "모르는 키워드" 안에 숨어있다.
이 에피소드에서 중요한 건 하나다. 빡빡이 상병은 WORD size라는 키워드를 알고 있었다. "아키텍처가 바뀌었다"는 정보에서 "WORD size가 달라졌을 것"이라는 추론을 바로 해낸 거다.
키워드를 아느냐 모르느냐. 이게 이 강의 전체를 관통하는 주제다.
키워드를 모르면 검색도 못 한다¶
금요일 오후 5시. 서버 모니터링 대시보드에 알람이 울린다. API 응답 시간이 평소 50ms에서 5초로 치솟았다.
주니어 A와 주니어 B가 동시에 문제를 본다. 둘 다 Grafana 대시보드를 켠다. CPU 사용률은 15%, 메모리도 정상이다. 그런데 한 패널이 눈에 띈다 - DB Connection Active 수가 pool_size 상한선인 5에 딱 붙어있고, Connection Wait Time이 4초를 넘기고 있다.
주니어 A의 접근:
A는 Grafana를 봤지만, 어떤 지표가 문제인지 판단이 안 된다. "CPU도 정상이고 메모리도 정상인데 뭐가 문제지?" 결국 구글로 간다.
검색 결과는 온갖 잡다한 글이 쏟아진다. "서버 사양을 올려라", "캐시를 써라", "CDN을 적용해라"... 어디서부터 시작해야 할지 모른다.
주니어 B의 접근:
B도 처음부터 원인을 알고 있었던 건 아니다. 하지만 Grafana에서 "Active Connection이 상한선에 붙어있고 Wait Time이 비정상"이라는 패턴을 보고, 예전에 들었던 "Connection Pool Exhaustion"이라는 키워드가 떠올랐다.
2번째 검색에서 Stack Overflow 답변을 찾았다. DB Connection Pool의 기본 크기가 5인데, 동시 요청이 그보다 많아서 대기가 발생한 거다. pool_size를 늘리고 max_overflow를 설정하니 해결됐다. (물론 커넥션이 제대로 반납되지 않는 누수 문제일 수도 있다. 이건 Ch.6에서 다룬다.)
차이가 뭔가? 둘 다 같은 대시보드를 봤다. 같은 그래프를 봤다. 하지만 A는 "느리다"라는 현상만 보였고, B는 "Connection이 상한선에 도달했다"는 단서를 읽어낼 수 있었다. 키워드를 알면 모니터링 지표에서 패턴이 보인다. 모르면 그래프가 있어도 그냥 선이다.
AI에게 지시할 때도 마찬가지다¶
2024년 이후 개발자의 상당수가 AI 코딩 도구를 쓴다. Claude, ChatGPT, Cursor, GitHub Copilot. 도구는 달라도 핵심은 같다 - AI에게 지시를 내려서 코드를 만들거나 문제를 해결한다.
그런데 같은 AI에게 같은 문제를 시켜도, 결과는 천차만별이다.
운영 중인 API의 응답이 느리다. 로그를 찍어보니 특정 엔드포인트에서 2초 이상 걸린다.
개발자 A의 프롬프트:
AI의 응답: Redis 캐시 레이어를 추가하자, 응답을 gzip으로 압축하자, CDN을 적용하자... 방향이 너무 넓다. 뭘 먼저 해야 할지 모르겠다.
개발자 B도 처음엔 "느리다"는 것만 알았다. 로그를 보다가 DB 쿼리가 반복적으로 나가는 걸 발견했고, "이건 I/O 쪽 문제다"라는 방향을 잡았다. 거기서 프롬프트를 쳤다:
N+1 쿼리
목록 1개를 가져온 후, 목록의 각 항목에 대해 개별 쿼리를 N번 더 날리는 패턴이다.
예를 들어 게시글 100개를 조회한 뒤, 각 게시글의 작성자 정보를 가져오기 위해 쿼리를 100번 더 날리면 총 101번의 쿼리가 실행된다. 1번이면 끝날 걸 101번 하는 셈이다.
자세한 내용은 Ch.13에서 다룬다.
AI의 응답: "쿼리가 반복적으로 나가는 걸 보면 N+1 문제일 가능성이 높습니다"라고 짚어주면서, joinedload(한 번의 쿼리로 관련 데이터를 함께 가져오는 방식)로 변경하는 방법과 쿼리 실행 계획 확인 방법까지 안내해준다.
같은 AI다. 같은 문제다. 하지만 A는 AI가 어디로 가야 할지 모르는 지시를 내렸고, B는 "I/O Bound"라는 키워드 하나로 문제의 영역을 좁혔다. B도 처음부터 N+1이라는 정확한 원인을 알고 있었던 건 아니다. "I/O Bound"라는 키워드가 방향을 잡아줬고, AI가 거기서부터 구체적인 원인을 찾아준 거다.
JD와 면접의 간극¶
채용 시장에서도 같은 이야기가 반복된다.
아래는 가상의 백엔드 개발자 채용 공고(JD)다. 실제 공고들을 참고해서 만든 것이니 현실과 크게 다르지 않다.
[백엔드 개발자 채용]
자격요건:
- Python 또는 Java 1년 이상 경험
- FastAPI, Django, Spring Boot 중 하나 이상 사용 경험
- MySQL 또는 PostgreSQL 사용 경험
- AWS 환경 배포 경험
- Git 기반 협업 경험
우대사항:
- Docker, Kubernetes 사용 경험
- CI/CD 파이프라인 구축 경험
- 대규모 트래픽 처리 경험
CS 키워드가 거의 없다. "프로세스", "스레드", "System Call", "인덱스" 같은 단어는 찾아보기 어렵다. (시니어 JD로 가면 "대규모 트래픽 처리", "동시성 제어", "DB 튜닝 경험" 같은 표현이 우대사항에 심심찮게 들어가지만, 신입·주니어 공고는 대체로 위와 같다.)
그런데 면접에 들어가면 이런 질문이 나온다:
- "프로세스와 스레드의 차이가 뭔가요?"
- "인덱스를 걸면 왜 빨라지나요?"
- "Connection Pool이 뭐고, 왜 필요한가요?"
- "async로 처리하면 항상 빨라지나요?"
이 챕터에서 반복 등장할 키워드 사전 (한 줄씩)
- Connection Pool: DB 커넥션을 미리 만들어두고 재사용하는 풀. 매 요청마다 연결을 새로 맺으면 느리고, 풀이 고갈되면 요청이 대기·실패한다. Ch.6·Ch.16에서 자세히.
- I/O Bound: 실행 속도가 디스크/네트워크/DB 응답 같은 I/O 대기 시간에 의해 제한되는 상태. CPU는 놀고 "기다리는" 게 병목. Ch.3에서 측정.
- CPU Bound: 실행 속도가 CPU 연산 자체에 의해 제한되는 상태. I/O 없이 숫자 계산만 돌리는 경우. Ch.3에서 측정.
- async (비동기): 완료를 기다리지 않고 다음 작업을 진행하는 프로그래밍 방식. I/O Bound에서는 유리, CPU Bound에서는 오히려 느려질 수 있음. Ch.3에서 직접 증명.
- N+1 쿼리: 목록 1건 가져온 뒤 각 항목마다 개별 쿼리를 N번 더 날리는 실수 패턴. 1건이면 끝날 걸 101건 날리게 된다. Ch.13에서 ORM과 함께.
이 용어들은 Ch.1 내내 반복해서 등장한다. 지금은 "한 줄 설명"만 머리에 걸쳐두면 된다. 깊이 들어가는 건 해당 챕터에서.
왜 JD에는 없는 걸 면접에서 물어보는가?
JD는 "이 도구를 쓸 줄 아느냐"를 묻는 거고, 면접은 "이 도구로 문제가 생겼을 때 해결할 수 있느냐"를 묻는 거다. 도구는 가르칠 수 있지만, 문제 해결 능력은 CS를 모르면 안 된다.
면접에서 Sliding Window를 설명하라고 한다고 치자.
Sliding Window (슬라이딩 윈도우)
일정한 크기의 "윈도우(창)"를 데이터 위에서 한 칸씩 밀면서 처리하는 알고리즘이다.
원래 데이터링크 계층에서 흐름 제어와 오류 제어(ARQ: Go-Back-N, Selective Repeat 등)를 위한 윈도우 관리 메커니즘으로 사용되었고, 이후 TCP가 전송 계층에서 같은 원리를 차용하여 흐름 제어(Flow Control)와 혼잡 제어(Congestion Control)에 활용했다. 그 자체가 "A 분야의 개념이 B에 차용된" 대표적인 사례다.
현재는 Stream 처리, 시계열(Time-Series) 데이터 분석, 실시간 모니터링 시스템 등에 널리 사용된다.
Ch.6에서 네트워크를 다룰 때 다시 등장한다.
외웠다면 이런 답변이 나온다:
이해했다면 이런 답변이 나온다:
"원래 데이터링크 계층의 흐름 제어에서 시작된 알고리즘입니다.
TCP에서는 수신자가 처리할 수 있는 버퍼 크기(rwnd)에 맞춰
송신자가 보내는 데이터양을 조절합니다.
ACK를 받기 전까지 최대 윈도우 크기만큼만 미리 보낼 수 있는 구조입니다."
어떤 답변 이후에 다음 질문이 "뜬금없이 튀어나올" 확률이 더 낮은가?
이해한 답변에는 꼬리를 물 수 있는 키워드가 들어있다. "버퍼 크기", "ACK", "혼잡 제어", "동적 조절". 면접관은 이 키워드를 잡고 자연스럽게 다음 질문으로 이어간다. 대화가 된다.
외운 답변에는 꼬리를 물 키워드가 없다. 면접관은 할 수 없이 다음 질문을 "아무 맥락 없이" 던져야 한다. "그럼 프로세스와 스레드의 차이가 뭔가요?" 하고 말이다.
면접관이 갑자기 프로세스와 스레드의 차이를 아무 맥락 없이 물어보는 이유가 뭘까? 깜짝 퀴즈를 내는 게 아니다. 이전 답변에서 이어갈 키워드를 못 찾았기 때문이다.
결국 CS로 회귀한다¶
WORD size 에피소드, 검색, AI, 면접. 전부 같은 이야기를 하고 있다.
키워드를 아느냐 모르느냐.
FastAPI, AsyncIO, Coroutine, Virtual Thread 같은 기술들이 유행한 지 꽤 됐다. 덕분에 CPU Bound니, I/O Bound니 같은 말이 입에 많이 붙었다.
그런데 이 말들이 이런 기술들 때문에 생긴 걸까? 아니다. 원래 있던 개념이다. 기술이 바뀌어도 그 밑에 깔린 CS는 바뀌지 않는다.
가끔 이런 일이 벌어진다:
- "나는 Async를 좋아하니까" 하고 전부 async로 때려 박는다. 그런데 느리다. (Ch.3에서 다룬다)
- 프로그램이 OOM(Out of Memory)이 났는데 왜 났는지 모른다. (Ch.4에서 다룬다)
- Stack Overflow가 왜 "overflow"인지 모른다. (역시 Ch.4에서 다룬다)
원인을 찾지 못하고 일단 해결한다? 결국 돌아온다. 더 큰 문제로. 왜냐하면 근본적인 원인을 찾지 못했기 때문이다.
근본적인 문제를 어떻게 찾는가? 결국 CS로 회귀한다. "왜 이게 문제인지"를 알아야 하기 때문이다.
이 강의는 그 "왜"를 파고드는 강의다.