Python Decorator

Python 데코레이터(Decorator) 완벽 가이드 @staticmethod, @classmethod, @property 그리고 사용자 정의 데코레이터까지

파이썬을 공부하다 보면 다음과 같은 문법을 자주 만나게 됩니다.

@staticmethod
def add(a, b):
    return a + b

@classmethod
def from_string(cls, data):
    pass

@property
def name(self):
    return self._name

처음에는 단순히 함수 위에 붙는 특별한 문법처럼 보이지만, 사실 이들은 모두 데코레이터(Decorator) 입니다.

데코레이터는 파이썬의 매우 강력한 기능 중 하나이며, 클래스 메서드, 정적 메서드, 프로퍼티뿐만 아니라 Flask, Django, FastAPI, PyTorch 등 다양한 프레임워크에서도 핵심적으로 사용됩니다.

이번 글에서는 데코레이터의 개념부터 시작하여 @staticmethod, @classmethod, @property, 그리고 사용자 정의 데코레이터까지 한 번에 정리해 보겠습니다.


1. 데코레이터(Decorator)란?

예를 들어 함수 실행 전후에 로그를 출력하고 싶다고 가정해 보겠습니다.

def hello():
    print("Hello Python")

실행 결과

Hello Python

함수도 객체(Object)이다

파이썬에서는 함수도 객체입니다.

따라서 변수에 저장할 수 있습니다.

def hello():
    print("Hello Python")

func = hello

func()

결과

Hello Python

함수를 다른 함수에 전달할 수 있다

def hello():
    print("Hello Python")

def execute(func):
    func()

execute(hello)

결과

Hello Python

함수를 인자로 전달할 수 있다는 특징 때문에 데코레이터를 구현할 수 있습니다.


2. 사용자 정의 데코레이터 만들기

함수 실행 전후에 로그를 출력하는 데코레이터를 만들어 보겠습니다.

def my_decorator(func):

    def wrapper():
        print("함수 실행 시작")

        func()

        print("함수 실행 종료")

    return wrapper

사용 방법

def hello():
    print("Hello Python")

hello = my_decorator(hello)

hello()

결과

함수 실행 시작
Hello Python
함수 실행 종료

3. @ 문법의 의미

위 코드는 다음과 같이 작성할 수 있습니다.

@my_decorator
def hello():
    print("Hello Python")

실제로는 아래 코드와 동일합니다.

def hello():
    print("Hello Python")

hello = my_decorator(hello)

즉,

@something

함수 = something(함수)

를 의미합니다.


4. 데코레이터를 사용하는 이유

데코레이터가 왜 필요한지 가장 쉽게 이해할 수 있는 예로, 여러 함수에 같은 안내 문구를 붙이고 싶을 때를 생각해 보겠습니다.

예를 들어 공부, 운동, 독서라는 세 개의 함수가 있고, 각각 실행될 때마다 앞뒤로 같은 메시지를 출력하고 싶다고 가정해 보겠습니다.

데코레이터가 없다면 각 함수마다 같은 코드를 반복해서 작성해야 합니다.

def study():
    print("=== 시작 ===")
    print("공부 중...")
    print("=== 끝 ===")

def exercise():
    print("=== 시작 ===")
    print("운동 중...")
    print("=== 끝 ===")

def read():
    print("=== 시작 ===")
    print("독서 중...")
    print("=== 끝 ===")

이렇게 작성하면 함수가 많아질수록 같은 코드가 계속 반복됩니다.

실행 결과는 다음과 같습니다.

study()
exercise()
read()
=== 시작 ===
공부 중...
=== 끝 ===
=== 시작 ===
운동 중...
=== 끝 ===
=== 시작 ===
독서 중...
=== 끝 ===

하지만 데코레이터를 사용하면 공통 기능을 한 번만 정의한 뒤 여러 함수에 쉽게 적용할 수 있습니다.

def add_message(func):
    def wrapper():
        print("=== 시작 ===")
        func()
        print("=== 끝 ===")
    return wrapper

@add_message
def study():
    print("공부 중...")

@add_message
def exercise():
    print("운동 중...")

@add_message
def read():
    print("독서 중...")

실행 결과는 다음과 같습니다.

study()
exercise()
read()
=== 시작 ===
공부 중...
=== 끝 ===
=== 시작 ===
운동 중...
=== 끝 ===
=== 시작 ===
독서 중...
=== 끝 ===

이 예제에서 중요한 점은, 만약 시작 문구를 "=== 시작 ==="에서 "작업을 시작합니다"로 바꾸고 싶다면 데코레이터 안의 코드만 수정하면 된다는 것입니다.
즉, 여러 함수의 코드를 하나씩 고칠 필요가 없습니다.

이처럼 데코레이터를 사용하면 같은 기능을 여러 함수에 반복해서 넣지 않아도 되므로, 코드가 더 짧아지고 관리하기도 쉬워집니다.


실무 활용 예시

  • 로그 출력
  • 실행 시간 측정
  • 캐싱(Cache)
  • 권한 검사
  • 인증(Authentication)
  • 예외 처리
  • API 호출 제한

5. @staticmethod

정적 메서드를 만드는 데코레이터입니다.

class Calculator:

    @staticmethod
    def add(a, b):
        return a + b

사용

result = Calculator.add(10, 20)

print(result)

결과

30

특징

정적 메서드는

  • self 없음
  • cls 없음
  • 객체 상태 사용 안 함
  • 클래스 상태 사용 안 함

즉, 클래스 안에 존재하지만 일반 함수처럼 동작합니다.


활용 예시

class StringUtil:

    @staticmethod
    def is_email(email):
        return '@' in email
print(StringUtil.is_email("test@gmail.com"))

결과

True

6. @classmethod

클래스 메서드를 만드는 데코레이터입니다.

class Student:

    school = "OpenAI 고등학교"

    @classmethod
    def show_school(cls):
        print(cls.school)

사용

Student.show_school()

결과

OpenAI 고등학교

cls란?

class Student:

    @classmethod
    def show_class(cls):
        print(cls)

결과

<class '__main__.Student'>

즉 cls는 현재 클래스를 의미합니다.


대표 활용: 대체 생성자

class Person:

    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_string(cls, data):
        name, age = data.split(',')
        return cls(name, int(age))

사용

person = Person.from_string("Tom,20")

7. @property

프로퍼티(Property)를 만드는 데코레이터입니다.

메서드를 속성처럼 사용할 수 있게 해줍니다.


일반적인 방법

class Person:

    def __init__(self, name):
        self._name = name

    def get_name(self):
        return self._name

사용

person = Person("Tom")

print(person.get_name())

결과

Tom

@property 사용

class Person:

    def __init__(self, name):
        self._name = name

    @property
    def name(self):
        return self._name

사용

person = Person("Tom")

print(person.name)

결과

Tom

메서드인데 마치 속성처럼 사용할 수 있습니다.


8. Setter 추가하기

class Person:

    def __init__(self, age):
        self._age = age  # 내부에서 관리하는 실제 값

    @property
    def age(self):
        return self._age

    @age.setter
    def age(self, value):

        if not isinstance(value, int):
            raise TypeError("나이는 정수여야 합니다.")

        if value < 0:
            raise ValueError("나이는 0보다 작을 수 없습니다.")

        self._age = value

사용

person = Person(20)

print(person.age)   # 나이 읽기

person.age = 21     # 나이 바꾸기

print(person.age)

결과

20
21

어떻게 동작할까?

위 코드에서 age는 그냥 일반 변수처럼 보이지만, 실제로는 메서드입니다.

하지만 @property를 붙였기 때문에 다음처럼 사용할 수 있습니다.

print(person.age)

이 코드는 내부적으로 age() 메서드를 직접 호출하는 것이 아니라, @property가 만든 getter를 실행해서 값을 가져옵니다.

그리고 아래 코드는

person.age = 21

setter를 실행합니다. 즉, 값을 바로 바꾸는 것처럼 보이지만 실제로는 중간에서 검사를 한 뒤에 값을 저장합니다.


왜 setter가 필요할까?

예를 들어 나이에 음수를 넣는 실수를 막고 싶다고 해 보겠습니다.

person.age = -3

이 코드는 setter를 거치기 때문에 다음과 같은 오류가 발생합니다.

ValueError: 나이는 0보다 작을 수 없습니다.

또한 숫자가 아닌 값을 넣는 것도 막을 수 있습니다.

person.age = "스무 살"

이 경우에는 다음과 같은 오류가 발생합니다.

TypeError: 나이는 정수여야 합니다.

