글의 목적

NestJS 기반의 헥사고날 아키텍처 예제
간단한 사용자 정보를 외부 시스템과 연동해 조회하는 구조를 구현
이 예제는 비즈니스 로직과 외부 연동을 분리하여 유지보수성을 높이는 것을 목적으로 함

nestjs 기반 예제.

1. user.module.ts

import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { ExternalUserAdapter } from './adapters/external-user.adapter';

@Module({
  controllers: [UserController],
  providers: [UserService, ExternalUserAdapter],
})
export class UserModule {}

2. user.service.ts

import { Injectable } from '@nestjs/common';
import { User } from './user.interface';
import { ExternalUserPort } from './ports/external-user.port';

@Injectable()
export class UserService {
  constructor(private readonly externalUserPort: ExternalUserPort) {}

  async getUserInfo(userId: string): Promise<User> {
    // 비즈니스 로직에서 외부 시스템을 직접 호출하지 않고 포트를 통해 호출
    return this.externalUserPort.fetchUser(userId);
  }
} 

3. ports/external-user.port.ts


export abstract class ExternalUserPort {
  abstract fetchUser(userId: string): Promise<User>;
}

4. adapters/external-user.adapter.ts

import { Injectable } from '@nestjs/common';
import { ExternalUserPort } from '../ports/external-user.port';
import { User } from '../user.interface';
import axios from 'axios';

@Injectable()
export class ExternalUserAdapter implements ExternalUserPort {
  async fetchUser(userId: string): Promise<User> {
    try {
      const response = await axios.get(`https://external-api.com/users/${userId}`);
      return response.data;
    } catch (error) {
      console.error('Error fetching user from external API:', error);
      throw error;
    }
  }
}

5. user.interface.ts

export interface User {
  id: string;
  name: string;
  email: string;
}

6. user.controller.ts

import { Controller, Get, Param } from '@nestjs/common';
import { UserService } from './user.service';
import { User } from './user.interface';

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

  @Get(':userId')
  async getUser(@Param('userId') userId: string): Promise<User> {
    return this.userService.getUserInfo(userId);
  }
}

포트와 어댑터의 자세한 설명

  1. 포트 (Port): 포트는 외부 시스템과의 통신을 추상화하는 인터페이스 역할을 합니다. 여기서는 ExternalUserPort가 포트로 사용됩니다.

    • 포트는 비즈니스 로직(UserService)이 외부와 어떻게 연동되는지 몰라도 되도록 도와줍니다.
    • 포트는 비즈니스 로직과 외부 시스템 사이에 인터페이스 역할을 하기 때문에, 비즈니스 로직은 이 포트를 통해서만 외부 데이터를 접근합니다.
    • 예를 들어, UserService는 외부에서 사용자를 가져오길 원하지만, 실제로 외부 시스템이 어떻게 구현되어 있는지는 알 필요가 없습니다. 대신 ExternalUserPort라는 인터페이스만 사용하여 데이터를 요청합니다.
  2. 어댑터 (Adapter): 어댑터는 포트를 구현한 실제 클래스입니다. 여기서는 ExternalUserAdapter가 어댑터로 사용됩니다.

    • 어댑터는 실제 외부 시스템과의 통신을 담당합니다. 즉, 포트가 정의한 인터페이스를 구현하여 외부 시스템의 세부사항을 처리합니다.
    • 이 예제에서는 axios를 사용해 외부 API와 실제로 통신하며, 외부 사용자 데이터를 가져옵니다.
    • 어댑터가 있기 때문에, 비즈니스 로직은 외부 시스템의 세부 사항을 몰라도 됩니다. 외부 API의 URL이 변경되거나 요청 방식이 바뀌어도, 이 어댑터만 수정하면 됩니다.
  3. UserService는 비즈니스 로직을 처리하며, 외부 시스템과의 통신을 포트(ExternalUserPort)를 통해 수행합니다.

  4. ExternalUserPort는 외부 연동을 위한 인터페이스(포트)로, 비즈니스 로직(UserService)이 외부 API의 세부사항을 알 필요 없이 동작할 수 있도록 해줍니다.

  5. ExternalUserAdapter는 실제로 외부 API와 통신하는 어댑터로, 포트를 구현합니다. 포트와 어댑터를 통해 비즈니스 로직과 외부 연동이 분리됩니다.

구조의 장점