동시성 및 비동기 처리
백엔드가 concurrency와 asynchronous processing 모델을 사용해 여러 작업을 효율적으로 처리하는 방법을 알아보세요.
백엔드 시스템에서 동시성이 중요한 이유
현대의 백엔드 시스템은 동시에 많은 요청을 처리해야 합니다. 이를 가능하게 하는 것이 바로 동시성입니다.
- 서버는 여러 사용자와 시스템으로부터 요청을 지속적으로 받습니다.
- 요청을 한 번에 하나씩만 처리하면 심각한 지연과 병목이 발생합니다.
- 동시성은 각 요청이 완전히 끝날 때까지 기다리지 않고도 여러 요청에서 작업을 진행할 수 있게 합니다.
상세정보
실제 시스템에서는 요청이 한 번에 하나씩 오는 것이 아니라, 지속적으로 그리고 종종 대량으로 도착합니다. 백엔드 서비스는 초당 수백 또는 수천 개의 요청을 받을 수 있으며, 각 요청은 계산, 데이터베이스 접근, 또는 외부 API 호출을 필요로 할 수 있습니다.
서버가 이러한 요청을 엄격하게 순차적으로 처리한다면, 새로운 요청은 이전 요청이 끝날 때까지 기다려야 합니다. 이로 인해 대기열이 빠르게 늘어나고 사용자에게는 허용할 수 없는 지연이 발생합니다.
문제는 네트워크 호출이나 디스크 접근처럼 기다림이 필요한 작업이 많기 때문에 더 심각해집니다. 이 대기 시간 동안 CPU는 유휴 상태가 되며, 이는 자원을 비효율적으로 사용하는 것입니다.
동시성은 작업을 겹쳐서 수행할 수 있게 함으로써 이 문제를 해결합니다. 하나의 요청이 I/O를 기다리는 동안 다른 요청을 처리할 수 있습니다. 이렇게 하면 시스템이 계속 활성 상태를 유지하고 전체 처리량이 향상됩니다.
그 결과, 백엔드 시스템은 많은 사용자를 동시에 처리하면서도 응답성을 유지할 수 있으며, 이는 현대 애플리케이션의 기본적인 요구 사항입니다.
동시성 vs 병렬성
동시성은 여러 작업이 진행 중인 상태를 관리하는 것이고, 병렬성은 여러 작업을 동시에 실행하는 것입니다.
- 동시성은 작업들 사이를 전환하면서 각 작업이 진행되도록 합니다.
- 병렬성은 여러 CPU 코어를 사용해 작업을 동시에 실행합니다.
상세정보
동시성은 시스템이 여러 작업을 어떻게 구조화하고 관리하는지에 초점을 둡니다. 서버는 하나의 요청 처리를 시작한 뒤, I/O를 기다리는 동안 잠시 멈추고, 그다음 다른 요청으로 전환할 수 있습니다. 작업들은 번갈아가며 진행되며, 이를 통해 제한된 자원에서도 시스템을 효율적으로 유지할 수 있습니다.
반면 병렬성은 하드웨어에 의존합니다. 머신에 여러 CPU 코어가 있다면, 여러 작업을 정확히 같은 시간에 실행할 수 있습니다. 각 코어는 자체 작업을 독립적으로 실행하여 전체 처리 용량을 높입니다.
핵심 차이는 동시성이 조정과 스케줄링에 관한 것인 반면, 병렬성은 동시에 실행하는 것에 관한 점입니다. 두 개념은 서로 다른 문제를 해결하지만, 종종 함께 사용됩니다.
예를 들어, 백엔드 서버는 비동기 기법을 사용해 수천 개의 동시 요청을 관리하면서, 동시에 여러 CPU 코어에 작업을 분산해 병렬 실행을 달성할 수 있습니다.
이 차이를 이해하는 것은 시스템 성능, 확장성, 그리고 다양한 백엔드 프레임워크가 부하가 걸렸을 때 어떻게 동작하는지를 추론할 때 중요합니다.
스레드
스레드는 프로세스 내의 실행 단위이며, 전통적인 백엔드 서버는 여러 스레드를 사용해 요청을 동시에 처리합니다.
각 레인은 자체 실행 경로를 실행합니다. 스레드가 많아질수록 동시에 처리할 수 있는 작업도 늘어나지만, 결국 CPU나 메모리가 한계가 됩니다.
- 각 스레드는 서버 내에서 독립적인 실행 흐름을 나타냅니다.
- 여러 스레드를 사용하면 서버가 여러 요청을 동시에 처리할 수 있습니다.
- 스레드 기반 모델은 Java Spring Boot 같은 백엔드 프레임워크에서 널리 사용됩니다.
상세정보
프로세스는 실행 중인 프로그램의 인스턴스이며, 그 프로세스 안에서 스레드는 실제로 작업을 수행하는 단위입니다. 하나의 프로세스는 여러 스레드를 포함할 수 있고, 각 스레드는 같은 메모리 공간을 공유하면서 독립적으로 실행됩니다.
전통적인 백엔드 시스템에서는 들어오는 요청이 스레드에 할당됩니다. 예를 들어 요청이 도착하면 서버는 스레드 풀에서 스레드를 하나 할당해 이를 처리할 수 있습니다. 그 스레드는 비즈니스 로직, 데이터베이스 쿼리, 응답 생성까지 요청을 처음부터 끝까지 처리합니다.
이 모델은 단순하고 이해하기 쉽습니다. 각 요청이 자체 스레드를 가지므로 실행 흐름이 분리되고, 개발자는 대부분 순차적인 스타일로 코드를 작성할 수 있습니다.
하지만 스레드는 공짜가 아닙니다. 각 스레드는 메모리를 소비하고 시스템에 오버헤드를 추가합니다. 동시 요청 수가 증가하면 너무 많은 스레드를 생성하는 것이 컨텍스트 스위칭과 자원 고갈로 인해 성능 저하를 일으킬 수 있습니다.
이러한 한계 때문에 현대 시스템은 종종 스레드 기반 접근과 비동기 기법을 함께 사용하지만, 많은 운영 환경의 시스템이 여전히 이 모델에 의존하고 있으므로 스레드를 이해하는 것은 여전히 중요합니다.
블로킹 vs 논블로킹 작업
블로킹 작업은 작업이 완료될 때까지 스레드를 대기시키는 반면, 논블로킹 작업은 스레드가 다른 작업을 계속할 수 있게 합니다.
작업이 I/O를 기다릴 때, 블로킹 모델은 작업자를 멈춥니다. 논블로킹 시스템은 기다리는 동안에도 레인을 활성 상태로 유지합니다.
- 블로킹 작업은 결과가 준비될 때까지 실행을 일시 중지합니다.
- 논블로킹 작업은 시스템이 다른 작업을 계속 처리할 수 있게 합니다.
상세정보
블로킹 작업에서는 스레드가 작업을 시작한 뒤, 그 작업이 완전히 완료될 때까지 기다린 다음 다음 단계로 넘어갑니다. 예를 들어, 스레드가 데이터베이스 쿼리를 보내면 데이터베이스가 응답할 때까지 유휴 상태로 있을 수 있습니다. 이 시간 동안 스레드는 다른 유용한 작업을 수행할 수 없습니다.
이는 많은 요청을 처리하는 시스템에서 큰 제약이 됩니다. 각 스레드가 I/O를 기다리는 데 상당한 시간을 쓰면, 서버는 처리량을 유지하기 위해 더 많은 스레드가 필요하고, 그만큼 자원 사용량과 오버헤드가 증가합니다.
논블로킹 작업은 다른 방식으로 동작합니다. 기다리는 대신 스레드가 작업을 시작하고 즉시 다른 작업을 계속 실행합니다. 결과가 준비되면 시스템에 알림이 전달되고, 원래 작업을 다시 이어서 진행할 수 있습니다.
이 방식은 하나의 스레드가 여러 작업을 효율적으로 관리할 수 있게 해 주며, 특히 대기 시간이 실행 시간보다 큰 I/O 중심 워크로드에서 효과적입니다.
블로킹과 논블로킹 동작의 차이를 이해하는 것은 매우 중요합니다. 이는 백엔드 시스템을 성능, 응답성, 확장성을 고려해 어떻게 설계할지에 직접적인 영향을 주기 때문입니다.
비동기 I/O
비동기 I/O를 사용하면 서버가 데이터베이스 쿼리나 네트워크 호출처럼 느린 작업을 기다리는 동안에도 다른 작업을 계속 처리할 수 있습니다.
- 많은 백엔드 작업은 데이터베이스나 API 같은 외부 시스템을 기다리는 과정을 포함합니다.
- 비동기 I/O는 이러한 대기 동안 스레드가 유휴 상태로 머무르는 것을 방지합니다.
- 이 접근 방식은 처리량과 자원 효율성을 향상시킵니다.
상세정보
백엔드 시스템에서 많은 작업은 CPU-bound가 아니라 I/O-bound입니다. 데이터베이스를 조회하거나, 외부 API를 호출하거나, 디스크에서 읽는 작업은 메모리 내 계산보다 훨씬 더 오래 걸릴 수 있습니다.
이러한 작업을 blocking 방식으로 처리하면, 스레드는 결과를 기다리는 동안 유휴 상태로 남아 시스템 자원을 낭비하고 서버가 처리할 수 있는 요청 수를 제한하게 됩니다.
비동기 I/O는 서버가 작업을 시작한 뒤 기다리지 않고 다른 작업으로 넘어갈 수 있게 해서 이 문제를 해결합니다. 시스템은 진행 중인 작업을 추적하고, 결과가 준비되면 처리를 다시 이어갑니다.
이를 통해 하나의 스레드 또는 소수의 스레드만으로도 많은 동시 요청을 효율적으로 관리할 수 있으며, 비동기 I/O는 고성능 백엔드 시스템의 핵심 기술이 됩니다.
이벤트 루프 모델
이벤트 루프 모델은 요청마다 스레드를 하나씩 할당하는 대신, 적은 수의 스레드로 작업을 비동기적으로 처리하여 많은 요청을 처리합니다.
각 요청이 자체 작업자를 차지합니다.
하나의 루프가 여러 작업을 순환합니다.
이벤트 루프가 무거운 작업을 스레드에 분배합니다.
- 단일 이벤트 루프가 큐에서 작업을 계속해서 처리합니다.
- 논블로킹 작업을 통해 시스템이 대기하지 않도록 할 수 있습니다.
- 비동기 작업이 완료되면 콜백이 실행됩니다.
상세정보
이벤트 루프 모델에서는 시스템이 각 요청마다 별도의 스레드를 할당하지 않습니다. 대신 중앙 루프가 새 작업을 계속 확인하고, 작업을 하나씩 실행합니다. 요청이 들어오면 이벤트 루프가 이를 처리하기 시작하고, 필요한 I/O 작업을 논블로킹 방식으로 시작합니다.
이러한 작업이 완료될 때까지 기다리는 대신, 이벤트 루프는 다른 들어오는 요청을 처리하러 이동합니다. 이렇게 하면 시스템이 계속 바쁘게 동작하고, 스레드 기반 모델에서 흔한 유휴 대기 시간을 줄일 수 있습니다.
비동기 작업이 끝나면 그 결과는 콜백 큐에 들어갑니다. 이벤트 루프는 결국 이 콜백을 가져와 해당 요청의 남은 작업을 완료합니다.
이 모델의 잘 알려진 예로는 Node.js가 있습니다. Node.js는 단일 스레드 이벤트 루프와 async I/O를 결합하여 수천 개의 동시 연결을 효율적으로 처리하며, 실행 흐름을 관리하기 위해 콜백과 promises에 의존합니다.
이 방식은 I/O 중심 워크로드에 매우 효율적이지만, 하나의 느린 작업이 다른 모든 작업을 지연시킬 수 있으므로 이벤트 루프가 블로킹되지 않도록 신중하게 설계해야 합니다.
경쟁 조건
경쟁 조건은 여러 스레드가 동시에 공유 데이터를 접근하고 수정할 때 발생하며, 그 결과 예측할 수 없고 잘못된 결과가 나올 수 있습니다.
- 경쟁 조건은 여러 스레드가 적절한 조정 없이 공유 데이터를 다룰 때 발생합니다.
- 최종 결과는 실행 시점과 순서에 따라 달라집니다.
- 이는 동시성 시스템에서 버그의 주요 원인입니다.
상세정보
경쟁 조건은 두 개 이상의 스레드가 같은 데이터를 동시에 읽고 업데이트할 때 발생합니다. 이러한 작업은 동기화되어 있지 않기 때문에, 어떤 스레드가 먼저 실행되는지에 따라 결과가 달라지며 시스템 동작이 예측 불가능해집니다.
예를 들어, 두 스레드가 같은 계좌 잔액을 읽고 둘 다 업데이트를 시도하면, 한쪽 업데이트가 다른 쪽을 덮어쓸 수 있습니다. 이로 인해 각 작업은 개별적으로는 올바르게 보이더라도 전체적으로는 잘못된 데이터가 생깁니다.
이러한 문제는 항상 발생하지 않을 수 있어 발견하기 어렵습니다. 아주 작은 타이밍 차이만으로도 실행 순서가 바뀌어, 재현하고 디버깅하기 어려운 버그가 생길 수 있습니다.
경쟁 조건을 방지하려면 시스템은 락, 원자적 연산, 데이터베이스 트랜잭션과 같은 동기화 기법을 사용합니다. 이러한 메커니즘은 한 번에 하나의 스레드만 공유 데이터를 수정할 수 있게 하거나, 작업이 간섭 없이 안전하게 실행되도록 보장합니다.
질문 섹션
1 / 5
이 레슨은 프리미엄 콘텐츠입니다
프리미엄으로 업그레이드하여 흐림 효과를 없애고 전체 내용을 읽어 보세요.