thumbnail
11_2. Transaction 적용하기
mongoDB
2023.04.04.

11_2. Transaction 적용하기

img105

몽고DB 문서를 보면 위와같이 트랜젝션 관련 예시 코드가 나온다.

// For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g.
// const uri = 'mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/?replicaSet=myRepl'
// For a sharded cluster, connect to the mongos instances; e.g.
// const uri = 'mongodb://mongos0.example.com:27017,mongos1.example.com:27017/'

// --- Start 몽고DB 서버 연결부분 ---
const client = new MongoClient(uri);
await client.connect();
// --- End 몽고DB 서버 연결부분 ---

// Prereq: Create collections.

// --- Start 예시 데이터 만들어주기 ---
await client
    .db('mydb1')
    .collection('foo')
    .insertOne({abc: 0}, {writeConcern: {w: 'majority'}});

await client
    .db('mydb2')
    .collection('bar')
    .insertOne({xyz: 0}, {writeConcern: {w: 'majority'}});
// --- End 예시 데이터 만들어주기 ---
// --- 여기까진 트랜젝션이랑 상관없다. ---
// --- 여기까진 테스트할 때 필요 데이터를 만들어주는 역할일 뿐이다. ---

// 몽고DB에서 트랜젝션을 사용하려면 콜렉션(=모델, 스키마)이 만들어져 있어야한다.

// --- 여기부터 트랜잭션 시작이다.
// 우선 세션(Session)이란걸 만든다.
// Step 1: Start a Client Session
//    - startSession: 몽구스에도 똑같은 메서드가 있다.  
//      이 메서드를 통해 세션을 만든다.
//      이 메서드를 사용하면 몽고DB에 가상 공간(메모리)이 만들어진다.
//      그 공간 위에 트랜젝션 작업들(데이터 불러오고 가공하는..)이 적용돼고, 마지막에 한번에 반영이된다.
//    - 이전에 isolate 분리된 환경에 대해 말했었다.  
//      그 분리된 환경을 위해 세션(가상 공간, 메모리)을 만들어주는 것이다.
const session = client.startSession();
[mongoDB11_2.md](mongoDB11_2.md)

// Step 2: Optional. Define options to use for the transaction
//    - 설정 부분이다. 일단 넘어가자.
const transactionOptions = {
    readPreference: 'primary',
    readConcern: {level: 'local'},
    writeConcern: {w: 'majority'}
};

// Step 3: Use withTransaction to start a transaction, execute the callback, and commit (or abort on error)
// Note: The callback for withTransaction MUST be async and/or return a Promise.
//    - 위에서 생성한 session에서 withTransaction 메서드를 호출한다.
//      이 안에 인자로 Promise 함수를 넣어줘야된다.
try {
    // withTransaction
    //  - transaction이 실행되고 있을 때, 또 다른 transaction 처리가 들어오면(같은 document를 처리하려고), 그 transaction을 막게된다.
    //    그럼 막혀진 transaction은 튕겨나는데, 그 튕겨진 transaction이 실패 처리가 되는 것이 아니고, 계속 재시도를 한다. 전 transaction이 끝날 때까지.
    //    즉, 앞의 transaction 처리가 끝나고, 이 transaction이 실행되는 것을 보장해준다.
    //  - withTransaction은 꼭 해주는게 좋다.
    //    Concurrency 문제가 있을 때, 당연히 필요한 처리인 것이다. (무조건 사용한다고 보면된다.)
    //  - transaction을 사용하면, 일시적인 오류가 생길 수 있는데, 그런게 생기더라도 알아서 재시도를 해준다.
    await session.withTransaction(async () => {
        // 이 안에서 트랜젝션으로 묶고싶은 로직을 넣어주면된다.
        //    - 이전 예시를 생각해보면 Comment를 생성하고 save하는 로직이 여기 들어있으면된다.
        const coll1 = client.db('mydb1').collection('foo');
        const coll2 = client.db('mydb2').collection('bar');

        // Important:: You must pass the session to the operations

        // 아래와 같이 두번째 인자(옵션)에 session이란걸 넣어줘야된다.
        //    - 이렇게하면 아래 작업은 session 안에서 묶여서 처리된다.
        //      위에도 이 부분이 중요하다고 언급되어있다.
        //      이곳에 로직이 적혀있어도 session을 안넘겨주면 일반적인 절차로 처리가된다.
        //      그러면 일관성, 롤백 이런 것들이 보장이 안되게 된다.
        await coll1.insertOne({abc: 1}, {session});
        await coll2.insertOne({xyz: 999}, {session});
    }, transactionOptions);
} finally {
    // 그리고 try 뒤에 finally를 붙여줄 수 있는데, finally는 성공을하던 실패를하던 무조건 실행되는 부분이다.
    await session.endSession(); // finally에서 session을 닫아줘야된다.
    // 성공하던 실패하던 session을 닫아주는 것이다.
    // 이 session은 이 transaction에서만 사용하는 것이기 때문이다.
    await client.close();
}

