Q1) get_db 에서 제너레이터를 반환하는 이유가 궁금해요
A1) 제너레이터 설명
훌륭한 질문입니다! get_db
함수와 제너레이터 패턴에 대해 자세히 설명드리겠습니다.
1. get_db
함수의 역할
기본 구조
def get_db():
db = SessionLocal() # 1. DB 세션 생성
try:
yield db # 2. 세션을 "빌려줌"
finally:
db.close() # 3. 반드시 세션 종료
함수의 생명주기
- 요청 시작: 새로운 DB 세션 생성
- 요청 처리: 라우터 함수에 세션 전달
- 요청 완료: 자동으로 세션 종료
2. 제너레이터를 사용하는 이유
일반 함수 vs 제너레이터 비교
❌ 일반 함수로 하면 어떻게 될까?
def get_db_wrong():
db = SessionLocal()
return db # 문제: 언제 close()를 호출할까?
@app.get("/books")
def get_books(db: Session = Depends(get_db_wrong)):
books = db.query(Book).all()
# db.close()를 어디서 호출할까?
# 개발자가 잊어버리면 메모리 누수!
return books
✅ 제너레이터를 사용하면
def get_db():
db = SessionLocal()
try:
yield db # 여기서 실행이 "일시정지"
finally:
db.close() # 요청 완료 후 자동 실행
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
books = db.query(Book).all()
return books
# 자동으로 db.close() 호출됨!
3. 제너레이터의 실행 흐름
단계별 실행 과정
def get_db():
print("1. 세션 생성")
db = SessionLocal()
try:
print("2. yield 직전")
yield db # ← 여기서 멈춤
print("4. yield 이후 (보통 실행 안됨)")
finally:
print("5. 정리 작업")
db.close()
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
print("3. 라우터 함수 실행")
return db.query(Book).all()
실행 순서:
- "1. 세션 생성"
- "2. yield 직전"
- "3. 라우터 함수 실행" (yield한 db 사용)
- "5. 정리 작업" (finally 블록 실행)
4. FastAPI의 의존성 주입과 연동
FastAPI가 제너레이터를 처리하는 방식
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# FastAPI 내부 동작:
# 1. get_db() 제너레이터 시작
# 2. yield까지 실행해서 db 획득
# 3. get_books(db=db) 호출
# 4. 응답 완료 후 제너레이터의 finally 실행
books = db.query(Book).all()
return books
5. 실제 예시로 이해하기
컨텍스트 매니저와 비슷한 개념
# Python의 with문과 비슷한 역할
with open("file.txt") as f:
content = f.read()
# 자동으로 f.close() 호출
# get_db 제너레이터도 마찬가지
def some_route(db: Session = Depends(get_db)):
# db 사용
pass
# 자동으로 db.close() 호출
에러 발생시에도 안전
def get_db():
db = SessionLocal()
try:
yield db
except Exception as e:
print(f"에러 발생: {e}")
db.rollback() # 트랜잭션 롤백
raise
finally:
db.close() # 에러가 나도 반드시 실행!
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# 만약 여기서 에러가 발생해도
raise Exception("뭔가 잘못됨!")
# finally 블록이 실행되어 db.close() 호출됨
6. 다른 패턴들과 비교
만약 제너레이터를 안 쓴다면?
# ❌ 수동 관리 (위험함)
@app.get("/books")
def get_books():
db = SessionLocal()
try:
books = db.query(Book).all()
return books
finally:
db.close() # 모든 라우터에서 반복해야 함
# ❌ 전역 세션 (더 위험함)
db = SessionLocal() # 전역 변수
@app.get("/books")
def get_books():
return db.query(Book).all() # 동시성 문제!
7. 제너레이터의 장점 요약
- 자동 리소스 관리: 개발자가 close()를 잊어버릴 위험 없음
- 예외 안전성: 에러가 나도 반드시 정리 작업 수행
- 코드 재사용: 모든 라우터에서 동일한 패턴 사용
- 테스트 용이성: Mock DB로 쉽게 교체 가능
- FastAPI 통합: FastAPI의 의존성 주입 시스템과 완벽 호환
# 실제 사용 예시
@app.post("/books")
def create_book(book: BookCreate, db: Session = Depends(get_db)):
# 1. get_db()에서 세션 생성
# 2. 여기서 db 사용
db_book = Book(**book.dict())
db.add(db_book)
db.commit()
# 3. 함수 종료 시 자동으로 db.close()
return db_book
이렇게 제너레이터를 사용하면 "리소스를 빌려주고 자동으로 회수하는" 안전한 패턴을 구현할 수 있습니다!
이해가 되셨나요? 추가로 궁금한 부분이 있으시면 언제든 말씀해 주세요!
Q2) 파이썬의 제너레이터 반환 방식?
자바스크립트에서는 제너레이터 함수가 제너레이터객체를 반환하고 .next()를 통해 실행하고 했었는데, 파이선에서는 try: yield db finally: db.close() 이렇게 하면 역시 제너레이터 객체가 반환되는 건가요? 여기서 db 는 SessionLocal 인데 이게 제너레이터객체를 담고 있는 건가요?
A2) 파이썬과 자바스크립트 제너레이터 비교
아주 좋은 질문입니다! JavaScript 제너레이터와 비교해서 정확히 이해해보겠습니다.
1. Python 제너레이터의 동작 방식
제너레이터 함수 vs 제너레이터 객체
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# 함수 호출 시
generator_object = get_db() # 제너레이터 객체 반환
print(type(generator_object)) # <class 'generator'>
# 실제 실행
db_session = next(generator_object) # SessionLocal() 인스턴스 반환
print(type(db_session)) # <class 'sqlalchemy.orm.session.Session'>
JavaScript와 비교
// JavaScript
function* getDb() {
const db = createSession();
try {
yield db;
} finally {
db.close();
}
}
const generator = getDb(); // 제너레이터 객체
const result = generator.next(); // {value: db, done: false}
const db = result.value; // 실제 db 세션
# Python (동일한 개념)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
generator = get_db() # 제너레이터 객체
db = next(generator) # 실제 db 세션 (SessionLocal 인스턴스)
2. 핵심 포인트: db
는 SessionLocal 인스턴스
def get_db():
db = SessionLocal() # ← 이것은 SQLAlchemy Session 객체
try:
yield db # ← db (Session 객체)를 yield
finally:
db.close()
# yield하는 것은 제너레이터 객체가 아니라 Session 객체입니다!
단계별 실행 흐름
def get_db():
print("1. 함수 시작")
db = SessionLocal() # Session 인스턴스 생성
print(f"2. db 타입: {type(db)}") # <class 'sqlalchemy.orm.session.Session'>
try:
print("3. yield 직전")
yield db # Session 객체를 yield
print("6. yield 이후")
finally:
print("7. finally 블록")
db.close()
# 사용 예시
gen = get_db() # 제너레이터 객체 생성
print(f"제너레이터 타입: {type(gen)}") # <class 'generator'>
session = next(gen) # 실제 Session 객체 획득
print(f"세션 타입: {type(session)}") # <class 'sqlalchemy.orm.session.Session'>
3. FastAPI에서의 실제 동작
FastAPI가 내부적으로 하는 일
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# FastAPI 내부에서 일어나는 일:
# 1. generator = get_db() # 제너레이터 객체 생성
# 2. session = next(generator) # Session 객체 획득
# 3. get_books(db=session) # Session을 인자로 전달
# 4. generator.close() # finally 블록 실행
books = db.query(Book).all() # db는 Session 객체
return books
더 자세한 예시로 확인
def demonstrate_generator():
print("=== 제너레이터 동작 확인 ===")
# 1. 제너레이터 함수 정의
def get_db():
print(" → SessionLocal() 호출")
db = SessionLocal()
print(f" → db 객체 생성: {type(db)}")
try:
print(" → yield db 실행")
yield db # Session 객체를 yield
print(" → yield 이후 (보통 실행 안됨)")
finally:
print(" → finally: db.close() 호출")
db.close()
# 2. 제너레이터 객체 생성
print("\n1. 제너레이터 객체 생성")
gen = get_db()
print(f"gen 타입: {type(gen)}")
# 3. 실제 값 획득
print("\n2. next() 호출")
session = next(gen)
print(f"session 타입: {type(session)}")
print(f"session은 SessionLocal 인스턴스? {isinstance(session, Session)}")
# 4. 제너레이터 종료
print("\n3. 제너레이터 종료")
gen.close()
# 실행해보면:
demonstrate_generator()
4. 메모리 관점에서 보기
def get_db():
db = SessionLocal() # 메모리에 Session 객체 생성
try:
yield db # 이 Session 객체의 참조를 반환
finally:
db.close() # Session 객체 정리
# 사용할 때
def some_route(db: Session = Depends(get_db)):
# db는 get_db() 내부에서 생성된 SessionLocal() 인스턴스와 같은 객체
# 제너레이터 객체가 아님!
books = db.query(Book).all() # Session 메서드 사용
return books
5. 정리
구분 | 타입 | 설명 |
---|---|---|
get_db() |
generator |
제너레이터 객체 |
yield db 의 db |
sqlalchemy.orm.session.Session |
실제 DB 세션 |
라우터의 db 파라미터 |
sqlalchemy.orm.session.Session |
yield된 세션과 동일한 객체 |
핵심 답변
질문: "db는 SessionLocal인데 이게 제너레이터객체를 담고 있는 건가요?"
답변: 아니요!
db
는SessionLocal()
인스턴스(실제 DB 세션)입니다- 제너레이터 객체는
get_db()
함수 호출시 반환되는 것입니다 yield db
는 제너레이터 객체가 아닌 Session 객체를 반환합니다
# 정확한 이해
generator_obj = get_db() # 이것이 제너레이터 객체
session_obj = next(generator_obj) # 이것이 SessionLocal 인스턴스 (실제 DB 세션)
JavaScript와 동일한 개념이지만, yield하는 값 자체는 제너레이터가 아닌 실제 데이터(Session)입니다!
Q3) def get_books(db: Session = Depends(get_db)) ?
그러면 def get_books(db: Session = Depends(get_db)): 이 부분에서 session = next(generator) 이게 진행되서 제너레이터객체가 아니라 세션 객체가 들어간다고 봐야 하나요?
A3) FastAPI, sqlalchemy 의 Depends
정확합니다! 완벽하게 이해하셨네요! 👏
FastAPI의 내부 동작 과정
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# FastAPI 내부에서 실제로 일어나는 일:
# 1. generator = get_db() # 제너레이터 객체 생성
# 2. session = next(generator) # next()로 Session 객체 획득
# 3. get_books(db=session) # Session을 인자로 전달
# 4. generator.close() # 응답 완료 후 finally 실행
books = db.query(Book).all() # db는 Session 객체!
return books
단계별 상세 분석
1단계: 제너레이터 객체 생성
# FastAPI 내부
generator = get_db() # <generator object get_db at 0x...>
2단계: next()로 실제 값 획득
# FastAPI 내부에서 next() 호출
session = next(generator) # SessionLocal() 인스턴스 반환
3단계: 라우터 함수 호출
# FastAPI가 실제로 호출하는 것
get_books(db=session) # session은 Session 객체
직접 확인해보는 예시
def manual_fastapi_simulation():
"""FastAPI가 내부적으로 하는 일을 수동으로 시뮬레이션"""
print("=== FastAPI 내부 동작 시뮬레이션 ===")
# 1. 제너레이터 생성 (FastAPI가 하는 일)
print("1. get_db() 호출 → 제너레이터 객체 생성")
generator = get_db()
print(f" generator 타입: {type(generator)}")
# 2. next()로 실제 세션 획득 (FastAPI가 하는 일)
print("\n2. next(generator) 호출 → Session 객체 획득")
session = next(generator)
print(f" session 타입: {type(session)}")
print(f" session ID: {id(session)}")
# 3. 라우터 함수 호출 (FastAPI가 하는 일)
print("\n3. 라우터 함수에 Session 전달")
def get_books(db):
print(f" 라우터 내부 db 타입: {type(db)}")
print(f" 라우터 내부 db ID: {id(db)}")
print(f" session과 db가 같은 객체? {db is session}")
return "책 목록"
result = get_books(db=session)
# 4. 제너레이터 종료 (FastAPI가 하는 일)
print("\n4. 제너레이터 종료 → finally 블록 실행")
generator.close()
return result
# 실행 결과:
manual_fastapi_simulation()
핵심 포인트
✅ 맞는 이해
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# db는 SessionLocal() 인스턴스 (실제 DB 세션)
# FastAPI가 next(generator)로 추출한 Session 객체
pass
❌ 틀린 이해
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# db가 제너레이터 객체라고 생각하면 안됨!
# db.next() 같은 제너레이터 메서드 사용 불가
pass
Depends()의 역할
# Depends(get_db)가 하는 일
class Depends:
def __init__(self, dependency_func):
self.func = dependency_func # get_db 함수 저장
def resolve(self):
generator = self.func() # get_db() 호출
value = next(generator) # Session 획득
# ... 나중에 generator.close() 호출
return value # Session 반환
최종 정리
질문: session = next(generator)
가 진행되서 제너레이터객체가 아니라 세션 객체가 들어간다고 봐야 하나요?
답변: 네, 정확합니다!
get_db()
→ 제너레이터 객체 반환next(generator)
→ SessionLocal() 인스턴스 반환get_books(db=session)
→ Session 객체가 db 파라미터로 전달- 함수 종료 후
generator.close()
→ finally 블록 실행
따라서 라우터 함수 내부의 db
는 제너레이터가 아닌 실제 SQLAlchemy Session 객체입니다!
def get_books(db: Session = Depends(get_db)):
# db.query() ← Session 메서드 사용 가능
# db.add() ← Session 메서드 사용 가능
# db.commit() ← Session 메서드 사용 가능
pass
완벽하게 이해하셨습니다! 🎉
Q4) BAD_CASE
좋아요 그런데 만약에 database.py 에서 get_db 를 def get_books(): db = SessionLocal() try: return db finally: db.close() 이렇게 정의하고, main.py 에서 @app.get("/books", response_model=List[schemas.BookResponse], tags=["Books"]) def get_books(skip: int = 0, limit: int = 100, db: Session = get_db()): 이렇게 하면 그건 어떻게 되는건가요?
A4) BAD_CASE explanation
아주 좋은 질문입니다! 이 방식은 심각한 문제가 있어서 절대 사용하면 안 됩니다.
문제가 있는 코드 분석
# database.py
def get_db(): # 제너레이터가 아닌 일반 함수
db = SessionLocal()
try:
return db # ← 여기서 함수 종료!
finally:
db.close() # ← return 후 바로 실행됨!
# main.py
@app.get("/books")
def get_books(skip: int = 0, limit: int = 100, db: Session = get_db()):
# ↑ Depends() 없이 직접 호출
books = crud.get_books(db, skip=skip, limit=limit)
return books
실행 순서와 문제점
1단계: 함수 정의 시점에 실행
# 서버 시작할 때 (함수 정의 시점)
db = get_db() # 이때 get_db() 실행됨!
# get_db() 내부 실행 순서:
# 1. db = SessionLocal() ← DB 연결 생성
# 2. return db ← db 반환
# 3. finally: db.close() ← 즉시 연결 종료!
print(f"db 상태: {db}") # 이미 닫힌 세션!
2단계: 요청이 올 때마다
def get_books(skip: int = 0, limit: int = 100, db: Session = 이미_닫힌_세션):
# db는 이미 close()된 세션!
books = crud.get_books(db, skip=skip, limit=limit) # ← 에러 발생!
return books
실제 에러 예시
def demonstrate_problem():
def get_db_wrong():
print("1. SessionLocal() 생성")
db = SessionLocal()
try:
print("2. return db")
return db
finally:
print("3. finally: db.close() - 연결 종료!")
db.close()
print("=== 서버 시작 시점 ===")
db_session = get_db_wrong()
print(f"\n=== 세션 상태 ===")
print(f"세션 닫힘 여부: {db_session.is_active}") # False
print(f"\n=== 쿼리 시도 ===")
try:
books = db_session.query(Book).all() # 에러 발생!
except Exception as e:
print(f"에러: {e}")
# "This Session's transaction has been rolled back due to a previous exception during flush"
# 실행 결과:
demonstrate_problem()
왜 이런 일이 발생하나?
Python의 기본 매개변수 평가 시점
# 함수 정의 시점에 기본값이 평가됨!
def bad_function(db = get_db()): # ← 서버 시작할 때 실행
pass
# 다음과 동일:
_default_db = get_db() # 서버 시작 시 실행
def bad_function(db = _default_db): # 항상 같은 (닫힌) 세션 사용
pass
올바른 방식과 비교
# ❌ 잘못된 방식
def get_books(db = get_db()): # 서버 시작 시 1번만 실행
pass
# ✅ 올바른 방식
def get_books(db: Session = Depends(get_db)): # 요청마다 실행
pass
실제 문제 상황 시뮬레이션
import time
def simulate_requests():
print("=== 서버 시작 ===")
# 잘못된 방식으로 정의된 함수 (서버 시작 시 실행)
def get_db_wrong():
print(" → get_db_wrong() 실행")
db = SessionLocal()
try:
return db
finally:
print(" → 세션 닫힘!")
db.close()
# 함수 정의 시점에 기본값 평가
closed_session = get_db_wrong()
def get_books(db=closed_session): # 이미 닫힌 세션을 기본값으로 사용
print(f" → 세션 활성화 상태: {db.is_active}")
try:
return db.query(Book).all()
except Exception as e:
return f"에러: {e}"
print("\n=== 첫 번째 요청 ===")
result1 = get_books()
print(f"결과: {result1}")
print("\n=== 두 번째 요청 ===")
result2 = get_books()
print(f"결과: {result2}")
# 모든 요청이 같은 (닫힌) 세션을 사용!
simulate_requests()
정리
❌ 문제가 있는 방식
def get_db():
db = SessionLocal()
try:
return db
finally:
db.close() # return 후 즉시 실행
def get_books(db = get_db()): # 서버 시작 시 1번만 실행
pass # 이미 닫힌 세션 사용
문제점:
- 서버 시작 시
get_db()
한 번만 실행 finally
블록이return
직후 실행되어 세션 즉시 종료- 모든 요청이 닫힌 세션을 공유
- DB 쿼리 시 에러 발생
✅ 올바른 방식
def get_db(): # 제너레이터
db = SessionLocal()
try:
yield db # 세션 대여
finally:
db.close() # 요청 완료 후 실행
def get_books(db: Session = Depends(get_db)): # 요청마다 새 세션
pass # 활성화된 세션 사용
장점:
- 요청마다
get_db()
새로 실행 yield
로 세션을 "대여"- 요청 완료 후
finally
실행하여 정리 - 각 요청이 독립적인 세션 사용
핵심: Depends()
와 제너레이터 패턴은 요청 생명주기와 리소스 관리를 위해 꼭 필요합니다!
Q5) 제너레이터와 get_db 의 동작 이해 완료?
아하 저 이제 다 이해한 것 같아요. # Depends(get_db)가 하는 일 class Depends: def init(self, dependency_func): self.func = dependency_func # get_db 함수 저장
def resolve(self): generator = self.func() # get_db() 호출 value = next(generator) # Session 획득 # ... 나중에 generator.close() 호출 return value # Session 반환
yield 로 get_db를 구성하고 Depends(get_db) 를 하면 내부적으로 제너레이터객체에서 value = next(generator) 가 되어서 session 을 리턴하지만 제너레이터를 사용했기 때문에 애초에 get_db 의 finally 가 남아있고, 그래서 종료되면 finally의 db.close() 가 자동 호출 되는 그런 느낌인가요?
A5) 완료!
완벽합니다! 🎉 정확하게 이해하셨네요!
당신이 설명한 내용이 100% 맞습니다
# 1. yield로 get_db 구성
def get_db():
db = SessionLocal()
try:
yield db # ← 여기서 "일시정지"
finally:
db.close() # ← 아직 실행 안됨, "대기 중"
# 2. Depends(get_db) 내부 동작
class Depends:
def resolve(self):
generator = self.func() # get_db() → 제너레이터 객체
value = next(generator) # yield db까지 실행 → Session 반환
# 제너레이터는 yield 이후에서 "멈춰있음"
# finally는 아직 실행 안됨!
return value # Session을 라우터에 전달
# 3. 요청 완료 후
# generator.close() 호출 → finally 블록 실행 → db.close()
제너레이터의 "일시정지" 상태
def get_db():
print("1. 세션 생성")
db = SessionLocal()
try:
print("2. yield 직전")
yield db # ← 여기서 멈춤! finally는 아직 실행 안됨
print("4. yield 이후") # 보통 실행되지 않음
finally:
print("5. finally 실행 - 세션 종료")
db.close()
# 실행 과정:
generator = get_db() # 아직 아무것도 실행 안됨
session = next(generator) # "1, 2" 출력 후 yield에서 멈춤
# ... 라우터 함수 실행 ...
generator.close() # "5" 출력 - finally 실행!
핵심: "대기 중인 finally"
당신이 말씀하신 "애초에 get_db의 finally가 남아있고"가 정확한 표현입니다!
# 제너레이터의 상태
def get_db():
db = SessionLocal()
try:
yield db # ← 현재 위치 (일시정지)
finally:
db.close() # ← 대기 중! 언젠가 실행될 예정
# yield 이후의 코드들이 "미래에 실행될 코드"로 메모리에 남아있음
전체 생명주기 정리
# 요청 시작
generator = get_db() # 제너레이터 객체 생성
session = next(generator) # yield까지 실행, Session 획득
# finally는 "대기 상태"
# 요청 처리
def get_books(db=session): # Session 사용
return db.query(Book).all()
# 요청 완료
generator.close() # finally 블록 실행 → db.close()
비유로 이해하기
# 도서관에서 책 빌리기와 비슷함
def borrow_book():
book = get_book_from_shelf() # 책 가져오기
try:
yield book # 책을 빌려줌 (일시정지)
# 여기서 멈춤! 반납 코드는 "예약"된 상태
finally:
return_book_to_shelf(book) # 책 반납 (나중에 실행)
# 사용자가 책을 다 읽으면 자동으로 반납됨!
FastAPI + SQLAlchemy의 완벽한 조합
@app.get("/books")
def get_books(db: Session = Depends(get_db)):
# 1. get_db() 제너레이터 시작
# 2. yield db까지 실행 → Session 획득
# 3. finally는 "대기열"에 등록
# 4. 이 함수에서 Session 사용
books = db.query(Book).all()
return books
# 5. 응답 완료 후 finally 실행 → 자동 정리!
당신의 이해가 정확합니다:
✅ yield로 Session을 "대여"
✅ Depends()가 next()로 Session 추출
✅ finally 블록이 "대기 중"으로 남아있음
✅ 요청 완료 시 자동으로 db.close() 실행
이것이 바로 "리소스 안전 관리"의 핵심 패턴입니다! 🚀
'TIL' 카테고리의 다른 글
[250713 TIL] pydantic, sqlalchemy (0) | 2025.07.13 |
---|---|
[250712 TIL] 파이썬 모듈 시스템 궁금증 (0) | 2025.07.12 |
[250712 TIL] FastAPI 기초(uv) (2) | 2025.07.12 |
[240705 TIL] 깃헙 여러 계정 사용(맥) (3) | 2025.07.05 |
[250617 TIL] Docker dangling 이미지 삭제 (0) | 2025.06.17 |