본문 바로가기
개발자 도전기/[STUDY] DATABASE

mongoDB | mongoose | 업무에서 사용하는 기능들 정리(v4.4 기준) - Transactions

by 답수 2022. 10. 31.
728x90
SMALL

 

 

 

 

DB를 다루는 사람이라면 당연하게 트랜잭션이라는 개념에 대해서 알아야 하고, 실제로 사용할 수 있어야 한다고 생각한다. 실제로 기술 면접을 봤을 때, DB 인덱스나 트랜잭션에 대한 질문을 많이 받았던 기억이 있다. 

 

현재는 몽고디비를 메인 스토리지로 사용하고 있지만, 취업 전에 여러 프로젝트를 진행할 때에는 MySQL을 주로 사용했었다. 그리고 어설프게나마 트랜잭션을 고려하면서 DB를 설계하고 해당 기능을 사용했었다.

 

 

▼ 트랜잭션 개념 다시 한 번 정리

더보기

트랜잭션(transaction)

질의(query)를 하나의 묶음 처리해서 만약 중간에 실행이 중단됐을 경우, 처음부터 다시 실행하는 Rollback을 수행하고, 오류없이 실행을 마치면 commit을 하는 실행 단위

쉽게 말해 트랜잭션은 한 번 질의가 시작되면 해당 질의가 모두 수행되거나 모두 수행되지 않는 작업 수행의 논리적인 단위이다. 트랜잭션은 DB 서버에 여러 개의 요청이 동시에 접근하는 등 데이터 부정합이 발생하는 것은 방지하기 위해서 사용된다. 부정합 상황을 없애기 위해서 하나의 프로세스에서만 DB처리를 하도록 설정하면 되겠지만, 이는 효율성 측면에서 매우 불리하기 때문에 병렬로 처리하는 상황에서 부정합을 방지하기 위해 트랜잭션을 사용하는 것이다.

 

트랜잭션 특성(ACID)

1. 원자성(Atomicity)

  • All or Nothing: 트랜잭션의 작업이 부분적으로 실행되거나 중단되지 않는 것을 보장. 즉 작업의 일부분만 실행되는 상황의 발생을 막음
  • 수행하고 있는 트랜잭션에 의해 변경된 내역을 유지하면서, 이전에 commit된 상태를 임시 영역에 따로 저장하여 원자성을 보장함. 만약 현재 작업 중인 트랜잭션에서 에러가 발생하면 현재 내역은 날라가고, 임시 영역에 저장되어 있던 상태로 rollback함

2. 일관성(Consistency)

  • 트랜잭션이 성공적으로 완료되면 일관적인 DB상태 유지
  • 데이터의 형식이 string이었다면, 이는 계속 string인 상태 유지
  • 서로 연관이 있는 tableA와 tableB가 있을 때, tableA의 제약조건이 변경되었다면, 다른쪽 테이블인 tableB에도 같은 변경사항이 적용되어야 함

3. 격리성(Isolation)

  • 트랜잭션이 수행되고 있을 때 다른 트랜잭션의 작업이 끼어들지 못하도록 보장
  • 데드락 상황이 발생하지 않도록 처리하는 것 중요

4. 지속성(Durability)

  • 성공적으로 수행된 트랜잭션은 영원히 반영됨

 

트랜잭션을 적절한 곳에 적절하게 사용하는 것이 매우 중요한 것은 알겠지만, 아직 레벨이 낮은 나는 여러 번의 시행착오를 거치면서 트랜잭션을 이용했어야 했다. 또한 몽고디비같은 NoSQL에서는 트랜잭션이라는 기능이 없다고 생각했다(그저 다 직접 구현해야 하는 것으로 생각함).

 

내가 사용하는 몽고디비같은 경우는 4.2버전부터 다중 도큐먼트에 대한 트랜잭션 기능이 추가되었다고 한다. 우리 팀은 현재 4.4버전을 사용하고 있고, 사용자를 휴면전환하거나 삭제하는 과정에서 트랜잭션을 사용하고 있다(다만 몽고디비 같은 NoSQL은 방대한 데이터를 수시로 읽고 쓰기 때문에 트랜잭션 같은 고비용 처리를 자주 사용하면 몽고디비를 사용하는 의미가 무색해질 수 있다고 생각한다). 암튼 공식 문서를 보면서 몽고디비의 트랜잭션을 정리해보자아

 

Transactions

몽고디비는 단일 도큐먼트의 작업을 ACID하게 처리할 수 있도록 지원한다. 또한 몽고디비는 NoSQL로 스키마리스한 특성 덕분에 하나의 도큐먼트에 유연하게 필요한 데이터를 필요한 형식에 맞게 저장할 수 있다. 그래서 DB를 다룰 때 ACID 보장에 대해 크게 걱정할 필요가 없다. 하지만 단일 도큐먼트에 많은 데이터를 포함하게 되면 데이터량이 방대해질 경우 오히려 성능면에서 비효율적이게 될 것이며, 결국 여러 컬렉션들을 하나의 트랜잭션으로 사용해야 하는 상황도 발생할 것이다.

 

공식 문서에서의 예제를 먼저 보려고 한다. 이 예제에서는 트랜잭션을 시작하고, 지정된 작업을 실행하고, 커밋(commit) 및 중단(abort)하는 트랜잭션 작업을 위한 new callback API를 사용한다.

 // 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/'
  const client = new MongoClient(uri);
  await client.connect();
  // Prereq: Create collections.
  await client
    .db('mydb1')
    .collection('foo')
    .insertOne({ abc: 0 }, { writeConcern: { w: 'majority' } });
  await client
    .db('mydb2')
    .collection('bar')
    .insertOne({ xyz: 0 }, { writeConcern: { w: 'majority' } });
  // Step 1: Start a Client Session
  const session = client.startSession();
  // 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.
  try {
    await session.withTransaction(async () => {
      const coll1 = client.db('mydb1').collection('foo');
      const coll2 = client.db('mydb2').collection('bar');
      // Important:: You must pass the session to the operations
      await coll1.insertOne({ abc: 1 }, { session });
      await coll2.insertOne({ xyz: 999 }, { session });
    }, transactionOptions);
  } finally {
    await session.endSession();
    await client.close();
  }

 

