From ec089e457a0c8f2f3ec6518fb4ec4b56fec7ea73 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 15:15:03 +0900 Subject: [PATCH 1/7] feat: create aop decorator --- src/common/aop/aspect.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/common/aop/aspect.ts diff --git a/src/common/aop/aspect.ts b/src/common/aop/aspect.ts new file mode 100644 index 0000000..3c3d7dc --- /dev/null +++ b/src/common/aop/aspect.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from '@nestjs/common'; + +export const ASPECT = Symbol('ASPECT'); + +export const Aspect = (metadataKey: string | symbol) => + SetMetadata(ASPECT, metadataKey); From db3c719e37742d48e4d54aa131106818d76a510f Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 15:16:39 +0900 Subject: [PATCH 2/7] feat: create lazy decorator interface --- src/common/aop/lazy-decorator.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/common/aop/lazy-decorator.ts diff --git a/src/common/aop/lazy-decorator.ts b/src/common/aop/lazy-decorator.ts new file mode 100644 index 0000000..40edb3f --- /dev/null +++ b/src/common/aop/lazy-decorator.ts @@ -0,0 +1,10 @@ +/** + * 데코레이터의 초기화를 모듈이 생성되는 시점까지 늦춥니다. + */ +export interface LazyDecorator { + wrap( + target: unknown, + originalFn: any, + options: void, + ): MethodDecorator | undefined; // TODO 반환 형식 +} From 7a3e36f6a18d65a8bb3d95301baf6aac649ec1c1 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 16:00:51 +0900 Subject: [PATCH 3/7] feat: create transactional decorator --- src/common/aop/transactional.decorator.ts | 31 +++++++++++++++++++++++ src/common/aop/transactional.ts | 5 ++++ 2 files changed, 36 insertions(+) create mode 100644 src/common/aop/transactional.decorator.ts create mode 100644 src/common/aop/transactional.ts diff --git a/src/common/aop/transactional.decorator.ts b/src/common/aop/transactional.decorator.ts new file mode 100644 index 0000000..55ab096 --- /dev/null +++ b/src/common/aop/transactional.decorator.ts @@ -0,0 +1,31 @@ +import { LazyDecorator } from './lazy-decorator'; +import { DataSource } from 'typeorm'; +import { Aspect } from './aspect'; +import { TRANSACTIONAL } from './transactional'; + +/** + * TODO Transaction 전파 등 다양한 케이스를 고려해야 함. + * 1. 해당 Decorator는 단순 명령을 처리하는 객체이므로, 내부 구현만 달라지면 OK + * 2. Repository에서 트랜잭션을 매니징할 객체를 시그니처에 명시한다. -> 매니징 객체의 타입이 상당히 애매하다. 어댑터로 추상화를 도울 수 있도록 개선. + */ +@Aspect(TRANSACTIONAL) +export class TransactionDecorator implements LazyDecorator { + constructor(private readonly dataSource: DataSource) {} // TypeORM에 의존하고 있으므로 주의 + + wrap(target: unknown, originalFn: any, _: void) { + return async (...args: any[]) => { + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + await queryRunner.startTransaction(); + try { + await originalFn.call(target, ...args, queryRunner.manager); + await queryRunner.commitTransaction(); + } catch (e) { + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + }; + } +} diff --git a/src/common/aop/transactional.ts b/src/common/aop/transactional.ts new file mode 100644 index 0000000..dbaf607 --- /dev/null +++ b/src/common/aop/transactional.ts @@ -0,0 +1,5 @@ +import { SetMetadata } from '@nestjs/common'; + +export const TRANSACTIONAL = Symbol('TRANSACTIONAL'); + +export const Transactional = () => SetMetadata(TRANSACTIONAL, true); From 9d734b6da3513213b00b998bd481f60e333de412 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 16:01:06 +0900 Subject: [PATCH 4/7] chore: create vscode debug setting --- .vscode/launch.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..49ef2b2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // IntelliSense를 사용하여 가능한 특성에 대해 알아보세요. + // 기존 특성에 대한 설명을 보려면 가리킵니다. + // 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요. + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "개발 디버그", + "skipFiles": ["/**"], + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start:dev"], + "autoAttachChildProcesses": true, + "restart": true, + "sourceMaps": true, + "cwd": "${workspaceRoot}", + "console": "integratedTerminal", + "protocol": "inspector" + } + ] +} From f4a4c85d95eef7d4acdda42ada52b0d04b996808 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 16:01:38 +0900 Subject: [PATCH 5/7] feat: create auto aspect executor --- src/common/aop/aspect.ts | 4 +- src/common/aop/auto-aspect.executor.ts | 75 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/common/aop/auto-aspect.executor.ts diff --git a/src/common/aop/aspect.ts b/src/common/aop/aspect.ts index 3c3d7dc..b6ce864 100644 --- a/src/common/aop/aspect.ts +++ b/src/common/aop/aspect.ts @@ -1,6 +1,6 @@ -import { SetMetadata } from '@nestjs/common'; +import { Injectable, SetMetadata, applyDecorators } from '@nestjs/common'; export const ASPECT = Symbol('ASPECT'); export const Aspect = (metadataKey: string | symbol) => - SetMetadata(ASPECT, metadataKey); + applyDecorators(SetMetadata(ASPECT, metadataKey), Injectable); diff --git a/src/common/aop/auto-aspect.executor.ts b/src/common/aop/auto-aspect.executor.ts new file mode 100644 index 0000000..f3b52fb --- /dev/null +++ b/src/common/aop/auto-aspect.executor.ts @@ -0,0 +1,75 @@ +import { Injectable, OnModuleInit } from '@nestjs/common'; +import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core'; +import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; +import { ASPECT } from './aspect'; + +/** + * 모듈 초기화 시 전체 프로바이더를 탐색하여 AOP를 활용하는 메서드를 찾습니다. + */ +@Injectable() +export class AutoAspectExecutor implements OnModuleInit { + constructor( + private readonly discoveryService: DiscoveryService, + private readonly metadataScanner: MetadataScanner, + private readonly reflector: Reflector, + ) {} + + onModuleInit() { + const providers = this.discoveryService.getProviders(); + + const lazyDecorators = this.lookupLazyDecorator(providers); + if (lazyDecorators.length === 0) { + return; + } + + providers + .filter((wrapper) => wrapper.isDependencyTreeStatic()) + .filter(({ instance }) => instance && Object.getPrototypeOf(instance)) + .forEach(({ instance }) => { + this.metadataScanner.scanFromPrototype( + instance, + Object.getPrototypeOf(instance), + (methodName) => { + lazyDecorators.forEach((lazyDecorator) => { + const metadataKey = this.reflector.get( + ASPECT, + lazyDecorator.constructor, + ); + + const metadata = this.reflector.get( + metadataKey, + instance[methodName], + ); + if (!metadata) { + return; + } + const wrappedMethod = lazyDecorator.wrap( + instance, + instance[methodName], + metadata, + ); + instance[methodName] = wrappedMethod; + }); + }, + ); + }); + } + + private lookupLazyDecorator(providers: InstanceWrapper[]) { + return providers + .filter((wrapper) => wrapper.isDependencyTreeStatic()) + .filter(({ instance, metatype }) => { + if (!instance || !metatype) { + return false; + } + + const aspect = this.reflector.get(ASPECT, metatype); + if (!aspect) { + return false; + } + + return instance.wrap; + }) + .map(({ instance }) => instance); + } +} From e87be427b732a10847c4fe66a971f54f069fddc7 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 16:01:59 +0900 Subject: [PATCH 6/7] fix: change lazy decorator signature --- src/common/aop/decorator.interface.ts | 1 + src/common/aop/lazy-decorator.ts | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) create mode 100644 src/common/aop/decorator.interface.ts diff --git a/src/common/aop/decorator.interface.ts b/src/common/aop/decorator.interface.ts new file mode 100644 index 0000000..e548cb9 --- /dev/null +++ b/src/common/aop/decorator.interface.ts @@ -0,0 +1 @@ +export type Decorator = (...args: any) => void | Promise; diff --git a/src/common/aop/lazy-decorator.ts b/src/common/aop/lazy-decorator.ts index 40edb3f..ac7112a 100644 --- a/src/common/aop/lazy-decorator.ts +++ b/src/common/aop/lazy-decorator.ts @@ -1,10 +1,8 @@ +import { Decorator } from './decorator.interface'; + /** * 데코레이터의 초기화를 모듈이 생성되는 시점까지 늦춥니다. */ export interface LazyDecorator { - wrap( - target: unknown, - originalFn: any, - options: void, - ): MethodDecorator | undefined; // TODO 반환 형식 + wrap(target: unknown, originalFn: any, options: void): Decorator | undefined; // TODO 반환 형식 } From 4dff9f7b4ef61449770e839833efca41a85b2231 Mon Sep 17 00:00:00 2001 From: Seongtae Date: Sun, 4 Feb 2024 16:02:16 +0900 Subject: [PATCH 7/7] feat: register aop module --- src/app.module.ts | 2 ++ src/common/aop/aop.module.ts | 10 ++++++++++ 2 files changed, 12 insertions(+) create mode 100644 src/common/aop/aop.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 06d8f9c..f5855b7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -14,6 +14,7 @@ import { SummaryModule } from './summary/summary.module'; import { TypeOrmConfigService } from './database/typerom-config.service'; import { OpenaiModule } from './openai/openai.module'; import { AppController } from './app.controller'; +import { AopModule } from './common/aop/aop.module'; @Module({ imports: [ @@ -101,6 +102,7 @@ import { AppController } from './app.controller'; : '', }), OpenaiModule, + AopModule, ], controllers: [AppController], providers: [], diff --git a/src/common/aop/aop.module.ts b/src/common/aop/aop.module.ts new file mode 100644 index 0000000..0a9e271 --- /dev/null +++ b/src/common/aop/aop.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { DiscoveryModule } from '@nestjs/core'; +import { AutoAspectExecutor } from './auto-aspect.executor'; +import { TransactionDecorator } from './transactional.decorator'; + +@Module({ + imports: [DiscoveryModule], + providers: [AutoAspectExecutor, TransactionDecorator], +}) +export class AopModule {}