replication은 DB 복제본을 여러 서버에 나눠서 저장할 때 자신을 지칭하는 이름이다.
MongoDB에서 Transaction을 사용하기 위해서는 replication 옵션을 켜야 하고 MongoDB 서버가 replication 값에 대해서 어떤 이름을 가졌는지 정해줘야 한다.
운영체제별 config file 위치
config file 에 다음 설정을 변경한다.
replication:
oplogSizeMB: 2000
// replSetName 은 원하는 이름으로 지정 가능
replSetName: rs0
enableMajorityReadConcern: true
MongoDB 서버 재시작
// 서버 재시작
MacBookPro ~ % brew services restart mongodb-community@6.0
// MongoDB 서버 접속
MacBookPro ~ % mongosh
// replication을 사용
test> rs.initiate()
// DB switch
rs0 [direct: other] test> use mongodb
switched to db mongodb
// 데이터 조회
rs0 [direct: primary] mongodb> db.products.find()
[
{
_id: ObjectId("639489cb3e8aff05670e251d"),
contents: 'This is shoes3',
name: 'shoes3',
price: 27000,
updated_at: ISODate("2022-08-01T03:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e251e"),
contents: 'This is shoes4',
name: 'shoes4',
price: 36000,
updated_at: ISODate("2022-08-01T04:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e251f"),
contents: 'This is shoes5',
name: 'shoes5',
price: 45000,
updated_at: ISODate("2022-08-01T05:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e2520"),
contents: 'This is shoes6',
name: 'shoes6',
price: 54000,
updated_at: ISODate("2022-08-01T06:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e2521"),
contents: 'This is backpack',
name: 'backpack',
price: 50000,
updated_at: ISODate("2022-08-02T04:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e2522"),
contents: 'This is shirt',
name: 'shirt',
price: 20000,
updated_at: ISODate("2022-08-03T05:00:00.000Z")
},
{
_id: ObjectId("639489cb3e8aff05670e2523"),
contents: 'This is glasses',
name: 'glasses',
price: 10000,
updated_at: ISODate("2022-08-04T06:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8773"),
contents: 'This is shoes1',
name: 'shoes1',
price: 20000,
updated_at: ISODate("2022-08-01T01:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8774"),
contents: 'This is shoes2',
name: 'shoes2',
price: 30000,
updated_at: ISODate("2022-08-01T02:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8775"),
contents: 'This is shoes3',
name: 'shoes3',
price: 40000,
updated_at: ISODate("2022-08-01T03:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8776"),
contents: 'This is shoes4',
name: 'shoes4',
price: 50000,
updated_at: ISODate("2022-08-01T04:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8777"),
contents: 'This is shoes5',
name: 'shoes5',
price: 60000,
updated_at: ISODate("2022-08-01T05:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8778"),
contents: 'This is shoes6',
name: 'shoes6',
price: 70000,
updated_at: ISODate("2022-08-01T06:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b8779"),
contents: 'This is backpack',
name: 'backpack',
price: 80000,
updated_at: ISODate("2022-08-02T04:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b877a"),
contents: 'This is shirt',
name: 'shirt',
price: 90000,
updated_at: ISODate("2022-08-03T05:00:00.000Z")
},
{
_id: ObjectId("6395c62572145921583b877b"),
contents: 'This is glasses',
name: 'glasses',
price: 100000,
updated_at: ISODate("2022-08-04T06:00:00.000Z")
}
]
Studio 3T에서도 설정 변경
Java 코드로 Connection 을 맺는 uri에도 replicaSet 설정을 query string 형식으로 추가해야한다.
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
MongoClient mongoClient = MongoClients.create(uri);
insertMany
, updateMany
, deleteMany
등의 Operator 는, Operator는 하나이지만, 개별 Document 단위로 Atomic operation 이 수행된다. )findAndModify
(조회와 수정을 한 opration으로 제공) operator를 제공한다. (SQL의 select for update 와 기능이 유사하다. update 를 하기 위해 select 를 진행하는 것이기 때문에 하나의 opration으로 묶어서 진행)중복 코드
import com.mongodb.ReadConcern;
import com.mongodb.ReadPreference;
import com.mongodb.TransactionOptions;
import com.mongodb.WriteConcern;
import com.mongodb.client.*;
import org.bson.Document;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.codecs.pojo.PojoCodecProvider;
import org.de.mongodb.model.Product;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
import static org.bson.codecs.configuration.CodecRegistries.fromRegistries;
public class Main {
public static void main(String[] args) {
// 클래스에 매핑해서 해석할 수 있는 정보를 MongoDB 드라이버에게 알려주어야 한다.
// Product에 대한 codec
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
// MongoClient 객체 하나가 하나의 connection을 의미
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase = mongoClient.getDatabase("mongodb").withCodecRegistry(codecRegistry);
// session 생성
ClientSession clientSession = mongoClient.startSession();
// transaction 조건
TransactionOptions transactionOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary())
.readConcern(ReadConcern.LOCAL)
.writeConcern(WriteConcern.MAJORITY)
.build();
ver.1 : Transaction의 명령어를 객체로 생성해서 실행
// Transaction의 명령어를 객체로 생성해서 실행
// ClientSession.withTransaction 에 transaction의 내용과 option 을 파라미터로 전달.
TransactionBody transactionBody = () -> {
MongoCollection<Document> coll1 = mongoDatabase.getCollection("foo");
MongoCollection<Document> coll2 = mongoDatabase.getCollection("bar");
Map<String, Object> input1 = new HashMap<>();
int id = new Random().nextInt();
int value = new Random().nextInt();
System.out.println("Test input: " + id + " : " + value);
input1.put("_id", id);
input1.put("field", value);
Map<String, Object> input2 = new HashMap<>();
input2.put("_id", id + 1);
input2.put("field", value + 1);
coll1.insertOne(clientSession, new Document(input1));
coll1.insertOne(clientSession, new Document(input2));
coll2.insertOne(clientSession, new Document(input1));
//coll1.insertOne(clientSession, new Document(input1));
return "inserted into collection in different databases";
};
clientSession.withTransaction(transactionBody, transactionOptions);
}
}
ver.2 : Transaction 을 명령어로 시작
장점 : 롤백을 명시적으로 할 수 있음
//Transaction 을 명령어로 시작
// 장점 : 롤백을 명시적으로 할 수 있음
clientSession.startTransaction(transactionOptions); // start
MongoCollection<Document> coll1 = mongoDatabase.getCollection("foo");
MongoCollection<Document> coll2 = mongoDatabase.getCollection("bar");
Map<String, Object> input1 = new HashMap<>();
int id = new Random().nextInt();
int value = new Random().nextInt();
System.out.println("Test input: " + id + " : " + value);
input1.put("_id", id);
input1.put("field", value);
Map<String, Object> input2 = new HashMap<>();
input2.put("_id", id + 1);
input2.put("field", value + 1);
coll1.insertOne(clientSession, new Document(input1));
coll1.insertOne(clientSession, new Document(input2));
coll2.insertOne(clientSession, new Document(input1));
clientSession.abortTransaction(); // 롤백
// 재시작
clientSession.startTransaction(transactionOptions);
// 원래라면 중복오류가 발생
// DB가 롤백 된 상태이기 때문에 오류가 발생하지 않는다.
coll1.insertOne(clientSession, new Document(input1));
// 커밋하고 종료
clientSession.commitTransaction();
}
}
MongoDB의 Transaction이 제대로 기능하기 위해서는 여러 조건의 제약이 따르므로 주의한다.
Operator(함수) 별로 사용할 수 있는 조건 또한 다르다.
위의 조건을 설정 했더라도, Transaction 자체에 대한 제약사항 또한 따로 있다.
Transaction의 가능 불가능 여부
MongoDB에서는 multi-document transaction에 대해서 무분별한 사용시 성능저하가 있을 수 있으니, 웬만하면 data schema design 으로 transaction 사용을 최소화 하는 것을 권장하고 있다.
Transaction(ACID)이 중요하고 서비스나 시스템의 중요도가 크다면 MongoDB보다는 RDBMS를 쓰는 것을 권장한다. MongoDB로 방법을 찾으려면 어떻게든 찾을 수는 있겠지만, 그것에 들어가는 시스템의 구축 비용과 구현 난이도, 디버깅 등 개발에 필요한 여러가지 요소들을 고려해본다면 애초에 목적에 맞게 디자인된 시스템을 필요한 곳에 제대로 사용하는 것이 효과적이고 효율적이다.
시스템/서비스의 ACID 요구사항 수준이 낮거나, MongoDB의 장점이 더욱 중요해서 사용하는 경우 Transaction을 사용하기 보다는 Schema Design(또는 데이터 모델링)을 잘해서 Document 단위의 Atomic Operation 으로 해결할 수 있도록 시스템/서비스/로직을 구성하는 것이 좋다.