
두 단어의 어색한 조합
Functional Mechanical Sympathy라는 제목의 발표 영상이 공개됐어요. 제목만 봐도 묘하게 긴장감이 느껴지는데요. '함수형 프로그래밍'과 '기계적 공감'은 보통 반대편에 서 있는 개념처럼 여겨지거든요. 한쪽은 '컴퓨터가 어떻게 돌아가는지는 신경 쓰지 말고 수학처럼 우아하게 코드를 쓰자'는 쪽이고, 다른 한쪽은 '컴퓨터가 실제로 어떻게 돌아가는지 알아야 빠른 코드를 쓸 수 있다'는 쪽이니까요. 이 글에서는 발표가 던지는 질문을 중심으로, 두 개념이 각각 뭔지, 그리고 왜 둘을 같이 이야기하는 게 의미 있는지 차근차근 풀어볼게요.
기계적 공감이 뭐냐면
'기계적 공감'이라는 말은 원래 F1 레이서 재키 스튜어트가 한 말에서 왔어요. 최고의 드라이버가 되려고 엔지니어가 될 필요는 없지만, 차가 어떻게 움직이는지 '느낄' 줄은 알아야 한다는 거죠. 이 표현을 소프트웨어 쪽으로 가져온 사람이 LMAX Disruptor로 유명한 마틴 톰슨이에요. 초저지연 금융 거래 시스템을 만들면서, 하드웨어 특성을 이해하고 코드를 짜면 성능이 몇 배에서 몇십 배까지 달라진다는 걸 보여줬죠.
핵심은 이거예요. 요즘 CPU는 메모리에서 값을 가져오는 게 계산하는 것보다 훨씬 느려요. 그래서 CPU 안에 캐시라는 작은 고속 저장소를 두는데, 여기에 필요한 데이터가 있으면 몇 나노초, 없어서 메인 메모리까지 가야 하면 100나노초 가까이 걸려요. 수십 배 차이죠. 비유하자면 책상 위에 있는 책을 집는 것과 도서관 서고까지 걸어가는 것의 차이예요.
그리고 CPU는 메모리를 한 바이트씩 가져오지 않고 캐시 라인(보통 64바이트) 단위로 뭉텅이로 가져와요. 그래서 데이터가 메모리에 연속으로 붙어 있으면 한 번 가져올 때 옆 데이터까지 딸려 와서 빨라지고, 여기저기 흩어져 있으면 매번 서고까지 왕복해야 해요.
그런데 함수형 코드는 왜 이 문제에 취약할까?
함수형 프로그래밍의 대표 특징은 불변성이에요. 한번 만든 데이터는 바꾸지 않고, 바꾸고 싶으면 새로 만들어요. 그리고 연결 리스트나 트리 같은 자료구조를 즐겨 쓰죠. 여기서 문제가 생겨요.
- 연결 리스트는 각 노드가 메모리 여기저기에 흩어져 있어서, 순회할 때마다 캐시 미스가 나기 쉬워요.
- 불변 데이터를 계속 새로 만들면 작은 객체가 엄청나게 많이 할당되고, 가비지 컬렉터가 바빠져요.
map,filter,reduce를 연달아 쓰면 중간 결과 리스트가 매 단계 새로 만들어질 수 있어요.- 대용량 데이터를 체이닝으로 처리할 때 중간 컬렉션이 몇 번 생기는지 한 번쯤 의식해 보세요. Kotlin이라면
asSequence(), Java라면 스트림의 지연 평가를 활용하는 식으로요. - 핫 패스(자주 실행되는 코드)에서는 박싱된 객체 리스트보다 원시 타입 배열이 훨씬 빠를 수 있어요.
- 무엇보다 추측하지 말고 측정하세요. JMH, perf, 프로파일러로 확인하기 전엔 '느리다'도 '빠르다'도 가설일 뿐이에요.
그래서 '함수형은 예쁘지만 느리다'는 인식이 오래 있었던 거예요.
그럼에도 둘은 화해할 수 있어요
흥미로운 건 함수형의 특징이 오히려 하드웨어 친화적인 최적화를 가능하게 한다는 점이에요. 몇 가지 예를 들어볼게요.
1. 퓨전(fusion): 순수 함수는 부작용이 없어서, 컴파일러가 map f . map g를 map (f . g)로 마음 놓고 합칠 수 있어요. 중간 리스트가 아예 사라지고 데이터를 한 번만 훑게 되죠. Haskell의 스트림 퓨전이나 Rust 이터레이터가 사실상 이 원리예요.
2. 참조 카운트 기반 제자리 수정: Lean 4나 Koka 같은 언어는 '이 값을 참조하는 곳이 나 하나뿐이면 새로 만들지 말고 그 자리에서 고쳐 쓰자'는 최적화를 해요. 겉으로는 불변인데 속으로는 효율적인 가변 코드처럼 동작하는 거죠.
3. 데이터 지향 설계: 객체 배열 대신 필드별 배열(Struct of Arrays)로 데이터를 두면 캐시 효율이 크게 좋아지는데, 이런 '데이터를 변환하는 파이프라인' 사고방식은 함수형과 정말 잘 맞아요.
4. 병렬성: 공유 상태를 바꾸지 않으니 여러 코어가 서로의 캐시 라인을 뺏고 뺏기는 '거짓 공유(false sharing)' 문제가 줄어들어요.
업계 흐름 속에서
최근 언어 설계 트렌드를 보면 이 두 세계가 점점 가까워지고 있어요. Rust는 함수형의 표현력을 가져오면서도 메모리 레이아웃을 개발자가 통제하게 했고, OCaml 5는 멀티코어 런타임을 도입했고, Java도 레코드와 값 타입(Valhalla 프로젝트)으로 '불변이면서 메모리에 촘촘히 붙은 데이터'를 지향하고 있어요. 결국 '추상화냐 성능이냐' 양자택일이 아니라, 좋은 추상화가 컴파일러에게 최적화 힌트를 준다는 방향으로 가고 있는 거죠.
한국 개발자에게 주는 시사점
실무에서 Kotlin, Scala, TypeScript, Java Stream API로 함수형 스타일을 많이 쓰실 텐데요. 몇 가지 바로 적용할 수 있는 습관이 있어요.
마무리
한 줄 정리: 함수형 프로그래밍의 순수함은 성능의 적이 아니라, 하드웨어를 잘 이해하는 컴파일러에게 최적화할 자유를 주는 도구가 될 수 있어요.
여러분 팀에서는 함수형 스타일 코드 때문에 성능 문제를 겪어본 적 있나요? 아니면 반대로 함수형으로 바꿨더니 오히려 빨라진 경험이 있나요? 경험담 공유해 주세요!
🔗 출처: Hacker News