본문 바로가기
프로그래밍

[프로그래밍] NestJS 서비스간의 기능 의존성 해결하기

by 개발 까마귀 2024. 2. 4.
반응형

NestJS 서비스간의 기능 의존성

안녕하세요. 개발 까마귀입니다.

이번에는 NestJS 서비스간의 기능 의존성에 대해 알려드리고자합니다.

각 모듈은 자기가 기능 구현을 할 때 다른 모듈에 기능이 필요할 때 가 있습니다.

이 때 다른 모듈의 `Service`를 그냥 Import 해온다거나 `Repository`를 Import 해온다거나 등의 방식을 사용해서 

기능을 구현합니다.

하지만 이렇게 방법을 할시에 '순환참조', '너무 많은 기능 노출', '의존성 문제' 등 많은 문제가 발생할수있습니다.

너무 많은 기능 노출 문제 및 의존성 문제

예를들어 `UserService`에는 `login`, `signup` 메서드가 있고 `PostService`에는 `createPost`, `updatePost`, `getPost` 메서드가 있습니다.

이 때  'getPost' 메서드에서 요청한 사용자가 차단한 사용자들이 작성한 게시글은 노출하면 안되는 기능이 필요하기에 `UserService`에 차단한 사용자의 ID를 제공하는 기능이 필요합니다.

이 때 `UserServicr`는 `getBlockUserIdsByUserId` 라는 메서드를 구현하고 `PostService`에서는 `UserService`를 통째로 import 해서 사용합니다. 

이 때 발생하는 문제점은 `PostService`는 `UserService`에 기존에 구현된 `login`, `signup` 메서드까지 알게됩니다.

이렇게 알 필요가 없는 메서드 기능까지 알게되면 잘못하다가 `PostService`에서 `getBlockUserIdsByUserId` 기능 말고 `UserService`에 있는 모든 기능을 마음대로 갖다 쓰게되면 의존성 문제와 버그가 발생하기 쉬운 구조가 되버립니다.

이는 꼭 `Service`가 아니고 `Repository`여도 같은 문제가 발생합니다.

순환참조 문제

`UserService`에서 사용자가 몇개의 게시글과 댓글 쓴 개수에 따라 포인트를 정산하려고 합니다.

그렇다면 `PostService` 에서 `getPostCount` 메서드와 `getCommentCount` 메서드를 제공할겁니다.

하지만 이 때 위 `너무 많은 기능 노출 문제 및 의존성 문제` 처럼 `PostService`가 `UserService`에 `getBlockUserIdsByUserId`를 원한다면

`UserService` -> `PostService`, `PostService` -> `UserService` 이렇게 순환 참조 문제가 발생합니다.

각 프레임워크에서는 이러한 순환 참조 문제를 발생하는거를 해결하기 위해 해결방법이 존재하지만 그냥 말 그대로 '순환참조 문제'만 해결하기에 다른 문제는 해결이 되지 않습니다. 

그러면 `몇개의 게시글과 댓글 쓴 개수에 따라 포인트를 정산` 로직만 따로 모듈 분리하면 되겠지만 이거는 임시방편입니다.

해결방법

그렇다면 어떻게 해야 위 세가지 문제를 해결할수있을까요?

해결조건은 아래와같습니다.

 

1. 우선 순환참조 문제를 방지하기 위해 단방향으로 기능을 제공하게 만들어야합니다.

2. 많은 기능을 노출을 막기 위해 함수 단위로 기능을 노출해야 합니다.

 

위 조건을 맞게 설계하려면 `Helper Module`이 필요합니다.

Helper Module

`Helper Module`의 특징은 아래와같습니다.

 

1.  다른 모듈의 기능을 import 하지 않는다.

2. 단일책임 원칙에 맞게 기능을 구현한다.

3. 함수 단위로 기능을 구현한다.

 

예시 코드는 아래와 같습니다.

// ./helpers/get-blocked-user-ids-by-user-id.helper
import { Injectable, Inject } from '@nestjs/common';
import { USER_REPOSITORY } from '@modules/user/user.di-tokens';
import { UserRepositoryPort } from '@modules/user/out/database/user.repository.port';

@Injectable()
export class GetBlockedUserIdsByUserIdHelper {
  constructor(
    @Inject(USER_REPOSITORY)
    private readonly userRepo: UserRepositoryPort,
  ) {}

  async helper(userId: number, blockType: 'POST' | 'COMMENT') {
    const userBlocks = await this.userRepo.findBlockedUsersByUserId(
      userId,
      blockType,
    );

    return userBlocks.map((userBlock) => userBlock.toUserId);
  }
}

 

