한마디 요약
괄호 하나 차이가 의미를 통째로 뒤집는다. f() 는 “지금 실행해서 반환값을 쓰겠다”, f 는 “함수 자체를 전달해서 나중에 실행하겠다”. AI Agent 개발에서는 이 구분이 곧 즉시 계산 vs 지연 계산(lazy evaluation), 일반 호출 vs 의존성 주입(Dependency Injection), 정적 값 vs Runnable 체인 의 분기점이 된다.
핵심 개념: 괄호 있음 / 없음
| 표현 | 의미 | 결과 타입 |
|---|---|---|
get_value() | 지금 실행 → 반환값 사용 | 반환 타입 (예: int) |
get_value | 함수 객체 자체 | <class 'function'> |
직접 확인
def get_value():
return 10
a = get_value() # 함수를 실행해 반환값 10 을 a 에 저장
print(a, type(a)) # 10 <class 'int'>
b = get_value # 함수 객체 자체를 b 에 저장 (실행 안 함)
print(b, type(b)) # <function get_value at 0x...> <class 'function'>
print(b()) # 10 ← b 를 나중에 실행1. 일반 Python: 반환값을 파라미터로 (즉시 실행)
def get_number():
print("get_number() 실행")
return 10
def add(x):
return x + 5
result = add(get_number())
# ↑ 여기서 get_number() 먼저 실행됨, 반환값 10 이 add 에 들어감
print(result) # 15
# 출력:
# get_number() 실행
# 15단계 분해:
1단계: Python 이 add(...) 의 인자를 먼저 평가
2단계: get_number() 실행 → 10
3단계: add(10) 호출 → 15
4단계: result = 15
이게 “즉시 평가(eager evaluation)“. 파이썬 기본 동작.
2. FastAPI Depends: 함수 자체를 전달 (지연 실행)
AI Agent API 서버에서 자주 쓰는 패턴. “DB 연결”, “현재 사용자”, “LLM 클라이언트” 같은 의존성을 매 요청마다 새로 만들어야 할 때.
from fastapi import FastAPI, Depends
def get_db():
print("→ get_db() 실행")
return "MySQL 연결"
app = FastAPI()
# ❌ 잘못된 방식: 서버 시작 시 1번만 실행
@app.get("/wrong")
def wrong_endpoint(db = get_db()):
# ↑ 괄호 있음! 즉시 실행
return {"db": db}
# ✅ 올바른 방식: 요청마다 실행
@app.get("/correct")
def correct_endpoint(db = Depends(get_db)):
# ↑ 괄호 없음! 함수 객체 전달
return {"db": db}왜 이렇게 동작이 갈리는가
get_db() (즉시 실행) 을 기본값으로 넣으면 함수경로(=none 의 의미) 에서 본 “mutable default” 함정과 같은 결이 된다. 기본값은 함수 정의 시점에 한 번만 평가되기 때문에, 엔드포인트가 등록될 때 한 번 실행되고 반환값이 고정된다.
Depends(get_db) (함수 객체 전달) 은 FastAPI 가 내부적으로 “이건 의존성이구나. 매 요청마다 이 함수를 호출해서 결과를 주입해야겠구나” 라고 인식하는 특수 마커. 다른 프레임워크의 “Inversion of Control” 과 같은 개념.
실행 시점 비교
서버 시작
├─ wrong_endpoint 등록
│ └─ get_db() 1회 실행 → "MySQL 연결" 이 기본값으로 고정됨
└─ correct_endpoint 등록
└─ (get_db 함수 객체만 참조. 실행 안 함)
GET /wrong 요청
└─ db = "MySQL 연결" ← 서버 시작 때 받은 값 재사용
GET /correct 요청 (매번)
└─ FastAPI 가 get_db() 를 호출 → 매번 새 "MySQL 연결" 값 주입
왜 “함수 자체 전달” 이 필요한가 (설계 의도)
| 목적 | 이유 |
|---|---|
| 매 요청/호출마다 다른 값 | DB 커넥션, LLM 세션, 사용자 컨텍스트 |
| 테스트 시 교체 | Depends(get_db) 를 mock 으로 override |
| 계산 비용 절약 (lazy) | 진짜 필요할 때까지 실행 미루기 |
| 체인 합성 | Runnable 끼리 ` |
| 조건부 실행 | ”이 조건일 때만 실행” 같은 로직 분기 |
AI Agent 에서의 대응 패턴
패턴 1: LangChain RunnableLambda (함수를 체인에 끼워 넣기)
from langchain_core.runnables import RunnableLambda
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
def format_docs(docs: list[str]) -> str:
return "\n\n".join(docs)
prompt = ChatPromptTemplate.from_template("문맥:\n{context}\n\n질문: {q}")
llm = ChatOpenAI()
# format_docs 를 "체인에 끼워넣기" 위해 Runnable 로 감싼다
chain = (
{"context": RunnableLambda(format_docs), "q": lambda x: x["q"]}
| prompt
| llm
)
chain.invoke({"docs": ["문서1", "문서2"], "q": "정리해줘"})format_docs 를 format_docs(docs) 로 즉시 실행하면 체인이 완성되지 않는다. RunnableLambda(format_docs) 로 함수 객체를 감싸 전달해야 invoke 시점에 지연 실행된다.
패턴 2: LangGraph 노드 등록
from langgraph.graph import StateGraph
def planner_node(state): ...
def executor_node(state): ...
graph = StateGraph(AgentState)
graph.add_node("planner", planner_node) # ← 괄호 없음, 함수 자체
graph.add_node("executor", executor_node) # ← 괄호 없음add_node("planner", planner_node(state)) 처럼 실수로 괄호를 붙이면 당장 실행돼서 state 가 없다며 에러. 그래프는 “나중에” 노드를 호출해야 하므로 반드시 함수 객체.
패턴 3: 콜백 등록
from langchain_core.callbacks import BaseCallbackHandler
def on_llm_start(serialized, prompts, **kwargs):
print("LLM 호출 시작")
chain.invoke(
query,
config={"callbacks": [on_llm_start]} # 함수 객체를 리스트에 담음
)
# LangChain 이 적절한 시점에 on_llm_start(...) 로 호출해준다함정: “함수 = 값” 이라는 것을 잊으면 생기는 실수
Python 에서 함수는 1급 객체(first-class object). 즉 변수에 담거나, 인자로 넘기거나, 리스트에 넣는 데 제한이 없다.
def add(a, b): return a + b
def sub(a, b): return a - b
ops = {"+": add, "-": sub} # 함수 객체를 딕셔너리에 저장
def apply(op: str, a: int, b: int):
return ops[op](a, b) # 꺼내서 실행
apply("+", 3, 5) # 8ops["+"] 는 함수 객체, ops["+"](3, 5) 는 그 객체를 호출. 이 차이를 정확히 보는 안목이 Agent 코드 읽기의 핵심.
실전 비교표
| 상황 | 괄호 있음 f(x) | 괄호 없음 f |
|---|---|---|
| 일반 변수 대입 | y = f(x) → y 는 반환값 | g = f → g 는 함수 별명 |
| 함수 인자 | 반환값 전달 | 함수 객체 전달 (콜백) |
| FastAPI 기본값 | 서버 시작 시 1회 실행 | Depends(f) 로 감싸 요청마다 실행 |
| LangChain 체인 | 값이 바로 체인에 못 들어감 | RunnableLambda(f) 로 삽입 |
| LangGraph 노드 | 즉시 실행 → 에러 | 정상 등록 |
파이썬 map/filter | 불가 (값 받지 않음) | map(f, iter) 가능 |
Callable 타입으로 “함수 객체를 받는다”고 명시하기
from typing import Callable
def run_with_logger(fn: Callable[[int], int], value: int) -> int:
print(f"실행: {fn.__name__}({value})")
return fn(value)
def square(x: int) -> int:
return x * x
run_with_logger(square, 5) # 실행: square(5) → 25
run_with_logger(square(5), 5) # ❌ 25 는 Callable 이 아님Callable[[ArgTypes], ReturnType] 으로 “이 파라미터는 함수여야 함” 을 타입으로 박아두면 실수 예방. 함수의 타입 힌트 참고.
관련 문서
- @데코레이터 (함수를 인자로 받는 대표 패턴)
- 클로저(Closure)란 (함수 객체가 “주변 변수” 를 기억하는 메커니즘)
- lambda와 순회함수 (이름 없는 함수 객체 즉석 생성)
- 함수의 타입 힌트 (
Callable힌트) - 함수경로(=none 의 의미) (기본값 평가 시점과 같은 결)
한마디 재요약
f() 는 결과를, f 는 기계를 건넨다. 프레임워크에게 “내가 지금 쓸 값” 을 주는지 “네가 필요할 때 돌릴 함수” 를 주는지에 따라 선택.