log를 통해 알아보는 Typeorm의 save

Dongwon Ahn·2024년 3월 17일
0

DB

목록 보기
1/2
post-thumbnail
post-custom-banner

최근 스터디를 위해 Promise.all를 다시 정리하다가 typeorm의 save에 대해 스스로 정확히 알고 사용하지 않는다는 것을 알게 되어 실제 실행해보면서 logging을 통해 Typeorm의 save가 어떻게 동작하는지 알아보겠습니다.

아래 예제로 구현되는 테스트는 Typerom의 0.3.17 버전과 mysql2의 3.6.2 버전을 사용했습니다.

Typeorm의 save란?

공식문서에 나와있는 save설명은 아래와 같습니다.

지정된 Entity 또는 Entity 배열을 저장합니다. 만약 엔티티가 database에 이미 존재하면 업데이트를 진행합니다. Entity가 database에 없다면 insert를 진행합니다.
지정된 모든 Entity를 하나의 transaction에 저장합니다. 또한 정의되지 않는 모든 속성을 건너뛰기 때문에 부분 업데이트를 진행합니다.

위 설명이 그렇듯 주로 insert또는 update를 할때 사용하거나, 여러 entity들을 처리할때 주로 사용했습니다. 부분 업데이트가 가능하기 때문에, update 메소드보다 편하게 사용하고 있었습니다.

await manager.save(user)
await manager.save([category1, category2, category3])

save 실행 시 동작하는 일

예제를 위해 간단한 Product Entity를 만들겠습니다.

@Entity()
export class Product extends BaseEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ type: 'varchar', comment: '상품명' })
  name: string;

  @Column({ type: 'int', comment: '가격' })
  price: number;

  @Column({ type: 'int', comment: '재고' })
  stock: number;

  @UpdateDateColumn({ type: 'timestamp', comment: '수정일' })
  updatedAt: Date;
}

typeorm의 경우 PrimaryGeneratedColumn를 통해 PK 를 지정할 수 있습니다.
UpdateDateColumn 의 경우 변경사항이 있으면 최신시간으로 update 해주는 메소드 입니다.

insert

async saveProduct(): Promise<void> {
  const product = new Product();
  product.name = 'test';
  product.price = 1000;
  product.stock = 100;

  await this.productRepository.save(product);
}

typeorm의 save는 PK 여부에 따라 insert, update를 수행하는 것이 다릅니다. 위 예제는 PK인 id가 없이 요청을 하기 때문에 아래 로그를 확인해보면, insert가 수행되는 것을 확인할 수 있습니다.
(insert외에 트랜잭션과 select문이 있는데 이 부분은 밑에서 따로 다루겠습니다.)

update

PK 여부에 따라 insert, update가 진행되기 때문에 위 예제에서 id를 추가해서 요청하겠습니다.

async saveProduct(): Promise<void> {
  const product = new Product();
  product.id = 1;
  product.name = 'test1';
  product.price = 10000;
  product.stock = 99;

  await this.productRepository.save(product);
}

위 예제 실행 시 typeorm은 아래 로그와 같이 PK인 id를 가지고 select를 진행한 후 update를 진행하는 것을 확인할 수 있습니다.

그럼 만약 DB에 없는 PK로 요청하면 어떻게 될까요? 위 예제에서 id를 1000으로 변경 후 실행해보겠습니다.

PK인 id를 통해 select를 진행 후 DB에 해당 데이터가 없기 때문에 insert를 수행하는 것을 확인할 수 있습니다.

그렇다면, DB에 PK인 id가 존재하고 save 요청한 데이터들이 변경 내용이 없으면 어떻게 수행될까요? 변경을 요청했기 때문에 UpdateDateColumn를 통해 updatedAt이 현재 시간으로 변경될까요?

Typeorm의 save는 select 후 현재 요청한 내용과 변경 내용이 없다면 update를 하지 않고 요청을 종료합니다. 그렇기 때문에 save를 통해 updatedAt을 최신화 하는 것에 유의해야 합니다.

Entity 배열 처리

공식문서에 따르면 Entity 배열도 처리가 가능합니다. 그렇기 때문에 저도 Entity를 배열로 만들어서 요청을 하는 경우가 많은데, 어떻게 동작하는지 한번 확인해보겠습니다. 반복문을 통해 간단하게 Entity 배열을 insert 하는 예제입니다.

async saveProduct(): Promise<void> {
  const productList: Product[] = [];
  for (let i = 0; i < 3; i++) {
    const product = new Product();
    product.name = `product${i}`;
    product.price = i * 1000;
    product.stock = i * 10;
    productList.push(product);
  }

  await this.productRepository.save(productList);
}


