정리

Middleware

  • 역할: 요청이 컨트롤러에 도달하기 전에 요청을 검증하고 승인하거나 로그 기록, 캐싱과 같은 작업을 수행하는 데 사용됩니다.
  • 사용 시점: 요청이 들어올 때 가장 먼저 실행됩니다.
  • 예시: 로깅, 인증, 요청 본문 파싱 등.
  • 구현 방법?: NestMiddleware 인터페이스를 구현합니다.
  • 장점: 코드 재사용성을 높이고, 여러 컨트롤러 및 라우트에 공통적으로 적용할 수 있는 로직을 구현하는 데 유용합니다.
  • 예시: 인증, 권한 부여, CORS 처리, 요청 로깅 등
  • 장점: 코드 재사용성을 높이고, 여러 컨트롤러 및 라우트에 공통적으로 적용할 수 있는 로직을 구현하는 데 유용합니다.

Guard

  • 역할 : 인증 및 권한 부여와 같은 보안 관련 작업에 사용됩니다. 특정 조건(예: 사용자 로그인 여부, 권한 확인)이 충족되는 경우에만 요청이 다음 단계로 진행되도록 합니다. 
  • 사용시점 : middleware 가 실행된 후에 실행
  • 구현방법? : CanAtive 인터페이스를 구현
  • 예시 : JWT 인증, RBAC(Role-Based Access Control) 구현, 특정 사용자만 특정 리소스에 액세스하도록 제한 (권한 검사)
  • 장점: 보안을 강화하고 민감한 데이터에 대한 부적절한 액세스를 방지하는 데 도움이 됩니다.

Interceptor

  • 역할: 요청 처리 전후에 추가 로직을 바인딩하거나, 반환된 결과나 발생한 예외를 수정할 수 있습니다.
    • 요청을 처리하기 전후로 추가적인 로직을 실행할 수 있도록 하며, 응답 변환, 추가 로깅, 에러 캐칭 등의 기능을 수행할 수 있습니다
    • 요청 및 응답 데이터를 변환하고, 로깅을 수행하거나, 성능을 향상시키는 작업을 수행하는 데 사용됩니다.
  • 사용 시점: Guard 다음에 실행됩니다.
  • 예시: 로깅, 캐싱, 응답 데이터 변환.
  • 구현 방법: NestInterceptor 인터페이스를 구현합니다.
  • 예시 : 로깅 캐싱, 응답 데이터 변환, 응답 시간 측정, 응답 헤더 추가, 응답 데이터 포맷 변환, 로깅
  • 장점 : 코드를 모듈화하고 재사용 가능하게 하며, 비즈니스 로직을 컨트롤러 코드와 분리하는 데 도움이 됩니다.

Pipe

  • 역할: 입력 데이터를 변환하거나 유효성을 검사합니다.
  • 사용 시점: Interceptor 다음에 실행됩니다.
  • 예시: 데이터 변환, 유효성 검사, 문자열 정리, 숫자 변환, 날짜 형식 변환, 데이터 유효성 검사
  • 구현 방법: PipeTransform 인터페이스를 구현합니다.
  • 장점 :코드 오류를 줄이고 데이터 무결성을 보장하는 데 도움이 됩니다.

Filter

  • 역할: 발생한 예외를 처리하고, 사용자에게 적절한 응답을 반환합니다.
  • 사용 시점: 컨트롤러나 서비스에서 예외가 발생했을 때 실행됩니다.
  • 예시: 예외 로깅, 사용자 친화적인 에러 메시지 반환, HTTP 오류 처리, 사용자 정의 오류 메시지 제공, 로깅
  • 구현 방법: ExceptionFilter 인터페이스를 구현합니다.
  • 장점: 애플리케이션의 안정성을 유지하고 예상치 못한 오류로 인해 사용자가 불편을 겪는 것을 방지하는 데 도움이 됩니다.

코드

user.service.ts

// imports 생략
// User 엔티티 와 dto 코드 생략

@Injectable()
export class UserService {
  constructor(
    private dataSource: DataSource,
    @InjectRepository(User) private userRepository: Repository<User>,
    private readonly configService: ConfigService,
    private readonly authService: AuthService,
  ) {}
  
