10/23/2023

TypeORM 메서드

발단

TypeORM에서 제공하는 여러 메서드 중에서 대부분 create(), insert(), save(), update(), remove(), delete()를 사용해야 하는 상황이 많았는데 문제는 이 메서드들의 차이점이다! 공식 문서에는 각 메서드의 기능에 대한 대략적인 설명이 있지만 정확한 차이점에 대한 명확한 설명은 없다. 프로젝트를 진행하면서 여러 레포지토리의 코드를 읽고 생성의 경우 create() 메서드 사용 후 save() 메서드를 사용했다. 하지만 분명 insert() 메서드도 존재한다. 또한 갱신의 경우 update() 메서드도 있지만 특정 경우는 findOne() 메서드로 대상을 찾은 다음 save() 메서드를 사용하는 코드도 보았다. 그러면서 자연스럽게 차이점에 대해 의문점이 떠올랐다. 여기서 차이점에 대해 알아본다~


create() vs. save()

create() 메서드는 데이터베이스에 정보를 저장하거나 유지하지 않는다. 데이터를 받고 엔티티의 새로운 인스턴스를 생성한 다음 데이터를 인스턴스에 할당한다. save() 메서드는 엔티티를 받고 데이터베이스에 저장한다. 즉, create() 메서드와 save() 메서드 사이의 주요 차이점은 다음과 같다. create() 메서드는 엔터티의 인스턴스를 생성하고 save() 메서드는 데이터를 영구적으로 저장한다. 그런데 save() 메서드는 엔티티의 필드를 가지는 어떤 종류의 객체라도 인자로 받을 수 있다. 즉, 특정 엔티티의 인스턴스를 먼저 만들지 않고도 필드를 직접 전달할 수 있는데 굳이 귀찮게 엔티티의 인스턴스를 먼저 만드는 이유는 무엇일까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async create(createUserDto: CreateUserDto) {
  try {
    const salt = await bcrypt.genSalt();
    const hashedPassword = await bcrypt.hash(createUserDto.password, salt);
    const roles = [createUserDto.role];
 
    // 사용자 엔티티 인스턴스를 생성한다(주의: 데이터베이스에 인스턴스를 저장하지 않는다.).
    const user = this.usersRepository.create({
      ...createUserDto,
      password: hashedPassword,
      roles,
    });
 
    // 데이터베이스에 엔티티 인스턴스를 저장한다.
    // save({...createUserDto, password: hashedPassword}); 엔티티 인스턴스 없이 직접 삽입 가능하다.
    return await this.usersRepository.save(user);
  } catch (error) {
    if (error?.errno === MySqlErrorCode.DUPLICATE_ENTRY) {
      throw new ConflictException('이메일 사용 중.');
    }
 
    throw new InternalServerErrorException('사용자 생성 중 오류 발생.');
  }
}
cs

엔티티 클래스는 테이블 혹은 컬렉션이 가지는 모든 다양한 속성을 정의하며 일반적으로 여기에는 비즈니스 논리가 전혀 포함되지 않는다. 그러나 엔티티 클래스에 유효성 검사 논리를 넣고 싶은 경우가 있다. 예를 들어, class-validator 패키지의 @IsPositive() 데코레이터와 @IsNumber() 데코레이터를 상품 엔티티에 추가할 수 있다. 따라서, 엔터티에 직접 유효성 검사 논리를 추가할 수 있다. 다시 말해, DTO 대신에 엔티티와 유효성 검사 논리를 연결할 수 있다. 그러면 save() 메서드를 호출하기 전에 유효성 검사를 실행해야 한다. 그렇다면 해당 유효성 검사를 실행하려면 먼저 사용자 엔티티 인스턴스를 만들어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity('items')
export class Item {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column({ length300, unique: true, nullable: false })
  name: string;
 
  // 엔티티에 숫자, 양수인지 유효성을 검사하는 데코레이터를 추가한다.
  @IsPositive()
  @IsNumber()
  @Column({ type: 'decimal', precision: 65, scale: 3, nullable: false })
  price: number;
 
  ...
}
cs


Hook

훅(Hook)은 특정 시점에 자동으로 호출되는 함수를 엔티티에 정의할 수 있게 해준다. 훅을 사용하면 극명한 차이점을 알 수 있다. 사용자 엔티티 인스턴스 또는 일반(plain) 객체로 save() 메서드를 호출할 수 있고 어떤 방식으로든 데이터는 데이터베이스에 저장된다. 하지만 엔티티 인스턴스를 저장하면 해당 인스턴스에 연결된 모든 훅이 실행되는 반면 일반 객체를 전달하고 저장하면 어떠한 훅도 실행되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;
 
  @Column({ length300, nullable: false })
  name: string;
 
  @Column({ length500, unique: true, nullable: false })
  email: string;
 
  @Column({ length500, nullable: false })
  password: string;
 
  ...
 
  @AfterInsert()
  insertLog() {
    console.log('사용자 저장 완료!');
  }
}
cs

일반 객체를 전달하면 로그는 나타나지 않는다.


save() & remove() vs. insert() & update() & delete()

발단에서 언급한 다른 메서드의 차이점은 다음과 같다. save() 메서드 및 remove() 메서드는 엔티티 인스턴스로 호출되기를 예상하며 엔티티로 호출하면 훅이 실행된다. 그러나 insert() 메서드, update() 메서드 또는 delete() 메서드는 일반 객체로 호출되기를 예상하며 데이터를 삽입, 갱신 또는 삭제 작업을 실행하는 경우 훅이 실행되지 않는다. 이 때문에 save() 메서드와 remove() 메서드를 선호할 수 있지만 성능 상 절충(trade-off)을 고려해야 한다. save() 메서드로 갱신을 수행하는 경우 update() 메서드가 데이터베이스로 딱 한 번의 이동(trip)을 통해 갱신을 수행하는 것과 달리 데이터베이스로 두 번의 이동(round trips)이 필요하다. 즉, 하나는 엔티티를 가져오기 위한 연산이고 다른 하나는 실제로 갱신하는 연산이다. remove() 메서드로 삭제를 수행하는 경우도 마찬가지이다. delete() 메서드는 한 번의 이동으로 충분하지만 save() 메서드처럼 두 번의 이동이 필요하다.

프로젝트에서 훅과 같은 기능을 사용하지 않아 아직 유용성을 크게 느끼지는 못하겠지만 헷갈리는 메서드를 정리해서 한결 편한 것 같다~ 이 때문에 가능하면 사용하는 메서드를 하나로 통일(즉 save()와 remove() 아니면 insert(), update() 그리고 delete())하는 것이 좋다는 글을 읽었는데 이번 리팩토링에서는 통일하지 않았는데 다음에는 그렇게 하자!

update: 2024.01.21

댓글 없음:

댓글 쓰기