Transactions and Operations

분산 트랜잭션은 여러 개의 작업, 콜렉션, 데이터베이스, 도큐먼트, 샤드(4.2버전 이상)에서 사용할 수 있다. 즉 기존의 콜렉션에 대한 CRUD에서도 트랜잭션 작업이 가능하다. 다만 샤드 간 Write 트랜잭션에서는 새 콜렉션을 만들 수 없다. 예를 들어, 하나의 샤드에 존재하는 기존 콜렉션에 writing 작업을 하면서 다른 샤드에 암시적으로 콜렉션을 생성하는 경우 몽고디비는 동일한 트랜잭션에서 두 작업을 모두 수행할 수 없다. 다음은 다중 도큐먼트 트랜잭션을 지원하는 CRUD 동작들이다.

Method Command Note
 
다음 쿼리 연산자 표현식 제외 :
이 메서드는 쿼리에 $match 집계 스테이지를 사용하고 $sum 표현식과 함께 $group 집계 스테이지를 사용하여 계산을 수행합니다.
샤딩되지 않은 컬렉션에서만 사용할 수 있습니다.
샤드된 컬렉션의 경우 $group 단계에서 집계 파이프 라인을 사용하세요. Distinct Operation을 참고
 
 
 
 
4.4 버전 이상의 경우, update 또는 replace 동작을 upsert: true 옵션을 주고 사용한다면 존재하지 않는 컬렉션을 즉시 생성합니다.
4.2 이하의 버전의 경우 upsert: true 옵션을 준 동작은 기존 컬렉션에 대해서만 실행되어 집니다.
DDL Operations을 참고
4.4 버전 이상의 경우, 존재하지 않는 컬렉션을 즉시 생성합니다.
4.2 버전 이하에서는 기존 컬렉션에 대해서만 실행되어 집니다.
DDL Operations을 참고
 
4.4 버전 이상의 경우, 존재하지 않는 컬렉션에 대한 insert 작업은 컬렉션을 즉시 생성합니다.
4.2 버전 이하에서는 기존 컬렉션에 대해서만 실행되어 집니다.
DDL Operations을 참고
4.4 이상의 버전에서 upsert: true 옵션을 주고 사용한다면 존재하지 않는 컬렉션을 즉시 생성합니다.
4.2 이하의 버전의 경우 upsert: true 옵션을 준 동작은 기존 컬렉션에 대해서만 실행되어 집니다.
DDL Operations을 참고
 
4.4 이상의 버전에서 insert와 update 작업에 upsert: true 옵션을 주고 사용한다면 존재하지 않는 컬렉션을 즉시 생성합니다.
4.2 이하의 버전의 경우 upsert: true 옵션을 준 동작은 기존 컬렉션에 대해서만 실행되어 집니다.
DDL Operations을 참고

 

Transactions and Sessions

트랜잭션은 세션과 연결된다. 즉 세션에 대한 트랜잭션을 시작한다. 주어진 시간에 세션에 대해 최대 하나의 열린 트랜잭션을 가질 수 있다. 드라이버를 사용할 때 트랜잭션의 각 작업은 세션과 연결되어야 한다. 세션이 종료되고 트랜잭션이 열려있으면 트랜잭션이 중단된다.

 

Read Concern / Write Concern / Read Preference / Write Preference

대부분의 관계형 데이터베이스는 단일 노드로 작동하는 아키텍처를 기본으로 하는 반면, 몽고디비는 분산 처리를 기본 아키텍처로 사용한다. 

이 부분은 여기 참고하여 공부..

 

 

Transactions Methods

startSession()

이 메소드는 세션과 연결하기 위해서 사용되고, 이 메소드를 사용하여 세션 옵션을 가진 도큐먼트를 가져올 수 있다.

db = db.getMongo().startSession({retryWrites: true, causalConsistency: true}).getDatabase(db.getName());
  • retryWrites: 일시적인 네트워크 오류 발생 시 쓰기를 재시도하는 기능

 

session.startTransaction()

트랜잭션을 커밋하거나 중단하는 시기를 더 세밀하게 제어하기 위해서 이 메소드를 사용할 수 있다.

const Customer = db.model('Customer', new Schema({ name: String }));

let session = null;
return Customer.createCollection().
  then(() => db.startSession()).
  then(_session => {
    session = _session;
    // Start a transaction
    session.startTransaction();
    // This `create()` is part of the transaction because of the `session`
    // option.
    return Customer.create([{ name: 'Test' }], { session: session });
  }).
  // Transactions execute in isolation, so unless you pass a `session`
  // to `findOne()` you won't see the document until the transaction
  // is committed.
  then(() => Customer.findOne({ name: 'Test' })).
  then(doc => assert.ok(!doc)).
  // This `findOne()` will return the doc, because passing the `session`
  // means this `findOne()` will run as part of the transaction.
  then(() => Customer.findOne({ name: 'Test' }).session(session)).
  then(doc => assert.ok(doc)).
  // Once the transaction is committed, the write operation becomes
  // visible outside of the transaction.
  then(() => session.commitTransaction()).
  then(() => Customer.findOne({ name: 'Test' })).
  then(doc => assert.ok(doc)).
  then(() => session.endSession());

 

 

728x90
LIST

댓글