횡단 관심사
- 관심사란, 쉽게 말해 구현하고자 하는 대상의 기능 집합으로 정의할 수 있습니다.
- 예를 들어, 수강신청 서비스를 만든다고 생각하면, 회원에 대한 관심사, 수업에 대한 관심사, 시간표에 대한 관심사로 나눌 수 있습니다.
- 이 때 어플리케이션 전역에서 사용할 수 있는 관심사를 횡단관심사로 부릅니다.
- 횡단관심사의 대표적인 예로, 로깅, 인증, 트랜잭션 처리 등이 있습니다.
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; }; }
요약
요약: