icon안동민 개발노트

NestJS 프레임워크


 NestJS는 효율적이고 확장 가능한 서버 사이드 애플리케이션을 구축하기 위한 진보적인 TypeScript 프레임워크입니다.

 Angular에서 영감을 받아 설계되었으며, 객체지향 프로그래밍(OOP), 함수형 프로그래밍(FP), 함수형 반응형 프로그래밍(FRP)의 요소를 결합합니다.

핵심 개념과 아키텍처

 NestJS는 모듈, 컨트롤러, 서비스로 구성된 계층적 구조를 가집니다.

 이 구조는 관심사의 분리와 의존성 주입을 통해 테스트 가능하고 느슨하게 결합된 애플리케이션을 만들 수 있게 합니다.

  1. 모듈 : 애플리케이션의 구조를 조직화
  2. 컨트롤러 : 요청을 처리하고 응답을 반환
  3. 서비스 : 비즈니스 로직 포함
  4. 프로바이더 : 의존성으로 주입될 수 있는 서비스, 리포지토리 등

프로젝트 구조와 주요 컴포넌트

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
 
@Module({
  controllers: [UsersController],
  providers: [UsersService],
})
export class AppModule {}
 
// users.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
 
@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
 
  @Get()
  findAll() {
    return this.usersService.findAll();
  }
 
  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}
 
// users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
 
@Injectable()
export class UsersService {
  private users = [];
 
  findAll() {
    return this.users;
  }
 
  create(createUserDto: CreateUserDto) {
    this.users.push(createUserDto);
    return createUserDto;
  }
}

의존성 주입(DI) 시스템

 NestJS의 DI 시스템은 TypeScript의 데코레이터와 리플렉션을 활용합니다.

import { Injectable } from '@nestjs/common';
 
@Injectable()
export class CatsService {
  private cats: string[] = ['Fluffy', 'Whiskers'];
 
  findAll(): string[] {
    return this.cats;
  }
}
 
@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}
 
  @Get()
  findAll(): string[] {
    return this.catsService.findAll();
  }
}

데코레이터를 활용한 기능 구현

  1. 라우팅
@Get(':id')
findOne(@Param('id') id: string) {
  return `This action returns a #${id} cat`;
}
  1. 미들웨어
import { Injectable, NestMiddleware } from '@nestjs/common';
 
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: Function) {
    console.log('Request...');
    next();
  }
}
  1. 가드
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
 
@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
  1. 인터셉터
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
 
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    console.log('Before...');
    const now = Date.now();
    return next
      .handle()
      .pipe(
        tap(() => console.log(`After... ${Date.now() - now}ms`)),
      );
  }
}

DTO와 엔티티 클래스

import { IsString, IsEmail, IsNotEmpty } from 'class-validator';
 
export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  readonly name: string;
 
  @IsEmail()
  readonly email: string;
}
 
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
 
@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column()
  name: string;
 
  @Column()
  email: string;
}

커스텀 데코레이터

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
 
export const User = createParamDecorator(
  (data: unknown, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  },
);
 
// 사용
@Get('profile')
getProfile(@User() user: UserEntity) {
  return user;
}

설정 관리와 환경 변수

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
 
@Injectable()
export class AppConfigService {
  constructor(private configService: ConfigService) {}
 
  get databaseUrl(): string {
    return this.configService.get<string>('DATABASE_URL');
  }
}

테스트 전략

  1. 단위 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
 
describe('UsersService', () => {
  let service: UsersService;
 
  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UsersService],
    }).compile();
 
    service = module.get<UsersService>(UsersService);
  });
 
  it('should be defined', () => {
    expect(service).toBeDefined();
  });
});
  1. E2E 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
 
describe('AppController (e2e)', () => {
  let app: INestApplication;
 
  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();
 
    app = moduleFixture.createNestApplication();
    await app.init();
  });
 
  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Hello World!');
  });
});

Best Practices

  1. 모듈화 : 기능별로 모듈을 분리하여 관리합니다.
  2. SOLID 원칙 준수 : 단일 책임, 개방-폐쇄, 리스코프 치환, 인터페이스 분리, 의존성 역전 원칙을 적용합니다.
  3. 레이어드 아키텍처 : 프레젠테이션, 비즈니스 로직, 데이터 액세스 레이어를 명확히 분리합니다.
  4. DTO 활용 : 계층 간 데이터 전송에 DTO를 사용하여 타입 안정성을 확보합니다.
  5. 예외 필터 사용 : 전역 예외 필터를 구현하여 일관된 에러 처리를 보장합니다.
  6. 캐싱 전략 : 성능 향상을 위해 적절한 캐싱 전략을 수립합니다.
  7. 마이크로서비스 아키텍처 고려 : 대규모 애플리케이션의 경우 마이크로서비스 아키텍처를 고려합니다.
  8. API 버전 관리 : API 버전을 관리하여 하위 호환성을 유지합니다.
  9. 로깅 및 모니터링 : 종합적인 로깅 및 모니터링 전략을 수립합니다.
  10. 보안 강화 : JWT, CORS 설정, 헬멧 미들웨어 등을 활용하여 보안을 강화합니다.

 NestJS는 TypeScript의 강력한 타입 시스템과 객체지향 프로그래밍의 장점을 결합하여 견고하고 확장 가능한 백엔드 애플리케이션을 구축할 수 있게 해줍니다.

 모듈화된 구조, 의존성 주입, 데코레이터 기반의 메타프로그래밍 등을 통해 코드의 재사용성과 테스트 용이성을 크게 향상시킬 수 있습니다.