AdonisJS와 NestJS 비교

flobeeee·2023년 1월 12일
1

Today I Learned

목록 보기
31/35
post-thumbnail

0. 개요

AdonisJS와 NestJS는 모두 JavaScript로 구축된 웹 애플리케이션 프레임워크입니다. 둘 다 확장 가능한 고성능 웹 애플리케이션을 쉽게 구축할 수 있도록 설계되었습니다.

AdonisJS는 Node.js 프레임워크로 구축되었으며 널리 사용되는 PHP 웹 프레임워크인 Laravel에서 크게 영감을 받았습니다. ORM, 쿼리 작성기 및 내장 인증 시스템과 같은 많은 내장 기능이 있습니다. AdonisJS는 또한 Laravel과 유사한 파일 구조 및 명명 규칙을 사용하므로 Laravel에 익숙한 개발자가 AdonisJS를 쉽게 선택할 수 있습니다.

반면에 NestJS는 TypeScript로 구축되었으며 Angular에서 영감을 받았습니다. 코드 구조화에 모듈식 접근 방식을 사용하여 대규모 코드베이스를 쉽게 구성하고 유지 관리할 수 있습니다. 또한 NestJS를 사용하면 Express.js 프레임워크를 기본 기반으로 사용하여 확장 가능한 고성능 웹 애플리케이션을 쉽게 구축할 수 있습니다. NestJS는 또한 데코레이터, 클래스, 파이프 및 가드와 같은 많은 기능을 제공하므로 유지 관리 및 확장 가능한 코드를 쉽게 작성할 수 있습니다.

요약하면 AdonisJS는 Laravel과 더 유사하고 NestJS는 Angular와 더 유사합니다. 두 프레임워크 모두 확장 가능한 고성능 웹 애플리케이션을 구축하는 데 적합하며 사용할 프레임워크의 선택은 개발자의 선호도와 프레임워크에 대한 친숙도에 따라 다릅니다.

AdonisJS and NestJS are both web application frameworks built with JavaScript. They are both designed to make it easy to build scalable, high-performance web applications.

AdonisJS is built with the Node.js framework and heavily inspired by Laravel, a popular PHP web framework. It has a lot of built-in features such as an ORM, a query builder, and a built-in authentication system. AdonisJS also uses a similar file structure and naming conventions as Laravel, making it easy for developers who are familiar with Laravel to pick up AdonisJS.

NestJS, on the other hand, is built with TypeScript and inspired by Angular. It uses a modular approach to structuring code, making it easy to organize and maintain large codebases. NestJS also makes it easy to build scalable, high-performance web applications by using the Express.js framework as its underlying foundation. NestJS also brings in many features like decorators, classes, pipes and guards which makes it easy to write maintainable and scalable code.

In summary, AdonisJS is more similar to Laravel and NestJS is more similar to Angular. Both frameworks are suitable for building scalable, high-performance web applications, and the choice of which one to use depends on the developer's preference and familiarity with the framework.

출처 : ChatGPT

NestJS는 IoC, DI 개념으로는 Spring와 비슷하다.

1. 구조

NestJS

유저와 관련된 파일은 유저 폴더에 모두 집어넣는다.

- users
	- dto
		- create-user.dto.ts
		- update-user.dto.ts
	- users.controller.spec.ts
	- users.controller.ts
	- users.entity.ts
	- users.module.ts
	- users.provider.ts
	- users.service.spec.ts
	- users.service.ts

AdonisJS

역할별로 폴더를 나누고, 각각 파일을 넣는다.

- Controllers/Http
	- UsersController.ts
- Models
	- User.ts
- Repositories
	- UserRepository.ts
- Validators
	- UserValidator.ts

2. Swagger

NestJS

1분만에 붙인다.
옵션은 데코레이터를 활용한다.

npm install --save @nestjs/swagger

// main.ts

import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

AdonisJS

