프로젝트를 새로 만들고, build.gradle 에 다음 의존성을 추가해준다.
dependencies {
implementation 'org.mongodb:mongodb-driver-sync:4.7.1'
compileOnly 'org.projectlombok:lombok:1.18.24'
annotationProcessor 'org.projectlombok:lombok:1.18.24'
}
다음과 같이 try-with-resource 구문으로 MongoClient
객체를 생성한다.
String uri = "mongodb://localhost:27017" // type your uri with configurations
try (MongoClient mongoClient = MongoClients.create(uri)) {
}
try (MongoClient mongoClient = MongoClients.create(uri)) {
MongoDatabase database = mongoClient.getDatabase("test");
MongoCollection<Document> collection = database.getCollection("movies");
Document doc = collection.find(eq("title", "The Favourite")).first();
System.out.println(doc.toJson());
}
MongoDatabase
: 데이터베이스를 가리키는 객체MongoCollection<T>
: T의 타입의 Document를 가지는 CollectionDocument
: 모든 Document를 담을 수 있는 기본 클래스, Map 인터페이스를 구현하므로 key-value에 대해서 Map처럼 활용할 수 있다.MongoDB와 operation은 주로 기본 함수와 JSON 형식의 메세지로 한다. 하지만 Java 언어의 특성상 타입시스템을 이용해서 더 안전하게 프로그래밍을 할 수 있다. 본 실습에서는 Java의 타입시스템을 적극 활용하는 방식을 사용한다.
MongoDB를 쓰는 가장 큰 이유는 객체지향 프로그래밍 모델을 Document에 그대로 사용할 수 있다는 장점 때문이다. JDBC에서 ORM을 따로 쓰는 것처럼 복잡한 설정 필요 없이 간단하게 POJO class를 Collection의 Document에 매핑할 수 있다.
Product.java
import java.time.LocalDateTime;
import org.bson.codecs.pojo.annotations.BsonId;
import org.bson.codecs.pojo.annotations.BsonProperty;
import org.bson.types.ObjectId;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public class Product {
@BsonId //이렇게 하면 아래 필드는 _id 로 되는 유니크 필드랑 매핑되는 값이다 라고 인지할 수 있음
Object id; //int 써도 되긴 하는데 몽고디비쓰면 웬만하면 Object id 쓰는 게 프라이머리키도 좋기 때문에 object id 쓴다.
String name;
@BsonProperty("updated_at") // 데이터 저장할 때 스네이크 케이스 쓰는 게 보기 좋아서 쓰는데 자바는 캐멀케이스로 이름짓는 게
// 디펙터 스텐다드다. 이럴 때 코드 검사할 때, 스네이크 케이스 있어? 고쳐 하는 워닝이 많이 뜨기 때문에
// 이렇게 해서 디비에선 어떤 이름이야. 라고 BsonProperty 어노테이션을 통해 명시해줄 수 있음.
// 실제로 디비에서는 updated_at의 데이터를 주더라도 updatedAt 필드에 매핑할 수 있음
// 내가 updatedAt 필드에 매핑해서 데이터를 넣더라도 디비에는 updated_at의 필드에 들어간다.
LocalDateTime updatedAt; // java.sql 타임스탬프 썼었는데 그대로 써도 된다.
String contents;
int price;
}
@BsonId
: _id 에 매핑되는 필드에 달아준다.@BsonProperty
: MongoDB에 저장되는 field 이름과 실제 Java 의 변수 이름이 다른 경우에 DB에서 사용하는 이름을 따로 명시해준다.@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@ToString
public static void main(String[] args) {
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
// 이게 없으면 Can't find a codec for class org.de.mongodb.model.Product. 에러메세지 나옴.
// 실제로 내가 클래스에 매핑한다고 했는데, 이게 진짜 자동으로 되는 게 아니라 이걸 해석할 수 있는 정보를 몽고디비 드라이버한테 알려줘야 함.
// 그걸 코덱이라고 함.
CodecRegistry codecRegistry = fromRegistries(getDefaultCodecRegistry(),fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017";
MongoClient mongoclient = MongoClients.create(uri); // 이 객체 하나가 하나의 connection을 의미함.
MongoDatabase mongoDatabase = mongoclient.getDatabase("de_mongodb").withCodecRegistry(codecRegistry);
MongoCollection<Product> collection = mongoDatabase.getCollection("products", Product.class);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
List<Product> products = new ArrayList<>();
products.add(new Product(null, "shoes1", LocalDateTime.parse("2022-08-01 01:00:00", formatter),
"This is shoes1", 10000));
products.add(new Product(null, "shoes2", LocalDateTime.parse("2022-08-01 02:00:00", formatter),
"This is shoes2", 20000));
products.add(new Product(null, "shoes3", LocalDateTime.parse("2022-08-01 03:00:00", formatter),
"This is shoes3", 30000));
products.add(new Product(null, "shoes4", LocalDateTime.parse("2022-08-01 04:00:00", formatter),
"This is shoes4", 40000));
products.add(new Product(null, "shoes5", LocalDateTime.parse("2022-08-01 05:00:00", formatter),
"This is shoes5", 50000));
products.add(new Product(null, "shoes6", LocalDateTime.parse("2022-08-01 06:00:00", formatter),
"This is shoes6", 60000));
products.add(new Product(null, "backpack",
LocalDateTime.parse("2022-08-02 04:00:00", formatter), "This is backpack", 50000));
products.add(new Product(null, "shirt", LocalDateTime.parse("2022-08-03 05:00:00", formatter),
"This is shirt", 20000));
products.add(new Product(null, "glasses", LocalDateTime.parse("2022-08-04 06:00:00", formatter),
"This is glasses", 10000));
InsertManyResult insertManyResult = collection.insertMany(products);
// wasAcknowledged() 메서드는 클라이언트가 보낸 insertMany 요청에 대해 서버가 성공적으로 응답(acknowledge)했는지를 확인한다.
// true이면 서버가 요청을 수신하고 처리했음을 의미한다.
// 여기서의 "acknowledge"는 네트워크 통신에서 사용하는 ACK(acknowledgement) 개념과 유사하며,
// 예를 들어 TCP 3-way handshake에서 SYN → SYN-ACK → ACK처럼 클라이언트가 요청을 보내고
// 서버가 이를 수신했음을 알리는 메커니즘과 동일한 의미를 갖는다.
if (insertManyResult.wasAcknowledged()){
System.out.println("insert ids : " + insertManyResult.getInsertedIds());
}
}
collection 객체에 insertOne 또는 insertMany 함수에 객체를 통채로 넘겨준다.
이것이 가능하려면 먼저 POJO를 해석할 수 있는 Codec이 등록되어있어야 한다.
find() 함수로 검색을 할 수 있다. 검색 결과는 MongoIterator인데, JDBC와 유사하게 cursor 방식으로 데이터를 확인할 수 있다.
MongoCursor<Product> cursor = productCollection.find().cursor();
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
com.mongodb.client.model.Filters ;패키지에 있는 필터들을 통해서 조회 조건을 타입 안정성과 함께 걸 수 있다.
import static com.mongodb.client.model.Filters.eq;
import static com.mongodb.client.model.Filters.regex;
MongoCursor<Product> cursor = productCollection.find(eq("price", 10000)).cursor();
MongoCursor<Product> cursor = productCollection.find(regex("name", "shoes")).cursor();
find 의 결과에 sort() 함수를 chaining 해서 정렬을 할 수 있다.
정렬의 종류는 com.mongodb.client.model.Sorts 클래스에 정적 함수로 있다.
import static com.mongodb.client.model.Sorts.descending;
MongoCursor<Product> cursor =
productCollection.find(regex("name", "shoes")).sort(descending("price")).cursor();
find의 결과에 projection() 함수를 chaining 해서 원하는 필드만 가져올 수 있다.
Projection의 종류는 import static com.mongodb.client.model.Projections 클래스에 정적 함수로 있다.
다만, POJO mapping을 하는 경우 POJO class 에 있는 필드는 exclude가 안된다. slice 역시 POJO class 형식과 다르면 안된다.
단순히 메타데이터를 가져올 때는 POJO로 그대로 매핑하는 것이 좋지만, aggregation등의 연산을 할 때는 Projection으로 필요한 데이
터만 사용해서 연산하는 것이 성능에 좋다.
MongoCursor<Product> cursor = productCollection.find(regex("name", "shoes"))
.projection(fields(include("name", "price"))).sort(descending("price")).cursor();
업데이트는 업데이트 대상을 고르는 filter, 그리고 업데이트할 내용인 update, 두 종류의 BSON parameter를 이용해서 update한다.
조건은 fields method 로 여러개를 이어 붙일 수 있다.
update는 set, unset, set on insert, increment, multiply, rename, min, max, current date 등 타입 안정성을 제공하는 전용 함수들이
많다. document를 보고 원하는 함수를 찾아서 사용한다.
UpdateResult updateResult = productCollection.updateMany(fields(regex("name", "shoes"), // filter
// shoes
gt("price", 10000)), // filter price > 10000
mul("price", 0.9) // 10% discount
);
if (updateResult.wasAcknowledged()) {
System.out.println("modified: " + updateResult.getModifiedCount());
}
삭제는 filter 조건만 넣으면 삭제가 된다.
연관된 document가 있는 경우 cascading 옵션이 없으므로 삭제시 참조하는 데이터를 어떻게 처리할지를 고민해서 자동화할 수 있도록 해야한다.
DeleteResult deleteResult = productCollection.deleteMany(fields(regex("name", "shoes"), // filter// shoes
gte("price", 10000), lt("price", 20000)) // filter 10000 <= price < 20000
);
if (deleteResult.wasAcknowledged()) {
System.out.println("deleted: " + deleteResult.getDeletedCount());
}
MongoDB 에서 집계를 위한 연산을 Aggregation Pipeline 기능을 통해서 할 수 있다.
Aggregation 에서 사용하는 대표적인 기능은 다음 3종류이다.
1. $match
: 원하는 조건에 맞는 document 만 고른다. SQL의 WHERE 조건절이라고 생각하면 된다.
2. $group
: 어느 단위로 묶어서 집계를 할지 정한다. SQL의 group by 라고 생각하면 된다.
3. $sort
, $limit
, $count
: 집계 결과에 대해서 한정한다.
MongoCursor<Document> cursor = productCollection.aggregate(
Arrays.asList(Aggregates.match(gt("price", 10000)),
Aggregates.group("$name", Accumulators.avg("avg_price", "$price")),
Aggregates.sort(descending("avg_price"))))
.iterator();
while (cursor.hasNext()) {
System.out.println(cursor.next());
}
MongoCollection<Product>
을자신이 설치한 mongodb의 config 에서 다음 설정을 세팅하고 mongodb를 재시작한다.
내 컴퓨터 환경은 Mac 이고 /opt/homebrew/etc/mongod.conf
에 conf파일이 위치해있었다.
replication:
oplogSizeMB: 2000
replSetName: rs0
enableMajorityReadConcern: true
brew services restart mongodb-community@8.0
프로세스를 재시작 한 뒤, mongosh 에 접속해서 다음 명령어를 실행한다.
mongosh
rs.initiate()
이후 find 명령어로 데이터가 잘 가져와지는지 확인한다.
use de-mongodb
db.products.find()
Studio3T 에서는 Connection 설정을 변경해야 접속할 수 있다.
Java 코드로 Connection 을 맺는 uri에도 replicaSet 설정을 query string 형식으로 추가해야한다.
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
MongoClient mongoClient = MongoClients.create(uri);
MongoDB에서는 기본적으로 Document 단위의 Atomic Operation을 제공한다.
여러개의 Document를 삽입/수정하는 Operation에 대해서도 Operation 단위가 아니라 Document 단위의 Atomic을 제공한다.
insertMany
, updateMany
, deleteMany
등의 Operator 는, Operator는 하나이지만, 개별 Document 단위로 Atomic operation이 수행된다.조회와 수정에 대해서 Atomic 을 제공하기 위해서 findAndModify
operator를 제공한다.
select for update
와 기능이 유사하다.개별 Document 단위의 Atomic으로 부족하다면, Transaction을 이용할 수 있다.
Transaction, Session의 개념은 RDBMS와 동일하다.
ClientSession clientSession = mongoClient.startSession(); // default
// mongoClient.startSession(ClientSessionOptions.builder().snapshot(false).causallyConsistent(false).build()); // ClientSession option
clientSession.close();
TransactionOptions transactionOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary())
.readConcern(ReadConcern.LOCAL)
.writeConcern(WriteConcern.MAJORITY)
.build();
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry =
fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase =
mongoClient.getDatabase("de-mongodb").withCodecRegistry(codecRegistry);
ClientSession clientSession = mongoClient.startSession();
TransactionOptions transactionOptions =
TransactionOptions.builder().readPreference(ReadPreference.primary())
.readConcern(ReadConcern.LOCAL).writeConcern(WriteConcern.MAJORITY).build();
TransactionBody transactionBody = () -> {
clientSession.startTransaction(transactionOptions);
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);
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry =
fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase =
mongoClient.getDatabase("de-mongodb").withCodecRegistry(codecRegistry);
ClientSession clientSession = mongoClient.startSession();
TransactionOptions transactionOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary()).readConcern(ReadConcern.LOCAL)
.writeConcern(WriteConcern.MAJORITY).build();
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.startTransaction(transactionOptions); // duplicate
clientSession.commitTransaction(); // commit & end
CodecProvider pojoCodecProvider = PojoCodecProvider.builder().automatic(true).build();
CodecRegistry codecRegistry =
fromRegistries(getDefaultCodecRegistry(), fromProviders(pojoCodecProvider));
String uri = "mongodb://localhost:27017/?replicaSet=rs0";
MongoClient mongoClient = MongoClients.create(uri);
MongoDatabase mongoDatabase =
mongoClient.getDatabase("de-mongodb").withCodecRegistry(codecRegistry);
ClientSession clientSession = mongoClient.startSession();
TransactionOptions transactionOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary()).readConcern(ReadConcern.LOCAL)
.writeConcern(WriteConcern.MAJORITY).build();
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(); // rollback & end
clientSession.startTransaction(transactionOptions); // start
coll1.insertOne(clientSession, new Document(input1));
clientSession.commitTransaction(); // commit & end
다만, MongoDB의 Transaction이 제대로 기능하기 위해서는 여러 조건의 제약이 따르므로 주의한다. 조건으로는 다음과 같은 조건들이
있다. Operator(함수) 별로 사용할 수 있는 조건 또한 다르다.
❗ In most cases, multi-document transaction incurs a greater performance cost over single document writes, and the
availability of multi-document transactions should not be a replacement for effective schema design. For many
scenarios, the denormalized data model (embedded documents and arrays) will continue to be optimal for your data and use cases. That is, for many scenarios, modeling your data appropriately will minimize the need for multi-
document transactions.
For additional transactions usage considerations (such as runtime limit and oplog size limit), see also Production Considerations.
실무적으로 조언을 한다면, Transaction(ACID)이 중요하고 서비스나 시스템의 중요도가 크다면 MongoDB보다는 RDBMS를 쓰는 것을 권장한다. MongoDB로 방법을 찾으려면 어떻게든 찾을 수는 있겠지만, 그것에 들어가는 시스템의 구축 비용과 구현 난이도, 디버깅 등 개발에 필요한 여러가지 요소들을 고려해본다면 애초에 목적에 맞게 디자인된 시스템을 필요한 곳에 제대로 사용하는 것이 효과적이고 효율적이다.
시스템/서비스의 ACID 요구사항 수준이 낮거나, MongoDB의 장점이 더욱 중요해서 사용하는 경우 Transaction을 사용하기 보다는 Schema Design(또는 데이터 모델링)을 잘해서 Document 단위의 Atomic Operation 으로 해결할 수 있도록 시스템/서비스/로직을 구성하자.