60억 번째 차례를 기다리며

동기, 비동기, 그리고 동시성

2025. 11. 1.

프롤로그: 뛰어다니는 것들

처음 비동기 코드를 볼 때, 내 머릿속에는 정체불명의 뭔가가 코드 사이를 뛰어다니고 있었다.

await를 보면 코드를 읽던 내가 그 키워드를 밟고 밖으로 빠져나간다. 콜스택을 빠져나와 큐로 들어가고, 이벤트 루프가 돌고 돌다가, 어느 순간 다시 돌아온다. Promise가 resolve되면 then이 실행되고, 그 then이 또 다른 Promise를 반환하고, 그 Promise의 상태 변화가 또 다른 리스너를 깨우고…

나는 이 모든 과정을 머릿속에서 한 걸음씩 따라갔다. “여기서 이렇게 되면, 저기가 저렇게 되고…” 마치 복잡한 핀볼 기계를 보는 것처럼, 공이 어디로 튀어갈지 눈으로 쫓아가며.

Promise의 내부 구현을 파헤치고, 이벤트 루프의 동작을 분석하고, 콜스택의 흐름을 하나하나 손으로 더듬었다. 체인의 고리 하나하나를, 상태 변화의 순간 하나하나를. “아 여기서 await 하면 잠깐 콜스택을 빠져나와 나중에 다시 실행되겠구나.” “아 여기서 resolve 되면 그 다음 then이 실행되겠구나.”

이해했다고 생각했다. 비동기를 정복했다고 착각했다.

하지만 코드가 복잡해질수록 머리가 터질 것 같았다. 여기저기 뛰어다니는 녀석들을 동시에 추적할 수가 없었다. 코드를 읽는 게 아니라, 코드가 나를 끌고 다녔다. await를 볼 때마다 내가 기다려졌다. 내가 밖으로 튕겨 나갔다.

뭔가 잘못됐다는 걸 알았다.

Syn-chronos: 팔짱끼기

지금 돌이켜보면, 내가 했던 건 비동기 코드를 동기적으로 이해하려는 시도였다.

Synchronous. Syn(함께) + chronos(시간). 시간을 함께 간다는 뜻이다.

한자로 동기(同期)도 정확히 같은 의미를 담고 있다. 동(同)은 “같다, 함께”이고 기(期)는 “기약하다, 정해진 때”다. 같은 시기를 약속한다는 거다. 시간적으로 같이 가기로 약속한 것. 서로 팔짱이든 뭐든 묶고 이인삼각으로 걷기.

visualize the maginitude one googol

양극단의 톱니바퀴들은 동기적이다.
daniel de bruin

함수 A를 호출하면 나는 A와 시간을 함께 보낸다. A가 1초 걸리면 나는 그 1초를 A와 함께 기다린다. A가 끝나야 비로소 B로 넘어간다. 시간은 하나의 선이다. 순차적이고, 직렬적이고, 예측 가능하다. 기차의 객차들처럼, 앞 객차가 가야 뒤 객차도 간다.

나는 비동기 코드를 이 하나의 시간선 위에 올려놓으려 했다. 모든 흐름을 순서대로, 한 걸음 한 걸음 따라가려 했다. 여기서 이렇게 되면, 다음엔 저렇게 되고, 그러면 또…

하지만 비동기 코드는 하나의 선이 아니었다. 여러 개의 선이었다. 아니, 애초에 선이 아니라 점들이었는지도 모른다. 각자 독립적으로 존재하는 점들. 내가 하나의 선으로 엮으려고 발악할수록, 코드는 내 손에서 빠져나갔다.

Synchronous한 사고로는 asynchronous를 이해할 수 없었다. 왜냐하면 그건 근본적으로 다른 시간성이었으니까.

2막: A-synchronous: 느슨해지기

그래서 다시 물었다. 비동기란 뭘까?

The term asynchronous refers to two or more objects or events that do not exist or happen at the same time, that is, they are not synchronous. When multiple related things happen without any being dependent on the completion of previous happenings, they are asynchronous.

MDN

MDN에는 이렇게 쓰여 있었다. “비동기란 둘 이상의 객체 또는 이벤트가 동시에 존재하지 않거나 발생하지 않는 경우를 말합니다.”

동시에 존재하지 않는다? 그렇다면 무엇이 동시에 존재하는 건가?

Asynchronous. A(부정) + synchronous. 시간을 함께 가지 않는다는 뜻이다.

시간의 묶임이 풀린다. A와 B가 더 이상 하나의 시간선을 공유하지 않는다. A는 A대로 자기 시간을 살고, B는 B대로 자기 시간을 산다. 기차 객차가 분리되어 각자 다른 선로를 달리는 것처럼.

도박묵시룩 카이지 인간 경마