위 예제를 수행했을 때 로그를 확인하면 하나의 트랜잭션에서 insert를 3번 하는 것을 확인할 수 있습니다. 그럼 위 예제에서 id만 추가해서 Entity 배열을 통해 update를 수행해보겠습니다.

update와 비슷하게 우선 PK 배열을 IN 조건을 통해 select 후 update한 다음에 변경데이터를 select 하는 것을 확인할 수 있습니다.

save 옵션

위 예제들의 로그를 확인해보면 단순하게 insert, update 되는 것이 아닌 다른 동작들이 있는 것을 확인할 수 있습니다.
트랜잭션, insert or update 후에 select 하는 등 save 수행시 다른 동작들이 있습니다.
Typrorm의 save의 옵션을 통해 위 동작들을 핸들링 하는 방법에 대해 알아보겠습니다.

export interface SaveOptions {
    data?: any
    listeners?: boolean
    transaction?: boolean
    chunk?: number
    reload?: boolean
}

트랜잭션 옵션

공식문서 설명에 따르면 save는 하나의 트랜잭션에서 동작을 합니다. 다만, 하나의 row를 insert, update 하는데는 트랜잭션은 불필요한 리소스 낭비가 될 수 있습니다. 왜냐하면 에러 발생시 DB의 시스템에 의해 원자적으로 처리가 되기 때문입니다.

async saveProduct(): Promise<void> {
  const product = new Product();
  product.name = 'test';
  product.price = 1000;
  product.stock = 100;

  await this.productRepository.save(product, { transaction: false });
}

옵션의 transaction의 default값이 true이기 때문에 위 예제와 같이 false를 명시해줘야 합니다. 아래 로그를 확인해보면 위의 예제와 달리 트랜잭션이 없는 것을 확인할 수 있습니다.

Entity 배열의 경우에 트랜잭션을 제거하는 경우 에러 발생시 데이터의 원자성을 보장못할 수 있기 때문에 유의해야 합니다.

reload 옵션

위 예제에서도 확인이 가능하듯이 insert, update 후 select를 진행하는 것을 확인할 수 있습니다. 데이터의 변경 후 select가 필요한 경우가 있지만, 위 예제 처럼 응답값을 return할 필요 없는 경우에는 해당 select는 불필요한 리소스 낭비가 될 수 있습니다.

async saveProduct(): Promise<void> {
  const product = new Product();
  product.name = 'test';
  product.price = 1000;
  product.stock = 100;

  await this.productRepository.save(product, {
    transaction: false,
    reload: false,
  });
}

위 예제에서 reload: false 를 추가했습니다. reload도 default 값이 true입니다. 아래 로그를 확인해보면 insert 후 select를 하지 않는 것을 확인할 수 있습니다.

update 전에 select를 안 할수는 없을까?

위 예제들의 결과를 보면 PK가 있는 경우는 select를 우선 하고, 나온 결과에 따라 insert 또는 update 아니면 아무런 동작을 하지 않습니다.

update의 비용이 select에 비해 상대적으로 비싸기 때문에 대다수의 경우 해당 처리가 효율적일 수 있습니다. 다만 무조건 update가 일어나는 동작일 경우 select 하는 것이 불필요한 리소스라고 생각할 수 있습니다.

하지만 save를 통해 처리하는 경우 update 이전에 select를 하지 않도록 제한 하는 것이 불가능합니다. 그렇기 때문에 그런 처리가 필요한 경우 save 대신 update 메소드를 사용해야 합니다.
아래 예제를 확인해보면 select 없이 update가 수행되는 것을 확인 할 수 있습니다.

async saveProduct(): Promise<void> {
  await this.productRepository.update({ id: 1 }, { stock: 100 });
}

결론

실제 동작하는 로그를 통해 typeorm의 save가 동작하는 것을 확인해봤습니다.
위에서 이야기한 불필요한 리소스 부분은 실제 프로젝트에서 큰 영향을 주지 않는 것일 수 있습니다.
하지만, 이번에 알아두시면 추후 성능개선 작업 등을 할 때 도움이 되지 않을까 생각하기에 typeorm을 사용하는 경우 한 번 훑어보시는 것도 좋을 꺼 같습니다.

혹시 제가 잘못 설명한 내용이 있으면 댓글에 남겨주시면 감사하겠습니다.

profile
Typescript를 통해 풀스택 개발을 진행하고 있습니다.
post-custom-banner

0개의 댓글