import { Logger, Module, Provider } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { entityList } from '@common/db/repository';
import { UserController } from './controller/user.controller';
import { UserService } from './service/user.service';
import { USER_CAHCE_REPOSITORY, USER_REPOSITORY } from './user.di-tokens';
import { UserRepository } from './out/database/user.repository';
import { GetBlockedUserIdsByUserIdHelper } from './helpers/get-blocked-user-ids-by-user-id.helper';

const controllers = [
  UserController
];

const Services: Provider[] = [
  UserService
]

const helpers: Provider[] = [
  GetBlockedUserIdsByUserIdHelper,
];

const repositories: Provider[] = [
  { provide: USER_REPOSITORY, useClass: UserRepository },
  { provide: USER_CAHCE_REPOSITORY, useClass: UserCacheRepository },
];

@Module({
  imports: [
    TypeOrmModule.forFeature(entityList),
  ],
  controllers: [...controllers],
  providers: [
    Logger,
    ...services,
    ...repositories,
    ...helpers,
  ],
  exports: [...helpers],
})
export class UserModule {}

위와 같이 "차단한 사용자의 ID를 제공하는 기능"인 "GetBlockedUserIdsByUserIdHelper" 모듈을 하나 만듭니다.

만든 `Helper` 모듈을 exports 해둡니다. 이 때 중요한 포인트는 exports에는 `Helper` 모듈만 존재하는겁니다.

`Helper` 모듈을 사용하는 예시 코드를 보시죠

import { Logger, Module, Provider } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { entityList } from '@common/db/repository';
import { UserModule } from '@modules/user/user.module';
import { postController } from './controller/post.controller';
import { postService } from './service/post.service';
import { SOCIAL_REPOSITORY } from './social.di-tokens';
import { SocialRepository } from './out/database/social.repository';

const controllers = [
  PostController
]

const services: Provider[] = [
  PostServices
]

const repositories: Provider[] = [
  { provide: SOCIAL_REPOSITORY, useClass: SocialRepository },
];

@Module({
  imports: [
    UserModule,
    TypeOrmModule.forFeature(entityList),
  ],
  controllers: [...controllers],
  providers: [Logger, ...services, ...repositories],
})
export class PostModule {}

 

import { Inject } from '@nestjs/common';
import { SOCIAL_REPOSITORY } from '@modules/social/social.di-tokens';
import { SocialPostRepositoryPort } from '@modules/social/out/database/social.repository.port';
import { GetBlockedUserIdsByUserIdHelper } from '@modules/user/helpers/get-blocked-user-ids-by-user-id.helper';
import {
  FindCommentServiceDto,
} from './dtos/find-comment.service.dto';

export class PostService {
  constructor(
    @Inject(SOCIAL_REPOSITORY)
    private readonly socialRepo: SocialPostRepositoryPort,
    private readonly getBlockedUserIdsByUserIdHelper: GetBlockedUserIdsByUserIdHelper,
  ) {}

  async execute(
    query: FindCommentQuery,
  ): Promise<FindCommentServiceDto> {
    const blockUserIds = await this.getBlockedUserIdsByUserIdHelper.helper(
      query.userId,
      'POST',
    );
    
    ...
  }
}

위와 같이 `Helper` 모듈이 속한 상위 모듈인 `User Module`를 import 하고 사용하면 됩니다.

Helper 모듈이 다른 모듈에 기능이 필요한 경우?

넵 위와같은 경우가 발생할겁니다.

이 때 저는 2개이상의 모듈의 기능을 통해 기능을 만들어야하는 경우에는 `Handler` 라는 개념으로 모듈을 만듭니다.

`Helper` 모듈은 특정 모듈에 속해서 만들어집니다. 하지만 `Handler`같은 경우에는 여러 모듈을 통해서 만들어지는 모듈이므로 특정 모듈에 속해있지 않다고 판단해 `common` 폴더 안에 `handler` 폴더를 만들어서 기능을 구현합니다.

정리

제가 소개한 방식으로 꼭 따라 할 필요는 없습니다.

소개해드린 방법 또한 여러가지 방법중 하나이니깐요.

`Helper` 라는 모듈을 통해서 순환참조 문제와 많은 기능 노출 문제는 막을수있지만 파일이 많이 만들어진다는 단점은 있습니다 ㅎㅎ

모든 방법은 장점만 있는 방법은 없기에 감안해서 잘 적용하시길 바랍니다.

 

감사합니다.

반응형

'프로그래밍' 카테고리의 다른 글

[프로그래밍] 추상화와 다형성  (0) 2024.02.04
[프로그래밍] TDD와 테스트 코드  (2) 2023.09.02

댓글