60억 개의 고독
도박묵시룩 카이지 인간 경마편

문득 상상했다. 지구가 거대한 컴퓨터고, 사람들이 프로세스인 세상을.

1초에 한 사람만 움직일 수 있다. 하지만 그 1초가 그 사람 입장에서는 느껴지지 않는다. 나는 집 앞 슈퍼를 가는데 200걸음(약 5분)이 걸렸지만, 지구 입장에서는 몇 시간, 몇 일이 걸렸는지 모른다. CPU가 프로세스에게 시간을 “가상화”시켜주는 것처럼.

내 입장에서 이 200걸음은 동기적이다. 한 걸음은 그 이전 걸음에 이어 존재한다. “나”라는 같은 시간축에 존재하는 것이다. 200걸음이든 2000걸음이든, 내 입장에선 내가 온전히 시간을 쓰고 있다. Syn-chronos. 나와 내 걸음이 시간을 함께 간다.

그런데 빨간 약을 먹었다. 매트릭스의 진실을 봤다.

각자 할당받은 시간만큼만 움직이는 사람들이 보이기 시작했다. 나는 슈퍼를 가고, 옆 사람은 밥을 먹고, 저 사람은 잠을 잔다. 우리는 같은 공간에 있지만, 같은 시간을 살지 않는다. 각자가 자기만의 시간축을 가지고 있다.

대화를 걸어본다. 하지만 그 사람에게 차례가 올 때까지 나는 기다려야 한다. 60억 지구인을 다 돌고 돌아 다시 그 사람의 차례가 와야 한다. 그때까지… 얼마나? 알 수 없다. 다음 1초에 바로 올 수도, 60억 번째가 될 수도.

이 두려움이 비동기다.

내가 할 수 있는 건 기다리는 것밖에 없다는 것. 상대방이 언제 응답할지 알 수 없다는 것. 각자가 독립적인 시간을 산다는 것.

외롭다.

네트워크 요청을 보낸다. 언제 올지 모른다. Promise를 만든다. 언제 resolve될지 모른다. 타이머를 설정한다. 정확히 언제 실행될지는… 사실 보장할 수 없다.

모든 비동기 작업은 외롭다. 각자가 자기만의 시간선 위에 서 있다. 평행선처럼. 서로를 볼 수는 있지만, 만날 수는 없다. 각자의 시간축에는 과거부터 미래까지 펼쳐져 있다. 동기적으로 존재한다, 자기 자신과는. 하지만 다른 이들과는? 동시에 존재하지 않는다.

A-synchronous.

코드를 다시 본다. 이제 뛰어다니는 무언가는 사라지고, 외로워 보이는 것들이 보인다. await를 만난다. “응 그래, 너가 알아서 해, 너의 시간대로.” Promise를 본다. “언젠가는 끝나겠지, 네 시간에.”

동기보다는 비동기가 세상의 본질임을 깨달았다. 지금 나와 내 주변 모든 것들이 비동기적이다. 세포 하나하나가 비동기적이다. 나는 그 비동기들의 집합체다. 콜백 지옥으로 형상화할 수도, Promise 체인으로 연결 지을 수도 있는.

편안해졌다. 비동기를 받아들였다. 독립성을 받아들였다. 외로움을 받아들였다.

3막: Con-currere, 함께 달리기

하지만 이야기는 여기서 끝나지 않았다.

최근 계속 마주친 키워드들이 있었다. 이벤트 루프. 스케줄러. React의 concurrent 렌더링. 우선순위 큐. CSS transition. Coordination.

처음엔 이해가 안 됐다. 비동기가 독립성이고 외로움이라면, 왜 이런 것들이 필요한가? 각자 알아서 살면 되는 거 아닌가?

영화 100M

외롭기에 매 순간 진지해질 수 있다
영화 100M

그때 또 다른 단어를 뜯어봤다. Concurrent.

Con(함께) + currere(달리다). 함께 달린다.

처음엔 이상했다. 함께? 비동기는 각자 독립적인 거 아니었나? 하지만 “함께 달린다”는 게 “같은 시간에 달린다”는 뜻이 아니었다.

마라톤을 생각했다. 수천 명의 선수가 함께 달린다. 이들은 concurrent하게 달린다. 하지만 synchronous하게 달리는 게 아니다. 각자 자기 속도로, 자기 전략으로 달린다. 어떤 선수는 빠르게 치고 나가고, 어떤 선수는 천천히 페이스를 유지한다. 하지만 모두 같은 레이스 안에 있다. 같은 시간 구간을 공유한다. 같은 도로를 나눠 쓴다.

여기서 역설이 생긴다.

