데코레이터는 함수를 감싸는 함수다
말로 들으면 추상적으로 느껴지지만, 동작 원리는 단순하다. 데코레이터는 함수를 인자로 받아서 함수를 돌려준다. 이때 돌려주는 함수는 보통 원래 함수를 호출하면서, 그 호출 앞뒤로 추가 동작을 덧붙이는 구조다.
가장 짧은 예제로 살펴보자:
shout가 바로 데코레이터입니다. 함수(greet)를 인자로 받아서, 원본을 호출한 뒤 결과를 대문자로 바꿔주는 새 함수(wrapper)를 만들어 돌려주죠. 그리고 greet = shout(greet)처럼 다시 할당하면 원래 함수가 감싸진 버전으로 교체됩니다.
이 재할당 패턴이 워낙 자주 쓰이다 보니, 파이썬이 아예 전용 문법을 마련해 놓았습니다.
@ 문법은 재할당을 위한 편의 문법이다
def 바로 위 줄에 붙이는 @name은, 함수 정의 직후에 name = name(...)을 쓰는 것과 똑같은 동작을 합니다:
@shout는 "이 함수에 shout 데코레이터를 적용하라"는 뜻으로 읽으면 됩니다. def 바로 뒤에 파이썬이 greet = shout(greet)를 실행하는 것과 똑같은 동작이에요. 타이핑만 줄어들었을 뿐이죠.
@name이 보이면 머릿속으로 function = name(function)으로 바꿔 읽으면 됩니다. 이 문법이 하는 일은 그게 전부예요.
인자 처리하기
대부분의 함수는 인자를 받습니다. 쓸만한 데코레이터라면 이 인자들을 그대로 넘겨줘야 하죠. 여기서 관용적으로 쓰이는 패턴이 *args, **kwargs입니다. 파이썬에서 어떤 인자든 받아내는 방법인데요, 래퍼 입장에서는 감싸는 함수가 어떤 인자를 기대하는지 신경 쓸 필요가 없기 때문입니다:
*args는 위치 인자를 전부, **kwargs는 키워드 인자를 전부 받아냅니다. 래퍼는 전달받은 인자를 그대로 원래 함수에 넘겨주고, 그 뒤에 데코레이터가 맡은 추가 작업을 수행합니다. 여기서는 결과를 대문자로 바꾸는 일이죠.
실무에서 쓰는 데코레이터도 대부분 이 모양을 따릅니다.
좀 더 실용적인 예제: 실행 시간 측정
함수가 실행되는 데 걸린 시간을 출력해 볼까요?
호출 전에 뭔가 실행하고, 호출 후에도 뭔가 실행하는 이 패턴이야말로 대부분의 데코레이터가 하는 일입니다. 로깅, 인증 체크, 재시도, 입력 검증까지 전부 같은 구조로 흘러갑니다.
원래 함수의 정체성 지키기: functools.wraps
함수를 데코레이팅한다는 건 사실 그 함수를 다른 함수로 갈아끼우는 것과 같습니다. 그래서 감싸진 함수는 원래 가지고 있던 __name__과 __doc__ 속성을 잃어버리게 됩니다:
greet.__name__이 "wrapper"로 바뀌고 docstring도 사라져 버렸습니다. 이러면 help()나 트레이스백, 함수를 들여다보는 각종 도구들이 전부 제대로 동작하지 않게 됩니다.
해결책은 한 줄이면 충분합니다. 내부 함수에 @functools.wraps(func)를 붙여주면 원래 함수의 메타데이터가 그대로 복사됩니다.
항상 내부 함수에 @wraps(func)를 붙여 주세요. 비용은 전혀 없고, 나중에 디버깅하다가 당황하는 상황을 막아 줍니다.
인자를 받는 데코레이터
데코레이터 자체에 설정값을 넘겨야 할 때가 있습니다. "이 함수를 최대 3번까지 재시도해라", "DEBUG 레벨로 로깅해라" 같은 경우죠. 이럴 땐 한 겹을 더 감싸야 합니다. 즉, 인자를 받아서 데코레이터를 반환하는 바깥쪽 함수가 하나 더 필요합니다.
3단 구조가 거창해 보이지만, 바깥쪽부터 순서대로 읽어보면 간단합니다:
repeat(times=3)은 함수 호출입니다. 이 호출이decorator를 반환하죠.decorator가 진짜 데코레이터입니다. 함수를 받아서 감싼 함수를 돌려줍니다.wrapper는 실제 호출 시점에 실행되는, 감싸진 함수입니다.
바로 이 구조가 @retry(times=5), @cache(maxsize=100)은 물론 @app.route("/users") 같은 프레임워크 데코레이터까지 모두 떠받치고 있습니다. 이 3단 패턴 하나만 눈에 익으면, 비슷한 형태의 데코레이터는 전부 같은 방식으로 읽힙니다.
데코레이터 중첩해서 쓰기
하나의 함수에 데코레이터를 여러 개 겹쳐 붙일 수도 있습니다. 이때는 아래에서 위로 쌓이는 순서라서, def에 가장 가까이 붙은 데코레이터가 가장 먼저 실행됩니다:
add_exclaim이 먼저 감싸면서 !를 붙입니다. 그다음 shout가 그 결과를 다시 감싸서 전체를 대문자로 바꿉니다. 최종 결과는 HI, ROSA!가 되죠.
순서가 중요합니다. 데코레이터 스택을 뒤집으면 대문자로 바꾼 뒤에 느낌표가 붙게 됩니다. 이 예제에서는 겉보기 결과가 똑같지만, JSON을 포맷하는 데코레이터를 생각해 보세요. 입력값을 로깅하는 데코레이터보다 먼저 실행되느냐 나중에 실행되느냐에 따라 결과가 완전히 달라질 수 있습니다.
파이썬 기본 제공 데코레이터
파이썬과 표준 라이브러리에는 실무 코드에서 심심찮게 마주치게 될 데코레이터들이 기본으로 들어 있습니다:
@property는 메서드를 계산 속성처럼 쓸 수 있게 만들어 줍니다.@staticmethod는self나cls를 사용하지 않는 메서드임을 표시합니다.@classmethod는 인스턴스 대신 클래스를cls로 받기 때문에 대체 생성자를 만들 때 유용합니다.@functools.lru_cache는 결과를 메모이제이션해서, 같은 인자로 다시 호출하면 캐시를 통해 빠르게 반환됩니다.
프레임워크에서 자주 보는 데코레이터(@app.route, @pytest.fixture, @dataclass)도 같은 원리로 동작합니다. 특별한 마법은 없고, 그냥 함수를 감싸는 함수일 뿐이죠.
언제 쓰고, 언제 쓰지 말아야 할까
데코레이터는 여러 함수에 동일한 동작 을 입히고 싶을 때 쓰세요. 실행 시간 측정, 로깅, 재시도, 권한 확인 같은 것들이 대표적입니다. 함수 본문에 이런 부가 로직이 섞이지 않게 분리하는 것이 핵심입니다.
반대로 이런 경우엔 데코레이터를 피하는 게 좋습니다.
- 특정 함수 하나에만 필요한 동작일 때. 그냥 함수 안에 직접 넣으세요.
- 테스트 목적으로만 쓰려는 경우. 픽스처나 파라미터로 처리하는 편이 훨씬 명확합니다.
- 데코레이터를 네다섯 개씩 쌓고 싶어질 때. 그 시점부터는 제어 흐름이 데코레이터 체인 속에 숨어버려서, 실제로 어떤 코드가 실행되는지 파악하려면 한 겹씩 벗겨 봐야 합니다. 차라리 평범한 헬퍼 함수가 읽기 편할 수 있습니다.
데코레이터는 날카로운 도구입니다. 잘 쓰면 코드가 깔끔해지고 의도도 뚜렷해지지만, 잘못 쓰면 프로그램이 하는 일을 가려버립니다. 쓸지 말지 고민될 땐 "명확함" 쪽으로 기우는 쪽을 추천합니다.
다음 주제: 타입 힌트
데코레이터를 작성하다 보면 자연스럽게 타입 힌트를 마주치게 됩니다. wrapper 함수의 시그니처에 타입을 달아두는 경우가 많거든요. 타입 힌트는 작은 기능이지만 효과가 금방 나타나는데, 바로 다음에 다뤄 보겠습니다.
자주 묻는 질문
파이썬에서 데코레이터란 무엇인가요?
데코레이터는 다른 함수를 인자로 받아서 새로운 함수를 돌려주는 함수입니다. 보통은 원래 함수에 기능을 덧붙인 형태의 함수를 반환하죠. 사용할 때는 def 위에 @데코레이터이름을 붙이면 되고, 이 @ 문법은 사실 func = 데코레이터이름(func)의 축약 표현입니다.
데코레이터는 주로 어디에 쓰나요?
함수 본문을 건드리지 않고 기능을 추가할 때 씁니다. 로깅, 실행 시간 측정, 캐싱, 인증 체크, 입력값 검증, 재시도 로직 같은 게 대표적이죠. 프레임워크에서도 엄청 많이 쓰이는데, Flask의 @app.route(...), pytest의 @pytest.fixture, 그리고 내장된 @property, @staticmethod 같은 것들이 모두 데코레이터입니다.
데코레이터를 직접 만들 수 있나요?
네, 얼마든지요. 데코레이터는 결국 '함수를 받아서 함수를 돌려주는 함수'일 뿐입니다. 보통은 내부에 작은 inner 함수를 하나 정의해서 원래 함수 호출 전후나 주변에 원하는 동작을 넣고, 그 inner 함수를 반환하는 식으로 만듭니다. 이때 원래 함수의 이름과 docstring이 사라지지 않도록 inner 함수 위에 functools.wraps를 꼭 붙여주세요.