🔄 블로킹과 논블로킹, 그리고 동기/비동기까지
개발을 하다 보면 자주 마주치는 개념들, 블로킹/논블로킹과 동기/비동기. 이들은 서로 다른 관점에서 시스템의 동작 방식을 설명하지만, 실제로는 밀접하게 연관되어 있습니다. 오늘은 이 개념들을 자세히 살펴보고, 실제 웹 서비스에서 어떻게 활용되는지 알아보겠습니다.
1. 블로킹(Blocking) vs 논블로킹(Non-blocking)
🔍 정의
- 블로킹: I/O 작업(
read()
,db.query()
등)을 수행할 때, 결과가 올 때까지 해당 쓰레드(또는 코루틴)가 완전히 멈춰 기다리는 방식 - 논블로킹: I/O 작업을 수행할 때, "지금 당장 처리할 데이터가 없더라도" 즉시 반환하여, 대기하지 않고 다음 로직을 실행할 수 있는 방식
💡 특징 비교
특성 | 블로킹 | 논블로킹 |
---|---|---|
호출 시 행동 | 결과 대기 | 즉시 반환 |
흐름 제어 | 기다린 후 다음 실행 | 바로 다음 실행, 결과는 폴링/이벤트로 처리 |
구현 복잡도 | 낮음 | 높음 (상태 관리 필요) |
동시성 | 낮음 (쓰레드 낭비) | 높음 (한 쓰레드로 많은 I/O 감당) |
⚖️ 장단점
블로킹
- 장점: 직관적, 예외 처리·디버깅 용이
- 단점: I/O 지연 시 전체 쓰레드 풀이 잠김
논블로킹
- 장점: I/O 대기 중에도 다른 작업 수행 가능, 단일 쓰레드로 높은 동시성
- 단점: 폴링 로직·이벤트 관리 필요, 구현 난이도 상승
💻 코드 예제
# 블로킹 방식
def blocking_io():
print("데이터베이스 쿼리 시작...")
result = db.query("SELECT * FROM users") # 쿼리 완료까지 대기
print("쿼리 결과:", result)
return result
# 논블로킹 방식
async def non_blocking_io():
print("데이터베이스 쿼리 시작...")
future = db.query_async("SELECT * FROM users") # 즉시 반환
print("다른 작업 수행 가능")
result = await future # 결과가 필요할 때까지 대기
return result
2. 동기(Synchronous) vs 비동기(Asynchronous)
🔍 정의
- 동기: 호출자가 "결과가 돌아올 때까지" 흐름을 멈추고, 리턴된 값을 받아야만 다음 코드를 실행
- 비동기: 호출 즉시 제어권을 돌려받아 다른 일을 수행하고, 결과는 콜백/Promise/Future 등으로 나중에 처리
💡 특징 비교
특성 | 동기 | 비동기 |
---|---|---|
호출 이후 흐름 | 결과 대기 후 순차 실행 | 즉시 리턴, 후속 처리 콜백 |
응답 지연 영향 | 직접 반영 (지연 시 전체 멈춤) | 지연 구간에 다른 작업 수행 |
구현 난이도 | 낮음 | 높음 (콜백·상태 관리 필요) |
3. 네 가지 조합
블록/논블 | 동기 | 비동기 | 대표 사례 |
---|---|---|---|
블로킹 + 동기 | 쓰레드가 I/O 완료까지 멈춤 | — | Django/Flask WSGI |
논블로킹 + 동기 | I/O 즉시 리턴, 폴링·확인 후 순차 처리 | — | 준비 상태만 체크 → 동기 로직 |
블로킹 + 비동기 | API 호출은 비동기, 워커 내부는 블로킹 | 워커가 콜백(완료 알림) | FastAPI + Celery |
논블로킹 + 비동기 | 이벤트 루프에서 I/O 즉시 반환 → 결과 콜백 | 결과 도착 시점에 비동기 처리 | Node.js, asyncio |
4. 웹 서비스 적용 관점
🔄 아키텍처별 적용
소규모 CRUD API
- 블로킹 + 동기: Django, Flask/WSGI
- 적합한 경우: 간단한 데이터 처리, 낮은 동시성 요구
실시간·고동시성
- 논블로킹 + 비동기: FastAPI(
asyncio
), Node.js WebSocket - 적합한 경우: 실시간 채팅, 알림 시스템
- 논블로킹 + 비동기: FastAPI(
무거운 백그라운드 작업 분리
- 블로킹 + 비동기: Celery 워커에 이미지/동영상 처리 위임
- 적합한 경우: 파일 처리, 이메일 발송
폴링이 필요할 때
- 논블로킹 + 동기: Redis 상태만 중간중간 확인하고, 준비되면 동기 처리
- 적합한 경우: 작업 상태 모니터링
🔄 Gunicorn 워커 구조
# Gunicorn 설정 예시
workers = 2
worker_class = 'sync' # 또는 'uvicorn.workers.UvicornWorker'
# sync 워커의 경우
# - 각 워커는 독립된 OS 프로세스
# - 싱글스레드, 블로킹 I/O
# - 최대 2개의 요청을 병렬 처리
5. 결론
블로킹/논블로킹과 동기/비동기는 서로 다른 관점에서 시스템의 동작을 설명하지만, 실제로는 밀접하게 연관되어 있습니다:
- 블로킹/논블로킹: "I/O 호출 시 쓰레드를 멈추느냐"를 결정
- 동기/비동기: "결과를 받아 흐름을 멈추느냐"를 결정
웹 서비스에서는 이 두 가지 특성을 적절히 조합하여 성능과 개발 생산성 사이에서 균형을 맞추는 것이 중요합니다. 상황에 따라 적절한 방식을 선택하고, 필요한 경우 여러 방식을 조합하여 사용하는 것이 현명한 접근 방법입니다.
💡 Tip: 실제 프로젝트에서는 요구사항과 시스템의 특성을 잘 파악하여, 적절한 조합을 선택하는 것이 중요합니다. 무조건 비동기/논블로킹이 좋은 것이 아니라, 상황에 맞는 선택이 필요합니다.