저는 멋진 Java SDK를 사용해 Couchbase와 웹 애플리케이션을 꽤 오랫동안 다뤄왔지만, Couchbase를 사용해 데이터를 저장하는 데스크톱 애플리케이션도 만들 수 있다는 사실을 알고 계셨나요? JavaFX와 같은 Java 데스크톱 프레임워크는 Couchbase Java SDK를 사용할 수 있지만, 클라이언트 대면 애플리케이션에서 서버용 SDK를 사용하는 것은 좋은 생각이 아닐 수도 있습니다. 대신 Couchbase의 모바일 솔루션을 사용하여 클라이언트를 대상으로 하는 데스크톱 애플리케이션을 구축할 수 있습니다. 이 솔루션은 Android와 동일한 API를 사용하지만 데스크톱을 염두에 두고 설계되었습니다.
이 데이터를 컴퓨터 간에 동기화하기 위해 JavaFX, Couchbase Lite, 심지어 Couchbase Sync Gateway를 사용하여 간단한 데스크톱 애플리케이션을 구축하는 방법을 살펴 보겠습니다.
요구 사항
이 프로젝트를 성공적으로 수행하려면 몇 가지 요구 사항을 충족해야 합니다.
- JDK 1.7+
- Maven
- 카우치베이스 동기화 게이트웨이
이 프로젝트에서는 Maven을 사용하여 종속성을 수집하고 JAR 파일을 빌드합니다. 동기화 게이트웨이가 반드시 필요한 것은 아니지만 애플리케이션에 동기화 지원을 추가하려는 경우에는 필요합니다.
Maven으로 새로운 JavaFX 프로젝트 만들기
기본적인 Maven 프로젝트를 만들어야 합니다. 이 작업은 원하는 IDE에서 수행하거나 수동으로 수행할 수 있습니다. 기본적으로 필요한 것은 다음과 같은 파일 및 디렉토리 구조입니다:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
src main java com couchbase CouchbaseSingleton.java Main.java Todo.java TodoFXController.java resources TodoFX.fxml pom.xml |
개발을 시작하면 각 파일에 포함된 내용에 대해 자세히 알아보겠습니다. 지금은 Maven을 설정해야 합니다. pom.xml 파일에 필요한 종속성을 가져올 수 있도록 합니다.
프로젝트의 pom.xml 파일을 열고 다음을 포함하세요:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
com.couchbase couchbase-javafx-example 4.0.0 Couchbase JavaFX Example jar 1.0 com.couchbase.lite couchbase-lite-java 1.3.0 com.couchbase.lite couchbase-lite-java-sqlcipher 1.3.0 junit junit 4.12 test com.zenjava javafx-maven-plugin 8.5.0 com.couchbase.Main org.apache.maven.plugins maven-compiler-plugin 3.3 1.7 1.7 maven-assembly-plugin 2.2.1 jar-with-dependencies true lib/ com.couchbase.Main make-assembly package single |
위의 Maven 파일에서 불필요한 세부 사항을 너무 많이 다루지 않고, 특히 몇 가지 사항에 주목하고자 합니다.
|
1 2 3 4 5 6 7 |
com.couchbase.lite couchbase-lite-java 1.3.0 |
위의 종속성에서 Couchbase Lite를 프로젝트에 포함하고 있음을 알 수 있습니다. Couchbase Lite는 서버 어딘가에 있지 않은 로컬 데이터베이스라는 점을 기억하세요. 데이터는 로컬에 저장되며 컴포넌트는 애플리케이션 내에 번들로 제공됩니다.
다음 플러그인도 주목할 필요가 있습니다:
|
1 2 3 4 5 6 7 8 9 10 |
com.zenjava javafx-maven-plugin 8.5.0 com.couchbase.Main |
위의 플러그인은 JavaFX 프로젝트를 생성하기 위한 것입니다. 물론 이 프로젝트 생성은 IntelliJ와 같은 IDE를 사용하면 필수는 아니지만 훨씬 더 쉽습니다.
카우치베이스 싱글톤 클래스 생성하기
JavaFX 프로젝트의 UI와 컨트롤러를 만드는 데 투자하기 전에 데이터를 어떻게 처리할지 고민해 보겠습니다.
간소화를 위해 프로젝트 전체에서 데이터를 관리하기 위한 싱글톤 클래스를 만드는 것이 좋습니다. 또한 애플리케이션의 모든 곳에서 쿼리를 작성할 필요가 없도록 데이터 리스너를 설정할 때 매우 효과적입니다. 이제 프로젝트의 src/main/java/com/couchbase/CouchbaseSingleton.java 파일을 열고 다음 코드를 포함하세요. 나중에 자세히 설명하겠습니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package com.couchbase; import com.couchbase.lite.*; import com.couchbase.lite.replicator.Replication; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.Map; public class CouchbaseSingleton { private Manager manager; private Database database; private Replication pushReplication; private Replication pullReplication; private static CouchbaseSingleton instance = null; private CouchbaseSingleton() { try { this.manager = new Manager(new JavaContext("data"), Manager.DEFAULT_OPTIONS); this.database = this.manager.getDatabase("fx-project"); View todoView = database.getView("todos"); todoView.setMap(new Mapper() { @Override public void map(Map<String, Object> document, Emitter emitter) { emitter.emit(document.get("_id"), document); } }, "1"); } catch (Exception e) { e.printStackTrace(); } } public static CouchbaseSingleton getInstance() { if(instance == null) { instance = new CouchbaseSingleton(); } return instance; } public Database getDatabase() { return this.database; } public void startReplication(URL gateway, boolean continuous) { this.pushReplication = this.database.createPushReplication(gateway); this.pullReplication = this.database.createPullReplication(gateway); this.pushReplication.setContinuous(continuous); this.pullReplication.setContinuous(continuous); this.pushReplication.start(); this.pullReplication.start(); } public void stopReplication() { this.pushReplication.stop(); this.pullReplication.stop(); } public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } public ArrayList query() { ArrayList results = new ArrayList(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator it = result; it.hasNext(); ) { QueryRow row = it.next(); document = row.getDocument(); results.add(new Todo(document.getId(), (String) document.getProperty("title"), (String) document.getProperty("description"))); } } catch (Exception e) { e.printStackTrace(); } return results; } } |
위의 내용은 이해하기 어려운 내용이지만 혼란을 피하기 위해 꼭 필요한 내용입니다.
내부 카우치베이스싱글턴 클래스에는 4개의 개인 변수가 있습니다. 데이터베이스 관리자를 사용하면 데이터베이스를 열 수 있을 뿐만 아니라 생성할 수도 있습니다. 복제 객체는 어느 방향으로든 동기화를 담당합니다.
에서 생성자 메서드를 사용하여 다음과 같은 데이터베이스를 생성하고 엽니다. fx-project 를 클릭하고 데이터 쿼리 시 사용할 뷰를 구성합니다. 이 뷰는 토도스 는 로컬 데이터베이스에 저장된 모든 문서에 대해 문서 ID와 문서의 키-값 쌍을 생성합니다. 로컬 데이터베이스에 저장된 모든 문서의 생성자 메서드는 정적 메서드를 통해 인스턴스화하기 때문에 비공개입니다. getInstance.
이 가이드의 후반부까지 동기화에 대해 살펴보지는 않겠지만, 기초를 다지는 것은 좋은 생각입니다. 기본적으로 특정 동기화 게이트웨이 URL에 대한 지속적인 복제를 정의하고 싶을 뿐입니다. 또한 애플리케이션이 종료될 때 복제를 중지할 수 있기를 원합니다. 이제 데이터를 저장하고 로드하는 방법을 살펴봅시다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public Todo save(Todo todo) { Map<String, Object> properties = new HashMap<String, Object>(); Document document = this.database.createDocument(); properties.put("type", "todo"); properties.put("title", todo.getTitle()); properties.put("description", todo.getDescription()); try { todo.setDocumentId(document.putProperties(properties).getDocument().getId()); } catch (Exception e) { e.printStackTrace(); } return todo; } |
우리의 저장 메서드는 Todo 객체를 생성하고 데이터베이스에 저장합니다. 그 결과는 Todo 호출 메서드에 반환되는 문서 ID가 포함된 객체입니다. 이 Todo 클래스는 간단합니다. 아이디, 제목, 설명과 같은 기본 정보를 받아들이고 그에 맞는 적절한 겟터와 세터 메서드가 있습니다. 참고로 다음과 같은 모양이며 프로젝트의 src/main/java/com/couchbase/Todo.java 파일을 만듭니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package com.couchbase; import java.util.*; public class Todo { private String documentId; private String title; private String description; Todo(String documentId, String title, String description) { this.documentId = documentId; this.title = title; this.description = description; } Todo(String title, String description) { this.documentId = UUID.randomUUID().toString(); this.title = title; this.description = description; } public void setDocumentId(String documentId) { this.documentId = documentId; } public String getDocumentId() { return this.documentId; } public String getTitle() { return this.title; } public String getDescription() { return this.description; } } |
이제 마지막 데이터 관련 함수인 쿼리 함수입니다.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public ArrayList query() { ArrayList results = new ArrayList(); try { View todoView = this.database.getView("todos"); Query query = todoView.createQuery(); QueryEnumerator result = query.run(); Document document = null; for (Iterator it = result; it.hasNext(); ) { QueryRow row = it.next(); document = row.getDocument(); results.add(new Todo(document.getId(), (String) document.getProperty("title"), (String) document.getProperty("description"))); } } catch (Exception e) { e.printStackTrace(); } return results; } |
우리가 만든 뷰를 기억하시나요? 이번에는 이를 쿼리해 보겠습니다. 결과 집합은 다음과 같은 배열로 로드됩니다. Todo 객체입니다. 이렇게 하면 데이터 계층 를 마무리하고 실제 애플리케이션 개발에 집중할 수 있게 되었습니다.
데스크톱 애플리케이션 디자인
필수는 아니지만 JavaFX 애플리케이션, 씬 빌더를 사용하면 그래픽 UI와 해당 컨트롤러 클래스를 매우 간단하게 만들 수 있습니다. 이 기능을 사용하지 않으려면 프로젝트의 src/main/resources/TodoFX.fxml 를 열고 다음 XML 마크업을 포함합니다:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
<!--?xml version="1.0" encoding="UTF-8"?--> <!--?import javafx.scene.control.*?--> <!--?import java.lang.*?--> <!--?import javafx.scene.layout.*?--> <textarea> <Button fx:id="fxSave" layoutX="530.0" layoutY="365.0" mnemonicParsing="false" prefHeight="25.0" prefWidth="60.0" text="Save" /> </children> </Pane> </code> </pre> <p>The above markup will give us a UI that looks like the following:</p> <p><img src="/wp-content/original-assets/2016/september/using-couchbase-in-a-javafx-desktop-application/todo-javafx-example.png" /></p> <p>Nothing too fancy in the above, correct?</p> <p>We have a simple UI with a list, two input fields, and a save button, as described in the XML markup. In the markup we also reference <code>com.couchbase.TodoFXController</code>. This is the logic that will be bound to the particular FX view. Open the project's <strong>src/main/java/com/couchbase/TodoFXController.java</strong> and include the following:</p> <pre> <code> package com.couchbase; import com.couchbase.lite.*; import com.couchbase.lite.Database.ChangeListener; import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.*; import javafx.util.Callback; import java.net.URL; import java.util.*; public class TodoFXController implements Initializable { private CouchbaseSingleton couchbase; @FXML private TextField fxTitle; @FXML private TextArea fxDescription; @FXML private ListView fxListView; @FXML private Button fxSave; @Override public void initialize(URL fxmlFileLocation, ResourceBundle resources) { try { this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); this.couchbase.getDatabase().addChangeListener(new ChangeListener() { @Override public void changed(Database.ChangeEvent event) { for(int i = 0; i < event.getChanges().size(); i++) { final Document retrievedDocument = couchbase.getDatabase().getDocument(event.getChanges().get(i).getDocumentId()); Platform.runLater(new Runnable() { @Override public void run() { int documentIndex = indexOfByDocumentId(retrievedDocument.getId(), fxListView.getItems()); for(int j = 0; j < fxListView.getItems().size(); j++) { if(((Todo)fxListView.getItems().get(j)).getDocumentId().equals(retrievedDocument.getId())) { documentIndex = j; break; } } if (retrievedDocument.isDeleted()) { if (documentIndex > -1) { fxListView.getItems().remove(documentIndex); } } else { if (documentIndex == -1) { fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } else { fxListView.getItems().remove(documentIndex); fxListView.getItems().add(new Todo(retrievedDocument.getId(), (String) retrievedDocument.getProperty("title"), (String) retrievedDocument.getProperty("description"))); } } } }); } } }); } catch (Exception e) { e.printStackTrace(); } fxListView.setCellFactory(new Callback<ListView<Todo>, ListCell<Todo>>(){ @Override public ListCell<Todo> call(ListView<Todo> p) { ListCell<Todo> cell = new ListCell<Todo>(){ @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); fxSave.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent e) { if(!fxTitle.getText().equals("") && !fxDescription.getText().equals("")) { fxListView.getItems().add(couchbase.save(new Todo(fxTitle.getText(), fxDescription.getText()))); fxTitle.setText(""); fxDescription.setText(""); } else { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Missing Information"); alert.setHeaderText(null); alert.setContentText("Both a title and description are required for this example."); alert.showAndWait(); } } }); } private int indexOfByDocumentId(String needle, ObservableList<Todo> haystack) { int result = -1; for(int i = 0; i < haystack.size(); i++) { if(haystack.get(i).getDocumentId().equals(needle)) { result = i; break; } } return result; } } </code> </pre> <p>There is a lot to take in when it comes to the above code, so we're going to break it down.</p> <pre> <code> @FXML private TextField fxTitle; @FXML private TextArea fxDescription; @FXML private ListView fxListView; @FXML private Button fxSave; </code> </pre> <p>Remember the FXML file? Each of the above variables are mapped to the components of that file. However, what we really care about is the use of the <code>Initializable</code> interface that we're implementing. This requires an <code>initialize</code> function where we'll setup our component and database listeners.</p> <pre> <code> fxListView.setCellFactory(new Callback<ListView<Todo>, ListCell<Todo>>(){ @Override public ListCell<Todo> call(ListView<Todo> p) { ListCell<Todo> cell = new ListCell<Todo>(){ @Override protected void updateItem(Todo t, boolean bln) { super.updateItem(t, bln); if (t != null) { setText(t.getTitle()); } } }; return cell; } }); </code> </pre> <p>Because we're using a custom <code>Todo</code> class in our list, we need to configure how the list rows show data. By default they are strings, but we actually want to extract the title of any of our data elements and show that instead.</p> <pre> <code> this.couchbase = CouchbaseSingleton.getInstance(); fxListView.getItems().addAll(this.couchbase.query()); </code> </pre> <p>In the above we are obtaining the open database instance, performing a query, and adding all the data to the FX list that is on the screen. This is a one time thing that is done when the application loads. All future data loads are done through a data listener.</p> <p>The database change listener will listen for all changes in data for as long as the application is open. The changes can happen in bulk, so we loop through the changes and retrieve the documents. If the document is new, add it to the list. If the document has a deleted indicator, remove it from the list. If the document is a change to an existing document, remove the old and add the new one to the list. This is all done in a <code>Platform.runLater</code> because the Couchbase change listener happens on a background thread. The changes to the UI must be done on the main thread.</p> <p>Finally we have our save button that has its own click event. If clicked, and the input elements are populated, save the data to the database.</p> <p>As of right now, we have an offline desktop application that saves data in Couchbase Lite. Now we can worry about data synchronization / replication.</p> <h2>Synchronizing Data Between Desktop and Server</h2> <p>The next part is easy thanks to Couchbase Mobile. You will need Couchbase Sync Gateway up and running before we start messing with the desktop application code. With Sync Gateway installed, create the following basic configuration file:</p> <pre> <code> { "log":["CRUD+", "REST+", "Changes+", "Attach+"], "databases": { "fx-example": { "server":"walrus:", "sync":` function (doc) { channel (doc.channels); } `, "users": { "GUEST": { "disabled": false, "admin_channels": ["*"] } } } } } </code> </pre> <p>The above configuration file is pretty much as basic as you can get. Create an <code>fx-project</code> partition with no read / write rules using the in-memory database. Note we're not using Couchbase Server here, but we could. Run this configuration file with Sync Gateway.</p> <p>Bouncing back into the JavaFX application. Open the project's <strong>src/main/java/com/couchbase/Main.java</strong> file and include the following:</p> <pre> <code> this.couchbase = CouchbaseSingleton.getInstance(); this.couchbase.startReplication(new URL("https://localhost:4984/fx-example/"), true); </code> </pre> <p>The above should be included in the <code>start</code> method. It will start the bi-directional replication process to our now running Sync Gateway instance.</p> <h2>Conclusion</h2> <p>You just saw how to create a simple JavaFX application that stores data in Couchbase Lite and synchronizes the data. To run this application using Maven and the command line, execute the following:</p> <pre> <code> mvn jfx:run </code> </pre> <p>Because Couchbase is so flexible, this same application can be extended to mobile with very few changes to the application code. If you'd like to download the full project in this guide, it can be found on GitHub <a href="https://github.com/couchbaselabs/couchbase-lite-javafx-example">here</a>.</p> </textarea> |