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 |
댓글