자바스크립트나 파이썬으로 비동기 코드를 짜본 분이라면 한 번쯤 느껴보셨을 거예요. 함수 하나를 async로 만들었더니, 그걸 호출하는 함수도 async가 되어야 하고, 또 그걸 호출하는 함수도... 이렇게 async가 코드 전체로 번져나가는 현상이요. 이걸 '함수 색칠 문제(function coloring problem)'라고 부르는데요. 함수가 '동기'와 '비동기' 두 종류로 나뉘어 버려서, 섞어 쓰려면 온갖 변환 코드가 필요해지는 문제예요. 오늘 소개할 글은 OCaml이라는 함수형 언어가 이 문제를 어떻게 우아하게 풀었는지 직접 써보고 정리한 후기인데, 다른 언어를 쓰는 분들에게도 시사점이 커요.
OCaml 5의 비밀 무기, 이펙트 핸들러
OCaml은 2022년 버전 5.0에서 큰 변화를 겪었어요. 멀티코어 지원과 함께 '이펙트 핸들러(effect handler)'라는 기능이 들어왔거든요. 이게 뭐냐면, 함수 실행 중에 '잠깐, 여기서 처리할 일이 생겼어요'라고 신호를 보내면, 바깥에서 그 신호를 받아 처리한 뒤 함수를 멈췄던 지점부터 다시 이어서 실행할 수 있게 해주는 장치예요. 예외(exception)와 비슷한데, 결정적인 차이는 던진 지점으로 되돌아갈 수 있다는 것. 일시정지와 재개가 가능한 예외라고 생각하면 돼요.
이게 왜 대단하냐면, 비동기 처리를 언어 문법이 아니라 라이브러리 차원에서 구현할 수 있게 해주거든요. 함수 입장에서는 그냥 평범한 순차 코드를 쓰는데, 파일을 읽는 순간 이펙트가 발생하고, 런타임이 그 사이에 다른 작업을 돌리다가, I/O가 끝나면 원래 코드로 돌아오는 거예요. 코드에 async/await 키워드가 등장할 필요가 아예 없어요.
Eio: 직접 스타일로 쓰는 동시성
Eio는 이 이펙트 핸들러 위에 만들어진 I/O 라이브러리예요. 기존 OCaml 생태계에서는 Lwt나 Async라는 라이브러리를 썼는데, 둘 다 자바스크립트의 프라미스(Promise)처럼 '미래에 완성될 값'을 주고받으며 체이닝하는 방식이었거든요. 코드가 온통 bind와 콜백으로 뒤덮이는 스타일이죠. Eio는 이걸 '직접 스타일(direct style)'로 바꿔요. 그냥 위에서 아래로 읽히는 평범한 코드를 쓰면, 보이지 않는 곳에서 런타임이 알아서 스케줄링해주는 거예요.
또 하나 눈여겨볼 건 '구조적 동시성(structured concurrency)'이에요. 이게 뭐냐면, 동시에 실행되는 작업들이 반드시 부모 스코프에 묶이도록 강제하는 설계예요. Eio에서는 Switch라는 스코프 안에서만 작업을 띄울 수 있고, 스코프가 끝나면 그 안의 작업도 전부 정리돼요. 백그라운드에서 잊혀진 채 떠도는 '유령 태스크'가 구조적으로 생길 수 없는 거죠. 에러가 나면 같은 스코프의 형제 작업들도 함께 취소되니까 뒷정리 걱정도 줄고요.
업계 전체가 같은 방향으로 가고 있어요
흥미로운 건 이 방향이 OCaml만의 실험이 아니라는 거예요. 자바는 버추얼 스레드(Project Loom)로 '스레드를 수백만 개 만들어도 괜찮은' 세상을 열면서 async 키워드 없는 동시성을 택했고, Go는 처음부터 고루틴으로 같은 철학을 구현했죠. 파이썬의 Trio 라이브러리와 코틀린 코루틴은 구조적 동시성 개념을 대중화했고요. 함수 색칠 문제로 고생해본 언어 커뮤니티들이 하나둘씩 '비동기는 런타임이 숨기고, 개발자는 순차 코드를 쓴다'는 결론으로 수렴하고 있는 거예요. OCaml의 이펙트 핸들러는 그중에서도 이론적으로 가장 깔끔한 접근이라 연구자들의 주목을 받고 있어요.
한국 개발자에게 주는 시사점
당장 OCaml을 실무에 쓸 일은 많지 않을 거예요. 하지만 이 흐름 자체는 알아둘 가치가 충분해요. 자바 백엔드를 하신다면 버추얼 스레드가 이미 실무 선택지가 됐고, 코틀린이나 파이썬을 쓴다면 구조적 동시성 개념이 코드 품질을 바로 높여주거든요. '내가 띄운 비동기 작업은 누가 책임지고 정리하는가'라는 질문 하나만 코드 리뷰에 추가해도 리소스 누수 버그가 눈에 띄게 줄어요. 그리고 함수형 언어의 실험이 몇 년 뒤 주류 언어의 기능이 되는 패턴은 계속 반복돼 왔으니까, 이펙트 핸들러라는 개념은 지금 미리 맛봐두면 좋아요.
마무리
핵심 한 줄: 동시성의 미래는 async/await를 잘 쓰는 게 아니라, async/await가 보이지 않게 만드는 쪽으로 가고 있어요. 여러분의 주력 언어에서는 비동기 코드 때문에 어떤 고생을 하고 계세요? 색칠된 함수와 보이지 않는 런타임, 어느 쪽이 더 좋은 설계라고 생각하세요?
🔗 출처: Hacker News