Ray Book
프론트엔드 디자인 패턴

Decorator 패턴, 기능을 감싸라

인증, 로깅, 캐싱 같은 횡단 관심사를 원본 함수 위에 하나씩 쌓아올리는 Decorator 패턴을 시각화합니다

design-patterndecoratorhocmiddleware

문제: 횡단 관심사의 반복

API 호출 함수를 만들었습니다.

async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

잘 동작합니다. 그런데 요구사항이 하나씩 추가됩니다.

  1. 인증 , 토큰이 없으면 요청을 보내지 마세요
  2. 로깅 , 호출 시작과 완료를 기록하세요
  3. 캐싱 , 같은 요청은 API를 다시 호출하지 마세요

직관적인 해결책은 fetchUser 안에 전부 넣는 것입니다.

async function fetchUser(id) {
  // 인증
  if (!getToken()) throw new Error('Unauthorized');
  // 로깅
  console.log(`호출: fetchUser(${id})`);
  // 캐싱
  if (cache.has(id)) return cache.get(id);

  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();

  cache.set(id, data);
  console.log(`완료: fetchUser(${id})`);
  return data;
}

문제가 보이시나요?

  1. 책임 과다 , API 호출 함수가 인증, 로깅, 캐싱까지 담당합니다. 단일 책임 원칙 위반입니다.
  2. 재사용 불가 , fetchProduct, fetchOrder에도 같은 인증/로깅/캐싱이 필요한데, 매번 복사해야 합니다.
  3. 조합 불가 , "로깅은 필요하지만 캐싱은 필요 없는" 버전을 만들려면 또 다른 함수를 작성해야 합니다.

Decorator 패턴은 이 문제를 해결합니다, 각 관심사를 독립적인 래퍼 로 만들어 원본 위에 쌓아올립니다.

Decorator 패턴

GoF의 정의를 한 줄로 요약하면 이렇습니다.

"객체에 동적으로 새로운 책임을 추가한다. 서브클래싱 대신 기능 확장의 유연한 대안을 제공한다."

핵심 아이디어는 간단합니다.

  • 원본을 수정하지 않는다 , 기존 함수/객체의 코드는 한 줄도 건드리지 않습니다.
  • 감싸서 확장한다 , 원본과 같은 인터페이스를 가진 래퍼를 만들어 기능을 추가합니다.
  • 겹겹이 쌓을 수 있다 , 래퍼 위에 래퍼를 쌓아 여러 기능을 조합합니다.

아래 시각화에서 데코레이터가 하나씩 쌓이는 과정을 확인하세요.

원본 함수1 / 6
fetchUser(id)fetch(`/api/users/${id}`)
원본 함수인증 데코레이터로깅 데코레이터캐시 데코레이터현재 실행 중
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}
원본 함수 fetchUser(id)가 있습니다. API를 호출해서 사용자 정보를 반환합니다. 이 함수에 인증, 로깅, 캐싱 기능을 추가해야 합니다.

Before/After

Before, 모든 관심사가 하나의 함수에:

async function fetchUser(id) {
  if (!getToken()) throw new Error('Unauthorized');
  console.log(`호출: fetchUser(${id})`);
  if (cache.has(id)) return cache.get(id);
  const res = await fetch(`/api/users/${id}`);
  const data = await res.json();
  cache.set(id, data);
  console.log(`완료: fetchUser(${id})`);
  return data;
}

After, 관심사별 데코레이터:

// 원본, API 호출만 담당
async function fetchUser(id) {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// 데코레이터들, 각각 하나의 관심사만 담당
function withAuth(fn) {
  return async (...args) => {
    if (!getToken()) throw new Error('Unauthorized');
    return fn(...args);
  };
}

function withLogging(fn) {
  return async (...args) => {
    console.log(`호출: ${fn.name}`, args);
    const result = await fn(...args);
    console.log(`완료: ${fn.name}`);
    return result;
  };
}

function withCache(fn) {
  const cache = new Map();
  return async (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = await fn(...args);
    cache.set(key, result);
    return result;
  };
}

// 조합, 필요한 데코레이터만 선택해서 쌓기
const enhancedFetch = withCache(withLogging(withAuth(fetchUser)));
const user = await enhancedFetch(42);

각 데코레이터는:

  • 원본과 같은 시그니처 를 가집니다 (함수를 받아 함수를 반환)
  • 하나의 관심사만 처리합니다
  • 독립적으로 테스트 가능합니다
  • 자유롭게 조합 가능합니다
// 캐싱 없이 로깅만
const loggedFetch = withLogging(withAuth(fetchUser));

// 인증 없이 캐싱만 (공개 API용)
const cachedFetch = withCache(fetchUser);

// 다른 함수에도 재사용
const enhancedFetchProduct = withCache(withLogging(withAuth(fetchProduct)));

Express 미들웨어, 실전 Decorator

Express/Koa의 미들웨어 체인은 Decorator 패턴의 대표적 실전 사례입니다. 요청이 미들웨어를 하나씩 통과하며, 각 미들웨어가 기능을 추가합니다.

const express = require('express');
const app = express();

// 데코레이터 1: 요청 로깅
app.use((req, res, next) => {
  console.log(`${req.method} ${req.url}`);
  next(); // 다음 레이어로
});

// 데코레이터 2: 인증 확인
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  next();
});

