본문 바로가기
👩🏻‍💻Computer Science/clean-code

Chapter5. 클래스 데코레이터

by devhertz 2026. 8. 30.

클래스 데코레이터

  • 파이썬에서는 숫자, 문자열, 함수, 클래스, 모듈, None 등 거의 모든 것이 객체
    • 객체가 아닌 것) if, for, def, import, +, =, 변수 식별자 이름… 등
  • 따라서 클래스도 객체이기 때문에 함수에 할 수 있는 일을 클래스에도 똑같이 가능
    • 파라미터로 전달 가능
    • 변수에 할당 가능
    • 메서드를 가질 수 있음
    • 데코레이터로 꾸밀 수 있음
  • 클래스 데코레이터: PEP-3129에서 도입, 함수 데코레이터와 거의 같음. 유일한 차이점은 래퍼(wrapper)가 함수가 아니라 클래스라는 것.
  • 장점)
    • 코드 재사용과 DRY 원칙을 지키기 좋다.
    • 여러 클래스가 특정 인터페이스나 규칙을 따르도록 강제할 수 있다.
    • 여러 클래스에 적용할 검사/기능을 데코레이터 한 곳에만 작성하면 된다.
    • 일단 작고 간단한 클래스를 만든 뒤, 나중에 데코레이터로 기능을 쉽게 추가할 수 있다.
    • 유지보수 시 기존 로직을 데코레이터로 쉽게 변경할 수 있다.
    • 메타클래스보다 훨씬 단순하고 권장되는 방식이다.
  • 단점 / 주의점)
    • 잘못 사용하거나 남용하면 코드가 복잡해지고 가독성이 떨어질 수 있다. (클래스 안에 정의한 속성과 메서드를 데코레이터 안에서 완전히 다른 용도로 바꿔버릴 수 있기 때문)

예제) 모니터링 플랫폼에서 다양한 이벤트(로그인, 로그아웃 등)를 외부 시스템으로 보낼 때 이벤트 직렬화

  • 로그인 이벤트는 데이터를 다르게 변환해서 이벤트마다 변환해야 함.
    • password 같은 민감 정보는 숨겨야 함
    • timestamp는 특별한 포맷으로 바꿔야 함
참고) 직렬화(Serialization)란?
  • 복잡한 객체(메모리에 있는 파이썬 객체)를 전송하거나 저장하기 쉬운 형태(주로 문자열이나 바이트, 딕셔너리 등)로 변환하는 과정
    • 반대로 다시 원래 객체로 되돌리는 것은 역직렬화(Deserialization)
  • (책 예제에서) LoginEvent 같은 파이썬 객체를 → {"username": "...", "password": "**민감한 정보 삭제**", ...} 같은 딕셔너리로 바꾸는 것.
    • LoginEvent는 파이썬 클래스의 인스턴스.
    • 외부 시스템(다른 서버, 로그 수집기, 메시지 큐, 데이터베이스 등)은 파이썬 객체를 직접 이해할 수 없음.
    • 그래서 전송 가능한 형태(보통 JSON으로 바꿀 수 있는 딕셔너리)로 변환해야 함
  • 사용되는 대표적인 예시)
    • API 응답을 JSON으로 보낼 때
    • 로그를 파일이나 원격 서버에 저장할 때
    • 메시지 큐(Kafka, RabbitMQ 등)에 메시지를 넣을 때
    • 데이터베이스에 객체를 저장할 때
    • 캐시(Redis 등)에 객체를 넣을 때
    • 마이크로서비스 간 통신할 때

안좋은 방법) 각 이벤트마다 전용 Serializer 클래스를 만드는 방식(1:1 직렬화 클래스 매핑)

class LoginEventSerializer:
    def __init__(self, event):
        self.event = event

    def serialize(self) -> dict:
        return {
            "username": self.event.username,
            "password": "**민감한 정보 삭제**",
            "ip": self.event.ip,
            "timestamp": self.event.timestamp.strftime("%Y-%m-%d %H:%M"),
        }

