kakasoo

마이크로서비스 분해전략 - NestJS 본문

프로그래밍/Backend

마이크로서비스 분해전략 - NestJS

카카수(kakasoo) 2023. 7. 9. 22:16
반응형

마이크로서비스 패턴이라는 책을 읽고, NestJS 코드로 다시 이해한다.

 

NestJS에서 가장 많이 볼 수 있는 구조는 Controller - Service - Repository 로 이어지는 계층화된 아키텍처 (layered artchitecture)다. 이는 표현 계층, 비즈니스 로직 계층, 영속화 (persistence) 계층이라고 하는데, Controller는 사용자 인터페이스 또는 외부 API가 구현된 계층, Service는 비즈니스 로직이 구현된 계층, 영속화 계층은 DB 상호작용 로직이 구현된 계층이라는 뜻이다. 여기에는 한 가지 문제가 있다. 과연 어플리케이션에 동작을 의뢰하는 계층이 Controller 밖에 없는가? NestJS를 해봤다면 이미 정답을 할 텐데, 배치 시스템이나 소켓 통신으로도 우리는 비즈니스 로직을 동작시킬 수 있다. 그러니 컨트롤러 하나만을 계층으로 갖는 구조가 절대적이라고 할 수는 없을 것이다. 마찬가지로 영속화 계층이 의존하는 DB도 하나 뿐이라고 할 수는 없을 것이다. 일반적인 경우에는 당연 DB가 1개 뿐이겠지만 그 또한 절대적이라고 할 수는 없다. 마지막 세 번째 문제는 비즈니스 로직 계층이 나머지 두 계층에 의존하는 형태로 정의된다는 것이다.

 

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  getUsers(): string[] {
    return this.userService.getUsers();
  }
}
@Resolver()
export class UserResolver {
  constructor(private readonly userService: UserService) {}

  @Query(() => [String])
  getUsers(): string[] {
    return this.userService.getUsers();
  }
}

만약 표현 계층이 2개 이상이라면?

 

hexagonal architecture in NestJS

비즈니스 로직들이 표현 계층과 영속화 계층에 의존하지 않으려면 어떤 아키텍처가 되어야 하는가?

import { Injectable } from '@nestjs/common';

export abstract class IGenericRepository<T> {
  abstract getAll(): Promise<T[]>;

  abstract get(id: string): Promise<T>;

  abstract create(item: T): Promise<T>;

  abstract update(id: string, item: T);
}

export class Cat {
  id: string;
  name: string;
  age: number;
}

@Injectable()
export class CatService {
  constructor(@Inject('catRepository') private readonly CatRepository: IGenericRepository<Cat>) {}
}

이해를 돕기 위한 코드를 첨부한다. 이 코드에서는 CatRepository의 타입이 IGenericRepository 로 되어 있다. 하지만 이 타입은 실제로는 추상 클래스이기 때문에 아무런 구현도 작성되어 있지 않다. 진짜 내부 동작은 어떻게 구현되어 있을까? 서비스 로직에서는 이걸 알 필요가 없다. 심지어 저 코드 상의 Repository가 어떤 데이터베이스인지, 사실은 NoSQL인지도 알 필요없다. 중요한 건, 저 CatRepository 내부에 getAll, get, create, update가 어떤 입력, 출력 타입들을 가지고 있는지다.

 

import { Module } from '@nestjs/common';
import { CatController } from 'src/controllers/animal.controller';
import { CatService } from 'src/providers/catservice';

@Module({
  controllers: [CatController],
  providers: [
    CatService,
    {
      provide: 'catRepository',
      useClass: CatMySQLService,
    },
  ],
})
export class CatModule {}

내부에서는 이런 식으로 필요한 것을 주입했을 수도 있다.

 

import { Module } from '@nestjs/common';
import { CatController } from 'src/controllers/animal.controller';
import { CatService } from 'src/providers/catservice';

@Module({
	imports: [DatabaseModule],
  controllers: [CatController],
  providers: [CatService],
})
export class CatModule {}
import { Module } from '@nestjs/common';
import { MongoDatabaseModule } from '../../frameworks/data-services/mongo/mongo-data-base.module';

@Module({
  imports: [MongoDatabaseModule],
  exports: [MongoDatabaseModule],
})
export class DatabaseModule {}

한 술 더 떠서 데이터베이스도 모듈화했을 수도 있다. 그래서 CatModule도 몰라도 되게 했을 수도 있다.

 

import { Module } from '@nestjs/common';
import { CatController } from 'src/controllers/animal.controller';
import { CatService } from 'src/providers/catservice';

@Module({
	imports: [DatabaseModule, CatUseCasesModule],
  controllers: [CatController],
})
export class CatModule {}

그리고 한 술 더 떠서, 컨트롤러와 서비스도 서로 다른 모듈로 구현하되, 컨트롤러가 서비스가 있는 모듈을 주입받아 동작하게끔 할 수도 있는 것이다.

 

folder structrues

가장 마지막에 구현된, 가장 추상화가 많이 되어 있는 상태의 폴더 구조는 아래와 같다.

src
  ㄴ controllers # 표현 계층 중 하나인 controller가 모여있다.
  ㄴ core        # 이 안은 전부 interface로 구현되어 있다.
    ㄴ abstracts # 내부 port에 해당하며 각 계층이 서로 모르게 하기 위한 타입들이다.
    ㄴ dtos # 우리가 아는 그 dto이며, 이 역시 인바운드 port에 해당한다.
    ㄴ entities  # DB를 매핑한 대상이며, 순수한 인터페이스로, 어떤 DB, ORM에도 종속되지 않는다.
  ㄴ frameworks  # 구체화된 서비스 ( ex. mysql, mongo 등 )를 주입할 수 있는 모듈들이 담겨 있다.
    ㄴ data-services
      ㄴ mongo
        ㄴ model # Mongo document들이 정의되어 있을 것이다.
      ㄴ mysql
        ㄴ model # 아마도 TypeORM, Prisma로 된 모델들이 정의되어 있을 것이다.
  ㄴ services    # NestJS의 서비스를 의미하는 게 아니라, framework에서 구체화된 대상을 추상화하는 계층이다.
  ㄴ use-cases   # NestJS provider를 따로 모듈로 분리해놓은 곳이다.
  ㄴ app.module.ts
  ㄴ main.ts

services 폴더가 헷갈릴 수 있는데, 내부에는 아래와 같은 파일이 있다.

 

import { Module } from '@nestjs/common';
import { MongoDatabaseModule } from '../../frameworks/data-services/mongo/mongo-data-base.module';

@Module({
  imports: [MongoDatabaseModule],
  exports: [MongoDatabaseModule],
})
export class DatabaseModule {}

 

아래는 이전에 설명한 다른 글이다.

 

Clean Architecture (NestJS)

어떤 기능을 구현하다보면 외부 요소에 대한 의존성을 강하게 가지게 되는 경우가 있다. 가장 간단한 예시로, 우리는 어떤 데이터베이스를 사용하느냐에 따라 내부 로직이 변경될 것이다. Postgre

kscodebase.tistory.com

 

반응형