MongoDB Atlas를 사용해 견고한 애플리케이션 구축하기
MongoDB의 기능을 활용하고 복제본 세트 투표를 정상적으로 처리하는 애플리케이션 코드를 작성하려면 다음을 수행해야 합니다.
최신 드라이버를 설치합니다.
모든 호스트를 지정하는 연결 문자열을 사용합니다.
재시도 가능 쓰기 및 재시도 가능 읽기를 사용합니다.
애플리케이션에 적합한
majority
쓰기 고려와 읽기 고려를 사용합니다.애플리케이션의 오류를 처리합니다.
최신 드라이버 설치
먼저 MongoDB 드라이버에서 해당 언어에 맞는 최신 드라이버를 설치합니다. 드라이버는 애플리케이션에서 데이터베이스에 쿼리를 연결하고 전달합니다. 최신 드라이버를 사용하면 최신 MongoDB 기능을 사용할 수 있습니다.
그런 다음 애플리케이션에서 종속성을 가져옵니다.
Maven 를 사용하는 경우 pom.xml
종속성 목록에 다음을 추가합니다.
<dependencies> <dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> <version>4.0.1</version> </dependency> </dependencies>
Gradle을 사용하는 경우, build.gradle
종속성 목록에 다음을 추가합니다:
dependencies { compile 'org.mongodb:mongodb-driver-sync:4.0.1' }
// Latest 'mongodb' version installed with npm const MongoClient = require('mongodb').MongoClient;
연결 문자열
배포의 모든 호스트를 지정하는 연결 string 을 사용하여 애플리케이션을 데이터베이스에 연결합니다. 배포에서 복제본 세트 투표 를 수행하고 새 프라이머리가 선택되면 배포의 모든 호스트를 지정하는 연결 string 이 애플리케이션 로직 없이 새 프라이머리를 검색합니다.
다음 중 하나를 사용하여 배포의 모든 호스트를 지정할 수 있습니다.
연결 string 은 옵션, 특히 retryWrites 및 writeConcern을 지정할 수도 있습니다.
연결 문자열을 사용하여 애플리케이션에서 MongoDB 클라이언트를 인스턴스화합니다.
// Create a variable for your connection string String uri = "mongodb://[<username>:<password>@]hostname0<:port>[,hostname1:<port1>][,hostname2:<port2>][...][,hostnameN:<portN>]"; // Instantiate the MongoDB client with the URI MongoClient client = MongoClients.create(uri);
// Create a variable for your connection string const uri = "mongodb://[<username>:<password>@]hostname0<:port>[,hostname1:<port1>][,hostname2:<port2>][...][,hostnameN:<portN>]"; // Instantiate the MongoDB client with the URI const client = new MongoClient(uri, { useNewUrlParser: true, useUnifiedTopology: true });
재시도 가능 쓰기 및 읽기
참고
MongoDB 버전 3.6 부터 4.2호환 드라이버 를 사용하는 경우, MongoDB는 기본적으로 쓰기와 읽기를 모두 한 번 재시도합니다.
재시도 가능한 쓰기
재시도 가능 쓰기 를 사용하면 특정 쓰기 작업이 실패할 경우 한 번만 다시 시도할 수 있습니다.
애플리케이션이 일시적으로 정상적인 기본 노드를 찾을 수 없는 일시적인 네트워크 오류와 복제본 세트 투표를 처리하기 위해서는 쓰기를 정확히 한 번만 재시도하는 것이 가장 좋습니다. 재시도가 성공하면 작업 전체가 성공하고 오류가 반환되지 않습니다. 작업이 실패하는 경우 다음과 같은 이유 때문일 수 있습니다.
지속적인 네트워크 오류 또는
잘못된 명령입니다.
작업이 실패하면 애플리케이션에서 오류 자체를 처리해야 합니다.
재시도 가능한 읽기
읽기 작업은 MongoDB 버전 3.6 및 4.2호환 드라이버 에서 시작하여 실패할 경우 자동으로 한 번만 재시도됩니다. 읽기 재시도를 위해 애플리케이션을 구성할 필요가 없습니다.
쓰기 및 읽기 고려
쓰기 고려와 읽기 고려를 사용하여 애플리케이션의 일관성과 가용성을 조정할 수 있습니다. 고려가 엄격할 수록 데이터베이스 작업이 더 강력한 데이터 일관성 보장을 기다리며, 일관성 요구 사항을 완화하면 더 높은 가용성을 제공합니다.
예시
애플리케이션에서 금전적 잔액을 처리하는 경우 일관성이 매우 중요합니다. 오래된 데이터나 롤백될 수 있는 데이터를 읽지 않도록 하기 위해 majority
쓰기 및 읽기 고려 (read concern)를 사용할 수 있습니다.
또는 애플리케이션이 수백 개의 센서에서 초당 온도 데이터를 기록하는 경우, 가장 최근의 판독값이 포함되지 않은 데이터를 읽더라도 걱정하지 않아도 됩니다. 일관성 요구 사항을 완화하여 해당 데이터에 더 빠르게 액세스할 수 있습니다.
쓰기 고려
연결 URI를 통해 복제본 세트의 쓰기 고려 수준 을 설정할 수 있습니다. string majority
쓰기 고려를 사용하여 데이터가 데이터베이스에 성공적으로 기록되고 지속되는지 확인합니다. 이는 권장되는 기본값이며 대부분의 사용 사례에 충분합니다.
majority
와 같이 승인이 필요한 쓰기 고려를 사용하는 경우, 해당 승인 수준에 도달하기 위한 최대 쓰기 시간 제한을 지정할 수도 있습니다.
모든 쓰기에 대한 wtimeoutMS 연결 문자열 매개 변수 또는
단일 쓰기 작업을 위한 wtimeout 옵션입니다.
시간 제한의 사용 여부와 사용하는 값은 애플리케이션의 컨텍스트에 따라 다릅니다.
중요
쓰기에 대한 시간 제한을 지정하지 않았고 쓰기 고려 (write concern) 수준을 달성할 수 없는 경우에는 쓰기 작업이 완료되지 않습니다.
readConcern
연결 URI를 통해 복제본 string 세트 설정하다 읽기 고려 (read concern) 수준 을 설정할 수 있습니다. 이상적인 읽기 고려 (read concern) 는 애플리케이션 요구 사항에 따라 다르지만 대부분의 사용 사례에는 기본값 으로 충분합니다. 기본값 읽기 고려를 사용하는 데 연결 string 매개변수는 필요하지 않습니다.
읽기 고려 (read concern)를 지정하면 애플리케이션이 데이터베이스에서 수신하는 데이터에 대한 보장을 개선할 수 있습니다.
참고
애플리케이션 에서 사용하는 쓰기 (write) 및 읽기 고려 (read concern) 의 특정 조합은 작업 순서 보장 에 영향을 줍니다. 이를 인과적 일관성 이라고 합니다. 인과적 일관성 보장 에 대한 자세한 내용은 인과 인과적 일관성 및 읽기 및 쓰기 고려를 참조하세요.
Error Handling
재시도 가능 쓰기 로 처리되지 않는 잘못된 명령, 네트워크 중단 및 네트워크 오류는 오류를 반환합니다. 오류에 대한 자세한 내용은 드라이버의 API 설명서를 참조하세요.
예를 들어 애플리케이션이 중복 _id
이(가) 있는 문서를 삽입하려고 하면 드라이버는 다음을 포함하는 오류를 반환합니다.
Unable to insert due to an error: com.mongodb.MongoWriteException: E11000 duplicate key error collection: <db>.<collection> ...
{ "name": : "MongoError", "message": "E11000 duplicate key error collection on: <db>.<collection> ... ", ... }
적절한 오류 처리가 없으면 오류로 인해 애플리케이션이 다시 시작될 때까지 요청 처리가 차단될 수 있습니다.
애플리케이션은 충돌이나 부작용 없이 오류를 처리해야 합니다. 중복된 _id
을(를) 삽입하는 애플리케이션의 이전 예에서 해당 애플리케이션은 다음과 같이 오류를 처리할 수 있습니다.
// Declare a logger instance from java.util.logging.Logger private static final Logger LOGGER = ... ... try { InsertOneResult result = collection.insertOne(new Document() .append("_id", 1) .append("body", "I'm a goofball trying to insert a duplicate _id")); // Everything is OK LOGGER.info("Inserted document id: " + result.getInsertedId()); // Refer to the API documentation for specific exceptions to catch } catch (MongoException me) { // Report the error LOGGER.severe("Failed due to an error: " + me); }
... collection.insertOne({ _id: 1, body: "I'm a goofball trying to insert a duplicate _id" }) .then(result => { response.sendStatus(200) // send "OK" message to the client }, err => { response.sendStatus(400); // send "Bad Request" message to the client });
이 예시의 삽입 작업은 _id
필드가 고유해야 하기 때문에 두 번째로 호출할 때 "중복 키" 오류가 발생합니다. 애플리케이션은 오류를 포착하고 클라이언트에게 알림을 보내며 앱은 계속 실행됩니다. 그러나 삽입 작업은 실패하므로 사용자에게 메시지를 표시할지, 작업을 다시 시도할지 또는 다른 작업을 수행할지를 결정해야 합니다.
항상 오류를 기록해야 합니다. 추가 처리 오류에 대한 일반적인 전략은 다음과 같습니다.
오류 메시지와 함께 오류를 클라이언트에 반환합니다. 이는 오류를 해결할 수 없어 사용자에게 조치를 완료할 수 없음을 알려야 할 때 좋은 전략입니다.
데이터베이스 백업에 쓰기. 이는 오류를 해결할 수 없지만 요청 데이터가 손실되는 위험을 감수하고 싶지 않을 때 좋은 전략입니다.
1회 기본 재시 도 이후에 작업을 다시 시도합니다. 이는 오류의 원인을 프로그래밍 방식으로 해결한 다음 다시 시도할 수 있는 경우에 좋은 전략입니다.
애플리케이션 컨텍스트에서 가장 적합한 전략을 선택해야 합니다.
예시
중복 키 오류의 경우, 오류를 기록해야 하지만 작업이 실패할 수 있으므로 작업을 다시 시도해서는 안 됩니다. 대신 대체 데이터베이스에 쓰고 나중에 해당 데이터베이스의 내용을 검토하여 정보가 손실되지 않도록 할 수 있습니다. 사용자는 다른 작업을 수행할 필요가 없으며 데이터가 기록되므로 클라이언트에 오류 메시지를 전송하지 않도록 선택할 수 있습니다.
네트워크 오류에 대한 계획
오류를 반환하는 것은 작업이 완료되지 않고 애플리케이션이 새 작업을 실행하지 못하도록 차단할 때 바람직한 동작일 수 있습니다. maxTimeMS 메서드를 사용하여 개별 작업에 시간 제한을 설정하고, 해당 시간 제한을 초과할 경우 애플리케이션에서 처리할 오류를 반환할 수 있습니다.
각 작업에 설정하는 시간 제한은 해당 작업의 컨텍스트에 따라 다릅니다.
예시
애플리케이션에서 inventory
컬렉션의 간단한 제품 정보를 읽고 표시하는 경우, 이러한 읽기 작업은 오래 걸리지 않는다고 가정할 수 있습니다. 비정상적으로 오래 실행되는 쿼리는 지속적인 네트워크 문제가 있음을 나타내는 지표입니다. 이 작업에서 maxTimeMS
를 5000, 즉 5초로 설정하면 네트워크 문제가 있다고 확신하는 즉시 애플리케이션이 피드백을 받습니다.
회복 탄력성 애플리케이션 예시
다음 예제 애플리케이션은 복원력이 뛰어난 애플리케이션을 구축하기 위한 권장 사항을 한데 모아 놓은 것입니다.
이 애플리케이션 은 http://localhost:3000 에 두 개의 엔드포인트를 노출하는 간단한 사용자 기록 API 입니다.
메서드 | 엔드포인트 | 설명 |
---|---|---|
GET | /users | users 컬렉션에서 사용자 이름 목록을 가져옵니다. |
POST | /users | 요청 본문에 name 이 필요합니다. users 컬렉션에 새 사용자를 추가합니다. |
참고
다음 서버 애플리케이션 은 NanoHTTPD 를 사용합니다. 및 실행 하기 전에 프로젝트 JSON 에 종속성으로 추가해야 합니다.
1 // File: App.java 2 3 import java.util.Map; 4 import java.util.logging.Logger; 5 6 import org.bson.Document; 7 import org.json.JSONArray; 8 9 import com.mongodb.MongoException; 10 import com.mongodb.client.MongoClient; 11 import com.mongodb.client.MongoClients; 12 import com.mongodb.client.MongoCollection; 13 import com.mongodb.client.MongoDatabase; 14 15 import fi.iki.elonen.NanoHTTPD; 16 17 public class App extends NanoHTTPD { 18 private static final Logger LOGGER = Logger.getLogger(App.class.getName()); 19 20 static int port = 3000; 21 static MongoClient client = null; 22 23 public App() throws Exception { 24 super(port); 25 26 // Replace the uri string with your MongoDB deployment's connection string 27 String uri = "mongodb://<username>:<password>@hostname0:27017,hostname1:27017,hostname2:27017/?retryWrites=true&w=majority"; 28 client = MongoClients.create(uri); 29 30 start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); 31 LOGGER.info("\nStarted the server: http://localhost:" + port + "/ \n"); 32 } 33 34 public static void main(String[] args) { 35 try { 36 new App(); 37 } catch (Exception e) { 38 LOGGER.severe("Couldn't start server:\n" + e); 39 } 40 } 41 42 43 public Response serve(IHTTPSession session) { 44 StringBuilder msg = new StringBuilder(); 45 Map<String, String> params = session.getParms(); 46 47 Method reqMethod = session.getMethod(); 48 String uri = session.getUri(); 49 50 if (Method.GET == reqMethod) { 51 if (uri.equals("/")) { 52 msg.append("Welcome to my API!"); 53 } else if (uri.equals("/users")) { 54 msg.append(listUsers(client)); 55 } else { 56 msg.append("Unrecognized URI: ").append(uri); 57 } 58 } else if (Method.POST == reqMethod) { 59 try { 60 String name = params.get("name"); 61 if (name == null) { 62 throw new Exception("Unable to process POST request: 'name' parameter required"); 63 } else { 64 insertUser(client, name); 65 msg.append("User successfully added!"); 66 } 67 } catch (Exception e) { 68 msg.append(e); 69 } 70 } 71 72 return newFixedLengthResponse(msg.toString()); 73 } 74 75 static String listUsers(MongoClient client) { 76 MongoDatabase database = client.getDatabase("test"); 77 MongoCollection<Document> collection = database.getCollection("users"); 78 79 final JSONArray jsonResults = new JSONArray(); 80 collection.find().forEach((result) -> jsonResults.put(result.toJson())); 81 82 return jsonResults.toString(); 83 } 84 85 static String insertUser(MongoClient client, String name) throws MongoException { 86 MongoDatabase database = client.getDatabase("test"); 87 MongoCollection<Document> collection = database.getCollection("users"); 88 89 collection.insertOne(new Document().append("name", name)); 90 return "Successfully inserted user: " + name; 91 } 92 }
참고
다음 서버 애플리케이션은 Express를 사용하며, 이를 실행하려면 프로젝트에 종속성으로 추가해야 합니다.
1 const express = require('express'); 2 const bodyParser = require('body-parser'); 3 4 // Use the latest drivers by installing & importing them 5 const MongoClient = require('mongodb').MongoClient; 6 7 const app = express(); 8 app.use(bodyParser.json()); 9 app.use(bodyParser.urlencoded({ extended: true })); 10 11 // Use a connection string that lists all hosts 12 // with retryable writes & majority write concern 13 const uri = "mongodb://<username>:<password>@hostname0:27017,hostname1:27017,hostname2:27017/?retryWrites=true&w=majority"; 14 15 const client = new MongoClient(uri, { 16 useNewUrlParser: true, 17 useUnifiedTopology: true 18 }); 19 20 // ----- API routes ----- // 21 app.get('/', (req, res) => res.send('Welcome to my API!')); 22 23 app.get('/users', (req, res) => { 24 const collection = client.db("test").collection("users"); 25 26 collection 27 .find({}) 28 // In this example, 'maxTimeMS' throws an error after 5 seconds, 29 // alerting the application to a lasting network outage 30 .maxTimeMS(5000) 31 .toArray((err, data) => { 32 if (err) { 33 // Handle errors in your application 34 // In this example, by sending the client a message 35 res.send("The request has timed out. Please check your connection and try again."); 36 } 37 return res.json(data); 38 }); 39 }); 40 41 app.post('/users', (req, res) => { 42 const collection = client.db("test").collection("users"); 43 collection.insertOne({ name: req.body.name }) 44 .then(result => { 45 res.send("User successfully added!"); 46 }, err => { 47 // Handle errors in your application 48 // In this example, by sending the client a message 49 res.send("An application error has occurred. Please try again."); 50 }) 51 }); 52 // ----- End of API routes ----- // 53 54 app.listen(3000, () => { 55 console.log(`Listening on port 3000.`); 56 client.connect(err => { 57 if (err) { 58 console.log("Not connected: ", err); 59 process.exit(0); 60 } 61 console.log('Connected.'); 62 }); 63 });