한마디 요약

괄호 하나 차이가 의미를 통째로 뒤집는다. 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_docsformat_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)   # 8

ops["+"] 는 함수 객체, 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] 으로 “이 파라미터는 함수여야 함” 을 타입으로 박아두면 실수 예방. 함수의 타입 힌트 참고.


관련 문서


한마디 재요약

f()결과를, f기계를 건넨다. 프레임워크에게 “내가 지금 쓸 값” 을 주는지 “네가 필요할 때 돌릴 함수” 를 주는지에 따라 선택.