NestJs 에서 Decorator를 활용해 Spring의 AOP 처럼 구현해보기

태그
Typescript
NestJs
횡단 관심사
  • 관심사란, 쉽게 말해 구현하고자 하는 대상의 기능 집합으로 정의할 수 있습니다.
  • 예를 들어, 수강신청 서비스를 만든다고 생각하면, 회원에 대한 관심사, 수업에 대한 관심사, 시간표에 대한 관심사로 나눌 수 있습니다.
  • 이 때 어플리케이션 전역에서 사용할 수 있는 관심사를 횡단관심사로 부릅니다.
    • 횡단관심사의 대표적인 예로, 로깅, 인증, 트랜잭션 처리 등이 있습니다.
 
Spring의 AOP
  • 스프링에는 AOP라는 횡단관심사를 처리하기 위한 기술이 존재합니다.
  • AOP를 구현하기 위해 쓰이는 대표적인 구현체인 JDK Dynamic Proxy는 런타임에서 Reflection 을 기반으로 작동합니다.
  • AOP를 설명하기 위해 자주 언급되는 선언적 트랜잭션 방식인 @Transactional 어노테이션이 대표적입니다.
@Service class Service { @Transactional public void account() { ... } }
  • 실제로 대상이 되는 코드에 트랜잭션의 시작과 끝에 해당하는 코드는 프록시로 제어됩니다.
  • Spring에서는 이외에도 JPA의 인터페이스 기반 쿼리 생성이나, Caching의 관심사 분리에도 동일한 AOP 방식으로 처리가 됩니다.
    • 그래서 종종 Spring의 핵심이자 근간이 되는 기술로 언급됩니다.
 
NestJs에서
  • Typescript 웹프레임워크 대표주자인 NestJs 에서 비슷하게 이러한 횡단 관심사를 구현하기 위한 방법으로 미들웨어(Middleware)와 데코레이터(decorator)가 존재합니다.
  • Spring의 JDK Dynamic Proxy가 Annotation을 이용해 Reflection 을 사용해 코드를 조작했듯이, TS/JS 에서도 Decorator와 Reflection을 이용할 수 있습니다.
 
예제로 데코레이터를 활용해서 횡단 관심사를 처리해봅니다. 예를 들어 인증 관심사를 처리하는 과정을 생각해봅시다.
User라는 Entity에 다음과 같은 사용자 정보가 담긴다고 가정합니다.
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'; @Entity({ name: 'users' }) export class User extends BaseEntity { @PrimaryGeneratedColumn('increment') id: number; @Column() username: string; @Column() password: string; @Column() email: string; }
그리고 사용자 정보를 가져오는 Controller 와 게시글을 생성하는 Controller가 있습니다.
user.controller.ts
// user.controller.ts import { Controller, Get, Param } from '@nestjs/common'; import { UserService } from './user.service'; @Controller('users') export class UserController { constructor(private readonly userService: UserService) {} @Get(':id') async getUser(@Param('id') userId: number) { const user = await this.userService.getUserById(userId); return { user }; } }
 
post.controller.ts
// post.controller.ts import { Controller, Post as HttpPost, Body } from '@nestjs/common'; import { PostService } from './post.service'; @Controller('posts') export class PostController { constructor(private readonly postService: PostService) {} @HttpPost() async createPost(@Body() postDto: { title: string; contents: string; authorId: string; password: string }) { const createdPost = await this.postService.createPost(postDto); return { post: createdPost }; } }
 
이 때 두 Controller는 인증된 사용자만이 접근가능하도록 구현하려고 합니다. 본래 NestJs에는 이러한 인증과 인가의 역할을 담당하는 Guard 라는 요소가 존재하지만, 여기서는 데코레이터를 기반으로 직접 구현해봅니다. 먼저 Decorator의 기본 요소부터 구현해봅시다.
export function Authenticate() { return function (){ } }
위와 같이 선언한 데코레이터는 클래스내 메서드, 필드, 파라미터에 선언할 수 있습니다.
getUser Controller에 다음과 같이 선언하여, getUser 호출이전에 Authenticate 데코레이터를 호출하여 인증되지 않은 사용자는 접근하지 않도록 하려고 합니다.
export class UserController { constructor(private readonly userService: UserService) {} @Authenticate() @Get(':id') async getUser(@Param('id') userId: number) { const user = await this.userService.getUserById(userId); return { user }; } }
 
앞서
import { BaseEntity } from 'typeorm'; import { HttpError } from './http-error'; export function Authorize() { return function ( _target: any, _propertyKey: string, descriptor: PropertyDescriptor, ) { const originalMethod = descriptor.value; async function newMethod(...args: any[]) { const id = args[0]; const password = args[1].password; const entity = (await entityClass .createQueryBuilder() .select() .where({ id, }) .getOne()) as unknown as { password: string; }; if (!entity) { return (() => { throw new HttpError(404, 'Entity not exist'); })(); } if (entity.password !== password) { return (() => { throw new HttpError(401, 'Wrong Password'); })(); } return originalMethod.apply(this, args); } descriptor.value = newMethod; }; }
 
 
 

요약

📌
요약: