adonisJS / transaction 사용하기 ( + 에러상황 4가지 해결)

flobeeee·2022년 2월 17일
0

시행착오

목록 보기
31/43

프로젝트에서 transaction을 진행하는 로직에 보완이 필요해서, 작업을 하는김에 정리해봤다.


🎢 transaction 개념

내가 정의해 본 transaction

한 번의 요청을 받아 DB 여러 테이블에 데이터를 넣어야 하는 경우,
모든 데이터입력에 성공하거나 혹은 실패하는 작업이다.

예를들어 내가 친구에게 만원을 송금한다.
1. 내 계좌에서 돈이 빠져나간다.
2. 친구 계좌에 돈이 들어간다.
이 과정은 둘다 성공하거나, 둘다 실패해야 한다.
만약 내 돈만 빠져나가고 친구계좌에 돈이 들어가지 않으면, 돈이 사라지는 것이다.

-> 내 돈만 빠져나가고 친구계좌에 돈이 들어가지 않으면,
transaction의 rollback으로 다시 내 계좌에서 돈이 빠져나가지 않은 상태로 만들어준다.

참고 : 트랜잭션이란 무엇인가?


🎢 adonisJS 로 transaction 적용

adonis에 있는 기본 예제이다.

import Database from '@ioc:Adonis/Lucid/Database'
import User from 'App/Models/User'

await Database.transaction(async (trx) => {
  const user = new User()
  user.username = 'virk' // 1. user 테이블에 데이터를 넣는다

  user.useTransaction(trx) // 2. transaction 부분 완료 (임시 저장개념)
  await user.save()

  /**
   * The relationship will implicitly reference the 
   * transaction from the user instance
   */
  await user.related('profile').create({ // 3. user 와 연결된 profile 테이블에 데이터를 넣는다.
    fullName: 'Harminder Virk',
    avatar: 'some-url.jpg',
  })
})

내가 보완한 코드

const trx = await Database.transaction() // transaction 시작
try {
    user.name = request.input('name') // 데이터를 넣는다.
    user.tel = request.input('tel')
    user.address = request.input('address')

    user.useTransaction(trx) // user 테이블 부분완료 (임시저장)
    await (await user.save()).refresh()
    
    post.title = request.input('title') // 데이터를 넣는다.
    post.body = request.input('body')
    post.isHidden = request.input('isHidden')

    post.useTransaction(trx) // post 테이블 부분완료 (임시저장)
    await (await post.save()).refresh()
    
    await trx.commit() // 완료 (여기까지 왔으면 문제없다는 것이다. 실제로 저장진행)

    return response.ok(user)
  
  } catch (error) {
     await trx.rollback() // 실패가 발생하면 이쪽으로 빠지는데, 철회 상태가 된다. (DB를 확인해보면 수행한 작업들이 아무것도 들어와 있지 않다.)

     throw new TransactionExceptionHandler()
  }

참고 : docs.adonisjs.com ( 공식문서 )


🎢 에러 Transaction query already complete, ...

모든 소스를 검토한 뒤에 마지막 점검을 위해 test를 돌렸다.
그리고 에러를 만났다.

Transaction query already complete, run with DEBUG=knex:tx for more info

같은 에러메세지를 3번 받았는데, 해결하다보니 모두 다른 원인이었다.

원인1 : 비동기가 제대로 걸려있지 않아서

// 수정 전
posts.map(async (post: Post) => {
  post.isHidden = request.input('isHidden)
                                
  post.useTransaction(trx)
  await (await post.save()).refresh()
})

// 수정 후
await Promise.all(
  posts.map(async (post: Post) => {
    post.isHidden = request.input('isHidden)
                                
    post.useTransaction(trx)
    await (await post.save()).refresh()
  })
)

위 데이터가 먼저 임시저장이 되고, 다른 데이터 로직이 저장이 되는 게 맞는데,
자꾸 위 데이터가 나중에 작동한다는 것을 알았다.

이는 map의 특성 때문이다.
mdn : map 메서드는 배열 내의 모든 요소 각각에 대하여 주어진 함수를 호출한 결과를 모아 새로운 배열을 반환합니다.

map의 내부는 promise객체라서 동기적으로 처리해주려면, promise.all로 해주면 된다.
순서대로 처리하려고 동기적으로 처리하는 것이다.
(동기, 비동기, promise 개념을 더 공부해야겠다는 필요성을 느꼈다.)

원인2 : commit 후에 transaction이 또 있었다.

const trx = await Database.transaction()
    try {
      // 1번 데이터 저장 로직
      
      // 2번 데이터 저장 로직
      
      // ...
      
      await trx.commit()

	  // 아래 코드를 await trx.commit()위로 올렸다.
      await DataHistory.changeDataHistory(user.id, ...)

      return response.created(channel)
    } catch (error) {
      await trx.rollback()
      throw new TransactionExceptionHandler()
    }

changeDataHistory 내에 있는 로직도
어떤 데이터를 어디에 넣었는지 DB에 저장하는 로직이라서 임시저장이 필요한 부분이다.
위치를 변경하니까 에러가 더 이상 뜨지 않았다.

원인3 : 잘못된 문법

const trx = await Database.transaction()
    try {
      // 1번 데이터 저장 로직
      
      // 2번 데이터 저장 로직
      
      // ...

      user.load('post') // -> await user.load('post')

      await trx.commit()

      return user
    } catch (error) {
      console.log('error', error)
      await trx.rollback()

      return {
        error: error,
      }
    }

load는 user에 연결된 post데이터를 가져오는 로직이라서, 비동기적으로 처리해야 한다.
await 과 load 는 짝꿍이다.


후기

이번 기회에 transaction 에 대해 많이 파악하게 되었다.
참고로 commit 혹은 rollback를 빼먹는 경우, DB가 뻗을 수 있다고 한다.
transaction을 적용할 때, 로직이 모두 완료되면 commit 을,
에러가 생기는 경우 rollback가 필수로 들어가 있는지 반드시 확인하는 습관을 가지자 !

🎢 통계데이터에 데이터수 미반영 에러

transaction을 적용하고 나서 며칠이 지난 후 데이터가 이상하다는 연락을 받았다.
확인해보니 실제 데이터와 그 데이터의 갯수를 세서 통계테이블에 기록하는 데이터의 숫자가 서로 달랐다.
확인해보니 새로 수정한 transaction때문이었다.

transaction 내에서 새로운 데이터 저장 -> 그 데이터까지 카운팅해서 통계데이터 저장인데.

새로운 데이터는 임시로 저장이 되고, 통계데이터는 실제데이터만을 카운팅해서 가져왔다.

-> 임시데이터까지 카운팅할 수 있게 trx키워드를 또 걸어줬다.

public static async userPostCount (user: User, trx:TransactionClientContract | null) {
  let query = Post.query().count('* as total')
  if (trx) {
    query.useTransaction(trx) // 이렇게 임시저장된 데이터도 쿼리에 걸어준다.
  }
  query.where('user_id', user.id)
 
  const total = await query.first()

  return total
}
profile
기록하는 백엔드 개발자

0개의 댓글