npm adonis5-swagger
문서대로 설치를 하고 나면 페이지는 바로 뜨지만, 내용이 없다.
문서에 있는 샘플코드를 활용하면 에러가 뜬다. (스웨거 띄어쓰기를 안맞춰서 에러가 뜬다.)
아래 작동가능한 샘플코드를 작성했다.

// app/Controllers/Http/UsersController.ts

export default class UsersController {
  /**
   * @swagger
   * /myInfo:
   *   get:
   *     tags:
   *       - 회원
   *     summary: 회원 정보
   *     responses:
   *       200:
   *         description: ok
   */
  public async hello() {
    return 'swagger test'
  }
}

// app/Models/User.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

/**
 * @swagger
 * components:
 *  schemas:
 *   User:
 *     type: object
 *     description: 유저
 *     properties:
 *       id:
 *         type: integer
 *       createdAt:
 *         type: string
 *         format: date-time
 *         description: 생성일
 *       updatedAt:
 *         type: string
 *         format: date-time
 *         description: 수정일
 *       name:
 *         type: string
 *         description: 유저 명
 *       type:
 *         type: string
 *         description: 구분(등급)
 *       status:
 *         type: string
 *         enum: [active, inactive]
 *         description: 상태( active:정상, inactive:차단)
 */
export default class User extends BaseModel {
  @column({ isPrimary: true })
  public id: number

  @column()
  public name: string

  @column()
  public email: string

  @column.dateTime({ autoCreate: true })
  public createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  public updatedAt: DateTime
}


3. 유닛테스트

NestJS

npm run test:watch 명령어를 통해 테스트 코드가 바뀌면 자동으로 테스트가 다시 돈다.

실제 디비에 접근하지 않고 테스트를 하기 때문에,
가짜 서비스로직을 테스트 파일에 생성해서 해당 로직의 호출유무를 확인한다.

컨트롤러와 서비스가 API 계약을 이행하는지 확인(공식문서 오피셜)
ensure that the controller and service fulfill their API contract.

컨트롤러테스트를 하지만, 컨트롤러 각각의 메소드에서 서비스로직을 불러올 때,
가짜 서비스로직을 불러온다.

( 실디비로 테스트 해보려고 연결을 시도했지만, 결국 실패했다.
이 부분이 신기했다. AdonisJs 프레임워크를 주로 사용했던 나는 생소한 테스트 방법이었다.)

const mockService = {
  create: jest
    .fn()
    .mockImplementation((book: CreateBookDto) =>
      Promise.resolve({ id: '1', ...book }),
    ),
  findAll: jest.fn().mockResolvedValue([
    {
      name: '방구석 미술관',
      genre: '예술',
    },
    {
      name: '슬픔의 방문',
      genre: '에세이',
    },
  ]),
  findOne: jest.fn().mockImplementation((id: number) =>
    Promise.resolve({
      name: '클린코드',
      genre: 'IT',
      id,
    }),
  ),
  delete: jest.fn().mockImplementation(() => Promise.resolve({})),
}

beforeEach(async () => {
    const app: TestingModule = await Test.createTestingModule({
      controllers: [BooksController],
      providers: [
        BooksService,
        {
          provide: BooksService,
          useValue: mockService,
        },
      ],
    }).compile()

    booksController = app.get<BooksController>(BooksController)
    booksService = app.get<BooksService>(BooksService)
  })

AdonisJS

.env.testing 파일을 통해 테스트 전용 DB에서 실제 로직을 테스트할 수 있다.
서비스운영중에 에러가 나면, 에러난 데이터를 테스트파일에 넣어서 실제 로직으로 테스트할 수 있는 장점이 있다.
테스트에서 에러가 나면, 서비스로직에서 어느부분이 문제인지 잡아낼 수 있다.

