MongoDB Transaction

MongoDB 드라이버와 Mongoose를 사용하여 트랜잭션을 처리하는 방법을 비교하고, withTransaction 사용의 장점을 설명합니다.

MongoDB 스터디 중, Transaction 에 대해 살펴보게 되었다. MongoDB Driver 에서 직접 Transaction 처리를 하는 경우와, Mongoose 에서 처리하는 경우를 살펴보았다.

MongoDB Driver (Node.js)

// 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: Collection 이 미리 생성되어 있어야 한다.
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: Transaction 처리를 위한 Client Session 을 생성한다.
const session = client.startSession();

// Step 2: (Optional) Transaction 옵션을 설정한다.
const transactionOptions = {
    readPreference: 'primary',
    readConcern: {
        level: 'local'
    },
    writeConcern: {
        w: 'majority'
    }
};

// Step 3: session.withTransaction 메서드를 통해 Transaction 을 시작한다. 콜백 함수를 통해 Transaction 비즈니스 로직을 처리한다. 비즈니스 로직을 처리한 후에, Commit 혹은 Rollback 처리한다.
// 주의: withTransaction 콜백은 async 함수이거나, Promise 를 반환해야 한다.
// 주의: 콜백 함수 내에서도, DB 변경시 두 번째 인자로 session 을 넘겨주어야, Transaction 처리된다. session 을 넘겨주지 않으면, 일반 DML 과 동일하게 동작한다.
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();
}


Mongoose

MongoDB Driver 에서 withTransaction 콜백 함수 내에서도 DML 시 session 객체를 항상 넘겨주어야 하는 것이 불만이었다. typeorm 에서는, Transaction 선언 시 Transaction 을 처리할 수 있는 EntityManager 가 생성되어서, 해당 EntityManager 을 통해서 처리한 DML 은 항상 Transaction 내에서 동작한다. 하지만 Mongoose 에서는 MongoDB Driver 와 같이, session 을 선언한 후에 session 객체를 DML 문에 같이 넣어주어야 한다.

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());


Mongoose 문서에서는 위와 같은 코드로 예시가 나와있는데, 개인적으로는 startTransaction() 보다는 withTransaction() 을 사용하는 것이 버그를 더 줄여줄 것 같다. 수동으로 Transaction 을 열고 닫는 것 보다는, withTransaction 을 통해 Transaction 의 상태관리는 Mongoose 에게 맡기고, 코드상으로는 비즈니스 로직에 집중하는 것이 간결하기 때문이다. (물론 session 객체는 수동으로 닫아줘야 한다는 아쉬움은 남아있다.)


참고문서

MongoDB Driver Mongoose 공식문서 Typeorm 공식문서


이것도 읽어보세요