Steam-ad-poc #2 Python의 동시성과 SQL Alchemy

M4r()·2025년 8월 14일
0

steam-ad-poc

목록 보기
2/3
post-thumbnail

지금까지 동시성에 대해 가지고 있던 인식

이번 프로젝트에 도입한 SQLAlchemy는 Engine이라는 객체를 통해 DB와의 연결을 관리합니다. 도입하는 과정에서 Engine 객체가 만들어주는 Connection 객체가 스레드 세이프하지 않다는 경고가 보였기 때문에 어떤 식으로 위험할 수 있는지, Python에서는 비동기를 어떻게 구성하고 어떻게 확장해나갈 수 있는지 정리해 둘 필요성을 느껴 Python의 비동기 처리와 SQLAlchemy에 적용할 때 고려해야 할 점에 대해 찾아본 내용을 기록으로 남기려고 합니다.

용어 정리

동시성(Concurrency): 여러 작업을 엇갈리게 진행해 여러 작업이 동시에 진행 되는 것처럼 보이게 함.

병렬성(Parallelism): 실제로 여러 코어/CPU에서 물리적으로 동시에 실행 함.

Python

Python의 특징

한 언어 안에 세 가지 실행 모델이 존재

  1. asyncio(코루틴+이벤트 루프, 기본 1 스레드에서 스케줄링)
  2. threading(동일 프로세스 내 다중 스레드, GIL 존재)
  3. multiprocessing(프로세스 분리로 GIL 우회, 진짜 병렬)

역할 분담

  1. I/O 바운드: 코루틴이 await로 루프에 제어권을 양도하며 효율화
  2. CPU 바운드: 프로세스 분리로 병렬화.

asyncio

요약

단일 스레드·이벤트 루프에서 코루틴을 협력적으로 스케줄링.

동작

이벤트 루프가 준비된 코루틴을 실행하고, 코루틴이 await 하면 즉시 루프에 제어권을 양보해 다음 작업을 돌립니다. I/O 완료/타임아웃/콜백이 생기면 해당 코루틴이 다시 스케줄.

주의

병렬성은 제공하지 않기 때문에 코루틴 안에서 CPU 무거운 연산/블로킹 함수를 실행하면 루프가 멈춤.

threading

요약

여러 스레드 사용 가능하지만 GIL 때문에 동시에 하나의 스레드만 Python 바이트코드를 실행.

장점

네트워크/파일 I/O처럼 I/O 대기 시간이 긴 작업에 대기 중 GIL을 잠시 풀어 효율을 올릴 수 있음.

예외적 병렬성

NumPy/OpenCV/압축 라이브러리 등 일부 C 확장은 내부에서 GIL을 해제하고 C단에서 병렬 처리하기도 합니다. 하지만 Python 바이트코드 자체의 병렬 실행은 불가.

주의

GIL이 있다고 데이터 레이스가 사라지는 건 아니기 때문에 공유 상태엔 락/큐 등 동기화가 필요.

multiprocessing

요약

프로세스 분리로 GIL을 우회해 진짜 병렬 실행을 제공.

장점

파싱, 전처리, 암·복호화, 수치 계산 등의 CPU 바운드에 효과적.

단점

메모리 복제, IPC/직렬화, 컨텍스트 스위칭 비용이 큼.

요약 선택 가이드

  • 외부 API 크롤링, DB I/O, 파일·네트워크: asyncio 우선

  • 파싱/전처리/압축/암복호화 등 CPU 무거움: multiprocessing

  • 혼합 시 원칙: 이벤트 루프는 I/O, 무거운 계산은 별도 프로세스를 사용하는 등 경계를 분리할 필요가 있음.

SQLAlchemy 사용시 고려해야 할 점

Engine

  • DB 연결 풀과 드라이버를 관리하는 프로세스 전역 객체. 내부적으로 동시성 제어가 되어 있어 스레드 세이프합니다.

  • 보통 DB별로 1개를 앱 시작 시 생성하고 종료 시 dispose 합니다.

Connection

  • 실제 DB 연결을 나타내며 스레드 세이프하지 않습니다. 스레드/태스크 간 공유 금지, 짧게 빌려 쓰고 곧바로 반납하는 전제가 필요합니다.

  • asyncio 환경에서도 동일하게, AsyncConnection을 코루틴(태스크) 간 공유하지 않습니다.

Session (ORM)

  • 변경 추적·플러시·트랜잭션을 다루는 Unit of Work 단위 객체로, 스레드 세이프하지 않습니다.

  • 작업/요청 단위로 1개 생성 → 처리 후 종료가 원칙입니다.

  • 트랜잭션 경계를 명확히 두고, 언제 commit 할지는 상위(Service/UoW)에서 결정합니다. 필요하면 flush까지만 수행합니다.

asyncio와의 결합 시 원칙

  • AsyncEngine/AsyncSession을 사용하되, 루프가 정지될 위험이 있기 때문에 동기 드라이버·블로킹 호출을 루프 안에 끼우지 않기.

  • 프로젝트에서 동기/비동기 드라이버를 혼용하지 않기.

운영 팁

  • 동시 접속이 많아질 수 있다면 풀 크기/타임아웃을 점검하고, 외부 API·DB에 과도한 동시성을 걸지 않도록 상한을 둡니다.

다음 글에서는

SQLAlchemy의 Engine은 스레드 세이프하도록 설계되어 있어, 애플리케이션 수명 동안 각 프로세스마다 DB별로 하나씩 Engine을 생성해 공유하는 구성이 권장됩니다.
서비스 로직에서는 Engine에 바인딩된 sessionmaker로 (Async)Session을 요청/작업 단위로 생성하고, 트랜잭션 경계를 명확히 하여 언제 commit할지를 상위 계층에서 결정합니다.
이를 일관되게 적용하기 위해 의존성 주입(Dependency Injection) 을 도입해, 전역에서는 Engine/SessionFactory 를, 요청/작업 스코프에서는 Session 을 주입받는 구조를 사용했습니다.
다음 글에서는 Python 의존성 주입을 어떻게 적용했는지 정리해 보려고 합니다.

profile
달리려고 해야 걸을 수 있다.

0개의 댓글