도림.로그

Tags

Series

About

비동기 프로그래밍 - 간단한 고찰(?)

2023. 07. 01.

비동기 프로그래밍

▶ Expand post list

    트렌드(?)

    최근에 언어를 불문하고 비동기 프로그래밍에 대한 관심이 굉장히 높은 것 같다. JavaScript에서는 Promise(혹은 microtask) 기반으로, Go나 Kotlin은 Coroutine을 기반으로, 그리고 Java는 주로 Reactive 패러다임을 기반으로 구현을 하는 듯 하다. (국내 한정의 이야기일 수 있다. 하지만 Java 진영의 가장 큰 프레임워크 중 하나인 Spring에서는 Future에 대한 사용은 거의 Deprecate 된 것 같다.) 개발 공부를 2017년에 시작한 입장에서 트렌드에 대한 이야기를 하기에는 아무래도 구력이 부족하지 않나 싶지만, 내가 생각하는 비동기 프로그래밍이 이토록 많은 관심을 받게 되는 이유, 특히 백엔드 시장에서 관심을 받게 되는 이유에 대해 궤변을 펼쳐보고자 한다.

    싱글 쓰레드로 시작해보자

    아무래도 싱글 쓰레드라는 용어가 비동기 프로그래밍을 다루다 보면 꼭 따라오는 것 같다. 내가 비동기 프로그래밍을 처음 접한 경로는 Node를 공부할 때 였는데, 그 때 Node 비동기 라는 키워드로 구글링을 하면 대부분의 아티클에서 접할 수 있었던 내용이 바로 비동기를 통해서 싱글 쓰레드에서도 동시성을 달성할 수 있다 라는 내용이었다. 동시성을 구현하는 가장 간단한 방법은 쓰레드를 여러개 생성해서, 각 쓰레드가 코드를 실행하게 하는 것이다. 그러면 CPU에서 지원하는 최대 병렬성만큼 실제로 코드가 동시에 실행된다. (이론적으로 CPU 제원에 적힌 쓰레드 갯수만큼 병렬적으로 실행할 수 있다.) 그러면 쓰레드가 1개인데 어떻게 동시성을 보장할 수 있을 것인가? 코드를 실행하는 프로세서가 단 1개인데? 이것을 비동기 프로그래밍으로 풀어낸 것이라고 볼 수 있다. 여러 JavaScript 런타임들은 이벤트 루프를 도입하여 싱글 쓰레드 환경에서 동시성을 달성할 수 있게 했다. Web API는 JavaScript의 인터프리터와는 별개로 동작하기 때문에 이런 동작을 구현하는 것이 가능했다. 여기서 알 수 있겠지만, JavaScript의 코드를 실행하는 런타임이 싱글 쓰레드로 돌아간다는 것이지, 브라우저나 Node 자체가 싱글 쓰레드만 사용한다는 것에는 어폐가 있다.

    멀티 쓰레드에서의 차용

    하지만 최근의 트렌드를 보면 멀티쓰레드 환경에서도 비동기 프로그래밍을 구현하고, 사용하고 있는 것을 심심찮게 찾아볼 수 있다. 심지어 나도 업무에서는 Kotlin Coroutine을 기반으로 하는 Spring 서버를 개발하고 있으니 말이다. 이렇게 비동기 프로그래밍이 대중화되는 데에는 다양한 기존 멀티 쓰레드 모델에서는 해결하기 어렵지만, 비동기 프로그래밍을 차용하면 더 수월하게 해결할 수 있기 때문이라고 생각한다.

    MSA의 대중화

    최근 대규모 서비스를 하고 있는 기업들은 십중팔구 MSA를 기반으로 서비스를 개발하고 있다. MSA의 정의에 대해서는 물어보는 사람마다 다양한 의견을 내지만 아마 여러가지 기능을 제공하는 하나의 서버 대신 부분적인 기능을 제공하는 여러개의 서버를 개발하는 것이라는 부분에 대해서는 다들 동의를 할 것이다. 그러면 이 MSA의 대중화가 비동기 프로그래밍과 대체 무슨 상관이 있을까?

    MSA를 도입하게 되면 거대한 서비스를 여러개의 마이크로서비스로 구성하게 되고, HTTP를 사용하든 gRPC를 사용하든 각 서비스들의 Dependency가 복잡하게 얽히기 시작한다. 기존에는 각 서비스들이 하나의 거대한 코드베이스에 담겨있었기 때문에 개별 서비스의 처리 지연에 대해서 고민할 필요가 없었다. 대부분의 지연은 너무 무겁게 작성된 로직이나 DB 쿼리 지연에서 발생했다. 무겁게 작성된 로직은 알고리즘 자체를 개선하여 해결할 수 있고, DB 쿼리 지연은 쿼리 튜닝이나 DB 자체 작업(샤딩 / 파티셔닝 / 슬레이브 추가 등)을 통해서 해결할 수 있었다. 즉, 멀티 쓰레드 모델에서 각각의 쓰레드가 요청을 하나씩 맡아서 수행하더라도 큰 문제가 되지 않았다. 하지만 개별 서비스들이 각각 API 통신을 통해서 연동하게 되면 개별 서비스의 처리 지연에 대해 필히 고민을 해야 하게 된다. 가령, 다음과 같은 상황을 생각해보자.

    내가 만든 API는 EndPoint A, B, C 를 제공하고 있다. 그리고 나는 연동 서비스 a, b, c를 사용하고 있고 각각 A-a, B-b, C-c 로 연동하고 있다.
    만약 멀티 쓰레드 모델을 사용하고 있고, 동시 쓰레드로 300개를 준비하고 있다. 그러면 실제로 동시 요청을 300개까지 받을 수 있다.
    a 서비스는 종종 지연이 있는 API이나, 상위 90%의 요청들은 대개 빠르게 응답한다. 따라서 API Timeout을 10초로 설정해 두었다.
    그런데, 특정 프로모션이 진행되어 A API에 과한 부하가 쏠리게 되었고, 연동 서비스 a가 버티지 못하고 장애가 발생하여 지연이 발생하기 시작했다. 이런 상황이라면, 대부분의 쓰레드가 A API 호출에 할당이 되어 있을 것이다. 이 수를 290개라고 가정하자.
    그러면 290개의 요청이 모두 실패하여 CircuitBreaker가 동작하기 전까지는 내 서버는 10초동안 최대 10개의 쓰레드로 B와 C API 요청을 처리해야한다. 거기다, A API 요청이 추가로 오게 되면 상황은 더 나빠진다. 최악의 경우 내 서버도 장애로 이어질 수도 있다.

    이제는 그저 DB라는 튜닝 가능한 인프라가 아니라, 다른 부서에서 개발하는 내 맘대로 개선할 수 없는 연동 서비스까지 지연 지점으로 인식하고 개발을 해야 하는 상황이 된 것이다. 이런 상황에서 멀티 쓰레드 모델은 그 특성 상 연동 지점의 지연에 취약하기 때문에, 연동 지점의 지연에도 유연하게 대응할 수 있는 비동기 프로그래밍이 매력적이지 않나라는 것이 첫 번째 생각이다.

    테스트와 설계 방법론의 진화

    객체 지향 프로그래밍, 함수형 프로그래밍을 위시하여 TDD, BDD 그리고 DDD, Clean Architecture 등의 여러 방법론들이 진화한 것도 비동기 프로그래밍이 관심을 받을 수 있는 동력이라고 생각한다. 비동기 프로그래밍의 최대 단점 중 하나는 디버깅이 어렵다는 것이다. 특히 디버깅을 하다 보면 본능적으로 위에서 아래로 로직의 진행을 보게 되는데, await등의 문법이 아니라 callback으로만 구성된 코드를 디버깅하는 것은 그야말로 뇌가 꼬이는 기분이 들게 한다. 이 때문에 멀티 쓰레드 모델이라는 대안이 있는 상황에서 비동기 프로그래밍은 그닥 매력적이지 않고, 유지 보수에 있어 복잡성을 높이는 방법론이었을 것이다. 하지만 소프트웨어 공학과 방법론이 발전하면서 이런 부분들을 극복할 수 있는 여러 방법론들과 더 나은 아키텍처가 제안되어 왔고, 이제 로직 자체에 대해서 검증할 수 있는 많은 수단이 생겼기 때문에 조금 더 부담 없이 비동기 프로그래밍을 도입할 수 있게 된 것이 아닌가 생각한다. (그럼에도 개인적으로 WebFlux 환경에서는 디버깅이 아직 너무 힘들다)

    더 높은 자원 효율

    그리고 실제로 특정 요청 부하에서부터 차이가 나기 시작하는 자원 효율과 Throughput은 역시 빼놓을 수 없는 비동기 프로그래밍의 장점이다. Reactive Stream, coroutine, Promise 등 다양한 용어를 사용하지만 이들 모두 기존 멀티 쓰레드 모델의 쓰레드에 비해서는 Context Switching 비용이 훨씬 작고, IO 등으로 인한 Blocking이 발생하더라도 그 즉시 다른 Context로 Switch하여 로직을 계속 수행할 수 있기 때문에 같은 스펙의 서버에서 더 높은 성능을 낼 수 있다. 부하를 높이면 높일수록 이 차이는 더 두드러진다. 서버의 효율은 IT 서비스를 하는 회사라면 인건비를 제외하고는 대부분의 고정 비용을 담당하게 되기 때문에, 이 역시 무시할 수 없는 비동기 프로그래밍의 장점이라고 볼 수 있다.

    그러면 굳이 필요하지 않을 때는 언제일까?

    비동기 프로그래밍 역시 은탄환은 아니다. 위에 서술한 내용을 보면 비동기 프로그래밍이 매력적인 지점은

    • MSA 환경이거나 연동 지점이 많을 때
    • 적절한 설계를 할 수 있는 여유가 있을 때
    • 테스트를 작성할 수 있는 환경일 때
    • 서버비용에 민감할 정도로 트래픽이 높을 때 정도로 볼 수 있다. (적절한 설계나 테스트가 필요하지 않다는 것이 아니다. 하지만 경험상 그런 것이 불가능하고 혼자의 힘으로 극복이 어려울 때가 분명 있다.) 전반적으로 매력적인 경우는 대용량 시스템일 경우라고 볼 수 있다. 즉, 서비스의 트래픽이 크지 않고, 사용하는 언어가 Node처럼 비동기를 기본으로 깔고 가는 언어가 아니라면 굳이 비동기 프로그래밍이 아니어도 될 때일 수도 있다는 것이다.

    다음은?

    비동기에 대한 궤변을 늘어놓은 김에, 몇 가지 언어에서 비동기 코드를 작성해야 할 때 어떻게 할 수 있는지 간단히 예제를 통해서 알아보고자 한다. JavaScript 비동기 프로그래밍 게시글로 돌아오도록 해보겠다.

    끝.

    #async#java#kotlin#reactive#spring#msa#oop

    © 2024, Built with Gatsby