
데코레이터 패턴: 기능을 덧입히는 포장지
기존 클래스를 수정하지 않고 새 기능을 추가합니다. 커피에 휘핑크림, 시럽, 샷 추가처럼 기능을 동적으로 조합. Python @decorator의 원리.

기존 클래스를 수정하지 않고 새 기능을 추가합니다. 커피에 휘핑크림, 시럽, 샷 추가처럼 기능을 동적으로 조합. Python @decorator의 원리.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

Django로 웹 개발하다가 이런 코드를 봤습니다:
@login_required
def view_profile(request):
return render(request, 'profile.html')
"@login_required가 뭐지? 저 골뱅이 표시는 대체 뭐하는 거야?" 선배에게 물었더니 "데코레이터야. 함수를 감싸서 기능을 추가하는 거지"라고 했습니다.
그런데 React 코드에서도 비슷한 걸 봤습니다:
export default withAuth(ProfileComponent);
그리고 Express로 API 만들 때도:
app.use(express.json());
app.use(authMiddleware);
"이게 다 연관이 있는 건가?" 궁금해졌습니다.
여러 가지가 헷갈렸습니다:
함수를 "감싼다"는 게 정확히 무슨 뜻인가? - 선물 포장처럼 뭔가 겉에 씌우는 건가? 그럼 원본 함수는 어떻게 되는 거지?
왜 굳이 데코레이터를 쓰나? - 그냥 상속으로 클래스를 확장하면 안 되나? 상속이 더 직관적인데?
Python의 @와 디자인 패턴의 데코레이터가 같은 건가? - 이름은 같은데 개념적으로 어떤 관계인 거지?
Express 미들웨어도 데코레이터인가? - 체이닝처럼 보이는데 이것도 데코레이터 패턴인가?
래퍼(Wrapper)와 데코레이터의 차이는 뭔가? - 둘 다 "감싸기"인 것 같은데 정확히 뭐가 다른 거지?
스타벅스에서 커피를 주문하는 상황을 떠올렸을 때 모든 게 명확해졌습니다. 메뉴판을 보니:
아메리카노: 4,500원
- 휘핑크림 추가: +500원
- 에스프레소 샷 추가: +600원
- 바닐라 시럽 추가: +500원
- 두유로 변경: +600원
만약 상속으로 이걸 구현한다면:
- Americano
- AmericanoWithWhippedCream
- AmericanoWithShot
- AmericanoWithVanilla
- AmericanoWithWhippedCreamAndShot
- AmericanoWithWhippedCreamAndVanilla
- AmericanoWithShotAndVanilla
- AmericanoWithWhippedCreamAndShotAndVanilla
- AmericanoWithWhippedCreamAndShotAndVanillaAndSoyMilk
- ...
4가지 옵션만으로도 2^4 = 16가지 조합이 나옵니다. 각각을 클래스로 만들어야 한다면? 지옥이죠. 게다가 "헤이즐넛 시럽"이라는 새 옵션이 추가되면? 모든 조합을 다시 만들어야 합니다.
데코레이터로 하면:
let coffee = new Americano(); // 기본: 4,500원
coffee = new WhippedCream(coffee); // 포장: +500원
coffee = new Shot(coffee); // 한 번 더 포장: +600원
coffee = new Vanilla(coffee); // 또 포장: +500원
// 총 6,100원
4개 데코레이터 클래스로 모든 조합을 만들 수 있습니다. 새 옵션 추가? 데코레이터 하나만 추가하면 됩니다.
이때 깨달았습니다: "상속은 컴파일 타임에 정적으로 결정되지만, 데코레이터는 런타임에 동적으로 조합할 수 있구나!"
데코레이터 패턴은 객체에 동적으로 새로운 책임(기능)을 추가하는 패턴입니다. 핵심은 "원본 객체를 수정하지 않고, 래퍼로 감싸서 기능을 확장한다"는 점입니다.
마치 선물을 포장하는 것처럼:
나는 이렇게 이해했습니다: "데코레이터는 원본 객체와 동일한 인터페이스를 유지하면서, 내부적으로 원본 객체를 참조해서 기능을 추가하는 래퍼다."
이 부분이 처음엔 이해가 안 갔습니다. 상속도 기능을 추가하는 거 아닌가? 근데 직접 비교해보니 차이가 확 와닿았습니다.
상속은 컴파일 타임에 정적으로 결정됩니다:
class Coffee {
cost() { return 4500; }
}
class CoffeeWithWhippedCream extends Coffee {
cost() { return 5000; }
}
class CoffeeWithShot extends Coffee {
cost() { return 5100; }
}
// 둘 다 원하면?
class CoffeeWithWhippedCreamAndShot extends Coffee {
cost() { return 5600; }
}
문제가 뭔가 하면:
데코레이터는 런타임에 동적으로 조합합니다:
// Component (기본 인터페이스)
class Coffee {
cost() {
return 4500;
}
description() {
return "아메리카노";
}
}
// Decorator 베이스
class CoffeeDecorator {
constructor(coffee) {
this.coffee = coffee; // 원본을 내부에 보관
}
cost() {
return this.coffee.cost(); // 원본에 위임
}
description() {
return this.coffee.description();
}
}
// Concrete Decorators
class WhippedCream extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 500; // 원본 + 추가 비용
}
description() {
return this.coffee.description() + ", 휘핑크림";
}
}
class Shot extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 600;
}
description() {
return this.coffee.description() + ", 샷 추가";
}
}
class Vanilla extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 500;
}
description() {
return this.coffee.description() + ", 바닐라 시럽";
}
}
// 사용 예시
let myCoffee = new Coffee();
console.log(myCoffee.cost()); // 4500
console.log(myCoffee.description()); // "아메리카노"
// 런타임에 동적으로 조합
myCoffee = new WhippedCream(myCoffee);
console.log(myCoffee.cost()); // 5000
myCoffee = new Shot(myCoffee);
console.log(myCoffee.cost()); // 5600
myCoffee = new Vanilla(myCoffee);
console.log(myCoffee.cost()); // 6100
console.log(myCoffee.description());
// "아메리카노, 휘핑크림, 샷 추가, 바닐라 시럽"
이렇게 하면:
나는 이 예시를 보고 "결국 이거였다. 상속은 '이다(is-a)' 관계고 정적이지만, 데코레이터는 '가진다(has-a)' 관계고 동적이다"라고 받아들였습니다.
데코레이터 패턴을 공부하면서 이 원칙이 확 와닿았습니다. GoF 디자인 패턴 책에서 강조하는 핵심 원칙 중 하나인데, "클래스 상속보다 객체 조합을 선호하라"는 뜻입니다.
상속의 한계:
조합의 장점:
나는 이렇게 이해했습니다: "상속은 강력하지만 유연하지 않고, 조합은 초기 설정은 복잡하지만 나중에 훨씬 유연하다." 데코레이터 패턴은 조합의 대표적인 예시입니다.
SOLID 원칙 중 하나인 개방-폐쇄 원칙(Open-Closed Principle)이 데코레이터 패턴에서 정말 명확하게 드러납니다.
"소프트웨어 엔티티(클래스, 모듈, 함수)는 확장에는 열려있고 수정에는 닫혀있어야 한다."
데코레이터 패턴을 쓰면:
예를 들어 "시나몬 파우더" 옵션을 추가한다면:
// 기존 코드는 전혀 건드리지 않음
class CinnamonPowder extends CoffeeDecorator {
cost() {
return this.coffee.cost() + 300;
}
description() {
return this.coffee.description() + ", 시나몬 파우더";
}
}
// 바로 사용 가능
let coffee = new Coffee();
coffee = new CinnamonPowder(coffee);
나는 이걸 보고 "OCP가 추상적으로만 느껴졌는데, 데코레이터 패턴에서는 구체적으로 와닿는다"라고 생각했습니다.
Python의 @ 문법은 처음 봤을 때 정말 신기했습니다. 디자인 패턴의 데코레이터와 개념은 같은데, 문법이 훨씬 간결합니다.
import time
def timer(func):
"""함수 실행 시간을 측정하는 데코레이터"""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs) # 원본 함수 실행
end = time.time()
print(f"{func.__name__} 실행 시간: {end - start:.2f}초")
return result
return wrapper
@timer # slow_function = timer(slow_function)
def slow_function():
time.sleep(2)
return "완료"
result = slow_function()
# 출력 - slow_function 실행 시간: 2.00초
@timer는 syntactic sugar입니다. 실제로는 slow_function = timer(slow_function)과 같은 의미죠.
나는 이렇게 이해했습니다: "@ 문법은 함수를 인자로 받아서 새 함수를 반환하는 higher-order function이다."
def log(func):
"""함수 호출을 로깅하는 데코레이터"""
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} 호출됨")
print(f" 인자: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f" 반환값: {result}")
return result
return wrapper
@log
def add(a, b):
return a + b
@log
def multiply(x, y):
return x * y
add(3, 5)
# [LOG] add 호출됨
# 인자: args=(3, 5), kwargs={}
# 반환값: 8
multiply(4, 7)
# [LOG] multiply 호출됨
# 인자: args=(4, 7), kwargs={}
# 반환값: 28
def login_required(func):
"""사용자 인증을 확인하는 데코레이터"""
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('/login')
return func(request, *args, **kwargs)
return wrapper
@login_required
def view_profile(request):
return render(request, 'profile.html')
@login_required
def edit_profile(request):
if request.method == 'POST':
# 프로필 수정 로직
pass
return render(request, 'edit_profile.html')
이제 view_profile과 edit_profile 함수는 자동으로 인증 체크를 합니다. 인증 안 된 사용자는 로그인 페이지로 리다이렉트되죠.
나는 이걸 보고 "횡단 관심사(cross-cutting concerns)를 분리하는 완벽한 방법이다"라고 생각했습니다. 로깅, 인증, 캐싱, 성능 측정 같은 기능을 핵심 비즈니스 로직과 분리할 수 있습니다.
@login_required
@timer
@log
def complex_operation(request):
# 복잡한 작업
time.sleep(1)
return "작업 완료"
# 실행 순서: login_required(timer(log(complex_operation)))
# 안쪽부터 바깥쪽으로 감싸짐
순서가 중요합니다:
log가 먼저 감쌈timer가 log를 감쌈login_required가 timer를 감쌈실행은 반대 순서: login_required → timer → log → 원본 함수
TypeScript(실험적 기능)에서도 데코레이터를 지원합니다. 클래스, 메서드, 프로퍼티, 파라미터에 적용 가능합니다.
// 메서드 데코레이터
function readonly(target: any, key: string, descriptor: PropertyDescriptor) {
descriptor.writable = false;
return descriptor;
}
function log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`${key} 호출됨, 인자:`, args);
const result = originalMethod.apply(this, args);
console.log(`${key} 반환값:`, result);
return result;
};
return descriptor;
}
class Calculator {
@readonly
version = "1.0.0";
@log
add(a: number, b: number): number {
return a + b;
}
@log
multiply(a: number, b: number): number {
return a * b;
}
}
const calc = new Calculator();
calc.version = "2.0.0"; // 에러: Cannot assign to read only property
calc.add(3, 5);
// add 호출됨, 인자: [3, 5]
// add 반환값: 8
TypeScript 데코레이터를 보면서 "메타프로그래밍의 세계가 열린다"는 느낌을 받았습니다. 코드의 구조 자체를 코드로 제어할 수 있다니.
Node.js Express를 쓰다 보면 미들웨어를 엄청 많이 씁니다. 근데 이것도 데코레이터 패턴이었습니다.
const express = require('express');
const app = express();
// 미들웨어 = 데코레이터
app.use(express.json()); // JSON 파싱 기능 추가
app.use((req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next(); // 다음 미들웨어로 넘김
});
// 인증 미들웨어
app.use((req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
// 토큰 검증 로직
req.user = verifyToken(token);
next();
});
// CORS 미들웨어
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// 실제 라우트 핸들러
app.get('/api/users', (req, res) => {
// 여기 도착하기 전에 위의 모든 미들웨어를 거침
res.json({ users: [] });
});
요청이 들어오면:
express.json() 미들웨어가 body를 파싱각 미들웨어가 요청 객체를 "감싸서" 기능을 추가하는 데코레이터입니다.
나는 이걸 이해하고 "Express 미들웨어는 데코레이터 패턴의 체인(chain of responsibility)이구나"라고 받아들였습니다.
React에서도 데코레이터 패턴을 자주 씁니다. Higher-Order Component(HOC)가 바로 그겁니다.
import React from 'react';
import { Redirect } from 'react-router-dom';
// HOC (데코레이터)
function withAuth(Component) {
return function AuthenticatedComponent(props) {
const user = useAuth(); // 커스텀 훅으로 인증 상태 확인
if (!user) {
return <Redirect to="/login" />;
}
return <Component {...props} user={user} />;
};
}
// 원본 컴포넌트
const Profile = ({ user }) => {
return (
<div>
<h1>프로필</h1>
<p>안녕하세요, {user.name}님</p>
</div>
);
};
// 데코레이터로 감싸서 export
export default withAuth(Profile);
withAuth는 컴포넌트를 받아서 인증 기능이 추가된 새 컴포넌트를 반환합니다. 원본 Profile 컴포넌트는 전혀 수정하지 않았습니다.
여러 HOC를 조합할 수도 있습니다:
function withLoading(Component) {
return function LoadingComponent({ isLoading, ...props }) {
if (isLoading) {
return <div>로딩 중...</div>;
}
return <Component {...props} />;
};
}
function withErrorBoundary(Component) {
return class ErrorBoundaryComponent extends React.Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <div>에러가 발생했습니다</div>;
}
return <Component {...this.props} />;
}
};
}
// 여러 데코레이터 조합
const EnhancedProfile = withAuth(withLoading(withErrorBoundary(Profile)));
// 또는 함수 조합 라이브러리 사용
import { compose } from 'redux';
const EnhancedProfile = compose(
withAuth,
withLoading,
withErrorBoundary
)(Profile);
나는 이걸 보고 "HOC는 컴포넌트 재사용의 핵심 패턴이다"라고 이해했습니다. 요즘은 Hooks로 많이 대체되긴 했지만, 개념은 여전히 중요합니다.
데코레이터 패턴의 활용 예시를 더 들어봅니다.
def memoize(func):
"""함수 결과를 캐싱하는 데코레이터"""
cache = {}
def wrapper(*args):
if args in cache:
print(f"캐시에서 반환: {args}")
return cache[args]
print(f"계산 중: {args}")
result = func(*args)
cache[args] = result
return result
return wrapper
@memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 처음 계산
print(fibonacci(10)) # 캐시에서 반환
import time
from functools import wraps
def rate_limit(max_calls, period):
"""API 호출 횟수를 제한하는 데코레이터"""
calls = []
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
now = time.time()
# period 이전 호출 기록 제거
calls[:] = [call for call in calls if call > now - period]
if len(calls) >= max_calls:
raise Exception(f"Rate limit exceeded: {max_calls} calls per {period} seconds")
calls.append(now)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, period=10) # 10초에 3번까지만 호출 가능
def call_api():
print("API 호출!")
return "응답 데이터"
# 3번까지는 성공
call_api() # OK
call_api() # OK
call_api() # OK
call_api() # Exception: Rate limit exceeded
나는 이런 실제 예시들을 보면서 "데코레이터는 횡단 관심사를 다루는 최고의 도구다"라고 받아들였습니다.
데코레이터가 만능은 아닙니다. 과도하게 쓰면 오히려 복잡도만 증가합니다.
# 나쁜 예 - 너무 많은 데코레이터
@login_required
@admin_required
@rate_limit(100, 60)
@cache(timeout=300)
@log
@timer
@retry(max_attempts=3)
@validate_input
@sanitize_output
@deprecated
def simple_function():
return "Hello"
# 실행 흐름 추적이 거의 불가능
이렇게 데코레이터를 10개 중첩하면:
나는 이렇게 정리했습니다: "데코레이터는 강력하지만 남용하면 독이 된다. 횡단 관심사에만 쓰고, 핵심 로직은 명시적으로 작성하자."
Java의 Stream API를 쓰면서 "이거 데코레이터 패턴 아닌가?"라고 생각했는데, 맞았습니다.
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");
List<String> result = names.stream() // Stream 생성
.filter(name -> name.length() > 3) // 데코레이터: 필터링 기능 추가
.map(String::toUpperCase) // 데코레이터: 대문자 변환 기능 추가
.sorted() // 데코레이터: 정렬 기능 추가
.collect(Collectors.toList()); // 최종 연산
// 결과: [ALICE, CHARLIE, DAVID]
각 메서드(filter, map, sorted)가 Stream을 감싸서 새로운 Stream을 반환합니다. 원본 Stream은 수정되지 않고(불변), 각 단계마다 기능이 추가된 새 Stream이 생성됩니다.
나는 이걸 보고 "함수형 프로그래밍의 파이프라인이 데코레이터 패턴이구나"라고 이해했습니다.
# 순서가 중요한 예시
@decorator_a
@decorator_b
def func():
pass
# 실행 순서: decorator_a(decorator_b(func))
# b가 먼저 감싸고, a가 그걸 또 감쌈
# 실행은 a → b → func 순서
나는 이걸 정리하면서 "데코레이터는 양날의 검이다. 적절히 쓰면 코드가 우아해지지만, 과하면 복잡도 폭발"이라고 받아들였습니다.
데코레이터 패턴을 공부하면서 깨달은 핵심:
@, Express 미들웨어, React HOC, Java Stream 등나는 데코레이터 패턴을 이렇게 한 문장으로 정리했습니다:
"상속 대신 조립으로, 컴파일 타임 대신 런타임으로, 수정 대신 확장으로 유연성을 확보하는 패턴"결국 이거였다. Python의 @login_required, Express의 app.use(), React의 withAuth() 모두 같은 개념이었습니다. 기존 객체/함수를 감싸서 기능을 추가하는 데코레이터 패턴.