04-02 16:52
Notice
Recent Posts
관리 메뉴

독산구너

NestJS Library 사용 본문

NestJS

NestJS Library 사용

독산구너 2024. 11. 19. 22:53

 

 

목차

     

     

    글의 목적

    NestJS Library 사용해서 얻을 있는 이점을 알아보고, 이를 멀티테넌씨 DB 구조 설계에 이용해보고자 합니다.

     

    NestJS Libraries

    NestJS에서는 공통으로 사용되고, 자주 재사용되는 코드를 구성하는 여러가지 방식이 존재합니다.

     

    1. 모듈화

    -> NestJS의 핵심 개념으로, 특정 로직을 모듈화시켜 다른 모듈에서 import 받아 사용할 수 있도록 지원합니다. 하지만 Single Application 내에서만 사용 가능하므로, 회사 내 또는 조직 내 공유해 사용하기에는 부족합니다.

     

    2. Npm pachaging

    -> 모듈은 재사용을 위해 npm 패키지가 가능합니다. 패키지는 npm install을 통해서 쉽게 사용이 가능합니다. 하지만 이는 조직 내 특화된 기능을 공유하기보다 범용적으로 재사용하도록, 외부적으로 배포하는 용도의 공유 수단입니다.

     

    3. NestJS Libraries

    -> monorepo 프로젝트 코드를 공유하도록 지원하는 NestJS 기능입니다. 설치가 불필요하고 같은 monorepo 내에서 바로 참조가 가능, 변경사항을 수정 즉시 적용 가능하다는 장점이 있습니다.

     

    방식의 차이점을 표로 표현하면 다음과 같습니다.

     

    항목 모듈화 NestJS Libraries npm 패키징
    설명 애플리케이션 내부에서 기능을 분리하여 재사용 가능한 구조로 설계. Monorepo에서 여러 애플리케이션 간에 재사용 가능한 모듈 설계. 외부 프로젝트 간에 공유 가능한 패키지로 배포.
    공유 범위 애플리케이션 내부에서만 공유. 같은 Monorepo 내에서만 공유. 세계 또는 조직 모든 프로젝트에서 공유 가능.
    구성 위치 단일 애플리케이션 내부 (src/ 디렉토리). Monorepo libs/ 디렉토리. 별도의 독립적인 패키지로 관리.
    의존성 관리 애플리케이션의 package.json 포함. Monorepo에서 의존성을 공유. 패키지가 독립적으로 의존성 관리.
    변경 사항 반영 애플리케이션 내부에서 바로 반영. Monorepo 내에서 즉시 반영 가능. 버전 배포 수동으로 업데이트 필요.
    설치 필요 여부 설치 불필요, 내부에서 바로 사용 가능. 설치 불필요, 같은 Monorepo 내에서 참조 가능. 설치 필요 (npm install 또는 yarn add).
    용도 기능 단위로 코드를 분리하여 유지보수와 테스트 용이. 내부적으로 재사용 가능한 모듈 설계와 공유. 외부에 배포 가능한 범용 라이브러리 설계.
    사용 사례 - 로깅 기능을 모듈로 분리- 인증 모듈 설계 - Monorepo에서 공통 AuthLibrary- 로깅 Library - JWT 라이브러리- 데이터베이스 클라이언트 패키지
    장점 - 코드 구조가 명확해짐- 유지보수 용이 - 빠른 개발 테스트 가능- 코드 중복 최소화 - 세계 개발자와 공유 가능- 프로젝트 코드 재사용
    단점 애플리케이션 외부에서는 재사용 불가능. Monorepo 외부에서는 사용 불가능. 배포 설치 과정이 필요.
    대상 환경 단일 애플리케이션. Monorepo 구조. 다중 프로젝트 환경.
    예제 AppModule 내에서 여러 서비스, 컨트롤러 정의. libs/auth 또는 libs/logger 생성. @nestjs/passport 같은 npm 패키지.

     

    Monorepo

    여기서, Monorepo에 대해 알아보고 넘어가겠습니다. monorepo 단일 리포지토리에서 여러 프로젝트를 관리하는 소프트웨어 개발 방식입니다. 코드의 버전관리와 공유가 간단해지고, 코드 재사용성이 증가됩니다. 또한 모든 프로젝트가 같은 환경에서 빌드 테스트 되므로 end-to-end 테스트를 쉽게 구성 가능하고, 여러 프로젝트에서 사용하는 패키지 의존성을 통합적으로 관리 가능하다는 장점이 있습니다.

     

    구조를 설명하면 다음과 같습니다.

    my-monorepo/
    ├── apps/                # 여러 애플리케이션 디렉토리
    │   ├── app1/            # 첫 번째 애플리케이션
    │   │   ├── src/
    │   │   ├── main.ts
    │   │   └── app.module.ts
    │   ├── app2/            # 두 번째 애플리케이션
    │   │   ├── src/
    │   │   ├── main.ts
    │   │   └── app.module.ts
    │   └── ...
    ├── libs/                # 공유 라이브러리 디렉토리
    │   ├── auth/            # 인증 관련 라이브러리
    │   │   ├── src/
    │   │   └── auth.module.ts
    │   ├── logger/          # 로깅 관련 라이브러리
    │   │   ├── src/
    │   │   └── logger.module.ts
    │   └── ...
    ├── package.json         # 공통 의존성
    ├── tsconfig.json        # 공통 TypeScript 설정
    └── nest-cli.json        # NestJS CLI 설정 파일

     

    여기서 Libraries monorepo 내에서 여러 프로젝트가 공통으로 사용 가능한 코드/모듈입니다.

     

    멀티테넌시 구조설계에 Libraries를 사용하는 이유

          1. 비즈니스 로직과 멀티테넌시 로직의 분리
            1. 멀티테넌시 구조의 구현 방식이 변경되더라도 수정 작업을 한곳에서만 수행하면 되므로 유지보수가 용이합니다.
            2. 애플리케이션에는 멀티테넌시 로직과 분리된 상태로 비즈니스 로직만 집중적으로 수행할 있습니다.
          2. 애플리케이션 확장성 확보
            1. 현재 개발 중인 업무 관리 플랫폼은 SaaS 형태로 제공되며, 워크스페이스별로 서로 다른 기능을 사용할 있도록 설계되어 있습니다. 테넌트별로 필요한 애플리케이션이 존재하고, 이러한 애플리케이션이 확장되고 증가함에 따라 데이터베이스 관리가 복잡해질 가능성이 높습니다. 데이터베이스 관련 로직을 분리하여 여러 애플리케이션에서 재사용 가능하도록 설계하면 효율적인 확장과 관리가 가능합니다.
          3. 멀티테넌시 구조 설계의 복잡성 해결
            1. 테넌트 생성 삭제, 테넌트의 데이터베이스 연결 관리, 공통 데이터베이스와 테넌트 데이터베이스의 분리, 트랜잭션 관리 다양한 요소를 고려해야 합니다. 이러한 로직을 하나의 애플리케이션 내에서 모듈화하기에는 복잡성이 높으므로, 별도의 라이브러리로 분리하는 것이 효과적입니다.

     

    Libaray 생성

    NestJS에서는 다음과 같은 방식으로 라이브러리 생성이 가능합니다.

    $ nest g library [생성하고자 하는 라이브러리 이름]

     

    다음과 같은 구조로 라이브러리가 생성됩니다.

    libs
     ㄴmy-library
    	ㄴsrc
    		ㄴindex.ts
    		ㄴmy-library.module.ts
    		ㄴmy-library.service.ts
    	ㄴtsconfig.lib.json

     

    그리고 nest.cli.json에는 다음과 같이 project 의 value로 들어가야 합니다. 이때 type은 application이 아닌 library로, entryFile은 main이 아닌 index로 설정해야 합니다.

    ** index.ts 파일의 exports 통해 코드를 내보냅니다.

    nest.cli.json

     

    Library 사용

    app.module.ts에 Library로 추가한 모듈을 import 받아 사용 가능합니다.

     

    - Database library 생성

    Database 라이브러리 내 DatabaseService

    DatabaseService에서는 테넌트 DB 생성과 각 테넌트DB 접근을 위한 getTenantDataSource 메서드가 정의되어 있습니다.

    controller에서는 Interceptor 사용해 해당 워크스페이스 DB datasource 받고, 트랜잭션을 시작합니다. Exception 발생 롤백합니다.

    @Injectable()
    export class TenantTxInterceptor implements NestInterceptor {
      constructor(private readonly databaseService: DatabaseService) {
      }
    
      async intercept(
        context: ExecutionContext,
        next: CallHandler<any>
      ): Promise<Observable<any>> {
        const req = context.switchToHttp().getRequest();
        const queryRunner: QueryRunner = await this.init(req.body.workspaceId);
    
        req.tenanatQueryRunnerManager = queryRunner.manager;
    
        return next.handle().pipe(
          catchError(async (e) => {
            await queryRunner.rollbackTransaction();
            await queryRunner.release();
    
            console.log(e);
            if (e instanceof ServiceException) {
              throw e;
            } else if (e instanceof BadRequestException) {
              throw e;
            } else {
              throw new ServiceException(ExceptionCode.DATABASE_ERROR);
            }
          }),
          tap(async () => {
            await queryRunner.commitTransaction();
            await queryRunner.release();
          })
        );
      }
    
      private async init(workspaceId: string): Promise<QueryRunner> {
        const dataSource: DataSource = await this.databaseService.getTenantDataSource(workspaceId);
        const queryRunner = dataSource.createQueryRunner();
        await queryRunner.connect();
        await queryRunner.startTransaction();
    
        return queryRunner;
      }
    }

     

    NestJS Library 사용을 통해

    1. 코드 재사용성 증가

    : 이전에는 특정 기능(db 연결, config 처리 등)이 프로젝트 내에서만 사용 가능하거나 중복 구현될 가능성이 높았습니다. Library를 사용하면서 다른 프로젝트에서도 동일 기능을 쉽게 재사용 가능하게 됐습니다.

     

    2. 관심사 분리

    : 비지니스 로직, 라우터 설정, 데이터베이스 연결 등이 혼합되어 코드의 역할이 불명확했으나, 멀티테넌시 관련 로직을 라이브러리로 분리해 어플리케이션 코드에는 비지니스 로직만을 구현하도록 관심사를 분리했습니다.

     

    3. 유지보수성 향상

    : 모든 로직이 한 프로젝트에 혼합되어 변경시 오류 발생 가능성이 높았으나, 라이브러리 사용을 통해 멀티테넌시 관련 수정사항을 라이브러리 내부에서만 수정하고, 외부에 영향을 주지 않는것이 가능해졌습니다.

     

    4. 테스트 용이성

    : NestJS의 Library는 독립적으로 테스트 가능하므로 monk 데이터를 사용해 분리되어 테스트가 가능해졌습니다.

     

     

    단점은 없는가?

    1. 아직은 작은 프로젝트이므로, 확장성과 코드 재사용을 고려한 라이브러리 사용 리팩토링 자체가 불필요한 비용발생이라고 할 수 있습니다.

    2. 디펜던시 관리가 복잡해질 수 있다고 생각합니다. 여러 프로젝트에서 공통된 라이브러리를 사용한다면, 어느 프로젝트의 특성에 맞게 라이브러리를 수정하기 어려워집니다. 또한 버전이 구분된다면 어느 프로젝트에서 어떤 버전을 사용할지 결정하고 테스트하는 비용이 발생할 것입니다.