test('postAction', async (assert) => {
    const created = await supertest(BASE_URL).post('/').send({
      'user_id': 'posttest', 
      'email': 'posttest@gmail.com', 
      'password': 'test', 
      'passwordConfirmation': 'test'
    }).expect(201)
    
    assert.equal(created.body.userId, 'posttest')
    assert.equal(created.body.email, 'posttest@gmail.com')

4. 유효성검사

NestJS

main.ts에 pipe를 설정해 유효성을 확인한다.
다양한 옵션을 적용할 수 있는데, 나는 2가지 옵션을 사용해봤다.

  • whitelist
    true로 설정하면 유효성 검사기가 유효성 검사 데코레이터를 사용하지 않는 속성의 유효성 검사(반환) 개체를 제거합니다.
  • forbidNonWhitelisted
    true로 설정하면 화이트리스트에 없는 속성을 제거하는 대신 유효성 검사기가 예외를 발생시킵니다.
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true, // 인자로 받는 것만 들어오게 할 수 있음
      forbidNonWhitelisted: true, // 메세지로 해당 인자는 사용할 수 없다고 안내함
    }),
  )
  await app.listen(3000);
}
bootstrap();

// controller.ts
import { CreateBookDto } from './dto/create-book.dto'

@Post()
postAction(@Body() PostBookDto: CreateBookDto): Promise<Books> {
  return this.booksService.create(PostBookDto)
}

// dto/create-book.dto.ts
import { IsString } from 'class-validator'

export class CreateBookDto {
  @IsString()
  readonly name: string

  @IsString()
  readonly genre: string
}

AdonisJS

validator 파일을 생성하고, controller에서 불러서 사용한다.
유효성 에러시 정의한 메세지가 리턴된다.

// UserController.ts

import CreateUser from 'App/Validators/CreateUserValidator'
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class UsersController {
  public async createUser({ request, response }: HttpContextContract) {
    await CreateUser.postLoginValidator(request)

    return response.ok('ok')
  }
}

// Validators/CreateUserValidator.ts

export default class CreateUserValidator {
  public static async postLoginValidator(request: RequestContract) {
    const validator = {
      schema: schema.create({
        email: schema.string(
          {
            trim: true,
          },
          [rules.email(), rules.exists({ table: 'user', column: 'email' })]
        ),
        password: schema.string({}, []),
      }),
      messages: {
        'email.string': 'email은 string 데이터 형식이어야 합니다.',
        'email.email': '잘못된 email 형식입니다.',
        'email.unique': '중복된 이메일입니다.',
        'email.required': 'email을 입력해 주세요.',
        'password.required': '비밀번호를 입력해 주세요.',
      },
    }
    await request.validate({
      schema: validator.schema,
      messages: validator.messages,
    })
  }
}

5. DB 연결 에러 상황

NestJS

npm run start:dev 로 서버를 켰을 때, DB가 연결이 안되어 있으면 터미널에서 바로 에러메시지가 나온다.

DB 데이터를 내보내는 API를 호출하면, 서버가 아예 켜지지 않아서, 서버연결 에러가 뜬다.

AdonisJS

npm run start 로 서버를 켜도, 에러메시지가 나지 않는다.

DB 데이터를 내보내는 API를 호출하면 에러가 발생한다.

6. end to end test 에서 데이터 밀어넣고 테스트하기

Controller 에 테이블을 truncate 하고 데이터 더미를 밀어넣을 수 있게 API를 만들었다.
NestJS에서 e2e 테스트를 할 때, api를 호출하고 결과를 확인하는 방식을 사용해서,
truncate을 작동하게 만들어놨다.

소스 확인(git hub)

7. Nest의 IoC, DI 개념

NestJS는 Singleton 패턴을 추구해서 인스턴스를 직접 생성하지 않고 모듈을 통해 종속성을 주입(DI)하는 걸 권장한다.
인스턴스를 생성 안했는데 쓴거니까 제어의 역전(IoC)이 된 걸로 볼 수 있다.(내가 하지 않았는데, 이미 되어있어서)

  • singleton 패턴
    인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것!
    메모리 낭비를 방지
    id를 발급하는 경우, 동일한 id를 발급하면 안되니까 사용한다.
    단점으로는 독립적인 테스트를 진행할때 그 객체를 사용하는 게 어렵다. (인스턴스 재사용하면 안되니까)
profile
기록하는 백엔드 개발자

0개의 댓글