회사 프로젝트 진행 중 사용자의 금전 정보가 특정 값 이하로 내려갔을 때, 사용자의 알림이 생성되어야 하는 요구사항이 있었다. 이 프로젝트에서는 Nest.js 프레임워크를 사용하고 있고, 서비스 레이어에서 이미 많은 비즈니스 로직이 구현되어 있었다.
비즈니스 로직 내부에서 절차적(금전 정보 update => 알림 save)으로 작성할 수도 있겠지만, 비즈니스 로직을 좀 더 역할과 책임을 분리해서 깔끔하게 작성할 수 있는 방법을 찾아보다가 TypeORM Subscriber를 사용해보게 되었다.
특정 엔티티에 Event Subscriber를 설정하여 DML(SELECT, INSERT, UPDATE, DELETE) 전후로 특정 작업 처리하도록 만들 수 있다.
비슷한 기능이 아마도 ORM 마다 있을 것으로 예상되는데, Mongoose에는 Middleware라고 하는 기능이 있다.
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number:
@Column()
age: number;
@AfterUpdate()
updateAge() {
this.age += 1
}
}
이런 식으로 User 엔티티에 UPDATE가 실행된 후에 별도의 업데이트 쿼리를 실행할 수 있다.
다만 TypeORM 내장 메서드(e.g. save(), update(), …)에서만 동작하고, 이 메서드가 트랜잭션 안에서 실행되고 있다면 리스너로 동작하는 쿼리를 트랜잭션에 포함시킬 수 없다.
현재 커넥션을 이용해서 무언가를 처리하고 싶다면, 별도의 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번 방법과 차이점은