파이썬 데코레이터: 함수를 꾸며주는 마법 (@ 기호의 정체)
다른 사람이 짠 파이썬 코드를 보다 보면 가끔 이런 줄이 튀어나와요.
@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 얘기를 해볼게요.
제 글이 도움이 되셨다면 댓글 & 공감 부탁드려요 😀
'Application > Python' 카테고리의 다른 글
| [Python] 파이썬 람다와 map filter, 한 줄 함수로 코드 짧게 쓰는 법 (0) | 2026.05.14 |
|---|---|
| [Python] 파이썬 리스트 컴프리헨션, 코드 한 줄로 줄이는 마법 (0) | 2026.05.13 |
| [Python] 파이썬 상속과 오버라이딩, 클래스를 또 만들지 마세요 (1) | 2026.05.12 |
| [Python] 파이썬 생성자(__init__) 쉽게 이해하기 (0) | 2026.05.11 |
| [Python] 파이썬 클래스, 객체지향이란 무엇일까 (0) | 2026.05.08 |