실험 환경

Nest.js, TypeORM , MySQL, TypeScript,

 

코드

1. update method

  async updateUsersNames(users: User[]) {
    const startTime = Date.now();
    for (const user of users) {
      await this.userRepository.update(user.id, user);
    }
    const duration = Date.now() - startTime;
    this.logger.log(
      `(update method) Bulk update of ${users.length} users completed in ${duration}ms`,
    );
  }

 

2.  createQueryBuilder method

 async updateUsersNamescreateQueryBuilder(users: User[]) {
    const startTime = Date.now();
    for (const user of users) {
      await this.userRepository
        .createQueryBuilder()
        .update(User)
        .set({ name: user.name })
        .where('id = :id', { id: user.id })
        .execute();
    }
    const duration = Date.now() - startTime;
    this.logger.log(
      `(createQueryBuilder) Bulk update of ${users.length} users completed in ${duration}ms`,
    );
  }

 

3. query 작성방식

 async updateUserNamesQuery(users: User[]) {
    const startTime = Date.now();
    const values = users
      .map((user) => `(${user.id}, ${user.name}, ${user.email})`)
      .join(', ');
    const query = `
        INSERT INTO user (id, name, email)
        VALUES ${values} as new
        ON DUPLICATE KEY UPDATE
          id = new.id,
          name = new.name,
          email = new.email,
      `;
    await this.userRepository.query(query);
    const duration = Date.now() - startTime;
    this.logger.log(
      `(query) Bulk update of ${users.length} users completed in ${duration}ms`,
    );
  }

 

4. save method

 async updateUsersNamesSave(users: User[]) {
    const startTime = Date.now();
    const values = users.map((user) => {
      const newUser = new User({ name: user.name, email: user.email });
      newUser.id = user.id;
      return newUser;
    });
    await this.userRepository.save(values);
    const duration = Date.now() - startTime;
    this.logger.log(
      `(save) Bulk update of ${users.length} users completed in ${duration}ms`,
    );
  }

 

 

Postman 으로 한번에 100개의 업데이트 요청 전송 결과

1. update

 

2 . createQuearyBuilder

 

3. query 작성

 

4. save

 

업데이트 방식 평균 처리 속도 (ms)
query 작성방식 7 ms
save 메소드 42.7ms
createQueryBuilder 메소드 68 ms
update 메소드 80 ms

 

1등 : query 작성방식

하지만 이 방식은 MySQL 과 같은 방식의 문법을 제공하는 데이터베이스에서만 사용이 가능하다는 단점이 존재한다.

 

2등: save 메소드

save 메소드는 주어진 데이터에 id 가 있으면 기존의 데이터 베이스 해당 데이터가 있는지 확인하고 있으면 업데이트를 한다.

주어진 데이터에 있는 id 가 데이터 베이스에 없으면 새로운 엔티티로 간주하고 데이터를 저장한다.

  - 데이터베이스가 자동으로 ID를 생성하도록 설정된 경우 : 주어진 id 를 무시하고 데이터베이스가 새로운 id 를 생성한다.

   

3등 : createQueryBuilder

for 문을 이용했지만, 이 방식을 이용하면 조인, 복잡한 조건, 서브쿼리 등을 활용할 수 있어서 유용해 보인다.

 

4등 : update 메소드

createQueryBuilder 와 같은 복잡한 쿼리 구성을 제공하지않아 간단한 업데이트를 할때 활용하면 좋을 것 같다.

 

 

 

결론

범용성을 고려하여 TypeORM 에서 제공해주는 메소드 save 를 이용하는 것이 좋아 보인다.

복잡한 방식의 조건 처리라면 createQueryBuilder  를 이용하는 방식이 더 좋아 보인다.

 

 

 

실험 환경

TypeORM, MySQL, Nest.js, TypeScript 이용

 

코드

// Insert
async bulkInsert(users: User[]) {
    const newUser = users.map((user: User) => new User({ name: user.name }));
    const startTime = Date.now();
    const insertResult = await this.userRepository.insert(newUser);
    const result = insertResult.identifiers.map((user: User) => {
      return { id: user.id };
    });
    const duration = Date.now() - startTime;
    this.logger.log(
      `Bulk "insert" of ${users.length} users completed in ${duration}ms`,
    );
    return { duration, result: result.length };
  }
  
// Save
async bulkSave(users: User[]) {
    const newUser = users.map((user: User) => new User({ name: user.name }));
    const startTime = Date.now();
    const result = await this.userRepository.save(newUser);
    const duration = Date.now() - startTime;
    this.logger.log(
      `Bulk "save" of ${users.length} users completed in ${duration}ms`,
    );
    return { duration, result: result.length };
  }

 

Postman 을 이용하여 실험

4 명의 유저

Insert 방식을 이용한 결과
save 를 이용한 결과

100 명을 저장해본 결과

 

300 명을 저장해본 결과

  insert save
4 명 3 ms (평균)  8 ms (평균)
100명 12 ms 43 ms
300명 28 ms 91 ms

 

결론

ms 단위의 차이라 미세하지만 데이터가 늘어나면 늘어날수록 insert가 더 빠르게 처리하는 것을 알 수 있다.

 

정리

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

 

 

+ Recent posts