  // 동시성 문제를 테스기하기 위해 추가
  delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  async signUp(signUpRequestDto: SignUpRequestDto): Promise<any> {
    const { password, email, name } = signUpRequestDto;
    const queryRunner = this.dataSource.createQueryRunner();

    await queryRunner.connect();
    await queryRunner.startTransaction();
    try {
      // lock 사용 -> 다른 트랜잭션이 같은 이메일의 사용자를 생성하는 것을 방지
      // pessimistic_write : 데이터를 처음 읽을 때 락을 걸고 트랜잭션이 끝날 때까지 락을 유지하는 방법 사용
      let user = await queryRunner.manager.findOne(User, 
        { where: { email }, lock: { mode: 'pessimistic_write' } });
      if (user) {
        throw new Error('Email already exists');
      }
      const saltRounds = this.configService.get<string>('SALT_ROUNDS');
      if (saltRounds === undefined) {
        throw new Error('SALT_ROUNDS is not defined in the configuration.');
      }
      const hashedPassword = await bcrypt.hash(password, parseInt(saltRounds));
      user = new User({ name, email, password: hashedPassword });

      await this.delay(5000); // 동시성 문제를 테스기하기 위해 추가
      
      const newUser = await queryRunner.manager.save(user);
      await queryRunner.commitTransaction();
      return new SignUpResponseDto({ ...newUser });
    } catch (error) {
      await queryRunner.rollbackTransaction();
      throw error;
    } finally {
      // 리소스 해제 필수
      await queryRunner.release();
    }
  }
}

 

 

Postman 을 이용하여 같은 유저 데이터를 전송한 결과

첫번째 요청

{
    "id": 1,
    "email": "test_lock@email.com",
    "name": "Tester"
}

 

두번째 요청

{
    "statusCode": 500,
    "message": "Internal server error"
}

- console 화면

https://github.com/typestack/class-validator?tab=readme-ov-file#usage

ValidationPipe 를 사용하기전에 아래 명령어를 이용하여  필요한 라이브러리를 설치한다.

$ npm i --save class-validator class-transformer

 

위의 라이브러리에서 데코레이터 @IsString() 과 같은 것을 호출하여 검증을 하게된다.

 

https://docs.nestjs.com/pipes

위 그림을 보면 ValidationPipe 는 Controller 에게 요청을 할 때만 작동을 하게되는 구조로 보인다.

그러므로 서비스 단에서 엔티티를 생성할 때 작성해둔 데코레이터가 해당 역할을 안하게 되는 것 같다.

 

엔티티를 생성할 때 검증을 하는 방법

방법 1 검증하기 위한 private 함수 생성

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { IsInt, Max, Min, validate } from 'class-validator';

@Entity()
export class TestEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @IsInt()
  @Min(0)
  @Max(10)
  @Column()
  rating: number;

  constructor(rating: number) {
    this.validate(rating);
    this.rating = rating;
  }
  
  private validate(rating: number){
    if (typeof rating !== 'number'){
      throw new Error();
    }
    if (rating < 0 || rating > 10){
      throw new Error();
    }
  }
}

 

 

방법 2  validate 라는 함수를 이용

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { IsInt, Max, Min, validate } from 'class-validator';

@Entity()
export class TestEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @IsInt()
  @Min(0)
  @Max(10)
  @Column()
  rating: number;

  constructor(rating: number) {
    this.rating = rating;

    validate(this).then((errors) => {
      // errors is an array of validation errors
      if (errors.length > 0) {
        console.log('validation failed. errors: ', errors);
      } else {
        console.log('validation succeed');
      }
    });
  }
}

 

 

방법 3 validateOrReject 를 이용

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { IsInt, Max, Min, validate, validateOrReject } from 'class-validator';

@Entity()
export class TestEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @IsInt()
  @Min(0)
  @Max(10)
  @Column()
  rating: number;

  constructor(rating: number) {
    this.rating = rating;

    validateOrReject(this).catch((errors) => {
      console.log('Promise rejected (validation failed). Errors: ', errors);
    });
  }
}

/* 에러 발생시 출력 예시

Promise rejected (validation failed). Errors:  [
  ValidationError {
    target: TestEntity { rating: 15 },
    value: 15,
    property: 'rating',
    children: [],
    constraints: { max: 'rating must not be greater than 10' }
  }
]

*/

 

참고자료

https://github.com/typestack/class-validator?tab=readme-ov-file#usage

https://docs.nestjs.com/techniques/validation

 

 

