Skip to content

Commit

Permalink
Merge pull request #258 from Quickchive/feat/#257-aop-decorator
Browse files Browse the repository at this point in the history
[Feat] AOP decorator 추가
  • Loading branch information
stae1102 authored Feb 4, 2024
2 parents 4f91ed8 + 4dff9f7 commit f341507
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 0 deletions.
22 changes: 22 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
// IntelliSense를 사용하여 가능한 특성에 대해 알아보세요.
// 기존 특성에 대한 설명을 보려면 가리킵니다.
// 자세한 내용을 보려면 https://go.microsoft.com/fwlink/?linkid=830387을(를) 방문하세요.
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "개발 디버그",
"skipFiles": ["<node_internals>/**"],
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "start:dev"],
"autoAttachChildProcesses": true,
"restart": true,
"sourceMaps": true,
"cwd": "${workspaceRoot}",
"console": "integratedTerminal",
"protocol": "inspector"
}
]
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down Expand Up @@ -101,6 +102,7 @@ import { AppController } from './app.controller';
: '',
}),
OpenaiModule,
AopModule,
],
controllers: [AppController],
providers: [],
Expand Down
10 changes: 10 additions & 0 deletions src/common/aop/aop.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
6 changes: 6 additions & 0 deletions src/common/aop/aspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Injectable, SetMetadata, applyDecorators } from '@nestjs/common';

export const ASPECT = Symbol('ASPECT');

export const Aspect = (metadataKey: string | symbol) =>
applyDecorators(SetMetadata(ASPECT, metadataKey), Injectable);
75 changes: 75 additions & 0 deletions src/common/aop/auto-aspect.executor.ts
Original file line number Diff line number Diff line change
@@ -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<any>[]) {
return providers
.filter((wrapper) => wrapper.isDependencyTreeStatic())
.filter(({ instance, metatype }) => {
if (!instance || !metatype) {
return false;
}

const aspect = this.reflector.get<string>(ASPECT, metatype);
if (!aspect) {
return false;
}

return instance.wrap;
})
.map(({ instance }) => instance);
}
}
1 change: 1 addition & 0 deletions src/common/aop/decorator.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type Decorator = (...args: any) => void | Promise<void>;
8 changes: 8 additions & 0 deletions src/common/aop/lazy-decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Decorator } from './decorator.interface';

/**
* 데코레이터의 초기화를 모듈이 생성되는 시점까지 늦춥니다.
*/
export interface LazyDecorator {
wrap(target: unknown, originalFn: any, options: void): Decorator | undefined; // TODO 반환 형식
}
31 changes: 31 additions & 0 deletions src/common/aop/transactional.decorator.ts
Original file line number Diff line number Diff line change
@@ -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();
}
};
}
}
5 changes: 5 additions & 0 deletions src/common/aop/transactional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { SetMetadata } from '@nestjs/common';

export const TRANSACTIONAL = Symbol('TRANSACTIONAL');

export const Transactional = () => SetMetadata(TRANSACTIONAL, true);

0 comments on commit f341507

Please sign in to comment.