FastAPI 교재 · 5편 · 의존성 주입

FastAPI 의존성 주입 — Depends 이해하기

똑같은 코드를 엔드포인트마다 베껴 쓰고 있다면 그건 신호다. 그 반복을 함수 하나로 묶어 "받아 쓰게" 하는 장치, Depends.

재사용 가능한 부품 블록이 여러 파이썬 함수 슬롯에 꽂히고 공통 의존성 모듈이 깔끔한 배선으로 연결되는 모듈형 아키텍처 컨셉 아이소메트릭 일러스트

4편까지 오면서 우리는 경로·쿼리 매개변수로 입력을 받고, Pydantic 모델로 요청 본문을 검증하는 법을 익혔다. 엔드포인트가 두세 개일 때는 별 문제가 없다. 그런데 라우트가 열 개, 스무 개로 늘어나기 시작하면 묘하게 거슬리는 장면이 보인다 — 똑같은 코드 토막이 함수마다 복사되어 붙어 있다.

5편의 주제 의존성 주입(Dependency Injection) 은 바로 이 반복을 없애는 도구다. 이름이 거창하지만 개념은 단순하고, FastAPI 에서는 Depends 라는 함수 하나로 거의 다 끝난다. 이 편을 마치면 공통 파라미터·인증·DB 세션 같은 것을 한 곳에 정의해 두고, 필요한 라우트에서 "받아 쓰는" 구조를 짤 수 있게 된다.

1. 같은 코드가 반복될 때 — 무엇이 문제인가

예를 들어 목록을 돌려주는 엔드포인트가 여러 개 있다고 하자. 상품 목록, 주문 목록, 사용자 목록… 이들은 대개 똑같은 페이지네이션 옵션을 받는다. 검색어 q, 건너뛸 개수 skip, 가져올 최대 개수 limit. 그래서 코드는 자연스럽게 이렇게 쌓인다.

@app.get("/items") def read_items(q: str | None = None, skip: int = 0, limit: int = 10): return {"q": q, "skip": skip, "limit": limit} @app.get("/users") def read_users(q: str | None = None, skip: int = 0, limit: int = 10): return {"q": q, "skip": skip, "limit": limit} @app.get("/orders") def read_orders(q: str | None = None, skip: int = 0, limit: int = 10): return {"q": q, "skip": skip, "limit": limit}

같은 세 줄짜리 매개변수 묶음이 세 군데에 똑같이 들어가 있다. 지금이야 별것 아니지만, 어느 날 limit 의 기본값을 10 에서 20 으로 바꾸기로 했다고 해 보자. 함수 세 곳을 전부 찾아 고쳐야 한다. 하나라도 빠뜨리면, 어떤 목록만 다르게 동작하는 미묘한 버그가 생긴다. 인증 검사나 DB 연결 코드라면 더 심각하다.

여기서 발상을 살짝 뒤집어 보자. 지금까지 각 함수는 자기에게 필요한 재료(파라미터)를 직접 구하고 있었다. 의존성 주입은 그 방향을 바꾼다. 함수는 "나는 이런 재료가 필요해" 라고 선언만 하고, 그 재료를 실제로 마련해서 손에 쥐여 주는 일은 프레임워크에 맡긴다.

쉬운 비유 — 요리사가 매번 직접 밭에 가서 채소를 캐 오는 대신, 손질된 재료를 받아서 요리에만 집중하는 것과 같다. 재료를 어디서 어떻게 마련하는지는 한 곳(의존성 함수) 에 적어 두고, 요리사(엔드포인트)는 그저 "재료 주세요" 라고 받아 쓴다. 재료 공급처가 바뀌어도 요리사 코드는 손댈 필요가 없다 — 이게 "주입(injection)" 이라는 말의 핵심이다.

2. Depends 첫 사용 — 공통 파라미터를 함수로 빼기

위의 중복된 세 줄을 별도 함수로 뽑아내 보자. 이 함수가 바로 의존성(dependency) 이다. 이름 그대로, 엔드포인트가 "의존하는" 코드다.

from fastapi import FastAPI, Depends app = FastAPI() def common_params(q: str | None = None, skip: int = 0, limit: int = 10): return {"q": q, "skip": skip, "limit": limit}