비동기의 본질은 독립성이다. 각자가 외롭게 자기 시간을 산다. 하지만 결국 이들은 하나의 시스템을 이루어야 한다. 사용자는 하나의 앱을 본다. 하나의 화면을 본다. 브라우저는 하나의 메인 스레드를 가진다. CPU는 한정된 코어를 가진다.

독립적인 여러 작업들이 좁은 길을 함께 가야 한다.

이게 coordination이다. 조정. 조율.

이벤트 루프는 외로운 콜백들을 조율한다. “너 차례야, 지금 실행해. 그다음은 네 차례.” 큐에 쌓인 작업들에게 순서를 부여한다. 공평하게, 또는 우선순위에 따라.

React의 concurrent 렌더링은 더 정교하다. 여러 상태 업데이트가 동시에 일어난다. 사용자가 입력을 한다. 백그라운드에서 데이터가 로딩된다. 애니메이션이 진행된다. 각자 독립적인 작업들이지만, 모두 하나의 메인 스레드를 공유해야 한다.

그래서 React는 시간을 쪼갠다. 큰 렌더링 작업을 작은 조각으로 나눈다. 긴급한 업데이트(사용자 입력)가 들어오면 양보한다. 덜 긴급한 업데이트(백그라운드 로딩)는 남는 시간에 조금씩 진행한다. 시간을 펼쳐놓는다. 급하지 않은 작업을 시간에 걸쳐 펼쳐놓으면, 시스템이 반응성을 유지한다.

CSS transition도 같은 이야기다. 상태 변화는 개념적으로 순간이다. 빨강에서 파랑으로, 한순간에 바뀐다. 하지만 transition은 이 순간을 시간 구간으로 펼쳐놓는다. 빨강과 파랑 사이의 모든 중간 색상들을 시간에 따라 배치한다. 급작스러운 변화가 부드러운 흐름이 된다.

결국 모두 같은 패턴이었다. 시간을 펼쳐놓기. 우선순위 매기기. 조율하기.

독립적인 작업들이 함께 달릴 수 있도록.

에필로그: 외로움을 이겨내는 법

비동기는 외로움에 관한 이야기였다. 각자가 독립적인 시간을 산다는 것. 서로를 기다릴 수밖에 없다는 것.

하지만 coordination은 그 외로움을 이겨내려는 시도다.

“세상의 본질인 외로움을, 불가능함을 어떻게든 이겨내기 위해 발악하는 이야기.”

이벤트 루프도, 스케줄러도, concurrent 렌더링도, 우선순위 큐도. 모두 외로운 작업들이 함께 달릴 수 있게 만드는 장치들이다. 각자의 독립성을 존중하면서도, 전체적으로 조화로운 경험을 만드는 것.

어원들을 다시 본다.

Syn-chronos. 팔짱끼기. 하나의 선으로 묶으려는 시도. 처음엔 이게 전부인 줄 알았다.

A-synchronous. 느슨해지기. 각자의 독립성. 외로움. 이게 세상의 본질임을 깨달았다.

Con-currere. 함께 달리기. 독립성을 인정하면서도, 함께 가는 법. 외로움을 이겨내는 법. 이게 coordination이었다.

결국 프로그래밍은 시간을 다루는 예술이다. 순차를 병렬로, 블로킹을 논블로킹으로, 이산을 연속으로, 원자를 구간으로. 끊임없이 시간을 재구조화한다.

그 목적은 하나다. 제한된 자원을 가장 효과적으로 배분하는 것. 그리고 그 배분 속에서도, 외로운 작업들이 함께 달릴 수 있게 만드는 것.

코드를 다시 본다. 이제 뛰어다니는 것도, 외로운 것도 보인다. 하지만 그것만 보이는 게 아니다. 그들이 함께 달리는 모습도 보인다. 각자의 시간을 살면서도, 전체로서는 하나의 흐름을 만드는 모습이.

비동기를 이해한다는 건, 결국 이 세 가지를 모두 보는 것이다. 묶임의 시도, 분리의 본질, 그리고 함께 달리는 법.


P.S. 연결에 관하여

이 글을 쓰면서, 최근 겪었던 일들이 계속 떠올랐다.

나는 평생 혼자 시간을 살아왔다. 동기적인 프로그램처럼. 완벽해질 때까지 혼자 시간을 쓰고, 준비됐을 때만 결과를 내놓았다.

시간은 제한되어 있다. 나는 완벽할 수 없다. 혼자서는 더더욱.

코드가 그랬던 것처럼, 나도 외로웠다.

독립적이지만 연결되는 것. 외롭지만 길을 좁혀 함께 달리는 것. 불완전한 신호라도 계속 보내는 것. 내 시간을 독점하지 않고 다른 사람의 시간과 섞이게 하는 것.

코드도, 사람도, 결국 같은 이야기다.