글의 목적
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 {}
- 이 모듈은 NestJS에서 모든 관련 클래스들을 한데 묶는 역할을 합니다.
- controllers: 사용자 요청을 처리하는 컨트롤러를 정의합니다. 여기서 UserController는 사용자로부터 요청을 받고 응답하는 역할을 합니다.
- providers: 서비스와 어댑터를 제공하여, 이 모듈 내에서 사용 가능하도록 합니다. 여기에는 비즈니스 로직을 수행하는 UserService와 실제 외부와 통신하는 ExternalUserAdapter가 포함됩니다.
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);
}
}
- UserService는 비즈니스 로직을 처리하는 부분입니다. 여기서는 사용자 정보를 가져오는 기능을 담당합니다.
- 포트를 통해 호출: 외부 시스템과의 연결을 직접 하지 않고, 포트(ExternalUserPort)를 통해 요청합니다. 이렇게 하면 외부 시스템이 변경되더라도 이 서비스 코드는 변경할 필요가 없습니다.
3. ports/external-user.port.ts
- 외부 연동을 위한 포트 (인터페이스)
export abstract class ExternalUserPort {
abstract fetchUser(userId: string): Promise<User>;
}
- ExternalUserPort는 외부 시스템과 통신하기 위한 인터페이스입니다.
- 추상 클래스로 작성되어 있으며, 여기서 정의된 메서드 fetchUser는 실제 구현(어댑터)에서 채워집니다.
- UserService는 이 포트를 사용하여 데이터를 가져오기 때문에, 외부 시스템이 어떤 방식으로 구현되었는지 알 필요가 없습니다.
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;
}
}
}
- ExternalUserAdapter는 외부 API와 실제로 통신하는 부분입니다. 여기서는 axios를 사용해 외부의 사용자 정보를 가져옵니다.
- fetchUser 메서드는 ExternalUserPort에서 정의된 인터페이스를 구현한 것입니다. 즉, 이 메서드는 외부 API와의 통신을 수행합니다.
- 이렇게 함으로써, 외부 시스템과의 연결 방법이 변경되더라도 이 어댑터만 수정하면 됩니다.
5. user.interface.ts
- 사용자 정보를 정의하는 인터페이스
export interface User {
id: string;
name: string;
email: string;
}
- User 인터페이스는 사용자 정보의 구조를 정의합니다. 각 사용자 객체는 id, name, email을 포함합니다.
- 이렇게 정의된 인터페이스를 통해 데이터 구조를 명확하게 하고, 타입스크립트의 타입 검사를 활용할 수 있습니다.
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);
}
}
- UserController는 사용자 요청을 받아 처리하는 부분입니다. 여기서 사용자는 특정 ID로 사용자 정보를 요청할 수 있습니다.
- @Get(':userId'): 사용자가 'user/{userId}' 형식으로 요청을 보내면, 이 경로에 매핑된 메서드가 호출됩니다.
- userService.getUserInfo(userId): 이 메서드는 UserService를 호출하여 사용자 정보를 가져옵니다.
- 컨트롤러는 주로 사용자 요청을 받고, 그에 대한 응답을 처리하는 역할을 합니다. 따라서, 비즈니스 로직은 서비스(UserService)에 위임합니다.
포트와 어댑터의 자세한 설명
-
포트 (Port): 포트는 외부 시스템과의 통신을 추상화하는 인터페이스 역할을 합니다. 여기서는 ExternalUserPort가 포트로 사용됩니다.
- 포트는 비즈니스 로직(UserService)이 외부와 어떻게 연동되는지 몰라도 되도록 도와줍니다.
- 포트는 비즈니스 로직과 외부 시스템 사이에 인터페이스 역할을 하기 때문에, 비즈니스 로직은 이 포트를 통해서만 외부 데이터를 접근합니다.
- 예를 들어, UserService는 외부에서 사용자를 가져오길 원하지만, 실제로 외부 시스템이 어떻게 구현되어 있는지는 알 필요가 없습니다. 대신 ExternalUserPort라는 인터페이스만 사용하여 데이터를 요청합니다.
-
어댑터 (Adapter): 어댑터는 포트를 구현한 실제 클래스입니다. 여기서는 ExternalUserAdapter가 어댑터로 사용됩니다.
- 어댑터는 실제 외부 시스템과의 통신을 담당합니다. 즉, 포트가 정의한 인터페이스를 구현하여 외부 시스템의 세부사항을 처리합니다.
- 이 예제에서는 axios를 사용해 외부 API와 실제로 통신하며, 외부 사용자 데이터를 가져옵니다.
- 어댑터가 있기 때문에, 비즈니스 로직은 외부 시스템의 세부 사항을 몰라도 됩니다. 외부 API의 URL이 변경되거나 요청 방식이 바뀌어도, 이 어댑터만 수정하면 됩니다.
-
UserService는 비즈니스 로직을 처리하며, 외부 시스템과의 통신을 포트(ExternalUserPort)를 통해 수행합니다.
-
ExternalUserPort는 외부 연동을 위한 인터페이스(포트)로, 비즈니스 로직(UserService)이 외부 API의 세부사항을 알 필요 없이 동작할 수 있도록 해줍니다.
-
ExternalUserAdapter는 실제로 외부 API와 통신하는 어댑터로, 포트를 구현합니다. 포트와 어댑터를 통해 비즈니스 로직과 외부 연동이 분리됩니다.
구조의 장점
- 외부 연동을 포트와 어댑터로 분리함으로써 비즈니스 로직을 독립적으로 유지할 수 있습니다.
- 새로운 외부 API 연동이 필요할 때, 기존 어댑터를 교체하거나 추가하는 방식으로 손쉽게 확장할 수 있습니다.
- 외부 API의 변경이 있을 경우, ExternalUserAdapter만 수정하면 비즈니스 로직에는 영향을 주지 않으므로 유지보수성이 높아집니다.
- 테스트할 때, 실제 API 호출 대신 ExternalUserPort를 모킹하여 테스트할 수 있어 테스트가 용이합니다.
- 모킹(Mock)을 통해 외부 시스템에 의존하지 않고 비즈니스 로직의 유닛 테스트를 손쉽게 수행할 수 있으며, 시스템의 안정성과 신뢰성을 확보할 수 있습니다.