common_params 는 평범한 함수처럼 보이지만, 매개변수에 적힌 타입힌트와 기본값은 우리가 4편에서 본 쿼리 매개변수 규칙 을 그대로 따른다. 이제 이 함수를 엔드포인트에 "주입" 한다. 방법은 인자 자리에 Depends() 를 적는 것뿐이다.

@app.get("/items") def read_items(commons: dict = Depends(common_params)): return {"source": "items", **commons} @app.get("/users") def read_users(commons: dict = Depends(common_params)): return {"source": "users", **commons}

이제 read_items 함수 본문에는 더 이상 q, skip, limit 가 보이지 않는다. 대신 commons 라는 변수 하나로 묶여 들어온다. 동작 순서는 이렇다.

누군가 /items?q=phone&limit=5 로 요청하면, FastAPI 는 먼저 의존성 함수 common_params 를 실행한다. 이때 URL 의 쿼리값들이 그 함수의 매개변수로 들어가 검증·변환을 거친다. 함수가 돌려준 딕셔너리가 곧 commons 의 값이 되어, 그제야 read_items 본문이 실행된다. 즉 FastAPI 가 의존성을 먼저 실행하고, 그 결과를 인자로 끼워 넣은 다음, 엔드포인트를 호출하는 것이다.

덤으로 따라오는 이득이 하나 더 있다. common_params 의 매개변수들도 결국 쿼리 매개변수이기 때문에, /docs 의 자동 문서에 q·skip·limit 가 그대로 노출된다. 의존성으로 빼냈다고 해서 문서에서 사라지지 않는다. 검증과 문서화 능력은 고스란히 유지된다.

3. 재사용과 클래스 의존성

의존성 주입의 진짜 값어치는 재사용 에서 나온다. 이제 페이지네이션 규칙을 바꾸고 싶으면 common_params 함수 한 곳만 고치면 된다. 그 함수를 Depends 로 받아 쓰는 모든 라우트가 동시에 새 규칙을 따른다. 라우트가 50 개여도 수정 지점은 하나다.

딕셔너리보다 명확한 클래스 의존성

의존성으로 함수만 쓸 수 있는 건 아니다. 클래스 도 의존성이 될 수 있다. 파이썬에서 클래스를 호출하면(Pagination()) __init__ 이 실행되니, FastAPI 입장에서는 클래스도 "호출 가능한 무언가" 일 뿐이다. 그래서 클래스 자체를 그대로 Depends 에 넘긴다.

class Pagination: def __init__(self, skip: int = 0, limit: int = 10): self.skip = skip self.limit = limit @app.get("/products") def read_products(page: Pagination = Depends(Pagination)): return {"skip": page.skip, "limit": page.limit}

__init__ 의 매개변수가 그대로 쿼리 매개변수가 된다. 결과로 page 에는 Pagination 객체가 들어오고, page.skip 처럼 점(.) 으로 또렷하게 꺼내 쓸 수 있다. 딕셔너리의 commons["skip"] 보다 오타에 강하고, 에디터의 자동완성도 받는다. 데이터가 단순하면 함수로, 구조가 좀 있으면 클래스로 — 정도로 기억하면 충분하다.

한 글자 줄이는 단축 표기page: Pagination = Depends(Pagination) 처럼 타입과 Depends 인자가 같은 클래스로 겹칠 때는, FastAPI 가 알아서 추론하도록 page: Pagination = Depends() 로 인자를 비워 둘 수 있다. 클래스를 의존성으로 쓸 때만 통하는 편의 문법이다.

4. yield 의존성 — 정리까지 책임지기

지금까지의 의존성은 값을 return 하고 끝났다. 그런데 어떤 자원은 다 쓰고 나서 반드시 닫아 줘야 한다. 대표적인 게 데이터베이스 세션이다. 열어서 쓰고, 끝나면 닫아야 연결이 새지 않는다. 이럴 때 의존성 함수에서 return 대신 yield 를 쓴다.

def get_db(): db = SessionLocal() try: yield db finally: db.close() @app.get("/articles") def read_articles(db = Depends(get_db)): # db 로 조회한다고 가정 return {"db_ready": db is not None}

