커넥션 풀의 연결(Connection Checkout)은 쿼리가 실행되는 순간부터 데이터베이스에 실제 요청을 보내거나 응답을 받는 순간(session.execute(), session.select(), …)부터 with 블록을 나갈 때까지(session.close() 호출 시까지) 계속 점유된다. with Session() 또는 async with AsyncSession() 블록에 진입하는 순간 커넥션 풀에 꺼내 쓰는 것이 아니지만(ref1), 최악의 경우 with 블록의 범위가 곧 커넥션의 점유 범위가 될 수 있음을 인지해야 한다.
특히 인터넷에서 흔히 찾을 수 있는 FastAPI 튜토리얼처럼 다음과 같이 컨트롤러 인자의 Depends()를 통해 세션을 주입하는 경우, 컨트롤러 내부에서 쿼리가 처음 실행되는 순간부터 get_user 함수가 끝날 때까지 연결을 유지할 수 있어 위험하다.
@router.get('...')
def problematic_controller(session = Depends(get_session)):
...
# problematic_controller 함수가 끝날 때까지 커넥션 점유 유지
Python
복사
아래처럼 세션 객체를 클래스의 멤버 변수로 관리하는 패턴도 마찬가지다. 보통 영속성 클래스는 생명 주기가 길다. 하지만 인스턴스가 삭제될 때까지 연결을 유지하기 때문에 더욱 위험하다.
class ProblematicRepository:
def __init__(self, session = Depends(get_session)):
self.session = session
...
def get_user(self):
stmt = select(User).where(User.id == 1)
user = self.session.query(User).filter_by(id=1).one() # 커넥션 점유 시작
# 이 순간부터 MyRepository 인스턴스가 삭제될 때까지 커넥션 점유 유지
Python
복사
위 두 경우에서 쿼리가 처음 실행되는 순간부터 요청에 응답하기까지 5초정도의 시간이 걸린다고 생각해 보자. 커넥션 풀의 최대 연결 수가 10개라고 할 때, 5초 안에 요청이 10개 이상 들어와 DB 연결을 시도한다면, 뒤늦게 들어온 요청들은 커넥션 자원을 대기하며 블로킹된다.
조금 더 나은 방법은 영속성 관련 함수(레포지토리 메서드)를 데코레이팅 패턴으로 감싸는 방법이다.
@with_db_session
def problematic_sync_function(session): # `session`은 데코레이터를 통해 전달받음
... # 앞의 코드
stmt = select(User).where(User.id == 1)
user = session.query(User).filter_by(id=1).one() # 커넥션 점유 시작
response = requests.get("https://slow-api.com/process") # 5초 소요: 이때도 커넥션 점유 중
user.external_data = response.json()
session.commit()
# 함수가 끝나야지만 커넥션 반납
Python
복사
이렇게 하면 연결을 지속하는 범위가 영속성 관련 함수 내부로 고정된다. 깜빡하고 세션을 닫지 않는 실수도 줄일 수 있다.
하지만 위와 같은 코드는 첫 번째 DB 호출 이후 함수가 종료되기 전까지 session.close()가 불가능하다. 이렇게 하나의 영속성 함수 내에서 DB와 상관없는 I/O 바운드 작업이 존재하여 추가적인 최적화가 필요하다면, 아래와 같이 DB 입출력 부분에만 컨텍스트 매니저를 사용하여 불필요한 부분에 DB 커넥션을 반납해야 한다.
def recommended_sync_function():
with Session() as session:
user = session.query(User).filter_by(id=1).one() # 커넥션 점유 시작
# with를 벗어나며 커넥션 반납
# 이 동안에는 DB 커넥션을 점유하지 않음
response = requests.get("https://slow-api.com/process") # 5초 소요
external_data = response.json()
with Session() as session:
user = session.query(User).filter_by(id=1).one() # 커넥션 점유 시작
user.external_data = external_data
session.commit()
# with를 벗어나며 커넥션 반납
Python
복사
parse me : 언젠가 이 글에 쓰이면 좋을 것 같은 재료을 보관해 두는 영역입니다.
1.
None
from : 과거의 어떤 원자적 생각이 이 생각을 만들었는지 연결하고 설명합니다.
1.
•
앞의 글에는 FastAPI 라우터에서 Depends로 DB 세션을 주입하는 것이 ‘좋은 것’이고, 그 차선이 데코레이터를 사용하는 것이라는 식으로 소개했다.
2.
•
앞의 글에서는 데코레이터를 사용하는 방법, 메서드 내에서 컨텍스트 매니저를 사용하는 두 가지 방법을 소개했다. 이 글의 내용에서 지적하는 내용을 완벽히 개선하려면 데코레이터 대신 컨텍스트 매니저를 선택해야 한다.
3.
•
커넥션 풀이 무엇인지는 앞의 글을 참고하라. 참고로, 이 문제는 (당연히) 애플리케이션 레벨 커넥션 풀이 아니라 미들웨어 커넥션 풀만 사용하는 경우에도 동일하게 적용되는 문제다.
4.
•
이 글의 내용을 앞의 프롬프트에 반영했다.
supplementary : 어떤 새로운 생각이 이 문서에 작성된 생각을 뒷받침하는지 연결합니다.
1.
None
opposite : 어떤 새로운 생각이 이 문서에 작성된 생각과 대조되는지 연결합니다.
1.
None
to : 이 문서에 작성된 생각이 어떤 생각으로 발전되거나 이어지는지를 작성하는 영역입니다.
1.
None
ref : 생각에 참고한 자료입니다.