nest.js

[고스락 티켓 2.0] nestjs transaction with repository

ImNM 2022. 8. 14. 01:20

typeorm 0.3.7 버전을 기준으로 작성중이다

 

고스락 티켓 예매 프로젝트(이하 고티켓 ) 에선 repostiory를 아예 파일로 따로 만들어서 

 Repository<Entity> 를 주입받아서 새로운 Repository로 만들어서 사용중이다.

이에 맞춰서 공식문서랑은 조금 다른 방식으로 트랜잭션을 풀어낸 방법을 공유하고자한다.

 

이글을 통해 얻어갈 수 있는것들

  • 트랜잭션 시작한 쿼리러너로 Repository를 받아오는 방법
  • 콜백이나 , 엔티티매니저가 아닌 Repository 로 트랜잭션시 사용하기

 이 블로그의 내용은 고스락 티켓 예매 프로젝트에서 사용중인 소스이다

 

[고스락 티켓 2.0] 두번째 프로젝트는 어떻게 달라졌을까요?

우선 고스락 티켓 예매 프로젝트의 목적은 기존 종이티켓으로 표를 팔러다니던 OB시절에서 정산의 어려움이나 공연 홍보의 어려움 또한 비대면 시대로 돌입하면서 학교에 모이는 인원이 적다보

devnm.tistory.com


공식 문서에 나오는 방식에는 두가지방식을 예시로 들었는데

데이타 소스에서 트랜젝션을 콜백으로 넘겨줘서 엔티티 매니저로 엔티티를 관리하는방법
쿼리 러너로 트랜잭션을 시작해서 엔티티 매니저로 엔티티를 관리하는 방법

크게 두가지다. 고티켓은 UserRepository 처럼 레포지토리를 따로 만들어서하고있는데

유저 레포지토리 예

 보통의 서비스 레이어에서 걸려야할 트랜잭션에 

위 두가지 방식으로 구성하면 애써 커스텀해서 만들어놓은 레포지토리를 쓰지도못하니.. 참 고민이였다. ( 엔티티 매니저를 통해서 쿼리를 날리기 때문 )

하나의 단적인 해결방법으로는 위두가지 방법을 이용해서 쿼리러너나 엔티티매니저를 인자로 넘겨줘서

그걸이용해서 엔티티를 변경하는 로직을 작성해야하는데...

 

이러면 위 사진처럼 Repository<User> 주입받은걸 쓰지도 못하고.

트랜잭션용도인거 아닌거를 구별해서 메소드를 써야하니 고민이 되는상황.

 

서비스 레이어에서 레포지토리를 DEFAULT Scope  으로 주입받은상황이고....

갑자기 userRepository의 엔티티 매니저를 트랜잭션마다 바꿔줄수 없는 상황이고...

그렇다고 엔티티 메니저를 레포지토리로 인자로 넘겨주긴... 너무나도 소스짜기가 귀찮을것 같았다.

그래서 트레이드 오프를 해서 쿼리러너로 트랜잭션을 시작한뒤

주입받은 Repository 타입으로 새로운 레포지토리를 만들어 동일한 커넥션을 유지하는 레포지토리를 쓰기로 했다.

 + 별첨.. 스프링은 스레드 로컬있어서 트랜잭션 전파도 되지마는...

네스트는 비슷하게 cls-hook 이용한 방법이 있지만

 

GitHub - odavid/typeorm-transactional-cls-hooked: A Transactional Method Decorator for typeorm that uses cls-hooked to handle an

A Transactional Method Decorator for typeorm that uses cls-hooked to handle and propagate transactions between different repositories and service methods. Inpired by Spring Trasnactional Annotation...

github.com

이거뭐 쓰기도 참 애매하다 마지막 커밋이 일년을 넘었다

 

 

 트랜잭션 시작한 queryRunner로 Repository를 받아오는 방법


async registerUser(): Promise<ResponseRegisterUserDto> {
    const queryRunner = this.dataSource.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();
    const userRepositoryFromDataSource = queryRunner.manager.getRepository(User);
    const connectedUserRepository = new UserRepository(
      userRepositoryFromDataSource
    );
    try {
      const user = new User();
      const signUser = await connectedUserRepository.saveUser(user);
      await queryRunner.commitTransaction();
    } catch (e) {
      await queryRunner.rollbackTransaction();
      throw e;
    } finally {
      await queryRunner.release();
    }
  }

 src/auth/auth.service.ts

 

매우 간략화한 소스이다... typeorm 모듈을 임포트 시키면 글로벌하게 DataSource를 주입받을 수 있다.

해당 데이타소스로부터 쿼리러너를 받고, 일단먼저 connect 와 트랜잭션을 시작한뒤에

User 엔티티로 Repository<User> 을 땡겨온다.

유저 레포지토리 예

아래사진처럼 우리가 만든 UserRepository 생성자로 넘겨주면서 커넥션이 연결 된 상태인 connectedUserRepository를 받아온다.

그러면 트랜잭션이 진행중인 우리가 만든 리포지토리를 쓸수가 있다. 아래 그림처럼 try catch 사이에 일부로 에러를 내면 롤백을 당한다.

 

좀더 편하게 트랜잭션 시작된 레포지토리 받아오기


export function getConnectedRepository<T>(
  type: { new (repository: Repository<ObjectLiteral>): T },
  queryRunner: QueryRunner,
  entity: EntityTarget<ObjectLiteral>
): T {
  const userRepositoryFromDataSource =
    queryRunner.manager.getRepository(entity);
  return new type(userRepositoryFromDataSource);
}

// usage

const connectedOrder = getConnectedRepository(
      OrderRepository,
      queryRunner,
      Order
);
const connectedTicket = getConnectedRepository(
      TicketRepository,
      queryRunner,
      Ticket
);

src/common/funcs/getConnectedRepository.ts

 

팀원들이 쓸 로직이라 좀 쓰기 쉽게 type 받아서 리턴해주는 메소드를 만들었다.

 뭐 간단하다...  제네릭이 딱히 필요없어 보긴한데 결국 type을 생성해서 반환해주는 형식이다.

아래처럼 롤백이 정상적으로 이루어지는 것을 볼 수 있다.

 


트랜잭션이 필요한 부분에 repository를 주입받은거 말고 새로 생성해야한다는 단점이

있긴하지마는... 이게 최선인것같다.

고민해봐도 기존 구조를 크게 해치지않고 , 팀원들이 잘 따라와줄수 있는 소스의 변경 범위 정도가

적당해서 적용해 보았다.

인증 관련 서비스에 예시코드 미리 쫘놓으니 주문, 티켓 서비스 등에서도

팀원 분들이 잘 적용해 주셨다.

위방법을 이용해서 레포지토리 실 디비 테스트 할때도 

스프링의 테스트 @Transactional 처럼 롤백 테스트 할 수있긴하다. 포스팅으로 함 적어보겠다!