흐름을 따라가 보자. 요청이 들어오면 FastAPI 가 get_db 를 실행하다가 yield db 에서 잠시 멈추고, 그 db 를 엔드포인트에 넘긴다. read_articles 가 응답을 다 만들고 나면, FastAPI 가 멈춰 뒀던 get_db 로 돌아와 yield 이후 코드, 즉 finally 블록을 실행한다. 그래서 db.close() 가 항상 호출된다. 중간에 에러가 나도 finally 라 빠짐없이 정리된다.

한마디로 yield 의존성은 "쓰기 전 준비 → 엔드포인트 실행 → 쓴 뒤 정리" 를 한 함수 안에 담는다. yield 윗부분은 준비, 아랫부분은 뒷정리다. 7편에서 SQLAlchemy 로 실제 DB 를 붙일 때 이 get_db 패턴을 거의 그대로 다시 만나게 되니, 지금은 모양에만 익숙해지면 된다.

중첩 의존성 — 의존성이 또 다른 의존성을 가질 때

의존성 함수 역시 평범한 함수라서, 그 안에서 또 다른 의존성을 받을 수 있다. 예컨대 토큰에서 사용자를 꺼내는 get_current_user 의존성이 DB 세션 get_db 에 의존하고, 어떤 엔드포인트는 그 get_current_user 에 의존하는 식이다. FastAPI 는 이 의존성 사슬을 끝까지 따라가 가장 안쪽부터 차례로 해결한 뒤 결과를 위로 전달한다. 우리는 사슬의 맨 끝에서 필요한 것만 Depends 로 받으면, 그 뒤에 줄줄이 매달린 준비 과정은 프레임워크가 알아서 처리한다. 인증처럼 단계가 여러 개인 로직을 깔끔하게 쌓을 수 있는 이유다.

주의 — 의존성은 요청 단위로 실행된다 — 한 요청 안에서 같은 의존성을 여러 곳이 받아도 FastAPI 는 기본적으로 한 번만 실행하고 결과를 공유한다(Depends(..., use_cache=True) 가 기본값). 다만 요청이 바뀌면 다시 실행되므로, 무거운 초기화(예: 거대한 모델 로딩)를 의존성에 넣으면 요청마다 반복될 수 있다. 그런 자원은 앱 시작 시 한 번만 준비해 두고, 의존성에서는 그 준비된 것을 꺼내 주기만 하는 게 정석이다.

5. 정리 — Depends 한 줄이 바꾸는 것

이번 편의 큰 그림을 다시 모아 본다.

패턴언제 쓰나핵심 형태
함수 의존성공통 쿼리·간단한 공유 로직Depends(common_params)
클래스 의존성구조 있는 값을 점(.)으로 꺼낼 때Depends(Pagination)
yield 의존성DB 세션 등 정리가 필요한 자원try / yield / finally
중첩 의존성인증처럼 단계가 여러 개일 때의존성 안에서 또 Depends

요약

의존성 주입은 "필요한 재료를 직접 구하지 말고 받아 쓴다" 는 한 문장으로 압축된다. FastAPI 에서는 인자 자리에 Depends() 를 적으면, 프레임워크가 의존성 함수를 먼저 실행해 그 결과를 끼워 넣어 준다. 덕분에 공통 파라미터·인증·DB 세션 같은 코드를 한 곳에 정의하고 어디서든 재사용할 수 있고, 규칙이 바뀌어도 수정 지점은 하나로 줄어든다. 단순하면 함수로, 구조가 있으면 클래스로, 정리가 필요하면 yield 로 — 이 세 가지 형태와, 의존성이 또 다른 의존성을 부르는 중첩 구조까지가 5편의 전부다. 다음 편에서는 늘어난 라우트와 의존성을 파일·폴더 단위로 쪼개 관리하는 법을 다룬다.

다음 편 예고 — APIRouter 로 프로젝트 구조화

한 파일에 다 몰아넣던 엔드포인트를 APIRouter 로 기능별로 나누고, 폴더 구조와 공통 의존성을 라우터 단위로 묶는 법을 배운다.

© 2026 주나이테크(주) @JUNAITECH