develjsw / nestjs-api

0 stars 0 forks source link

특정 테이블 업데이트 이벤트 발생, 후 처리 일괄 적용 (Entity Listeners OR Entity Subscribers) #18

Open develjsw opened 1 month ago

develjsw commented 1 month ago
  1. 이슈 : 특정 A 테이블에서 업데이트가 발생한 경우, B 테이블의 데이터를 조회하여 A 테이블 컬럼 변경 작업이 필요한 상황. 이미 여러 곳에서 업데이트 작업이 되어 있었음. 이에 공통 함수를 만들고 각각 넣어주는 방법이 있었지만, 누락 가능성이 존재하여 다른 방안을 찾게 됨.

  2. 해결 방안 : (미사용) 1 ) Entity Listeners : 엔티티 클래스 내에서 직접 메서드를 정의하여 특정 이벤트를 리스닝함. 예를 들어, @BeforeInsert, @AfterUpdate와 같은 데코레이터를 사용하여 엔티티의 특정 이벤트가 발생할 때 실행될 메서드를 정의할 수 있음.

    2 ) Entity Subscribers : 엔티티와 관련된 이벤트를 처리하기 위해 별도의 클래스를 정의하고 EntitySubscriberInterface를 구현함. 이 클래스는 특정 엔티티와 관련된 여러 이벤트를 구독하고 처리할 수 있음.

    1번과 2번 방식 중에서 고민하였으나, 1번 방식은 의존성이 너무 높아지기 때문에 2번 방식을 택함.

    ## 1번 방식 예시 ##
    
    import { AfterUpdate } from 'typeorm'
    
    @Entity('테이블명')
    export class 엔티티명 {
    
      @AfterUpdate()
      함수명() {
          // TODO : 로직 작성
      }
    
    }
    ## 2번 방식 예시 ##
    
    @Module({
      imports: [], 
      controllers: [],
      provider: [구독 서비스]
    })
    export class 모듈명 {}
    
    ---------------------------------
    
    export default () => ({
     database: {
         mysqlDb: {
             type: 'mysql',
             host: 호스트명,
             subscribers: ['dist/**/entities/mysql/*.subscriber{.ts,.js}'],
             ...
         }
     }
    });
    
    ---------------------------------
    
    import {
      EntitySubscriberInterface,
      EventSubscriber,
      TransactionCommitEvent,
      TransactionRollbackEvent,
      TransactionStartEvent,
      UpdateEvent
    } from 'typeorm';
    import { 엔티티명 } from './엔티티명.entity';
    
    @EventSubscriber()
    export class 구독서비스명 implements EntitySubscriberInterface<엔티티명> {
      // 1회만 실행되도록 플래그 값 설정
      private updateOccurred = false;
    
      // 트랜잭션 시작 전
      async beforeTransactionStart(event: TransactionStartEvent): Promise<void> {
          this.updateOccurred = false;
      }
    
      // 업데이트 쿼리 실행 후
      async afterUpdate(event: UpdateEvent<엔티티명>): Promise<void> {
          if (event.entity instanceof 엔티티명) {
              this.updateOccurred = true;
          }
      }
    
      // 트랜잭션 커밋 전
      async beforeTransactionCommit(
          event: TransactionCommitEvent
      ): Promise<void> {
          if (this.updateOccurred) {
              // TODO : 엔티티가 업데이트된 경우에만 실행할 로직
              try {
    
              } catch (error) {
                  console.log(error);
              }
          }
      }
    
      // 트랜잭션 커밋 후
      async afterTransactionCommit(event: TransactionCommitEvent): Promise<void> {
          this.updateOccurred = false;
      }
    
      // 트랜잭션 롤백 전
      async beforeTransactionRollback(
          event: TransactionRollbackEvent
      ): Promise<void> {
          this.updateOccurred = false;
      }
    }
  3. 참고 사항 : 2번 방식에서 afterUpdate으로만 처리하려고 하였으나, queryRunner를 사용 중인 곳에서 발생하는 transaction에 대한 이슈와 transaction에서 발생하는 쿼리마다 afterUpdate가 호출되는 부분이 있어 beforeTransactionCommit에서 처리하되, updateOccurred 플래그 값을 두어 1번만 실행되도록 설정

develjsw commented 1 month ago
  1. 변경된 해결 방안 : 현재에는 업데이트 발생 지점이 많지 않아 이슈가 발생할 가능성이 없었지만, 순환 참조 이슈 등 사이드 이펙트가 존재할 것이라 판단되어, B 테이블 데이터 조회 후 A 테이블 컬럼 변경 작업을 모듈화하고 A 테이블 변경 지점들을 찾아 적용하는 방식으로 변경 함