Application/Python

[Python] 파이썬 데코레이터: 함수를 꾸며주는 마법 (@ 기호의 정체)

devsalix 2026. 5. 15. 09:26
728x90

파이썬 데코레이터: 함수를 꾸며주는 마법 (@ 기호의 정체)

다른 사람이 짠 파이썬 코드를 보다 보면 가끔 이런 줄이 튀어나와요.

@app.route("/")
def home():
    ...

 

함수 위에 @ 뭐시기가 한 줄 붙어있는 거. 처음 보면 이게 도대체 뭔가 싶거든요. 저도 처음 봤을 때 "이게 무슨 자바 어노테이션 같은 건가?" 하면서 검색창을 열었던 기억이 나요. 검색해보면 "데코레이터"라는 답이 나오는데, 설명이 또 어렵습니다. "함수를 받아서 함수를 반환하는 함수"라니, 한국말인데 한국말이 아닌 것 같죠.

오늘은 이 @ 기호 하나의 정체를 끝까지 파볼게요. 결론부터 말씀드리면, @는 그냥 단축키예요. 진짜 그게 다입니다.


함수도 변수에 담을 수 있어요

파이썬 데코레이터를 이해하려면 먼저 한 가지 감각이 필요해요. 파이썬에서는 함수도 숫자나 문자열처럼 그냥 값이라는 사실이요.

def 인사():
    print("안녕하세요")

말하기 = 인사   # 괄호를 안 붙였어요. 함수 자체를 담는 거예요
말하기()        # 안녕하세요

 

인사()가 아니라 인사라고 쓴 거 보이시죠? 괄호를 안 붙이면 "그 함수를 실행해라"가 아니라 "그 함수를 그냥 가리켜라"라는 뜻이에요. 그래서 말하기라는 변수에 함수가 통째로 들어간 겁니다.

지난 글에서 map이나 filter에 함수를 인자로 넘겼던 거 기억나시죠? 그것도 같은 원리였어요. 함수가 값처럼 다뤄지니까 인자로도 넘길 수 있었던 거죠.


함수가 함수를 돌려주는 그 순간

다음 단계가 좀 새로운데요, 함수 안에서 함수를 정의하고, 그 안쪽 함수를 바깥으로 돌려줄 수도 있어요.

def 만들기():
    def 안쪽():
        print("나는 안에서 만들어진 함수예요")
    return 안쪽   # 안쪽 함수 자체를 반환

내함수 = 만들기()   # 내함수에 안쪽이 담김
내함수()            # 나는 안에서 만들어진 함수예요

 

만들기()를 호출하면 안쪽 함수가 튀어나와서 내함수에 담겨요. 그리고 내함수()로 실행하면 그제서야 진짜 동작이 일어나는 거예요.

이 모양이 어색하시면 아래 데코레이터에서 무너집니다. 한 번만 천천히 다시 읽어주세요.

함수 안에 함수를 정의하고, 그걸 return으로 돌려준다. 이게 핵심이에요.


@ 없이 데코레이터 만들어보기

이제 진짜 데코레이터예요. 거창해 보이지만 정의는 한 줄로 끝나요.

함수를 받아서, 기능을 살짝 추가한 새 함수를 돌려주는 함수.

 

선물 포장이랑 똑같아요. 안에 든 물건(원래 함수)은 그대로인데, 포장지(추가 기능)가 한 겹 둘러진 형태죠.

def 꾸미기(원래함수):       # 함수를 인자로 받고
    def 감싸기():           # 새 함수를 안에서 만들어서
        print("=== 시작 ===")
        원래함수()          # 가운데에 원래 함수 끼워 넣고
        print("=== 끝 ===")
    return 감싸기           # 새 함수를 돌려준다

def 인사():
    print("안녕하세요")

인사 = 꾸미기(인사)   # 인사를 꾸미기에 통과시킨 결과로 덮어쓰기
인사()
# === 시작 ===
# 안녕하세요
# === 끝 ===

 

마지막 줄을 잘 보세요. 인사 = 꾸미기(인사). 원래 인사 함수를 꾸미기에 넣어서 포장한 다음, 그 결과로 인사라는 이름을 덮어쓴 거예요. 이제 인사()를 부르면 포장된 버전이 실행됩니다.


파이썬 @ 기호의 정체, 알고 보면 별거 없어요

자, 이게 오늘의 하이라이트예요. 위 코드를 @로 바꿔볼게요.

def 꾸미기(원래함수):
    def 감싸기():
        print("=== 시작 ===")
        원래함수()
        print("=== 끝 ===")
    return 감싸기

@꾸미기
def 인사():
    print("안녕하세요")

