트랜잭션을 잘 사용하기 위해서...!

여리·2023년 8월 21일
0

서비스를 런칭하기 위한 프로젝트를 하고 있다.

프로젝트 사용기술 : NestJS(typeorm 활용), MySQL, graphql

이번 프로젝트에서 API를 생성하는 역할에서 DB의 연결에 대해서 트랜잭션을 사용하게 됐다.

DB(mysql)를 효율적으로 사용하기 위해서 목적에 맞도록 구분해놓았다
예시로 구분하자면

#제품정보
table product{
	id int primary key increment
    title varchar
    desc text
    main_image_url varchar
    created_at timestamp default current_timestamp
    updated_at timestamp default current_timestamp on update current_timestamp
}

#제품 세부이미지
table prodct_image{
	id int primary key increment
    image_url varchar
    product_id
    created_at timestamp default current_timestamp
    updated_at timestamp default current_timestamp on update current_timestamp
    .
    .
    .
    기타 테이블...
}

위와같은 여러 테이블에 대해서 생성, 수정, 삭제 등에 대한 기능을 framework로 활용하고자 할때 하나의 api에 대해서 여러 테이블을 활용해야하는 경우가 발생한다.

서론에서 얘기했던것 처럼 나는 여러 패키지를 활용해서 개발을하여 효율성을 갖고 코드를 작성해 나갔다.

typeorm을 통해서 API를 생성하는데 typeorm은 자체적으로도 트랜잭션을 활용할 수 있는 막강한 기능을 갖고있다.

하지만 과유불급이라고 하나의 기능에 트랜잭션을 중첩, 중복해서 사용하게되면 화를 면치 못한다.

플로우로 흐름을 느껴보자

product 정보 row : 키보드(title) / 기계식 키보드(desc) / main-image-url(main_image_url)
product_iamge 정보 row : 세부 image1 / 세부이미지2
- 플로우 (트랜잭션이 한 번에 2개가 사용되는 경우)
    - 트랜잭션 1 열린다.
    - 트랜잭션 2 열린다.
    - 트랜잭션 1 - 기계식 키보드를 적출식 키보드로 바꾼다.
    - 트랜잭션 2 - 제품의 desc를 조회한다. (기계식)
    - 트랜잭션 1 - 닫힌다.
    - 트랜잭션 2 - 닫힌다.
    - 트랜잭션 2 - 다시 조회한다.  이때는 (적출식) 으로 뜰 것임
	- 한 요청 → 한 비지니스 로직에서 트랜잭션을 여러개 쓰다보면 값의 정합성이 안맞는 경우가 발생할 수 도 있음

이런 경우가 있을 수 있기 때문에 이럴때에는 코드의 정합성을 위해서 사용하는 방법을 제시할 수 있다.

근본 해결방법 : 트랜잭션의 중복사용을 없애는 것이다.

방법은 여러가지가 있을 수 있다.

  1. 중복사용하려는 트랜잭션중 하나를 직접적인 쿼리작성한다.
  2. 쿼리빌더를 통해 쿼리함수를 작성한다.

현재는 2가지정도로 생각나는데 결국 근본은 트랜잭션의 confilct를 없앤다.이다.

쿼리빌더와 함수형 쿼리 작성에 대해서는 상황에 따라 맞추어 선택을 해야할 것 이다.
쿼리빌더는 복잡한 형태의 쿼리를 만들어 줄 수 있기 때문에 복잡한 쿼리에 대해서 재사용성을 활용하고 싶다면 쿼리빌더를 활용, 단순한 쿼리의 재사용성이라면 함수형 쿼리로 활용. 단순항 쿼리를 활용한다면 함수형 쿼리를 작성해도 무방할 테니 말이다.

그렇다면 트랜잭션이 중복으로 발생해서 문제가 있을 수 있는지 확인하려면???

여러가지 방법이 있을 수 있겠지만 가장 확실한건 테스트를 해보는 것이다.

간단한 트랜잭션 코드를 만들어보자. 나는 쿼리러너를 통해 만들어보았다.

const func=async(
	paprameter:string):Promise<product>=>{
  const checkParameter=await this.productRepo.findOne({where: parameter})
  
  const queryRunner= database.createQueryRunner(); //쿼리러너 생성
  await queryRunner.connection(); //쿼리러너 연결
  await queryRunner.startTracsaction(); //트랜잭션 시작
  try{
    const result1 = await database.query(...);
   .
	const result2 = await database.query(...);
   .
   .
   
   	await queryRunner.manager.save(result1)
    await queryRunner.manager.save(result2)
  await queryRunner.commitTransaction(); //트랜잭션 커밋
  }catch(err){
  await queryRunner.rollbackTransaction(); //실패시 트랜잭션 롤백
  }finally{
  await queryRunner.release(); //트랜잭션 종료
  }
  
}

위와같은 코드를 보았을때 checkParameter의 typeorm 활용과 queryRunner의 typeorm 활용이 이루어졌다.

이때 둘다 트랜잭션이 작동되는 작업이라고 할때 checkParameter의 트랜잭션과 queryRunner의 여러 메소드들의 트랜잭션들이 작동된다고 가정하자.
(하나의 가설을 정한것 뿐이니 코드의 내용이나 개념에 대해서 너무 깊게 생각하지말길 바란다.)

그러면 이럴때 DB안에 데이터들 안에서 confilct가 나올 수 있기때문에 우리가 console.log()를 찍는것과 같이 throw error를 던져보는 것이다.

const func=async(
	paprameter:string):Promise<product>=>{
  const checkParameter=await this.productRepo.findOne({where: parameter})
  
  const queryRunner= database.createQueryRunner(); //쿼리러너 생성
  await queryRunner.connection(); //쿼리러너 연결
  await queryRunner.startTracsaction(); //트랜잭션 시작
  try{
    const result1 = await database.query(...);
                                         
    const err=new Error('트랜잭션 에러 확인')
    throw err
   .
	const result2 = await database.query(...);
   .
   .
    //위 트랜잭션 에러확인을 하고나서 이상이 없다면 다음 쿼리에서 또 확인
   	//const err=new Error('트랜잭션 에러 확인')
    //throw err
   	await queryRunner.manager.save(result1)
    await queryRunner.manager.save(result2)
  await queryRunner.commitTransaction(); //트랜잭션 커밋
  }catch(err){
  await queryRunner.rollbackTransaction(); //실패시 트랜잭션 롤백
  }finally{
  await queryRunner.release(); //트랜잭션 종료
  }
  
}

그렇게 하나하나 확인해서 DB에 의도한데로 영향을 주고받는지.. 다른 영향은 없는지 확인해볼 수 있다.

노가다같은게 없지 않지만.. 가장확실하게 확인해볼 수 있는 방법중에 하나다.
무결성(?)을 위한 부분에서는 아주 확실하다고 생각한다.

불안함을 없애기 위한 나같은 유형의 사람이라면 이 방법을 추천한다 !

더 좋은 방법이 있다면 댓글 부탁드립니다..
그리고 더 개선된 방법이 있으면 그때 개정해야겠다 !

profile
beckend developer

0개의 댓글