
이번 프로젝트에 도입한 SQLAlchemy는 Engine이라는 객체를 통해 DB와의 연결을 관리합니다. 도입하는 과정에서 Engine 객체가 만들어주는 Connection 객체가 스레드 세이프하지 않다는 경고가 보였기 때문에 어떤 식으로 위험할 수 있는지, Python에서는 비동기를 어떻게 구성하고 어떻게 확장해나갈 수 있는지 정리해 둘 필요성을 느껴 Python의 비동기 처리와 SQLAlchemy에 적용할 때 고려해야 할 점에 대해 찾아본 내용을 기록으로 남기려고 합니다.
동시성(Concurrency): 여러 작업을 엇갈리게 진행해 여러 작업이 동시에 진행 되는 것처럼 보이게 함.
병렬성(Parallelism): 실제로 여러 코어/CPU에서 물리적으로 동시에 실행 함.
단일 스레드·이벤트 루프에서 코루틴을 협력적으로 스케줄링.
이벤트 루프가 준비된 코루틴을 실행하고, 코루틴이 await 하면 즉시 루프에 제어권을 양보해 다음 작업을 돌립니다. I/O 완료/타임아웃/콜백이 생기면 해당 코루틴이 다시 스케줄.
병렬성은 제공하지 않기 때문에 코루틴 안에서 CPU 무거운 연산/블로킹 함수를 실행하면 루프가 멈춤.
여러 스레드 사용 가능하지만 GIL 때문에 동시에 하나의 스레드만 Python 바이트코드를 실행.
네트워크/파일 I/O처럼 I/O 대기 시간이 긴 작업에 대기 중 GIL을 잠시 풀어 효율을 올릴 수 있음.
NumPy/OpenCV/압축 라이브러리 등 일부 C 확장은 내부에서 GIL을 해제하고 C단에서 병렬 처리하기도 합니다. 하지만 Python 바이트코드 자체의 병렬 실행은 불가.
GIL이 있다고 데이터 레이스가 사라지는 건 아니기 때문에 공유 상태엔 락/큐 등 동기화가 필요.
프로세스 분리로 GIL을 우회해 진짜 병렬 실행을 제공.
파싱, 전처리, 암·복호화, 수치 계산 등의 CPU 바운드에 효과적.
메모리 복제, IPC/직렬화, 컨텍스트 스위칭 비용이 큼.
외부 API 크롤링, DB I/O, 파일·네트워크: asyncio 우선
파싱/전처리/압축/암복호화 등 CPU 무거움: multiprocessing
혼합 시 원칙: 이벤트 루프는 I/O, 무거운 계산은 별도 프로세스를 사용하는 등 경계를 분리할 필요가 있음.
DB 연결 풀과 드라이버를 관리하는 프로세스 전역 객체. 내부적으로 동시성 제어가 되어 있어 스레드 세이프합니다.
보통 DB별로 1개를 앱 시작 시 생성하고 종료 시 dispose 합니다.
실제 DB 연결을 나타내며 스레드 세이프하지 않습니다. 스레드/태스크 간 공유 금지, 짧게 빌려 쓰고 곧바로 반납하는 전제가 필요합니다.
asyncio 환경에서도 동일하게, AsyncConnection을 코루틴(태스크) 간 공유하지 않습니다.
변경 추적·플러시·트랜잭션을 다루는 Unit of Work 단위 객체로, 스레드 세이프하지 않습니다.
작업/요청 단위로 1개 생성 → 처리 후 종료가 원칙입니다.
트랜잭션 경계를 명확히 두고, 언제 commit 할지는 상위(Service/UoW)에서 결정합니다. 필요하면 flush까지만 수행합니다.
AsyncEngine/AsyncSession을 사용하되, 루프가 정지될 위험이 있기 때문에 동기 드라이버·블로킹 호출을 루프 안에 끼우지 않기.
프로젝트에서 동기/비동기 드라이버를 혼용하지 않기.
SQLAlchemy의 Engine은 스레드 세이프하도록 설계되어 있어, 애플리케이션 수명 동안 각 프로세스마다 DB별로 하나씩 Engine을 생성해 공유하는 구성이 권장됩니다.
서비스 로직에서는 Engine에 바인딩된 sessionmaker로 (Async)Session을 요청/작업 단위로 생성하고, 트랜잭션 경계를 명확히 하여 언제 commit할지를 상위 계층에서 결정합니다.
이를 일관되게 적용하기 위해 의존성 주입(Dependency Injection) 을 도입해, 전역에서는 Engine/SessionFactory 를, 요청/작업 스코프에서는 Session 을 주입받는 구조를 사용했습니다.
다음 글에서는 Python 의존성 주입을 어떻게 적용했는지 정리해 보려고 합니다.