Comet5의 잡다한 블로그
기술자료

비동기는 빠르지만, 이해하기는 느리다

2026-04-24 · Comet5

비동기는 빠르지만, 이해하기는 느리다

성능을 개선해야 하는 순간이 오면, 자연스럽게 비동기 처리를 떠올리게 된다. I/O 작업을 기다리는 동안 다른 일을 처리할 수 있고, 전체 처리량을 크게 끌어올릴 수 있기 때문이다. 특히 네트워크 요청이나 디스크 접근이 많은 시스템에서는 async/await이나 이벤트 기반 구조가 거의 기본적인 선택처럼 여겨진다.

문제는 이 방식이 “빠르게 동작하는 시스템”을 만드는 대신, “이해하기 어려운 시스템”을 만들어낸다는 점이다. 동기 코드에서는 실행 흐름이 위에서 아래로 자연스럽게 이어진다. 함수가 호출되고, 결과가 반환되고, 그 다음 로직이 실행된다. 문제가 생기면 콜스택을 따라가면서 어떤 흐름에서 에러가 발생했는지 비교적 직관적으로 파악할 수 있다.

하지만 비동기 코드에서는 이 흐름이 끊어진다. 함수는 실행을 시작하고, 중간에 작업을 예약한 뒤 바로 반환된다. 실제 작업은 이벤트 루프나 다른 스레드에서 나중에 실행된다. 이 순간부터 코드의 “작성 순서”와 “실행 순서”가 분리된다. 눈에 보이는 코드와 실제 동작 사이에 간극이 생기는 것이다.

이 간극이 가장 크게 드러나는 지점이 바로 디버깅이다. 에러가 발생했을 때 콜스택을 확인해보면, 우리가 기대했던 호출 흐름이 아니라 전혀 다른 형태로 나타난다. 비동기 경계를 넘어가는 순간 기존의 스택 정보가 사라지거나 단절되기 때문이다. 흔히 “콜스택이 끊긴다”는 표현을 쓰는데, 이때부터 문제를 추적하는 난이도가 급격히 올라간다.

예를 들어 여러 개의 비동기 요청이 동시에 실행되고 있는 상황을 생각해보면, 특정 에러가 어떤 요청의 어떤 단계에서 발생했는지를 파악하는 것 자체가 쉽지 않다. 로그를 남기더라도 실행 순서가 뒤섞이기 때문에, 단순히 시간 순으로 나열된 로그만으로는 전체 흐름을 재구성하기 어렵다. 결국 더 많은 컨텍스트 정보와 추적 도구가 필요해진다.

async/await은 이런 문제를 어느 정도 완화해준다. 콜백 지옥을 피하고, 코드를 동기식처럼 읽을 수 있게 만들어준다. 하지만 이것이 비동기의 본질적인 복잡성을 제거해주지는 않는다. 여전히 실행은 비동기적으로 이루어지고, 경쟁 상태(race condition)나 순서 의존성 같은 문제는 그대로 남아 있다.

이벤트 기반 구조에서는 이 복잡성이 더 극단적으로 드러난다. 특정 이벤트가 언제 발생할지, 어떤 순서로 처리될지, 여러 이벤트가 동시에 발생했을 때 어떤 상태가 만들어질지 예측하기 어려워진다. 코드 상에서는 명확해 보이던 로직이 실제 실행 환경에서는 전혀 다른 결과를 만들어내기도 한다.

그래서 비동기 시스템을 다룰 때는 단순히 문법을 이해하는 것 이상이 필요하다. 실행 모델 자체를 이해하고, 흐름을 추적할 수 있는 장치를 함께 설계해야 한다. 요청 단위의 컨텍스트를 유지하거나, 트레이싱을 통해 흐름을 시각화하거나, 중요한 지점마다 의미 있는 로그를 남기는 방식이 필수적으로 따라온다.

또한 가능한 한 비동기 경계를 최소화하는 것도 하나의 전략이다. 모든 것을 비동기로 만드는 것이 아니라, 정말 필요한 지점에서만 사용하고 나머지는 단순한 흐름을 유지하는 것이다. 이는 성능과 복잡도 사이의 균형을 맞추는 데 도움이 된다.

결국 비동기는 강력한 도구지만, 그만큼 비용이 따른다. 처리량을 얻는 대신, 이해와 디버깅의 난이도를 지불해야 한다. 그래서 중요한 것은 “비동기를 쓸 수 있는가”가 아니라 “이 복잡도를 감당할 가치가 있는가”를 판단하는 것이다.

비동기를 도입하는 순간 시스템은 더 빨라질 수 있다. 하지만 동시에, 그 시스템을 이해하는 속도는 느려질 수 있다. 그 두 가지를 함께 고려할 때, 비로소 올바른 선택을 할 수 있다.