NestJS 밋업 : Shall We NestJS? -2

영슈·2023년 6월 14일
0

Nest

목록 보기
3/6
post-thumbnail

https://www.youtube.com/watch?v=AHSHjCVUsu8
위의 밋업 동영상의 내용을 요약한 글이다.
세번째 발표를 두 번째에 작성

NestJS 에서 Hexagonal Architecture 구현하기

팀 스파르타 이동현 주관 발표

  1. Hexagonal Architecture란?
  2. Nest.js 에서 Hexgonal Architecture 를 적용하려면?
  3. 먼저 삽질한 사람의 소소한 제안

Hexagonal Architecture란

  • 아키텍쳐는 견고해야 한다. ( 수정 과 삽입이 용이해야함 )

  • 고수준의 의존성 방향을 지양해야함.

  • 기존 Layered Architecture

  • 각 단계가 서로에게 의존 함.
    => 기존 Layered Architecture 에서 의존성을 역전시킴!
    => 외부에 의존하지 않는 도메인 로직을 가질 수 있음!

  • 핵사고날 아키텍쳐는 기본적으로 외부 & 내부 존재

  • 내부는 도메인 계층으로 , 순수 비즈니스 로직 캡슐화 한 영역

  • 외부는 기존 아키텍쳐 에서 , 도메인 영역 분리한 영역

  • Port / Adapter Pattern

  • Infrastructure 나 Interface 에 영향 받지않는 비즈니스 도메인 코드 작성 할 수 있음.

이론상은 완벽

  1. 기술 스택 변경 용이 ( Mysql -> Postgresql )
  2. 새로운 API 추가 용이
  3. 테스트하기도 쉬움.
  4. 외부 요인으로 인해 도메인 모델 수정 할 필요 X

( 여담 ) 왜 Hexagonal ?

  • 숫자 6에 큰 의미 X
  • 충분히 많은 수 Port 와 Adaptor 그리려다 보니 육각형

Nest.js 에서 Hexgonal Architecture 를 적용하려면

  • 결국 Port 는 Interface
  • 내부 영역에서 사용할 규칙 미리 정의 ( 규칙 하에서 소통 )
    => 외부 영역에 노출하는 유스케이스 대한 규칙 정해놓음.
    ( 내부는 외부에 의존하지 않음 )
  • Adapter 는 Infrastructure 와 Port 사이 Communcatin 담당
  • 다른 인프라스트럭쳐 마다 다른 어댑터가 들어가서 구현

Port

  • Port 는 두 가지 , In-Port , Out-Port 가 존재
  • 도메인으로 들어오는 요청 : IN ( API )
  • 도메인이 외부로 요청하는 것 : OUT ( DB , Messaging )
    Why? : Port 자체에는 차이가 없지만 , Adapter 구현에 차이가 있음1

Domain

export type Applicants = any;
export class Recruiter {
    constructor(
        public userId : string,
        public name : string,
        public email : string,
        public availableApplicants : Applicants[],
        )
  		{}
  	viewResume(applicant:Applicant){
      	applicant.clickNumbers +=1;
    }
  	saveResume(applicant:Applicant){
      applicant.isSaved = true;
    }
  sendInterviewRequest(applicant:Applicant){
    applicant.status = 'interview';
  }
}

In-Port

import { viewResumseRespDto } from '../../adapter/in/web/dto/response/viewResume-response-dto';
import { saveResumeRespDto } from '../../adapter/in/web/dto/response/saveResume-response-dto';
import { sendInterviewRequestRespDto } from '../../adapter/in/web/dto/response/sendInterviewRequest-response-dto';

export interface ApplicantLoadInPort {
  viewResume() : Promise<viewResumeRespDto>;
  saveResume() : Promise<saveResumeRespDto>;
  sendInterviewRequest() : Promise<sendInterviewRequestRespDto>;
}
  • Port 는 In/Out 유사하게 생김.

Out-Port

export interface IRecruiterRepository {
  	findRecruiterById(_id : ObjectId) : Promise<Recruiter>;
  	findApplicantById(_id : ObjectId) : Promise<Applicant>;
  	updateRecruiter(recruiter : Recruiter);
}

In-Adapter

  • 실제 Interface 구현이 Domain 내부에 있는 것이 핵심.
export class ApplicantService implements ApplicantLoadInPort {
  	constructor(
  		@Inject(Logger) private readonly logger : LoggerService ,
        @Inject(ApplicantPersistenceAdapter) private readonly applicantLoadPort : ApplicantLoadPort ,
       	@Inject(ApplicantStatusPersistenceAdapter) private readonly applicantStatusLoader : ApplicantStatusLoader,
        @Inject(InterviewPersistenceAdapter) private readonly interviewPersistenceAdapter : InterviewPersistencePort)
  		{}
 	async findApplicant(resumeId) : Promise<Applicant> {
      	return await this.applicantLoadPort.loadApplicant(resumeId);
    }
}
  • Service 는 In-Port 의 구현체
  • port Interface 에서 정의된 함수 하나하나 구현
  • 서비스 계층은 Out Port 의 사용처 이며 , In Port 의 구체적인 구현처
    => 서비스 계층을 복잡하지 않게 유지하는 게 중요
@Controller('resume')
export clas ApplicantController {
  	constructor(
      @Inject(ApplicantService) private readonly applicantService : ApplicantLoadInPort,
      @Inject(Logger) private readonly logger : LoggerService,){}
  • Controller 는 포트에 의존성 주입해 사용

=> 결론적으론 , 외부로 나가는 의존성 없는 도메인을 가짐.

먼저 삽질한 사람의 소소한 제안

Nosql

  • NoSQl 로 먼저 실험해보자!
    => 복잡한 도메인을 표현력 있는 형태로 저장!
  • sql 은 어쩔수 없이 , 복잡해진 관계로 링크 필드가 생길수 밖에 없음.
    ( 여러 단어로 인해 , 단어의 의미가 무너짐 )
    => Repository 의 수를 줄일수 있음

EventBus : inter-domain communication

  • 헥사곤 아키텍쳐를 준수하면 너무 작성할 코드가 많아질 우려
  • 결국은 , Adapter 와 Port 모두 구현해야하는데 , 매우 비효율적
    => Event Bus 를 이용해 PipeLine 을 줄이자.
export class newInterviewRequestedEvent {
  	constructor(){
      	public readonly name : string,
        public readonly recruiterName : string,
    }
}
  • Event 정의
this.eventBus.publish( 
  new newInterviewRequestedEvent(applicant.name,recruiter.name),
);
  • Event 발행

QnA

  • readonly 와 readwrite 접근은 어떻게 해야하나?
    => CQRS ( Command and Query Responsibility Segregation ) 적 접근
  • Adapter 는 왜 소켓을 위해 사용하는 건가?
    => 어댑터는 도메인이 사용하는 모든 컴포넌트를 위해 존재
  • 참고 자료?
    => "클린 아키텍쳐 구현하기 책" 참조
  • Hexgonal Architecture VS MSA Architecture
    => 헥사곤은 결국 Clean Architecture 지향점 , MSA 로 전환도 쉽게 가능함
    ( 헥사곤 은 Code Level , MSA 는 Service Level )
  • 어댑터를 사용했을때의 장점이란?
    => 도메인에서 In-Port 완료 , 컨트롤러는 주입해 사용만 함.
    ( 의존성 방향이 도메인으로 향함. )
  • 네이밍은 어떻게?
    => In-Port 는 usecase , Out-Port 는 CRUD + Domain 이름

0개의 댓글