위에서 확인할 수 있듯이, 코드 자체는 크게 달라지는 것은 없다.
위와 같이 session을 만들어주고, withTransaction으로 감싸주고, session을 닫아주기만하면 된다.

20230404 224114

그런데 아까는 똑같은 데이터 생성할 때, 몇 초 안걸렸는데 지금은 훨씬 오래 걸린다.
그 이유는 좀 전에는 병렬로 처리됐기 때문이다.
즉, 서로 덮어씌워버리면서 작업이돼서 빠르게 처리됐던 것이다.


이번엔 트랜젝션(Transaction)을 걸어버려서 병렬로 API 요청을 했음에도, 데이터베이스에서 막아준 것이다.
같은 document(문서)를 수정하는 요청이 여러개가 있으니, 그런 애들은 동기적으로 처리가 되게끔 트랜젝션(Transaction)이 처리해준 것이다. 데이터베이스에서.

물론 같은 document(문서)를 수정하는 요청이 아니면, 병렬로 처리된다.

20230404 224528

일단 블로그 20개가 추가되었고, 확인해보면

20230404 224712

commentsCount랑 comments에 들어있는 comment 갯수가 다르다.

20230404 224907

해당 comment를 blogId로 검색해보면, commentsCount에 명시된 5개가 나온다.
위 comment들을 createdAt으로 정렬해보겠다.

20230404 225435 20230404 225510

{blog: ObjectId('642c28b25e6de7cfe04ddc17')}

ObjectId에서 첫부분이 타임스탬프에 해당이된다.
그래서 여기서 시간을 추출해낼 수 있는데, ObjectId로 정렬을 하게되면, createdAt으로 정렬하는 것과 똑같은 결과가 나온다.
그래서 위에서 createdAt: -1 조건을 넣어서 탐색하나 빼서 탐색하나 결과값은 동일하게 나온다.


그리고 이런식으로 Transaction으로인해 Concurrency 문제가 발생하지 않는다고 하더라도, 문제없이 데이터가 적용된다고 하더라도
어떠한 이유 때문에

  • 네트워크 장애
await Promise.all([
    comment.save({ session }),
    blog.save(),
])

comment.save({ session })은 성공했는데, blog.save()는 실패할 수도 있다.
둘 다 처리가되던지, 아니면 처리가 안되던지 해야되는데, 하나만 처리되고 하나가 실패했을 때가 문제이다.

이는 굉장히 치명적인 문제이다.

하지만 이 또한 Transaction의 Atomicity로 인해 해결이된다.
즉, 결론은 그런 경우일 수록 Transaction을 더더욱 사용해야된다는 뜻이다.

Thank You for Visiting My Blog, Have a Good Day 😆
© 2022 Developer hyungju-lee, Powered By Gatsby.