Insert 와 createQueayBuilder().insert 를 이용하면
하나의 레코드 혹은 여러 레코드를 한번에 데이터베이스에 입력이 가능하다.

 

공식문서에서는

insert 를 할때는 createQueayBuilder().insert 를 이용한 방법이 성능적인 측면에서 가장 효율적이다.

[https://orkhan.gitbook.io/typeorm/docs/insert-query-builder]

 

정리

  • 간단한 대량 삽입: insert 메서드를 사용하면 코드가 간결하고 이해하기 쉬움.
  • 복잡한 대량 삽입: createQueryBuilder는 복잡한 SQL 쿼리와 조건을 처리하는 데 더 적합
  • 성능: 대량의 데이터를 삽입할 때는 createQueryBuilder가 더 나은 성능을 발휘

 

insert  사용 예시

mport { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async createUsers() {
    const users = [
      { name: 'John Doe', email: 'john.doe@example.com' },
      { name: 'Jane Doe', email: 'jane.doe@example.com' },
      { name: 'Alice', email: 'alice@example.com' },
    ];

    await this.userRepository.insert(users);
  }
}

createQueryBuilder 사용 예시

mport { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepository: Repository<User>,
  ) {}

  async createUsers() {
    const users = [
      { name: 'John Doe', email: 'john.doe@example.com' },
      { name: 'Jane Doe', email: 'jane.doe@example.com' },
      { name: 'Alice', email: 'alice@example.com' },
    ];
    
    await this.userRepository.createQueryBuilder()
      .insert()
      .into(User)
      .values(users)
      .orIgnore() // 오류를 무시하고 유효한 데이터가 포함된 행만 삽입
      .execute();
  }
}

 

create

예시  email  검증

// 'class-validator' 패키지에서 필요한 클래스와 함수를 가져옵니다.
import {
  registerDecorator,
  ValidationOptions,
  ValidatorConstraint,
  ValidatorConstraintInterface,
  ValidationArguments,
} from 'class-validator';

// 이것은 특정 것을 검증하기 위한 데코레이터입니다. 여기서는 이메일을 검사하고 있습니다.
@ValidatorConstraint({ async: false })
class IsCustomEmailConstraint implements ValidatorConstraintInterface {
  // 이 'validate' 함수는 제공된 이메일이 유효한지 확인합니다.
  validate(email: string, args: ValidationArguments) {
    // 이메일이 유효하다고 간주되려면 일치해야 하는 패턴입니다.
    const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/;
    // 이메일이 패턴과 일치하지 않는지 확인
    if (!emailRegex.test(email)) {
      // 일치하지 않으면 '잘못된 형식'이라고 말합니다.
      args.constraints[0] = 'invalidFormat';
      return false;
    }

    // 이메일의 첫 부분(‘@’ 전)에 특수 문자가 있는지 검사합니다.
    const specialChars = /[!#$%&'*+/=?^_`{|}~-]/;
    const localPart = email.split('@')[0];

    // 이메일의 첫 부분의 첫 번째 또는 마지막 문자가 특수 문자인지 확인합니다.
    if (specialChars.test(localPart[0]) || specialChars.test(localPart[localPart.length - 1])) {
      args.constraints[0] = 'specialCharFirstOrLast';
      return false;
    }

    // 이메일의 첫 부분에서 두 특수 문자가 나란히 있는지 확인합니다.
    for (let i = 0; i < localPart.length - 1; i++) {
      if (specialChars.test(localPart[i]) && specialChars.test(localPart[i + 1])) {
        args.constraints[0] = 'consecutiveSpecialChars';
        return false;
      }
    }

    // 모든 검사를 통과하면, 이메일은 유효합니다.
    return true;
  }

  // 이 함수는 무엇이 잘못되었는지에 따라 기본 오류 메시지를 제공합니다.
  defaultMessage(args: ValidationArguments) {
    const failureReason = args.constraints[0];
    switch (failureReason) {
      case 'invalidFormat':
        return '이메일 형식이 잘못되었습니다. 표준 이메일 형식(예: user@example.com)을 따라야 합니다.';
      case 'specialCharFirstOrLast':
        return '이메일이 유효하지 않습니다. 이메일 주소의 첫 번째 또는 마지막 문자로 특수 문자가 올 수 없습니다.';
      case 'consecutiveSpecialChars':
        return '이메일이 유효하지 않습니다. 특수 문자가 연속해서 두 번 이상 나타날 수 없습니다.';
      default:
        return '이메일이 유효하지 않습니다.';
    }
  }
}

// 이 함수는 사용자 정의 이메일 검증기를 생성하기 위해 사용됩니다.
export function IsEmailCustom(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    // 이 코드는 클래스의 속성에 사용자 정의 이메일 검증기를 등록합니다.
    registerDecorator({
      target: object.constructor, 
      propertyName: propertyName, 
      options: validationOptions, 
      constraints: [],            
      validator: IsCustomEmailConstraint, 
    });
  };
}

 

