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

 

 

const pattern = /^[a-zA-Z0-9\uAC00-\uD7A3\s]*$/
  • ^ : 문자열 시작
  • \uAC00-\uD7A3 : 한글문자 ( 가 부터 힣 까지)
  • \s  : 공백 문자 ( 스페이스, 탭..)
  • * : 한번 이상 반복 가능
  • $ : 문자열의 끝

데이터를 엔티티에 저장할 때 예상되는 문제

동시성 이슈 (Concurrency Issues)

두명 이상의 사용자가 동시에 같은 데이터의 삽입 요청을 했을 때 모든 요청에 담긴 데이터가 중복이 없다고 판단되어 동시에 데이터가 삽입되는 현상.

 
이런 상황을 '레이스 컨디션'이라고 합니다. 레이스 컨디션은 두 개 이상의 프로세스나 스레드가 동시에 같은 데이터에 접근하려고 할 때 발생할 수 있습니다.
 

해결방법

Lock

락은 데이터를 보호하고, 다른 트랜잭션이 동시에 같은 데이터를 수정/삽입 하지 못하게 합니다.

하지만 락을 사용하면 다른 트랜잭션이 락이 해제될 때까지 기다려야 하므로 성능 문제가 발생 할 수 있습니다.

  •  [프로그래밍/SQL] - [ SQL ] Lock  
    • 소극적 락(Pessimistic Locking) : 데이터를 처음 읽을 때 락을 걸고 트랜잭션이 끝날 때까지 락을 유지하는 방법입니다.
    • 적극적 락(Optimistic Locking) : 데이터에 버전 번호나 타임스탬프를 추가하여 데이터를 불러온 후 수정된 경우에만 업데이트가 발생하도록 하는 방법입니다.

MVCC (Multi-version Concurrency Control)

MVCC는 여러 트랜잭션이 동시에 같은 데이터에 접근할 수 있게 합니다. 이는 락을 사용하는 것보다 성능이 좋지만, 데이터 중복 문제를 완전히 해결하지는 못합니다.

 
예를 들어, 한 트랜잭션이 데이터를 확인한 후 다른 트랜잭션이 같은 데이터를 추가하고, 첫 번째 트랜잭션이 데이터를 추가하려 할 때 중복이 발생할 수 있습니다.

MVCC는 동시에 여러 트랜잭션이 데이터베이스에 접근할 때 일관성을 유지하면서 동시성을 높이는 기술입니다. 각 트랜잭션은 데이터의 특정 버전을 보게 되며, 실제 데이터를 직접 변경하는 대신 변경 시점의 스냅샷을 참조합니다. 이렇게 하면 여러 트랜잭션이 동시에 같은 데이터를 읽을 수 있습니다.

 

Unique 데코레이션

@Unique() 데코레이션은 데이터베이스 테이블의 특정 컬럼이나 컬럼 조합에 유니크 제약조건을 적용하여, 그 필드에 중복된 값이 저장되지 않도록 합니다.

주의점

  • 데이터베이스가 유니크 제약조건을 위반할 경우, 일반적으로 예외가 발생합니다. 이 예외를 적절히 처리해야 데이터베이스 에러로 인한 프로그램 중단을 방지할 수 있습니다.
  • 모든 데이터베이스와 ORM이 @Unique 주석을 지원하지는 않을 수 있으므로, 사용하기 전에 해당 ORM 또는 프레임워크 문서를 확인해야 합니다.

락의 종류

데이터베이스가 데이터 자원을 어떻게 락을 거는지에 대한 구체적인 메커니즘

 

  • 공유 락(Shared Locks): 여러 트랜잭션이 동시에 데이터를 읽을 수 있게 해주는 락입니다. 한 트랜잭션이 데이터에 공유 락을 걸고 있을 때, 다른 트랜잭션도 그 데이터를 읽을 수 있지만, 쓸 수는 없습니다.

 

  • 배타 락(Exclusive Locks): 한 트랜잭션이 데이터에 배타 락을 걸면, 그 트랜잭션만이 데이터를 읽고 쓸 수 있으며, 다른 어떤 트랜잭션도 해당 데이터에 접근할 수 없습니다.

락 사용전략 

데이터베이스의 자원에 접근할 때 발생할 수 있는 충돌을 관리하는 방식에 대한 전략

 

  • 소극적 락(Pessimistic Locking): 이 전략에서는 충돌이 발생할 것을 예상하고, 데이터를 사용하기 전에 락을 걸어 다른 트랜잭션의 접근을 차단합니다. 즉, 데이터를 처음 읽을 때 락을 걸고 트랜잭션이 끝날 때까지 락을 유지하는 방법입니다. 소극적 락은 보통 배타 락을 사용하여 데이터의 수정이 예상되는 경우 사전에 다른 트랜잭션의 접근을 차단합니다.

 

  • 적극적 락(Optimistic Locking): 충돌이 드물게 발생할 것으로 예상할 때 사용하는 전략입니다. 데이터를 실제로 수정할 때까지 락을 걸지 않고, 대신 데이터의 버전을 체크하여 업데이트 시점에 충돌을 감지합니다. 즉, 데이터에 버전 번호나 타임스탬프를 추가하여 데이터를 불러온 후 수정된 경우에만 업데이트가 발생하도록 하는 방법입니다. 적극적 락은 락을 사용하지 않는 상태에서 데이터의 일관성을 확인하는 방법으로, 공유 락과 배타 락의 전통적인 사용법과는 조금 다릅니다.
 

 

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

+ Recent posts