본문 바로가기
개발/NestJS

TypeORM에서 insert() 사용 시 @BeforeInsert가 동작하지 않는 문제 해결하기 🤔

by coking 2024. 11. 20.

안녕하세요! 오늘은 TypeORM을 사용하면서 겪었던 재미있는 이슈 하나를 공유하려고 합니다.
특히 비밀번호 해싱같은 작업을 할 때 자주 마주치는 문제인데요, 이 글을 통해 여러분의 소중한 시간을 아낄 수 있었으면 좋겠습니다! 😊

🚨 문제 상황

관리자 계정을 생성하는 API를 만들던 중이었습니다. 당연히 비밀번호는 해시화해서 저장해야 하니, Entity에 @BeforeInsert 데코레이터를 사용했죠.

@Entity('admin_user')
export class AdminUserEntity extends CommonEntity {
  @Column()
  userId: string;

  @Column()
  password: string;

  @BeforeInsert()
  async hashPassword() {
    if (this.password) {
      const salt = await bcrypt.genSalt();
      this.password = await bcrypt.hash(this.password, salt);
    }
  }
}

서비스 로직은 이렇게 작성했습니다:

async createAdminUser(body: CreateAdminUserDto) {
  await this.adminUserRepository.insert(body);
}

그런데... 🤦‍♂️ 데이터베이스를 확인해보니 비밀번호가 해시화되지 않고 그대로 저장되어 있었어요!

🔍 원인 파악

알고보니 TypeORM의 insert() 메서드는 Entity의 lifecycle hooks를 실행하지 않는다고 합니다.
즉, @BeforeInsert, @AfterInsert 같은 데코레이터들이 동작하지 않는 거죠!

✨ 해결 방법

다행히도 이 문제를 해결할 수 있는 방법이 두 가지나 있습니다.

1️⃣ create()와 save() 메서드 사용하기

가장 추천하는 방법입니다. Entity의 lifecycle hooks를 그대로 활용할 수 있어요.

async createAdminUser(body: CreateAdminUserDto) {
  // create()로 엔티티 인스턴스 생성
  const adminUser = this.adminUserRepository.create(body);

  // save()로 저장 (이 때 @BeforeInsert hook이 실행됨)
  return await this.adminUserRepository.save(adminUser);
}

2️⃣ insert() 사용 시 직접 구현하기

insert()를 사용해야 하는 경우라면, 해싱 로직을 직접 구현할 수 있습니다.
이 때는 코드 재사용성을 위해 유틸리티 클래스를 만드는 것을 추천드려요!

// utils/password.util.ts
export class PasswordUtil {
  static async hash(password: string): Promise<string> {
    const salt = await bcrypt.genSalt();
    return bcrypt.hash(password, salt);
  }
}

// service
async createAdminUser(body: CreateAdminUserDto) {
  const adminUserData = {
    ...body,
    password: await PasswordUtil.hash(body.password)
  };

  await this.adminUserRepository.insert(adminUserData);
}

🤔 어떤 방법을 선택해야 할까?

두 가지 방법 모두 장단점이 있습니다:

create() + save() 방식

✅ Entity의 모든 lifecycle hooks 활용 가능
✅ 데이터 검증과 처리가 일관적
❌ 단일 쿼리 대비 약간의 성능 차이

insert() + 직접 구현 방식

✅ 단순 INSERT라서 성능상 이점
✅ 로직을 명확하게 제어 가능
❌ Entity의 lifecycle hooks 활용 불가
❌ 추가 유틸리티 코드 필요

💡 결론

특별한 이유가 없다면 create()save() 조합을 사용하는 것을 추천드립니다.
Entity의 lifecycle hooks를 활용할 수 있고, 코드도 더 깔끔해지니까요!

하지만 대량의 데이터를 처리하거나 성능이 특별히 중요한 경우라면,
insert()를 사용하고 필요한 로직을 직접 구현하는 것도 좋은 선택이 될 수 있습니다.

🌟 꿀팁!

Entity에서 비밀번호 해싱이 제대로 동작하는지 확인하고 싶다면,
이렇게 로그를 찍어보세요:

async createAdminUser(body: CreateAdminUserDto) {
  console.log('1. 원본 비밀번호:', body.password);

  const adminUser = this.adminUserRepository.create(body);
  console.log('2. create() 후:', adminUser.password);

  const savedUser = await this.adminUserRepository.save(adminUser);
  console.log('3. save() 후:', savedUser.password);

  return savedUser;
}

이렇게 하면 각 단계별로 비밀번호가 어떻게 변하는지 쉽게 확인할 수 있답니다! 😉


도움이 되셨나요? 여러분의 소중한 시간을 아낄 수 있었길 바랍니다!
더 좋은 정보로 다음에 또 찾아뵙겠습니다. 감사합니다! 👋

댓글