@dataclass
class LoginEvent:
    SERIALIZER = LoginEventSerializer

    username: str
    password: str
    ip: str
    timestamp: datetime

    def serialize(self) -> dict:
        return self.SERIALIZER(self).serialize()
  • LoginEvent 객체를 직렬화하기 위해 별도의 LoginEventSerializer 클래스를 만들어 1:1로 매핑함
  • 문제점)
    • 클래스가 너무 많아진다. 이벤트 종류가 늘어날 때마다 Serializer 클래스도 하나씩 더 만들어야 함
    • 유연하지 않다. 다른 클래스에서도 password를 숨기고 싶으면 코드를 복사하거나 함수로 분리해서 여러 곳에서 호출해야 함
    • 표준화가 어렵다. 모든 이벤트 클래스에 serialize() 메서드가 있어야 하고, 믹스인을 써도 상속을 제대로 활용했다고 보기 어려움

좋은 방법) 필드마다 어떻게 변환할지를 데코레이터에 인자로 넘겨서, 클래스에 serialize() 메서드를 자동으로 추가하는 방식

from datetime import datetime
from dataclasses import dataclass

def hide_field(field) -> str:
    return "**민감한 정보 삭제**"

def format_time(field_timestamp: datetime) -> str:
    return field_timestamp.strftime("%Y-%m-%d %H:%M")

def show_original(event_field):
    return event_field

class EventSerializer:
    def __init__(self, serialization_fields: dict) -> None:
        self.serialization_fields = serialization_fields

    def serialize(self, event) -> dict:
        return {
            field: transformation(getattr(event, field))
            for field, transformation in self.serialization_fields.items()
        }

class Serialization:
    def __init__(self, **transformations):
        self.serializer = EventSerializer(transformations)

    def __call__(self, event_class):
        def serialize_method(event_instance):
            return self.serializer.serialize(event_instance)
        
        event_class.serialize = serialize_method
        return event_class

@Serialization(
    username=show_original,
    password=hide_field,
    ip=show_original,
    timestamp=format_time,
)
@dataclass
class LoginEvent:
    username: str
    password: str
    ip: str
    timestamp: datetime
  • Serialization이라는 클래스 데코레이터를 구현하여 __init__으로 변환 규칙을 넘겨받고, __call__을 통해 원본 이벤트 클래스에 serialize 메서드를 직접 주입.
  • 결과적으로 다른 코드를 분석할 필요 없이, 클래스 상단에 적힌 데코레이터의 전달 인수만 읽어도 각 필드의 처리 방식을 직관적으로 파악할 수 있어 가독성이 높아짐.

동작순서)

  1. @Serialization(...)LoginEvent 클래스를 받는다.
  2. 전달된 변환 함수들(hide_field, format_time 등)을 이용해 EventSerializer를 만든다.
  3. LoginEvent 클래스에 serialize() 메서드를 동적으로 추가한다.
  4. 최종적으로 수정된 클래스를 반환한다.
# 결과적으로 password는 숨겨지고, timestamp는 포맷팅된 딕셔너리가 나옴
event = LoginEvent("user", "secret", "192.168.0.1", datetime.now())
print(event.serialize())

장점)

  • 다른 클래스 코드를 열어보지 않아도, 데코레이터 인자만 보면 “어떤 필드가 어떻게 처리되는지” 한눈에 알 수 있음.
  • 새로운 이벤트 클래스가 생겨도 데코레이터만 붙이면 됨.
  • 변환 로직(hide_field 등)을 여러 곳에서 재사용 가능.

다른 유형의 데코레이터

  • 데코레이터의 @ 문법은 함수, 메서드, 클래스 이외에도 제너레이터나 코루틴뿐만 아니라 이미 데코레이트가 완료된 객체 위에도 스택 형태로 계속 쌓아 올릴 수 있음.

위의 예제에서도 원본 클래스에 @dataclass를 먼저 적용해 속성 컨테이너로 만든 후, 그 위에 @Serialization 데코레이터를 스태킹하여 메서드를 동적으로 추가하는 패턴.

@Serialization(...)       # 2. serialize 메서드 추가
@dataclass                # 1. 먼저 데이터 클래스로 만들어줌
class LoginEvent:
    ...

댓글