ValidatorConstraintInterface

  커스텀 validation 로직을 제공하기 위해 꼭 구현해야하는 인터페이스 

→ 자신이 만든 custom valdators (검증자) 에 있는 custion validation(검증) 규칙을 제공하기 위해 꼭 구현해야하는 interface

→ implements 를 이용하여 해당 속성을 다른 클래스에 주입

 

ValidationArguments

→ 유효성 검사 과정에 대한 정보를 제공하기 위해 사용

→ validation 규칙을 확인하는 과정 중에 보내고 싶은 메세지를 저장

추가 코드 설명 : IsEmailCustom

IsEmailCustom 함수는 이메일 주소의 유효성을 검사하는 사용자 정의 검증기를 적용하기 위해 사용됩니다. 이 함수는 다음과 같이 구성되어 있습니다:

  1. 함수 인자: validationOptions - 이것은 선택적 인자로, 검증기의 동작을 설정할 수 있는 옵션을 제공합니다. 예를 들어, 특정 메시지를 표시하거나, 검증 실패 시 행동을 정의할 수 있습니다.
  2. 반환되는 함수: 이 함수는 object (대상 객체)와 propertyName (속성 이름)을 인자로 받습니다. 이 두 인자를 사용하여 특정 클래스의 특정 속성에 검증기를 적용합니다.
  3. registerDecorator 함수: 이 내장 함수는 실제로 검증기를 해당 속성에 연결하는 역할을 합니다. 여기서는 몇 가지 주요 옵션을 설정합니다:
    • target: 검증기를 적용할 클래스의 생성자입니다. 즉, 검증기가 어떤 클래스의 어떤 속성에 적용될지 정의합니다.
      • object.constructor는 JavaScript에서 매우 중요한 개념 중 하나, object.constructor는 해당 객체의 생성자 함수를 참조합니다. 생성자 함수는 객체를 생성하고 초기화하는 데 사용되는 특별한 메소드입니다.
    • propertyName: 검증기가 적용될 속성의 이름입니다.
    • options: 검증 시 적용할 추가 옵션들입니다. (예:  에러메세지)
    • constraints: 검증기에 전달할 추가적인 제약 조건을 배열 형태로 제공합니다.
    • validator: 실제로 이메일 유효성을 검사할 로직을 포함하고 있는 파일을 지정합니다. : IsCustomEmailConstraint 클래스

object.constructor의 역할

  • registerDecorator에서 object.constructor를 사용하는 이유는, 해당 데코레이터를 적용할 "클래스"를 정확하게 지정하기 위함입니다. 클래스의 인스턴스(즉, 객체)가 제공될 때, object.constructor를 통해 이 객체가 어떤 클래스에서 생성되었는지를 알 수 있습니다. 이 정보를 사용하여 클래스 레벨에서 속성이나 메소드에 특정 데코레이터를 연결할 수 있습니다.
  • 예시
class Person {
    constructor(name) {
        this.name = name;
    }
}

let joey = new Person("joey");

joey.constructor // Person class 의 구조가 출력됩니다.

데코레이터 사용 예시 

export class SignUpRequestDto {
  @IsEmailCustom()
  email: string;
}

 @IsEmailCustom() 은 자동으로 registerDecorator를 호출하여, SignUpRequestDto 클래스의 email 속성에 유효성 검사기를 적용합니다. 즉, target 파라메터에 SignUpRequestDto 클래스가 할당되어 SignUpRequestDto 구조를 파악하고, propertyName 에 할당된 email에 유효성 검사기인 "IsCustomEmailConstraint" 이 적용 되어집니다.

 

 

참고자료

- https://medium.com/@admin_1497/nestjs%EC%97%90%EC%84%9C-custom-validator-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0-ffd94c5447f2#26a5

- class-validator 라이브러리

 

+ Recent posts