Python 데코레이터(Decorator) 소개: 함수의 기능을 확장하는 방법

Ad Placeholder

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를 이용해 함수의 인자와 반환값을 데코레이터에서 처리하는 방법을 알아보겠습니다.

jiji

AI, Python, 생활정보와 정부 정책 정보를 쉽게 설명하는 정보 블로그입니다.

Ad Placeholder
Scroll to Top