인사()
# === 시작 ===
# 안녕하세요
# === 끝 ===

 

결과가 똑같죠? 그럴 수밖에 없는 게, 이 두 코드가 완전히 같은 코드거든요.

@꾸미기
def 인사():
    ...

 

이 두 줄은 정확하게 아래와 같습니다.

def 인사():
    ...
인사 = 꾸미기(인사)

 

그게 다예요. @꾸미기 한 줄이 인사 = 꾸미기(인사)를 대신 적어주는 단축 표기일 뿐입니다. 마법도 어노테이션도 아니고, 그냥 타이핑 줄여주는 문법 설탕이에요.


매개변수 있는 함수는 어떻게요?

위 예제는 인사 함수가 인자를 안 받아서 깔끔했는데, 보통 함수는 인자를 받잖아요. 그럴 땐 감싸기 함수도 인자를 받아서 그대로 넘겨줘야 해요.

def 꾸미기(f):
    def 감싸기(*args, **kwargs):     # 인자를 뭐든 다 받기
        print("호출 전")
        결과 = f(*args, **kwargs)    # 그대로 원래 함수에 넘김
        print("호출 후")
        return 결과
    return 감싸기

@꾸미기
def 더하기(a, b):
    return a + b

print(더하기(3, 5))
# 호출 전
# 호출 후
# 8

 

*args, **kwargs는 "인자가 몇 개든, 어떤 형태든 다 받아라"라는 뜻이에요. 지금은 이 정도만 알고 넘어가셔도 충분합니다. 데코레이터 쓸 땐 거의 공식처럼 이 패턴을 그대로 쓰거든요.


실전 예제: 함수 실행 시간 재기

python decorator의 진짜 매력은 실용 예제에서 나와요. 함수 하나가 얼마나 걸리는지 측정하고 싶다고 해볼게요.

import time

def 시간측정(f):
    def 감싸기(*args, **kwargs):
        시작 = time.time()
        결과 = f(*args, **kwargs)
        걸린시간 = time.time() - 시작
        print(f"{f.__name__} 실행: {걸린시간:.4f}초")
        return 결과
    return 감싸기

@시간측정
def 합계(n):
    return sum(range(n))

합계(1000000)
# 합계 실행: 0.0231초

 

원래 합계 함수에는 시간 재는 코드가 한 줄도 없어요. 그런데 @시간측정 한 줄로 시간 측정 기능이 붙었죠. 본체는 건드리지 않고 기능만 추가한 거예요. 이게 데코레이터의 진짜 가치예요.


자주 만나는 실수

처음 데코레이터 짤 때 거의 100% 만나는 에러가 있어요.

def 데코(f):
    def wrapper():
        f()
    # return wrapper 를 깜빡!

@데코
def 인사():
    print("안녕")

인사()  # TypeError: 'NoneType' object is not callable

 

return wrapper를 빼먹으면 데코 함수가 None을 돌려주고, 인사 = None이 되어버려요. 그래서 인사()를 부르면 "None은 호출할 수 없다"는 에러가 나옵니다. 데코레이터에서 에러 만나시면 십중팔구는 return을 빠뜨린 경우예요.

참고로 데코레이터를 씌우면 함수의 원래 이름이 사라지는 부작용이 있어서, from functools import wraps를 쓰고 @wraps(f)를 안쪽에 한 줄 붙이는 게 관례예요. 지금은 "그런 게 있다"만 기억해두시면 됩니다.

직접 해보세요

호출될 때마다 횟수를 세는 데코레이터를 만들어보세요.

def 횟수세기(f):
    카운트 = 0
    def 감싸기(*args, **kwargs):
        nonlocal 카운트
        카운트 += 1
        print(f"{f.__name__} 호출 횟수: {카운트}")
        return f(*args, **kwargs)
    return 감싸기

@횟수세기
def 인사():
    print("안녕")

인사()  # 인사 호출 횟수: 1
인사()  # 인사 호출 횟수: 2

 

nonlocal은 안쪽 함수에서 바깥 변수를 바꿀 때 필요한 키워드예요. 변형으로 "짝수번째 호출에만 메시지 출력하기"도 도전해보시면 재밌습니다.

이제 남이 짠 코드에서 @app.route, @login_required 같은 게 보여도 별로 안 무서우실 거예요. "아, 저 함수를 누군가가 한 번 포장해서 새로 정의해놓은 거구나" 하고 읽히실 테니까요. 다음에는 이런 외부 라이브러리들을 본격적으로 가져다 쓰기 전에, 작업 환경을 깔끔하게 분리해주는 가상환경과 pip 얘기를 해볼게요.


 


제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀

 

 
728x90