최근에 Remix를 써보면세 백엔드를 복습하기로 했습니다.
그 일환으로 NestJS를 공부하고 있는데요,
NestJS 창시자가 속해있으면서 풀스택 컨설팅(이라고 하고 SI라 읽는) 회사의 공식 블로그에 양질의 글이 많은걸 발견했습니다.
해당 글의 번역입니다.
https://trilon.io/blog/dependency-inversion-principle
- 고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화(예: 인터페이스)에 의존해야 합니다.
- 추상화(인터페이스)는 세부 사항(구체적인 구현 클래스)에 의존해서는 안됩니다. 세부사항(구체적인 구현)은 추상화에 의존해야 합니다.
주 : 결국 구체 클래스를 임포트 하지 말고, 이 경우 중간에 인터페이스를 집어넣으란 말
SOLID의 이 원칙은 DI(Dependency Injection) 시스템과 가장 밀접하게 연결되어 있습니다.
이 패턴(DI)은 의존성 역전 원칙을 따르는 소프트웨어 조각을 동작하는 응용 프로그램으로 결합하는 데 도움이 됩니다.
따라서 하위 수준 모듈에서 상위 수준 소비자까지 특정 구현을 제공합니다.
NestJS는 고맙게도 우리에게 정말 좋은 DI 시스템을 제공하며,
이 기사에서는 의존성 역전 원칙을 따를 때 이 시스템을 사용하는 방법을 보여주고자 합니다.
예제
GitHub의 리포지토리에 대한 데이터를 분석할 애플리케이션을 만듭니다.
우리의 첫 번째 작업은 특정 기여자가 검토해야 하는 지정된 리포지토리에 대한 활성 풀 요청을 반환하는 엔드포인트를 구현하는 것입니다.
리뷰어 ID와 레포지토리 ID를 이용해, 리뷰해야 하는 Pull Request 목록 반환
import { Controller, Get, HttpModule, HttpService, Param } from '@nestjs/common';
import { GithubPullRequest } from './github-pull-request';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PullRequest } from 'app/model';
@Controller()
export class RepositoryStatisticsController {
constructor(private http: HttpService) {}
@Get('repository/:repositoryId/reviewer/:reviewerId/pending-prs')
getReviewerPendingPrs(
@Param('repositoryId') repositoryId: string,
@Param('reviewerId') reviewerId: string
): Observable<GithubPullRequest[]> {
return this.http
.get<{ data: GithubPullRequest[] }>(`https://api.github.com/repos/${repositoryId}/pulls`)
.pipe(
map(res => res.data),
map(prs => prs.filter(pr => pr.reviewers.some(reviewer => reviewer.id === reviewerId)))
);
}
}
컨트롤러와 서비스 분리
import { Controller, Get, HttpService, Param } from '@nestjs/common';
import { GithubPullRequest } from './github-pull-request';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { GithubService } from 'app/infrastructure';
@Controller()
export class RepositoryStatisticsController {
constructor(private githubService: GithubService) {}
@Get('repository/:repositoryId/reviewer/:reviewerId/pending-prs')
getReviewerPendingPrs(
@Param('repositoryId') repositoryId: string,
@Param('reviewerId') reviewerId: string
): Observable<GithubPullRequest[]> {
getReviewerPendingPrs(id: string): Observable<number> {
return this.githubService.getReviewerPendingPrs(repositoryId, reviewerId);
}
}
import { HttpService, Injectable } from '@nestjs/common';
import { GithubPullRequest } from './github-pull-request';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class GithubService {
constructor(private http: HttpService) {}
getReviewerPendingPrs(repositoryId: string, reviewerId: string): Observable<GithubPullRequest[]> {
return this.http
.get<{ data: GithubPullRequest[] }>(`https://api.github.com/repos/${id}/pulls`)
.pipe(
map(res => res.data),
map(prs => prs.filter(pr => pr.reviewers.some(reviewer => reviewer.id === reviewerId)))
);
}
}
의존성 역전 원칙 구현
컨트롤러에서 구현을 적절하게 숨겼다고 말할 수 있으려면 추상화를 도입해야 합니다.
TypeScript를 사용하면 모든 Type을 구현할 수 있으므로 추상 클래스가 될 수 있습니다.
import { Observable } from 'rxjs';
import { PullRequest } from 'app/model';
export abstract class RepositoryDataFetcher {
abstract getReviewerPendingPrs(repositoryId: string, reviewerId: string): Observable<PullRequest[]>;
}
import { Observable } from 'rxjs';
import { PullRequest } from 'app/model';
export RepositoryDataFetcherToken = Symbol('RepositoryDataFetcher');
export interface RepositoryDataFetcher {
getReviewerPendingPrs(repositoryId: string, reviewerId: string): Observable<PullRequest[]>;
}
주 : 인터페이스는 런타임에 존재하지 않으므로, 값 타입으로 사용할 수 없습니다.
변환 후 인터페이스가 더 이상 존재하지 않아 객체 값이 비어 있습니다.
문자열 키를 값으로 사용하고 데코레이터를 삽입하여 문제에 대한 해결책이 있습니다.
@Module({
controllers: [ UsersController ],
components: [
{ provide: 'IUserRepository', useClass: UserRepository }
],
})
export class UserController {
constructor(@Inject('IUserRepository') private userService: IUserRepository) {}
}
추상화는 더 이상 리턴 타입으로 GithubPullRequest에 의존할 수 없습니다.
이것은 원칙의 두 번째와 관련이 있습니다.
PullRequest라는 추상화를 도압합니다.
import { Controller, Get, Inject, Param } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { RepositoryDataFetcher, RepositoryDataFetcherToken } from 'app/interfaces';
import { PullRequest } from 'app/model';
@Controller()
export class RepositoryStatisticsController {
// as an abstract class
constructor(private repositoryDataFetcher: RepositoryDataFetcher) {}
// or as an interface
constructor(
@Inject(RepositoryDataFetcherToken) private repositoryDataFetcher: RepositoryDataFetcher
) {}
@Get('repository/:repositoryId/reviewer/:reviewerId/pending-prs')
getReviewerPendingPrs(
@Param('repositoryId') repositoryId: string,
@Param('reviewerId') reviewerId: string
): Observable<PullRequest[]> {
getReviewerPendingPrs(id: string): Observable<number> {
return this.repositoryDataFetcher.getReviewerPendingPrs(repositoryId, reviewerId);
}
}
import { HttpService, Injectable } from '@nestjs/common';
import { GithubPullRequest, GithubPullRequestMapper } from './github-pull-request';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PullRequest } from 'app/model';
import { RepositoryService } from 'app/interfaces';
@Injectable()
export class GithubRepositoryService implements RepositoryDataFetcher {
constructor(private http: HttpService) {}
getReviewerPendingPrs(repositoryId: string, reviewerId: string): Observable<PullRequest[]> { {
return this.http
.get<{ data: GithubPullRequest[] }>(`https://api.github.com/repos/${id}/pulls`)
.pipe(
map(res => res.data),
map(prs => prs.filter(pr => pr.reviewers.some(reviewer => reviewer.id === reviewerId)),
map(res => res.data.map(GithubPullRequestMapper.toModel))
);
}
}
의존성 주입 시스템과 결합하기
추상 클래스 사용 버전
import { HttpModule, Module } from '@nestjs/common';
import { RepositoryDataFetcher } from 'app/interfaces';
import { GithubRepositoryService } from './github-repository.service';
@Module({
imports: [HttpModule],
providers: [
GithubRepositoryService,
{ provide: RepositoryDataFetcher, useExisting: GithubRepositoryService }
],
exports: [RepositoryDataFetcher]
})
export class GithubInfrastructureModule {}
인터페이스 사용 버전
주 : 원치않는 데이터 경합(상태 공유) 방지를 위해서인것 같음
import { HttpModule, Module } from '@nestjs/common';
import { RepositoryDataFetcherToken } from 'app/interfaces';
import { GithubRepositoryService } from './github-repository.service';
@Module({
imports: [HttpModule],
providers: [
GithubRepositoryService,
{ provide: RepositoryDataFetcherToken, useExisting: GithubRepositoryService }
],
exports: [RepositoryDataFetcherToken]
})
export class GithubInfrastructureModule {}
import { Module } from '@nestjs/common';
import { RepositoryStatisticsController } from './repository-statistics.controller';
import { GithubInfrastructureModule } from 'app/infrastructure-github';
@Module({ imports: [GithubInfrastructureModule], controllers: [RepositoryStatisticsController] })
export class RepositoryStatisticsModule {}
다양한 저수준 모듈을 사용할 수 있는 유연성
Hexagonal Architecture를 따를 때 인프라 모듈과 완전히 독립적인 고수준 모듈을 만들고 싶을 수 있습니다.
그런 다음 인프라 모듈을 동적(dynamic)으로 만드는 것을 고려할 수 있습니다.
환경 변수에 따라 다른 인프라 모듈 임포트하기
모듈 import를 구체적인 모듈 임포트에서 로직을 포함한 import로 변경
import { DynamicModule, Module, ModuleMetadata } from '@nestjs/common';
import { RepositoryStatisticsController } from './repository-statistics.controller';
@Module({
controllers: [RepositoryStatisticsController]
})
export class RepositoryStatisticsModule {
static withInfrastructure(
infrastructure: ModuleMetadata['imports']
): DynamicModule {
infrastructure = infrastructure ?? [];
return {
module: RepositoryStatisticsModule,
imports: [...infrastructure],
};
}
}
import { Module } from '@nestjs/common';
import { RepositoryStatisticsModule } from './repository-statistics.module';
import { GithubInfrastructureModule } from 'app/infrastructure-github';
import { BitbucketInfrastructureModule } from 'app/infrastructure-bitbucket';
const infrastructure =
process.env.provider === 'BITBUCKET'
? [BitbucketInfrastructureModule]
: [GithubInfrastructureModule];
@Module({
imports: [RepositoryStatisticsModule.withInfrastructure(infrastructure)],
})
export class AppModule {}
import { Module } from '@nestjs/common';
import { RepositoryStatisticsController } from './repository-statistics.controller';
import { GithubInfrastructureModule } from 'app/infrastructure-github';
import { BitbucketInfrastructureModule } from 'app/infrastructure-bitbucket';
@Module({
imports: [
...(process.env.provider === 'BITBUCKET'
? [BitbucketInfrastructureModule]
: [GithubInfrastructureModule]),
],
controllers: [RepositoryStatisticsController],
})
export class RepositoryStatisticsModule {}
주 : 구체적인 모듈을 가져오는 로직을 다른 모듈의 정적 메서드로 빼냄
상위 모듈이 아닌 하위 모듈에서 처리
개인적으로 이 패턴이 제일 낫다고 봄
import { Module } from '@nestjs/common';
import { RepositoryStatisticsController } from './repository-statistics.controller';
import { RepositoryStatisticsInfrastructureModule } from './repository-statistics-infrastructure.module';
@Module({
imports: [RepositoryStatisticsInfrastructureModule.register()],
controllers: [RepositoryStatisticsController],
})
export class RepositoryStatisticsModule {}
언제 DIP를 사용하나요?
옵션 열어두기(확장성)
테스팅
업무 나누기
정리
'FrontEnd' 카테고리의 다른 글
이벤트 루프에 대한 이해도를 파악할 수 있는 면접질문 (0) | 2022.10.23 |
---|---|
리액트의 의존성 주입 [NestJs의 모듈로 살펴보는] (0) | 2022.10.23 |
타입스크립트 데코레이터 완벽 가이드[A Complete Guide to TypeScript Decorators] (0) | 2022.10.22 |
Remix로 알아보는 전역 상태 관리와 프론트엔드 개발의 미래 (2) | 2022.10.21 |
리액트를 위한 이벤트 버스🚌 [Event Bus for React] (0) | 2022.10.20 |