// 데코레이터 3: JSON 파싱
app.use(express.json());

// 원본 핸들러
app.get('/api/users/:id', (req, res) => {
  res.json(getUser(req.params.id));
});

요청 흐름: 로깅 → 인증 → JSON 파싱 → 핸들러. 각 미들웨어는 next()를 호출해 다음 레이어로 넘깁니다. 앞서 만든 withAuth, withLogging과 구조적으로 동일합니다.

React의 Higher-Order Component (HOC)

React에서 Decorator 패턴은 HOC (Higher-Order Component) 로 구현됩니다. HOC는 컴포넌트를 받아서 기능이 추가된 새 컴포넌트를 반환합니다.

// HOC: 인증 데코레이터
function withAuth(WrappedComponent) {
  return function AuthenticatedComponent(props) {
    const { user } = useAuth();
    if (!user) return <LoginRedirect />;
    return <WrappedComponent {...props} user={user} />;
  };
}

// HOC: 로딩 상태 데코레이터
function withLoading(WrappedComponent) {
  return function LoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <Spinner />;
    return <WrappedComponent {...props} />;
  };
}

// 데코레이터 합성
const EnhancedDashboard = withAuth(withLoading(Dashboard));

Dashboard 컴포넌트는 자신이 감싸져 있는지 모릅니다 . 인증 로직과 로딩 UI를 알 필요 없이, 자신의 역할 (대시보드 렌더링) 에만 집중합니다.

참고 : React 16.8 이후 훅(Hooks)이 도입되면서 HOC보다 커스텀 훅이 선호되고 있습니다. 하지만 HOC는 여전히 라우트 보호, 레이아웃 래핑 등에서 사용됩니다.

TC39 데코레이터 (Stage 3)

JavaScript에도 데코레이터 문법이 도입되고 있습니다. TC39 데코레이터 제안은 현재 Stage 3 이며, TypeScript 5.0부터 네이티브로 지원합니다.

// TC39 데코레이터, @문법으로 메서드를 감싸기
function logged(originalMethod, context) {
  return function (...args) {
    console.log(`호출: ${context.name}`);
    const result = originalMethod.call(this, ...args);
    console.log(`완료: ${context.name}`);
    return result;
  };
}

class UserService {
  @logged
  async getUser(id) {
    const res = await fetch(`/api/users/${id}`);
    return res.json();
  }
}

@loggedgetUser 메서드를 감싸서 로깅 기능을 추가합니다. 원본 메서드의 코드는 변경되지 않습니다.

주의 : TypeScript의 experimentalDecorators 옵션은 TC39 이전의 레거시 데코레이터입니다. TC39 Stage 3 데코레이터와는 API가 다르므로 혼동하지 마세요. 새 프로젝트에서는 TC39 데코레이터를 사용하세요.

Decorator vs 상속

Decorator 패턴이 해결하는 핵심 문제는 서브클래싱의 경직성 입니다.

상속Decorator
확장 시점컴파일 타임 (정적)런타임 (동적)
조합클래스마다 별도 정의 필요자유롭게 조합
예시AuthFetchUser, CachedFetchUser, AuthCachedFetchUser...withAuth(withCache(fetchUser))
클래스 수조합 수만큼 폭발데코레이터 수만큼만

인증, 로깅, 캐싱 3개를 상속으로 조합하면 경우의 수가 2³ = 8개의 서브클래스가 필요합니다. Decorator는 3개의 래퍼만으로 모든 조합을 만들 수 있습니다.

언제 Decorator를 쓸까?

쓰세요:

  • 횡단 관심사 가 여러 함수/컴포넌트에 반복될 때 (인증, 로깅, 캐싱, 에러 핸들링)
  • 기능 조합이 동적으로 바뀌어야 할 때
  • 원본 코드를 수정하지 않고 기능을 추가해야 할 때

쓰지 마세요:

  • 데코레이터가 1-2개 만 필요할 때, 직접 구현이 더 명확합니다
  • 데코레이터 체인이 너무 깊어질 때, 디버깅 시 스택 트레이스를 추적하기 어려워집니다
  • 원본의 인터페이스를 바꿔야 할 때, Decorator는 인터페이스를 유지해야 합니다

다음 글에서는 Proxy 패턴 을 다룹니다. 객체 접근을 가로채서 유효성 검증, 캐싱, 지연 로딩을 구현하는 방법, "접근을 가로채는 기술"을 살펴보겠습니다.