Python에는 기존 함수의 코드를 직접 수정하지 않고도 함수에 새로운 기능을 추가할 수 있는 방법이 있습니다.
바로 데코레이터(Decorator)입니다.
데코레이터는 처음 보면 @ 기호 때문에 특별한 문법처럼 보이지만, 기본 원리는 생각보다 단순합니다.
앞에서 배운 함수 객체와 일급 객체 개념을 이해하고 있다면 데코레이터의 동작 원리도 쉽게 이해할 수 있습니다.
데코레이터란?
데코레이터는 간단하게 말하면 함수를 받아서 새로운 기능을 추가한 함수를 반환하는 구조입니다.
예를 들어 다음과 같은 함수가 있다고 해보겠습니다.
def greet():
print("안녕하세요")
이 함수에 실행 전후로 메시지를 출력하는 기능을 추가하고 싶다고 가정해보겠습니다.
가장 단순하게 생각하면 함수 내부를 직접 수정할 수 있습니다.
def greet():
print("함수 실행 전")
print("안녕하세요")
print("함수 실행 후")
하지만 기존 함수의 코드를 직접 수정하지 않고 기능을 추가하고 싶다면 데코레이터를 사용할 수 있습니다.
데코레이터의 기본 구조
먼저 데코레이터의 가장 기본적인 형태를 살펴보겠습니다.
def decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
여기서 decorator()는 함수를 하나 전달받습니다.
그리고 내부에서 wrapper()라는 새로운 함수를 만든 다음 반환합니다.
구조를 그림으로 표현하면 다음과 같습니다.
기존 함수
↓
decorator()
↓
기능이 추가된 wrapper 함수
↓
새로운 함수 반환
이것이 데코레이터의 기본 원리입니다.
데코레이터를 직접 적용해보기
위에서 만든 데코레이터를 함수에 적용해보겠습니다.
def decorator(func):
def wrapper():
print("함수 실행 전")
func()
print("함수 실행 후")
return wrapper
def greet():
print("안녕하세요")
greet = decorator(greet)
이제 greet를 호출하면 어떻게 될까요?
greet()
결과:
함수 실행 전
안녕하세요
함수 실행 후
원래 greet()는 "안녕하세요"만 출력했습니다.
하지만 decorator(greet)를 거치면서 실행 전과 실행 후에 추가 동작이 들어갔습니다.
실제로 어떤 일이 일어난 것일까?
다음 부분이 핵심입니다.
greet = decorator(greet)
처음에는 greet가 원래 함수 객체를 가리키고 있습니다.
greet
↓
원래 greet 함수
decorator(greet)를 실행하면 데코레이터가 원래 함수를 전달받습니다.
greet
↓
decorator()
↓
wrapper 반환
그리고 반환된 wrapper를 다시 greet라는 이름에 저장합니다.
greet
↓
wrapper 함수
따라서 이후에
greet()
를 실행하면 사실상 wrapper()가 실행됩니다.
그리고 wrapper() 내부에서 원래의 greet()를 호출합니다.
greet()
↓
wrapper()
↓
"함수 실행 전"
↓
원래 greet()
↓
"안녕하세요"
↓
"함수 실행 후"
데코레이터를 이해할 때 이 흐름을 이해하는 것이 가장 중요합니다.
@ 기호를 사용하는 방법
Python에서는 데코레이터를 더 간단하게 적용할 수 있도록 @ 문법을 제공합니다.
앞에서 다음과 같이 작성했습니다.
greet = decorator(greet)
이 코드는 다음과 같이 표현할 수 있습니다.
@decorator
def greet():
print("안녕하세요")
두 방식은 기본적으로 같은 의미입니다.
@decorator
def greet():
print("안녕하세요")
위 코드는 개념적으로 다음과 같이 생각할 수 있습니다.
def greet():
print("안녕하세요")
greet = decorator(greet)
따라서 @decorator는 함수에 데코레이터를 적용한다는 의미입니다.
데코레이터의 실행 과정
다음 코드를 기준으로 살펴보겠습니다.
def decorator(func):
def wrapper():
print("실행 전")
func()
print("실행 후")
return wrapper
@decorator
def greet():
print("안녕하세요")
greet()
실행 과정은 다음과 같습니다.
1. greet 함수 정의
↓
2. @decorator 적용
↓
3. decorator(greet) 실행
↓
4. wrapper 함수 생성
↓
5. wrapper 반환
↓
6. greet가 wrapper를 가리킴
↓
7. greet() 호출
↓
8. wrapper() 실행
↓
9. 원래 greet() 실행
처음에는 복잡해 보이지만 핵심은 기존 함수가 데코레이터를 거쳐 새로운 함수로 교체된다는 것입니다.
왜 데코레이터를 사용하는가?
데코레이터의 가장 큰 장점은 여러 함수에 공통 기능을 반복해서 추가할 수 있다는 것입니다.
예를 들어 함수가 실행될 때마다 실행 시간을 기록하고 싶다고 해보겠습니다.
함수가 여러 개라면 각각의 함수에 다음 코드를 직접 작성할 수도 있습니다.
def first():
print("실행 시작")
# 실제 작업
print("실행 종료")
def second():
print("실행 시작")
# 실제 작업
print("실행 종료")
하지만 이런 방식은 같은 코드가 반복됩니다.
데코레이터를 사용하면 공통 기능을 하나의 데코레이터로 분리할 수 있습니다.
def log(func):
def wrapper():
print("함수 실행:", func.__name__)
func()
return wrapper
그리고 필요한 함수에 적용합니다.
@log
def first():
print("첫 번째 함수")
@log
def second():
print("두 번째 함수")
이제 두 함수를 실행하면 공통적으로 로그가 출력됩니다.
first()
second()
결과:
함수 실행: first
첫 번째 함수
함수 실행: second
두 번째 함수
이처럼 데코레이터를 사용하면 기존 함수의 핵심 로직과 부가 기능을 분리할 수 있습니다.
데코레이터에서 wrapper를 사용하는 이유
앞에서 계속 wrapper라는 이름을 사용했습니다.
def decorator(func):
def wrapper():
print("실행 전")
func()
print("실행 후")
return wrapper
wrapper는 특별히 정해진 이름이 아닙니다.
다음처럼 다른 이름을 사용해도 됩니다.
def decorator(func):
def new_function():
print("실행 전")
func()
print("실행 후")
return new_function
하지만 데코레이터에서는 원래 함수를 감싸서 추가 기능을 수행하는 내부 함수를 흔히 wrapper라고 부릅니다.
그래서 관례적으로 많이 사용되는 이름입니다.
데코레이터는 함수를 감싼다
데코레이터를 이해할 때 Wrapper(래퍼)라는 표현을 기억하면 좋습니다.
기존 함수가 있다고 하면,
원래 함수
데코레이터가 이를 감쌉니다.
┌─────────────────────┐
│ wrapper │
│ │
│ ┌─────────────┐ │
│ │ 원래 함수 │ │
│ └─────────────┘ │
│ │
└─────────────────────┘
그래서 데코레이터는 기존 함수의 실행 전후에 필요한 작업을 추가하기 좋습니다.
예를 들어 다음과 같은 기능을 추가할 수 있습니다.
- 실행 로그 기록
- 실행 시간 측정
- 권한 확인
- 입력값 검증
- 캐싱
- 예외 처리
- 공통적인 전처리와 후처리
다만 실제로 이런 기능을 제대로 구현하려면 함수의 인자와 반환값을 함께 처리해야 합니다.
이 부분은 다음 글에서 자세히 다루게 됩니다.
현재 데코레이터의 한계
지금까지 만든 데코레이터는 다음과 같은 함수에는 사용할 수 있습니다.
@decorator
def greet():
print("안녕하세요")
하지만 인자가 있는 함수에는 그대로 사용할 수 없습니다.
예를 들어 다음 함수가 있다고 해보겠습니다.
def greet(name):
print(f"{name}님 안녕하세요")
앞에서 만든 데코레이터를 적용하면 문제가 발생합니다.
@decorator
def greet(name):
print(f"{name}님 안녕하세요")
그리고 다음과 같이 호출하면,
greet("Python")
wrapper()가 인자를 받을 수 없기 때문에 오류가 발생합니다.
현재 wrapper()는 다음과 같이 만들어져 있기 때문입니다.
def wrapper():
...
즉, 데코레이터가 적용되는 함수의 인자를 제대로 전달하려면 wrapper에서도 인자를 처리할 수 있어야 합니다.
또한 원래 함수가 반환하는 값도 그대로 전달해야 하는 경우가 있습니다.
이 부분이 데코레이터를 실제로 사용할 때 매우 중요한 내용입니다.
마무리
Python의 데코레이터는 기존 함수의 코드를 직접 수정하지 않고 새로운 기능을 추가하는 방법입니다.
기본적인 구조는 다음과 같습니다.
def decorator(func):
def wrapper():
func()
return wrapper
그리고 @ 문법을 이용해 간단하게 적용할 수 있습니다.
@decorator
def greet():
print("안녕하세요")
데코레이터의 핵심은 앞에서 배운 함수 객체와 일급 객체 개념과 연결됩니다.
함수를 다른 함수에 전달하고, 새로운 함수를 반환할 수 있기 때문에 이러한 구조가 가능한 것입니다.
다만 지금까지 만든 데코레이터는 인자가 없는 함수만 제대로 처리할 수 있다는 한계가 있습니다.
다음 글에서는 이 문제를 해결하기 위해 *args, **kwargs를 이용해 함수의 인자와 반환값을 데코레이터에서 처리하는 방법을 알아보겠습니다.