즉, setter를 사용하면 데이터를 저장하기 전에 조건을 검사할 수 있어서 잘못된 값이 들어가는 것을 방지할 수 있습니다.


_age처럼 밑줄을 붙일까?

self._age

처럼 밑줄을 붙이는 이유는 이 값이 클래스 내부에서 관리하는 값이라는 뜻을 나타내기 위해서입니다.

파이썬에서는 밑줄이 붙은 변수에 접근하는 것을 완전히 막지는 않지만, 보통 외부에서 직접 건드리지 말라는 의미로 사용합니다.

예를 들어 아래처럼 직접 바꾸면 검사를 건너뛸 수 있습니다.

person._age = -100

이렇게 하면 setter를 통하지 않기 때문에 잘못된 값이 들어갈 수 있습니다.

그래서 실제로는 person._age를 직접 바꾸지 않고, person.age = ...처럼 property와 setter를 통해 값을 바꾸는 것이 좋습니다.


쉽게 이해하는 비유

setter는 문 앞의 경비원과 비슷합니다.

  • person.age로 값을 읽을 때는 getter가 값을 보여 줍니다.
  • person.age = 21로 값을 바꿀 때는 setter가 먼저 확인합니다.
  • 값이 올바르면 통과
  • 값이 잘못되면 거절

즉, setter는 데이터를 저장하기 전에 한 번 더 검사해 주는 안전장치입니다.


정리

@property@setter를 함께 사용하면 다음과 같은 장점이 있습니다.

  • 값을 읽을 때는 속성처럼 간단하게 사용할 수 있다
  • 값을 바꿀 때는 중간에서 검사를 할 수 있다
  • 잘못된 데이터가 들어가는 것을 막을 수 있다
  • 코드가 더 깔끔하고 사용하기 쉬워진다

즉, @property는 읽기 편하게 만들어 주고, @setter는 값을 안전하게 바꿀 수 있게 해 줍니다.


9. 사용자 정의 데코레이터 실전 예제

함수 실행 시간을 측정하는 데코레이터를 만들어 보겠습니다.

import time

def timer(func):

    def wrapper(*args, **kwargs):

        start = time.time()

        result = func(*args, **kwargs)

        end = time.time()

        print(f"실행 시간: {end - start:.3f}초")

        return result

    return wrapper

사용

@timer
def work():

    time.sleep(1)

work()

결과

실행 시간: 1.001초

10. 여러 개의 데코레이터 적용

@decorator1
@decorator2
def hello():
    pass

실제 실행 순서

hello = decorator1(
            decorator2(hello)
        )

즉 아래에서 위 순서로 적용됩니다.


11. 세 가지 메서드 비교

class Example:

    class_var = 100

    def instance_method(self):
        print(self.class_var)

    @classmethod
    def class_method(cls):
        print(cls.class_var)

    @staticmethod
    def static_method():
        print("Hello")
구분selfcls객체 생성
인스턴스 메서드OXO
클래스 메서드XOX
정적 메서드XXX

12. 데코레이터 정리

데코레이터역할
@staticmethod정적 메서드 생성
@classmethod클래스 메서드 생성
@property메서드를 속성처럼 사용
@xxx.setter프로퍼티 값 설정
사용자 정의 데코레이터기능 확장

핵심 요약

@staticmethod

  • 객체 정보 사용 안 함
  • 클래스 정보 사용 안 함
  • 유틸리티 함수 구현 시 사용

@classmethod

  • cls 사용
  • 클래스 변수 접근 가능
  • 대체 생성자 구현 시 사용

@property

  • 메서드를 속성처럼 사용
  • Getter / Setter 구현 가능
  • 데이터 검증에 유용

사용자 정의 데코레이터

  • 로그 출력
  • 실행 시간 측정
  • 권한 검사
  • 예외 처리
  • 캐싱

데코레이터는 단순히 함수 위에 붙는 문법이 아니라, 파이썬의 함수 객체 개념을 활용한 강력한 기능입니다. 특히 @staticmethod, @classmethod, @property를 이해하면 클래스 설계를 더욱 깔끔하게 할 수 있으며, 나아가 Flask의 @app.route, PyTorch의 @torch.no_grad, Dataclass의 @dataclass 같은 고급 기능도 훨씬 쉽게 이해할 수 있습니다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다