[Javascript] Decorator

bluewhale·2022년 1월 10일
2

Javascript

목록 보기
3/3
post-thumbnail

Decorator

ES6부터 자바스크립트에 데코레이터 문법이 추가되었다. 데코레이터 함수는 그 이름처럼, 다른 객체를 꾸며주는 역할을 하는 함수로, 클래스 프로프터(혹은 메서드)를 인자로 받아 이를 수정/확장하는 함수이다. 데코레이터는 함수를 일급 객체로 정의하는 모든 언어에서 선언이 가능하며, 파이썬이나 자바에서 웹 프레임워크를 경험한 분들에게는 아마 굉장히 친숙한 문법이 아닐까 싶다. 개인적으로 자바/스프링을 공부할 때, 데코레이터의 편리함에 감동했던 경험이 있다.

@Entity
@Getter
@NoArgsConstructor
public class User {

    public User(String email, String password) {
        this.email = email;
        this.password = password;
    }

    @Id
    @GeneratedValue
    @Column(name = "user_id")
    private Long id;
    ...

데코레이터 만세...

Babel

자바스크립트의 데코레이터는 ES6에 새롭게 추가된 문법으로 아직 자바스크립트에서 정식 지원하고 있지는 않다. 따라서, 데코레이터를 사용하기 위해서는 babel을 설치하여 트랜스파일링을 거친 코드를 실행해야 한다.

$ npm i -D @babel/cli @babel/node @babel/core @babel/plugin-proposal-decorators
// .babelrc
{ "plugins": [["@babel/plugin-proposal-decorators", { "legacy": true }]]}

Target, Property, Descriptor

데코레이터 함수는 target, property, descriptor 3개의 변수를 인자로 받는다.

  • target은 데코레이터가 적용될 객체에 해당한다.
  • property는 데코레이터가 적용될 객체의 프로퍼티 이름을 의미한다.
  • descriptor는 해당 객체의 프로퍼티에 대한 세부 정보(ex, writable, value)를 담고 있다.

Example

아래 예제에서는 클래스 메서드에 데코레이터를 적용하여 콘솔로 출력한 결과이다.
target, property, descriptor는 각각 인스턴스, 메서드 명, 메서드의 속성에 대한 값을 갖고 있다.

// main.js
function myDecorator(target, key, descriptor) {
  const fn = descriptor.value || descriptor.initializer.call(this);

  descriptor.value = function (...args) {
    console.log('==================================');
    console.log('target.consturtor: ', target.constructor);
    console.log('key: ', key);
    console.log('descriptor: ', descriptor);
    console.log('==================================');

    fn.call(this, args);
  };
}

class MyClass {
  @myDecorator
  doSomething() {
    console.log("I'm doing some work!");
  }
}

const instance = new MyClass();
instance.doSomething();
# 코드 실행
$./node_modules/.bin/babel-node main.js

# 출력 결과
==================================
target.consturtor:  [class MyClass]
key:  doSomething
descriptor:  {
  value: [Function (anonymous)],
  writable: true,
  enumerable: false,
  configurable: true
}
==================================
I'm doing some work!

Usage

데코레이터를 활용하면, 클래스의 프로퍼티 혹은 메서드의 값을 바꾸는 것 뿐만 아니라, 메서드 실행 전후로 추가적인 코드를 실행하는 등, 코드를 수정하지 않고 기능을 확장할 수 있다.

예를 들어, API 엔드포인트 함수 전후로 실행되는 반복적인 기능(ex, DB 연결, 사용량 제한, 사용자 요청/응답 로깅) 등을 미들웨어로 정의하기 위해 데코레이터를 활용할 수 있다. 특히, express와 같은 미들웨어 기능을 지원하지 않는 프레임워크나, API 엔드포인트마다 서로 다른 미들웨어가 적용되어야 할 때 개인적으로 유용하게 사용하였다.

현재 Next.js를 사용하여 개발하고 있는 서비스에서, 여러 이유로 Next.js 12 버전부터 지원된 미들웨어 기능을 도입하기에는 부담스럽다는 결정이 내린 적이 있었다. 그래서, 반복적으로 사용되는 로직을 처리하기 위해 아래와 같은 데코레이터들을 정의하고 미들웨어로 활용하고 있다.

// withMongoDB.ts
// req 객체에 mongoDB 클라이언트 객체를 프로퍼티로 추가하는 데코레이터
export function withMongoDB(
  _target: any,
  _propertyName: string,
  descriptor: AsyncFunctionDescriptor,
) {
  const handler = descriptor.value!;

  descriptor.value = async (req: NextApiRequest, res: NextApiResponse) => {
    req.mongo = await connectMongo();

    return await handler(req, res);
  };
}
// withMongoDBTransaction.ts

// mongoDB 연결 시, 높은 read/write Concern과 트랜잭션을 추가한 데코레이터 
// 데이터 정합성이 중요하고, 트랜잭션으로 실행되어야 하는 쿼리를 실행할 때 사용한다.
export function withMongoDBTransaction(
  _target: any,
  _propertyName: string,
  descriptor: AsyncFunctionDescriptor,
) {
  const handler = descriptor.value!;

  descriptor.value = async (req: NextApiRequest, res: NextApiResponse) => {
    const mongo = await connectMongo(true);
    req.mongo = mongo;

    const session = mongo.getSession();
    const transactionOptions: TransactionOptions = {
      readConcern: new ReadConcern('majority'),
      writeConcern: { w: 'majority', j: true },
    };

    let error;
    try {
      await session.withTransaction(async () => {
        return await handler(req, res);
      }, transactionOptions);
    } catch (err) {
      error = err;
    } finally {
      await session.endSession();

      if (error) throw error;
    }
  };
}
// withRateLimit.ts
// API 엔드포인트 함수 실행에 앞서, request에 대한 rate limit를 체크하는 데코레이터
export function withRateLimit(opt: { duration?: number; max?: number } = {}) {
  const duration = opt.duration || 60; // 1 min
  const max = opt.max || 100; // 100

  function inner(_target: any, _propertyName: string, descriptor: AsyncFunctionDescriptor) {
    const handler = descriptor.value!;

    descriptor.value = async (req: NextApiRequest, res: NextApiResponse) => {
      const rateLimit = createRateLimiter({ duration, max });
      const key = getRequestID(req); // request 정보를 파싱하여 키를 생성
      
      try {
        const result = await rateLimit.consume(key, 1);

        if (!res.headersSent) {
          res.setHeader('Retry-After', result.msBeforeNext / 1000);
          res.setHeader('X-RateLimit-Limit', rateLimit.points);
          res.setHeader('X-RateLimit-Remaining', result.remainingPoints);
          res.setHeader('X-RateLimit-Reset', new Date(Date.now() + result.msBeforeNext).getTime());
        }
      } catch {
        throw new ApiError('TOO_MANY_REQUESTS');
      }

      return await handler(req, res);
    };
  }
  return inner;
}

References

profile
안녕하세요

0개의 댓글