
요즘 나오는 CPU들, 코어 수가 정말 많죠. 노트북도 기본 8코어, 서버는 수십 코어가 흔한 시대인데요. 그런데 이상하게도 우리가 짜는 코드는 여전히 코어 하나만 열심히 굴리는 경우가 많아요. 클럭 속도가 알아서 올라가서 코드가 저절로 빨라지던 시대, 이른바 '공짜 점심'은 이미 20년 전에 끝났거든요. 이제 남는 성능을 끌어내려면 개발자가 직접 병렬 프로그래밍을 해야 하는데, 이게 또 악명이 높죠. 스레드, 락, 데드락... 단어만 들어도 머리가 아픈 분들을 위해, 병렬 프로그래밍을 대하는 마음가짐을 '선(Zen)'이라는 이름으로 정리한 글이 있어서 그 관점을 바탕으로 핵심 원칙들을 풀어볼게요.
동시성과 병렬성, 여기서 헷갈리면 계속 꼬여요
먼저 용어 정리부터 할게요. 이게 뭐냐면, 카페로 비유하면 딱 이해되거든요. 바리스타 한 명이 주문을 받고, 에스프레소가 내려지는 동안 다음 주문을 받고, 우유를 데우면서 또 다른 손님을 응대하는 게 '동시성(Concurrency)'이에요. 일을 잘게 쪼개서 번갈아 처리하는 구조죠. 반면 바리스타 세 명이 각자 음료를 진짜로 동시에 만드는 게 '병렬성(Parallelism)'이고요. 동시성은 프로그램의 구조에 관한 이야기고, 병렬성은 실제 실행에 관한 이야기라는 말이 그래서 나오는 건데요. CPU 코어를 전부 활용하고 싶다면 우리에게 필요한 건 병렬성이에요. 그런데 많은 개발자가 스레드만 여러 개 띄우면 병렬이 되는 줄 알았다가, 스레드끼리 서로 기다리느라 오히려 싱글 스레드보다 느려지는 경험을 하곤 하죠.
암달의 법칙: 코어를 아무리 사도 20배가 한계인 이유
병렬 프로그래밍에서 가장 먼저 겸손해져야 하는 지점이 암달의 법칙(Amdahl's Law)인데요. 이게 뭐냐면, 프로그램에서 병렬화할 수 없는 부분이 조금이라도 있으면 그 부분이 전체 성능 향상의 상한선을 정해버린다는 법칙이에요. 예를 들어 코드의 95%를 완벽하게 병렬화해도 나머지 5%가 순서대로 실행되어야 한다면, 코어를 백 개, 천 개를 붙여도 전체 속도는 최대 20배 이상 빨라질 수 없어요. 그러니까 무작정 스레드 수를 늘리기 전에 '내 프로그램에서 진짜 순차적으로 돌아야만 하는 부분이 어디인지'부터 찾는 게 순서인 거죠. 병목이 디스크 입출력이나 네트워크 대기라면 병렬화가 아니라 비동기 처리가 답일 수도 있고요.
진짜 적은 스레드가 아니라 '공유되는 상태'예요
병렬 프로그래밍이 어려운 근본 원인은 여러 스레드가 같은 데이터를 동시에 고치려 드는 상황이에요. 실행 순서에 따라 결과가 달라지는 걸 레이스 컨디션(경쟁 상태)이라고 하는데, 이걸 막으려고 락(자물쇠처럼 한 번에 한 스레드만 접근하게 하는 장치)을 걸면 이번엔 서로의 락을 기다리다 전체가 멈춰버리는 데드락이 찾아오거든요. 그래서 요즘의 지혜는 락을 잘 쓰는 법이 아니라, 락이 필요 없는 구조를 만드는 쪽으로 모여요.
첫째, 데이터를 아예 나눠버리세요. 이미지 1만 장을 처리해야 한다면 스레드 4개가 2,500장씩 자기 몫만 만지면 락이 필요 없잖아요. 이런 문제를 '당황스러울 만큼 쉬운 병렬(embarrassingly parallel)' 문제라고 부르는데, 실무 문제의 상당수가 사실 여기에 속해요. 둘째, 상태를 공유하지 말고 메시지를 주고받으세요. Go 언어의 유명한 격언인 '메모리를 공유해서 통신하지 말고, 통신해서 메모리를 공유하라'가 바로 이 이야기예요. 셋째, 애초에 값을 바꿀 수 없는 불변(immutable) 데이터로 만들면 몇 개의 스레드가 동시에 읽어도 안전하고요.
언어들이 이 철학을 담아내는 방식
이 원칙들은 최신 언어 설계에 그대로 녹아 있어요. Go는 고루틴과 채널로 메시지 전달을 언어의 기본기로 만들었고, Rust는 소유권 시스템으로 데이터 레이스가 있는 코드를 아예 컴파일 단계에서 거부해버려요. Erlang과 Elixir는 상태를 공유하지 않는 수만 개의 가벼운 프로세스가 메시지만 주고받는 액터 모델로 수십 년 전부터 이 길을 걸었고요. 파이썬도 오랫동안 발목을 잡던 GIL(한 번에 한 스레드만 실행되게 하던 전역 잠금)을 걷어내는 프리 스레딩 작업이 진행되면서 진짜 병렬 시대를 준비하고 있죠.
우리 실무에는 어떻게 적용할까요
백엔드 개발자라면 대량 데이터 배치 처리, 이미지나 영상 변환, 크롤링, 머신러닝 전처리 같은 데서 바로 써먹을 수 있어요. 순서는 이래요. 먼저 작업이 정말 CPU 병목인지 확인하고, 데이터가 독립적으로 쪼개지는지 보고, 쪼개진다면 락 없이 나눠서 돌리고, 결과만 마지막에 합치는 거죠. 락을 잡는 건 이 모든 게 안 될 때의 최후 수단이고요.
정리하면, 병렬 프로그래밍의 선(Zen)은 '락을 정교하게 다루는 기술'이 아니라 '공유 자체를 없애는 설계'에 있다는 거예요. 여러분은 실무에서 병렬 처리를 어디까지 해보셨나요? 데드락이나 레이스 컨디션으로 밤새 고생했던 경험담도 궁금해요.
🔗 출처: Hacker News