[TypeORM] Subscriber 사용해보기(in Nest.js)

🌩 es·2022년 8월 21일
0
post-thumbnail

Intro

회사 프로젝트 진행 중 사용자의 금전 정보가 특정 값 이하로 내려갔을 때, 사용자의 알림이 생성되어야 하는 요구사항이 있었다. 이 프로젝트에서는 Nest.js 프레임워크를 사용하고 있고, 서비스 레이어에서 이미 많은 비즈니스 로직이 구현되어 있었다.
비즈니스 로직 내부에서 절차적(금전 정보 update => 알림 save)으로 작성할 수도 있겠지만, 비즈니스 로직을 좀 더 역할과 책임을 분리해서 깔끔하게 작성할 수 있는 방법을 찾아보다가 TypeORM Subscriber를 사용해보게 되었다.

TypeORM Subsriber란 무엇인가?

특정 엔티티에 Event Subscriber를 설정하여 DML(SELECT, INSERT, UPDATE, DELETE) 전후로 특정 작업 처리하도록 만들 수 있다.
비슷한 기능이 아마도 ORM 마다 있을 것으로 예상되는데, Mongoose에는 Middleware라고 하는 기능이 있다.

Nest.js에서 TypeORM Subscriber를 어떻게 사용하는가?

1. 엔티티 클래스 안에 데코레이터를 이용해서 엔티티 리스너를 단다.

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number:

  @Column()
  age: number;

  @AfterUpdate()
  updateAge() {
    this.age += 1
  }
}

이런 식으로 User 엔티티에 UPDATE가 실행된 후에 별도의 업데이트 쿼리를 실행할 수 있다.

다만 TypeORM 내장 메서드(e.g. save(), update(), …)에서만 동작하고, 이 메서드가 트랜잭션 안에서 실행되고 있다면 리스너로 동작하는 쿼리를 트랜잭션에 포함시킬 수 없다.

현재 커넥션을 이용해서 무언가를 처리하고 싶다면, 별도의 Subscriber 클래스를 작성해야 한다.

2. 별도의 Subscriber 클래스를 작성한다.

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number:

  @Column()
  coin: number;

  @OneToMany(type => Notification, notification => notification.user)
  notifications: Notification[]
}

@Entity()
export class Notification {
  @PrimaryGeneratedColumn()
  id: number:

  @Column()
  message: string;

  @ManyToOne(type => User, user => user.notifications)
  user: User

  @Column({ name: “user_id” })
  userId: number;
}

위와 같이 사용자에 대한 User 엔티티, 알림 정보에 대한 Notification 엔티티가 있다고 가정하고(하나의 User가 여러 Notification을 가질 수 있음), 사용자의 coin 정보가 업데이트 되었을 때 알림을 생성하는 코드를 작성해보자.

import { Connection, EntitySubscriberInterface, EventSubscriber } from “typeorm”;

@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
  constructor(
    // 1. 현재 커넥션을 의존성으로 주입한다.
    private connection: Connection,
  ) {
    // 2. 현재 클래스를 subscriber로 등록한다.
    connection.subscribers.push(this);
  }

  // 3. 구독하고자 하는 엔티티로 반환
  listenTo(): any {
    return User;
  }
  
  // 4. 어떤 이벤트에 대해 구독할 것인지 정한다.
  async afterUpdate(event: UpdateEvent<User>) {
    // 4-1. User 엔티티가 업데이트 된 후 어떤 로직을 실행할지 구현한다.
    const coinGotUpdated = event.updatedColumns.find((updatedColumn) => updatedColumn.propertyName === “coin”);
        
    if (coinGotUpdated) {
      await event
        .manager
        .getCustomRepository(NotificationRepository)
        .save({
          userId: event.entity.id,
          message: “코인이 추가되었습니다.,
        });
    }
  }
}

별도의 클래스를 작성해서 EntitySubscriberInterface를 구현해주면 된다. Nest.js에서는 모듈의 providers에 Subscriber를 등록해주어야 한다.

@Module({
  imports: [],
  controllers: [UserController],
  providers: [UserSubscriber],
  exports: [UserService],
})
export class UserModule {}

1번 방법과 차이점은

  • 현재 커넥션을 이용해서 추가적인 DML 작업이 가능하다.
  • 현재 커넥션에서 트랜잭션이 진행되고 있으면, 트랜잭션 안에서 실행 후 COMMIT 처리된다.
  • 내장 메서드 뿐만 아니라 쿼리빌더로 작성해도 이벤트를 발생시킬 수 있다.
  • event 객체를 통해 현재 커넥션에서 업데이트된 엔티티의 어떤 프로퍼티가 변경되었는지(event.updatedColumns)와 업데이트 전/후 값도 알 수 있다.

결론

  • 꼭 사용할 필요는 없지만, 요구사항에 따라 사용하면 비즈니스 로직이 좀 더 가벼워져서 유지보수하기에 편해질 거 같다.
profile
완벽주의가 아닌 완성주의(블로그 이동 중...)

0개의 댓글

관련 채용 정보