데코레이터(Decorator: @)

박태정·2026년 5월 17일

Python Deep Dive

목록 보기
9/9

저번 편에서는 스코프 문제와 클로저의 개념, 그리고 nonlocal까지 정리했었다. 클로저 공부를 하면서 "클로저가 왜 필요한지 아직 모르겠다"고 했었는데, 오늘 데코레이터를 공부하면서 필요성을 알게되었다.


데코레이터란?

데코레이터를 사용하는 건 사실 엄청 간단하다.

@somedecorator
def some_function():
    print("데코레이터 쓰고 있음!")

사용자 정의함수 위에 @ 기호 하나 붙이면 끝이다. 근데 이걸 직접 만드는 건 완전히 다른 얘기다. 제대로 만들려면 지금까지 공부한 개념들이 전부 필요하다.

  • 일급 객체: 함수를 변수에 담고, 인자로 넘기고, 반환할 수 있어야 함
  • 클로저: 외부 함수의 변수를 내부 함수가 기억할 수 있어야 함
  • 가변 인자 (*args): 어떤 함수든 감쌀 수 있어야 함

데코레이터는 이 세 가지가 전부 합쳐진 결과물이다.


데코레이터 직접 만들기

실습으로 함수 실행 시간을 측정하는 데코레이터를 만들었다.

import time

def performance_clock(func):        # 함수를 매개변수로 받음 (자유변수)
    def performance_clocked(*args): # 내부 함수 (실제 동작)
        st = time.perf_counter()
        result = func(*args)        # 원래 함수 실행
        et = time.perf_counter() - st
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f"[{et}s] {name}({arg_str}) -> {result}")
        return result
    return performance_clocked      # 함수 자체를 반환

구조를 뜯어보면 완전히 클로저다.

  • performance_clock이 외부 함수, performance_clocked가 내부 함수
  • func가 자유변수(계속 유지되는 변수)로서 내부 함수에서 계속 기억됨
  • 마지막에 performance_clocked 함수 자체를 반환 (일급 객체)

여기서 클로저가 왜 필요한지 짚고 넘어가야 한다. 내가 처음에 이해가 안 됐던 부분이다.

performance_clocked가 실행되는 시점을 생각해보면, 이미 performance_clock은 실행이 끝난 상태다. 함수가 끝났으면 내부 변수인 func는 사라져야 정상이다.

근데 performance_clocked 입장에서 보면 func(*args)를 실행해야 한다. 이미 종료된 외부 함수의 변수 func가 필요한 것이다.

이게 클로저가 해결해주는 문제다. performance_clocked가 반환될 때 func를 자유변수로 묶어서 같이 들고 나온다. 그래서 performance_clock이 끝난 이후에도 func가 사라지지 않고 살아있는 것이다.

# 이렇게 적용하는 순간
@performance_clock
def int_to_eng_func(seconds):
    ...

# 내부적으로 이렇게 동작한다
# int_to_eng_func = performance_clock(int_to_eng_func)
# -> performance_clock은 이미 종료됐지만
# -> 반환된 performance_clocked 안에 int_to_eng_func(func)가 살아있다

클로저가 없었다면 데코레이터는 구조 자체가 불가능하다. 외부 함수가 종료되는 순간 func가 사라지기 때문에, 나중에 실제로 함수를 호출할 때 func(*args)를 실행할 방법이 없어진다. 클로저가 func를 붙잡아두기 때문에 데코레이터가 동작할 수 있는 거다.


@ vs 수동 적용

데코레이터를 적용하는 방법이 두 가지인데, 동작은 완전히 동일하다.

# 수동 적용 -> 직접 실행
none_deco1 = performance_clock(int_to_eng_func)
none_deco1(1.5)

# 데코레이터 적용
@performance_clock
def int_to_eng_func(seconds):
    ...

int_to_eng_func(1.5)  # 원함수 이름으로 바로 호출 가능

@performance_clock은 사실 int_to_eng_func = performance_clock(int_to_eng_func)를 축약한 것이다. @ 기호가 이걸 자동으로 처리해주는 것뿐이다.


데코레이터를 쓰는 이유: 관심사 분리

처음에 "그냥 함수 만들어서 적용하면 되는 거 아닌가?"라는 생각이 들었다.

사실 쓰는 이유는 지금은 얼추 이해되는데 아직도 언제 쓰는게 가장 적합한 상황인지 약간은 모호하다.

찾아보니까 코드 재사용성, 유연한 기능 추가 같은 이유들이 나왔는데 솔직히 잘 와닿지 않았다.

나한테 제일 와닿은 이유는 관심사 분리였다.

# 비즈니스 로직 (함수의 본래 동작)
def int_to_eng_func(seconds):
    response = llm.invoke([f'{seconds}이거 영어로 어떻게 읽어?']).content
    print(response)

# 부가 기능 (시간 측정)
@performance_clock
def int_to_eng_func(seconds):
    ...

함수가 하는 일(비즈니스 로직)과 부가 기능(시간 측정, 로깅, 인증)의 관심사가 다르다. 데코레이터는 이 둘을 분리해준다. 원래 함수 코드를 건드리지 않고 기능을 붙이거나 뺄 수 있고, 여러 함수에 같은 부가 기능을 붙일 때 코드 중복도 없다.

데코레이터의 장단점을 정리하면 이렇다.

장점단점
중복 제거, 코드 간결가독성이 떨어질 수 있음
공통 기능(로깅, 인증, 시간 측정) 분리디버깅이 불편함
조합해서 사용 용이단일 함수로 쓰는 게 나은 경우도 있음

LangGraph @tool과의 연결

요즘 LangGraph를 공부하면서 @tool 데코레이터를 쓰고 있었는데, 이게 정확히 오늘 배운 원리다.

from langchain_core.tools import tool

@tool
def search_web(query: str) -> str:
    """웹에서 정보를 검색합니다."""
    ...

에이전트에 도구를 등록할 때 @tool을 붙여주면 그 함수가 LLM이 호출할 수 있는 도구로 변환된다. 이게 그냥 그냥 동작하는 게 아니라, 오늘 배운 데코레이터 구조 그대로다. 파이썬 문법 공부가 요즘 공부하고 있는 실제 AI 에이전트 개발이랑 이렇게 직접 연결될 줄은 몰랐다.


챕터05 전체를 돌아보면, 일급 객체 → 클로저 → 데코레이터 순서로 하나의 흐름으로 연결된다는 게 명확하게 보인다. 각각 따로 외우는 개념이 아니라, 앞 개념이 다음 개념의 기반이 되는 구조다.

클로저가 왜 필요한지 처음에는 모르겠고 이해도 안됐는데, 데코레이터를 만들어보니까 이해가 되었다.

0개의 댓글