
10년 넘게 회자되는 클래식
게임 프로그래밍 언어 "Wren"의 창시자이자 "Crafting Interpreters" 책으로 유명한 로버트 나이스트롬(Bob Nystrom)이 2015년에 쓴 글이 다시 돌고 있어요. 제목은 "What Color is Your Function?"인데, 우리말로 옮기면 "당신의 함수는 무슨 색입니까?" 정도가 되겠네요. 10년이 지난 지금도 비동기 프로그래밍을 이야기할 때 거의 매번 인용되는 글이에요.
이 글이 풀어내는 핵심 문제는 이거예요. 자바스크립트나 파이썬 같은 언어에서, 함수를 "동기 함수"와 "비동기 함수" 두 종류로 나눠놓는 것 자체가 언어 설계의 큰 문제다. 나이스트롬은 이걸 "함수에 빨간색과 파란색을 칠해놓은 상황"에 비유했어요.
함수의 색깔이라는 비유
비유를 풀어볼게요. 어떤 함수는 빨간색, 어떤 함수는 파란색이라고 칩시다. 규칙은 이래요. 빨간 함수는 파란 함수만 호출할 수 있어요. 반대로 파란 함수는 다른 파란 함수만 호출할 수 있고, 빨간 함수를 호출하려면 특별한 키워드(예: await)가 필요해요. 게다가 빨간 함수를 호출하는 함수는 자동으로 빨간색이 돼버려요. 한 번 빨간색이 묻으면 호출 체인 전체가 빨갛게 물드는 거예요.
자바스크립트로 바꿔서 보면, 빨간색은 async 함수, 파란색은 일반 함수예요. 파이썬도 마찬가지고요. C#, 러스트, 코틀린도 비슷한 구조를 갖고 있어요. 이게 왜 문제냐면, 어떤 라이브러리 함수를 쓰려고 보니까 그게 비동기 함수더라, 그러면 그걸 부르는 내 함수도 비동기로 만들어야 하고, 내 함수를 부르는 상위 함수도 비동기로 만들어야 하고... 이게 전염병처럼 코드 전체로 퍼져요. 이걸 "async가 전염된다(async contagion)"고도 표현하죠.
왜 이런 구조가 생겼나
역사적으로 보면 이건 "콜백 지옥"을 해결하려다 생긴 부작용이에요. 노드.js 초창기엔 비동기 작업을 콜백 함수로 처리했거든요. 파일 읽고 끝나면 호출되는 콜백, 그 안에서 또 DB 조회하고 끝나면 호출되는 콜백, 그 안에서 또... 이렇게 중첩되면서 코드가 오른쪽으로 계속 들여쓰기되는 "피라미드 오브 둠"이 만들어졌어요.
이걸 해결하려고 프로미스(Promise)가 나왔고, 그 위에 더 읽기 좋은 문법으로 async/await이 추가됐어요. 결과적으로 코드는 동기 코드처럼 위에서 아래로 읽히게 됐죠. 그런데 그 대가로 "함수의 색깔"이라는 구조적 문제가 들어와버린 거예요.
나이스트롬이 글에서 지적하는 건 이거예요. 단순히 "문법이 좀 불편하다"가 아니라, 고차 함수(higher-order function)를 쓰기 어려워진다는 점이에요. map, filter, reduce 같은 함수들이 콜백을 받는데, 그 콜백이 비동기 함수면 어떻게 처리할 거예요? JavaScript의 Array.prototype.map은 비동기 콜백을 제대로 처리 못해요. 그래서 Promise.all(arr.map(async fn)) 같은 우회 패턴이 필요해지죠. 함수형 프로그래밍의 우아함이 깨지는 지점이에요.
색깔 없는 언어들
그럼 이 문제를 우회한 언어들도 있어요. Go가 대표적이에요. Go에는 async 키워드가 없고, 모든 함수가 같은 "색깔"이에요. 비동기 처리는 함수 호출 앞에 go 키워드를 붙여서 고루틴(goroutine)으로 띄우고, 채널(channel)로 통신하면 끝이에요. 함수 자체는 동기/비동기 구분이 없어요. 그래서 라이브러리 작성자도 사용자도 색깔 문제를 신경 쓸 일이 없거든요.
Erlang/Elixir도 마찬가지예요. BEAM 가상 머신 위에서 모든 프로세스가 경량 스레드처럼 돌아가고, 메시지 패싱으로 통신하니까 "이 함수는 비동기야" 같은 표시가 필요 없어요.
Java도 최근에 Project Loom으로 가상 스레드(Virtual Threads)를 도입하면서 이 방향으로 움직였어요. 가상 스레드를 쓰면 동기 코드처럼 짜도 OS가 알아서 블로킹을 처리해주니까, async 키워드 없이도 고성능 비동기 처리가 가능해진 거예요. 자바 21 LTS에 정식 포함됐고, 이제 백엔드 자바 개발자들의 새 표준이 되고 있죠.
반대로 Rust는 의도적으로 "색깔이 있는" 길을 택했어요. Rust의 async는 제로코스트 추상화를 위해 컴파일 타임에 상태 머신으로 변환되는데, 이게 성능 면에선 최고지만 학습 곡선이 가팔라요. Rust 커뮤니티에서도 "async Rust는 사실상 다른 언어 같다"는 말이 나올 정도예요.
한국 개발자에게 주는 시사점
한국에서 가장 많이 쓰이는 백엔드 언어들을 보면 이 문제가 직접적으로 와닿아요. 자바 스프링 진영은 한동안 Reactor(WebFlux) 같은 리액티브 스타일로 가다가, 최근엔 다시 가상 스레드 기반의 동기 코드로 돌아오는 흐름이 보여요. "async가 전염되는 복잡성"보다 "가상 스레드로 단순하게 짜고 런타임에 맡기는" 쪽이 실용적이라는 판단이죠.
Python 쪽도 비슷해요. FastAPI가 인기를 끌면서 async/await을 본격적으로 쓰기 시작했는데, 라이브러리마다 동기/비동기 버전이 따로 있고(예: requests vs httpx, psycopg2 vs asyncpg), 둘을 섞어 쓰면 골치가 아파지거든요. 이게 정확히 나이스트롬이 말한 "함수의 색깔" 문제예요.
실무에서 비동기 코드를 다룰 때 명심할 점이 몇 가지 있어요. 첫째, 꼭 비동기여야 하는 이유가 있는지 먼저 따져보세요. I/O 바운드(네트워크, 파일, DB) 작업이 많고 동시 처리가 중요할 때만 의미가 있어요. CPU 작업은 비동기로 만들어도 빨라지지 않아요. 둘째, 언어가 가상 스레드를 지원한다면 적극 활용하세요. 자바 21+, Kotlin의 코루틴, Go의 고루틴은 색깔 문제 없이 동시성을 다룰 수 있는 좋은 도구예요. 셋째, 라이브러리 선택할 때 동기/비동기 일관성을 유지하세요. 두 세계를 섞으면 데드락이나 성능 저하가 발생하기 쉬워요.
마무리
10년 전 글이지만 본질을 정확히 짚었기 때문에 지금도 유효해요. 언어 설계자들이 "색깔 없는 동시성"을 향해 움직이고 있다는 점도 이 글의 통찰을 뒷받침하고요. 비동기 프로그래밍이 어려운 건 여러분 탓이 아니라 언어 설계의 근본적인 문제일 수 있어요.
여러분은 어떻게 생각하세요? async/await은 결국 과도기적 해결책이고, 가상 스레드 같은 방향이 미래일까요? 아니면 명시적인 비동기 표시가 코드 가독성과 성능 예측 가능성 면에서 여전히 가치가 있을까요?
🔗 출처: Hacker News
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공