소개
그리고 카우치베이스 모바일 동기화 게이트웨이 변경사항 피드는 모바일 배포에서 이벤트를 모니터링할 수 있는 방법을 제공합니다. 이 피드를 사용하면 정교한 비즈니스 로직을 작성할 수 있습니다. 피드를 검토하고 이해하는 데 도움이 되는 도구를 만들었습니다. 다음에서 소개와 설명을 읽을 수 있습니다. 파트 1 의 두 부분으로 구성된 시리즈입니다. 이 코드는 피드 청취의 예시이기도 합니다.
코드
앱 코드의 주요 클래스를 여기에 포함시켰습니다. 이것은 첫 번째 버전이므로 많은 개선 사항을 사용할 수 있습니다. 매개변수는 모두 하드 와이어되어 있습니다. 프로젝트 확인 여기 깃허브에서 에서 업데이트를 확인하세요. 앱 빌드, 실행 및 패키징에 대한 지침도 여기에서 확인할 수 있습니다.
JavaFX: 컨트롤러 클래스
JavaFX는 간단한 앱을 컨트롤러 클래스와 선언적 UI로 나눕니다. 컨트롤러를 살펴보겠습니다.
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 |
패키지 com.카우치베이스.모바일; 가져오기 com.카우치베이스.lite.*; 가져오기 com.fasterxml.잭슨.핵심.JsonProcessingException; 가져오기 javafx.애플리케이션.플랫폼; 가져오기 javafx.콩.값.변경 리스너; 가져오기 javafx.콩.값.관찰 가능한 값; 가져오기 javafx.컬렉션.FXCollections; 가져오기 javafx.컬렉션.관찰 가능 목록; 가져오기 javafx.이벤트.액션 이벤트; 가져오기 javafx.fxml.FXML; 가져오기 javafx.장면.제어.*; 가져오기 javafx.장면.제어.텍스트 필드; 가져오기 자바.io.IOException; 가져오기 자바.net.MalformedURLException; 가져오기 자바.net.URL; 가져오기 자바.활용.지도; 가져오기 정적 com.카우치베이스.모바일.런타임.매퍼; public 클래스 컨트롤러 구현 라이브 쿼리.변경 리스너, 변경 리스너, SGMonitor.변경 피드 리스너, DBService.복제 상태 리스너 { 비공개 정적 final 문자열 sync_gateway_host = "http://localhost"; 비공개 정적 final 문자열 SG_PUBLIC_URL = sync_gateway_host + ":4984/" + DBService.데이터베이스; 비공개 정적 final 문자열 SG_ADMIN_URL = sync_gateway_host + ":4985/" + DBService.데이터베이스; 비공개 정적 final 문자열 toggle_inactive = "-fx-background-color: #e6555d;"; 비공개 정적 final 문자열 TOGGLE_ACTIVE = "-fx-background-color: #ade6a6;"; 비공개 정적 final 문자열 toggle_disabled = "-fx-background-color: #555555;"; @FXML 비공개 ListView 문서 목록; 비공개 관찰 가능 목록 문서 = FXCollections.관찰 가능한 배열 목록(); @FXML 비공개 텍스트 영역 콘텐츠 텍스트; @FXML 비공개 텍스트 영역 변경 피드; @FXML 비공개 텍스트 필드 사용자 이름 텍스트; @FXML 비공개 텍스트 필드 비밀번호 텍스트; @FXML 비공개 토글 버튼 applyCredentialsBtn; @FXML 비공개 토글 버튼 syncBtn; 비공개 DBService 서비스 = DBService.getInstance(); 비공개 데이터베이스 db = 서비스.getDatabase(); 비공개 SGMonitor 변경사항 모니터; 비공개 라이브 쿼리 라이브 쿼리; |
이 첫 번째 목록에는 보일러 플레이트 코드가 많이 나와 있습니다. 클래스 자체 내에서 UI를 위한 여러 리스너를 구현하여 파일 수를 줄였습니다. 이것은 예시를 위한 것입니다.
그리고 @FXML
어노테이션은 프레임워크가 UI의 일부에 자동으로 바인딩하는 모든 필드를 표시합니다.
다음은 초기화입니다. JavaFX는 표준 라이프사이클의 일부로 이 메서드를 호출합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@FXML 비공개 void 초기화() { 문서 목록 초기화(); 문서 목록.setItems(문서); setState(applyCredentialsBtn, false); setState(syncBtn, false); 서비스.추가 복제 상태 리스너(이); 변경사항 모니터 = new SGMonitor(SG_ADMIN_URL, "false", "true", "0", "all_docs", 이); 변경사항 모니터.시작(); } 비공개 void 문서 목록 초기화() { 쿼리 쿼리 = db.createAllDocumentsQuery(); 쿼리.모든 문서 모드 설정(쿼리.모든 문서 모드.포함_삭제됨); 라이브 쿼리 = 쿼리.toLiveQuery(); 라이브 쿼리.추가 변경 리스너(이); 라이브 쿼리.시작(); 문서 목록.getSectionModel().선택한 항목 속성().추가 리스너(이); } |
문서 목록 초기화를 자체 루틴으로 분리했습니다. 문서 목록은 문서 목록
변수를 설정합니다. 차례로 문서 목록
는 전달한 항목 목록이 변경될 때마다 UI를 업데이트합니다.
클라이언트 데이터베이스에서 변경 사항이 있는지 모니터링하기 위해 실시간 쿼리를 설정했습니다. 이는 '모든 문서' 쿼리를 통해 이루어집니다. 모든 문서 쿼리에는 연결된 보기가 필요하지 않습니다. 저는 포함_삭제됨
를 설정하면 도구가 데이터베이스에서 삭제된 문서가 어떻게 보이는지 보여줄 수 있습니다.
다른 바인딩이 제자리에 있으면, 우리는 단지 문서
목록에 추가합니다. 이 작업을 수행하는 라이브 쿼리 리스너는 나중에 자세히 살펴보겠습니다.
다음 몇 줄은 몇 개의 토글 버튼의 초기 상태를 설정합니다. 추가 리스너가 있어야 동기화
버튼이 복제본의 실제 상태와 일치하도록 설정합니다. 이에 대한 자세한 내용은 이 글의 뒷부분에서 확인할 수 있습니다.
동기화 게이트웨이를 모니터링하기 위해 별도의 클래스를 작성했습니다. 초기화 코드는 새 모니터 인스턴스를 생성하고 시작하여 완료했습니다.
다음 섹션에는 여러 리스너가 포함되어 있습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// LiveQuery.ChangeListener 오버라이드 public void 변경됨(라이브 쿼리.변경 이벤트 이벤트) { 만약 (이벤트.getSource().같음(라이브 쿼리)) { 플랫폼.runLater(() -> { 쿼리 열거자 행 = 이벤트.getRows(); 문서.clear(); 행.forEach(쿼리 행 -> 문서.추가(쿼리 행.getDocumentId())); }); } } |
다음은 로컬 데이터베이스가 변경될 때마다 호출되는 라이브 쿼리 리스너입니다. 저는 이 도구를 대규모 데이터베이스 작업용으로 설계하지 않았습니다. 그래서 데이터가 변경될 때마다 모든 문서를 다시 읽는 무차별 대입 방식을 택했습니다. 그래서 getRows
메서드는 인덱싱을 수행하는 열거자를 반환합니다. JavaFX는 다음과 같은 경우 UI 업데이트를 처리합니다. 문서
변경됩니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 리스트뷰 변경 리스너 오버라이드 public void 변경됨(관찰 가능한 값 관찰 가능, 문자열 oldId, 문자열 newId) { 만약 (null == newId) 반환; 지도 속성 = db.getDocument(newId).getProperties(); 시도 { 문자열 json = 매퍼.쓰기값을 문자열로(속성); 콘텐츠 텍스트.setText(예쁜 텍스트(json)); } catch (JsonProcessingException ex) { ex.프린트스택트레이스(); 대화 상자.디스플레이(ex); } } |
이 리스너는 사용자가 문서 목록에서 항목을 클릭할 때 추적을 처리합니다. 항목은 문서 ID이므로 선택 항목을 사용하여 데이터베이스에서 직접 문서를 가져올 수 있습니다.
1 2 3 4 5 |
// SGMonitor.ChangesFeedListener 오버라이드 public void onResponse(문자열 body) { 변경 피드.앱드 텍스트(예쁜 텍스트((문자열) body)); } |
변경 피드 결과를 가져오기 위해 콜백 방식을 사용했습니다. 인터페이스는 SGMonitor
클래스입니다. 메서드는 하나뿐입니다. 이 구현에서는 피드 응답의 본문을 가져와 변경 피드 텍스트 창에 있는 기존 텍스트에 붙이기만 하면 됩니다. 읽기 쉽도록 약간의 서식도 적용했습니다.
1 2 3 4 5 |
// DBService.ReplicationStateListener 오버라이드 public void onChange(부울 isActive) { setState(syncBtn, isActive); } |
마지막으로 복제 활동에 대한 리스너를 추가했습니다. 이 인터페이스는 DBService 헬퍼 클래스에서 제공됩니다. 복제 상태 감지에 대해 조금 작성했습니다. 여기. 이 앱의 경우 복제가 실행 중인지 여부만 알면 됩니다. 동기화
버튼 상태를 일관되게 유지합니다. 이렇게 하면 사용자가 동기화를 시작하려고 하지만 실패하는 경우를 처리합니다. 예를 들어 사용자가 인증 자격 증명을 제공해야 하지만 제공하지 않은 경우 이런 일이 발생할 수 있습니다.
다음으로 UI 요소에 바인딩된 몇 가지 메서드가 있습니다. JavaFX는 대부분의 배선을 처리합니다.
1 2 3 4 5 6 7 8 9 10 11 12 |
@FXML 비공개 void 적용 자격 증명 토글(액션 이벤트 이벤트) { 문자열 사용자 이름 = null; 문자열 비밀번호 = null; 만약 (applyCredentialsBtn.is선택됨()) { 사용자 이름 = 사용자 이름 텍스트.getText(); 비밀번호 = 비밀번호 텍스트.getText(); } DBService.getInstance().setCredentials(사용자 이름, 비밀번호); applyCredentialsBtn.setStyle(applyCredentialsBtn.is선택됨() ? TOGGLE_ACTIVE : toggle_inactive); } |
여기서는 해당 버튼이 전환될 때마다 인증 자격 증명을 사용하도록 설정했습니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@FXML 비공개 void 저장 콘텐츠 클릭(액션 이벤트 이벤트) { 지도 속성 = null; 문서 문서; 시도 { 속성 = 매퍼.읽기 값(콘텐츠 텍스트.getText(), 지도.클래스); } catch (IOException ex) { ex.프린트스택트레이스(); 대화 상자.디스플레이(ex); } 만약 (속성.containsKey("_id")) { 문서 = db.getDocument((문자열) 속성.get("_id")); } else { 문서 = db.createDocument(); } 시도 { 문서.putProperties(속성); } catch (카우치베이스 라이트 예외 ex) { ex.프린트스택트레이스(); 대화 상자.디스플레이(ex); } } |
이 코드에는 몇 가지 흥미로운 항목이 있습니다. 잭슨 오브젝트 맵퍼
인스턴스를 사용하여 콘텐츠 창의 텍스트를 속성 맵으로 변환할 수 있습니다.
다음으로 항목을 확인합니다. _id
. 카우치베이스 모바일은 시스템 사용을 위해 "_"로 시작하는 대부분의 속성을 보유합니다(특별한 예외가 있을 수 있음). 변환하려는 텍스트에 다음이 포함된 경우 _id
를 클릭하면 기존 문서를 편집하는 것으로 간주합니다. 그렇지 않으면 새 문서를 만듭니다.
간단히 말해서 문서를 만들고 업데이트하는 두 가지 예가 있습니다. 이 방법이 선호되는 업데이트 방법은 아니지만 많은 경우에 충분합니다. 업데이트에 대해 자세히 알아보세요. 여기.
1 2 3 4 5 6 7 8 9 10 11 |
@FXML 비공개 void 동기화 토글(액션 이벤트 이벤트) { 시도 { syncBtn.setDisable(true); syncBtn.setStyle(toggle_disabled); 서비스.토글복제(new URL(SG_PUBLIC_URL), true); } catch (예외 ex) { ex.프린트스택트레이스(); 대화 상자.디스플레이(ex); syncBtn.setDisable(false); } } |
이것은 토글에 반응합니다. 동기화
버튼을 클릭합니다. 리스너를 사용하여 다른 곳에서 상태를 확인한다는 점을 기억하세요.
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 |
@FXML 비공개 void 종료클릭(액션 이벤트 이벤트) { // 모든 것을 우아하게 종료하기 변경사항 모니터.중지(); 라이브 쿼리.중지(); 서비스.중지복제(); db.닫기(); db.getManager().닫기(); 플랫폼.exit(); } 비공개 void setState(토글 버튼 btn, 부울 활성) { btn.setSelected(활성); btn.setStyle(활성 ? TOGGLE_ACTIVE : toggle_inactive); btn.setDisable(false); } 비공개 문자열 예쁜 텍스트(문자열 json) { 문자열 out = null; 시도 { 개체 객체 = 매퍼.읽기 값(json, 개체.클래스); out = 매퍼.writerWithDefaultPrettyPrinter().쓰기값을 문자열로(객체); } catch (예외 ex) { ex.프린트스택트레이스(); } 반환 out; } } |
나머지 코드는 헬퍼 비트와 종료하기 전에 모든 것을 종료하는 부분일 뿐입니다.
데이터베이스 도우미 클래스
이것은 간단한 데이터베이스 헬퍼 클래스의 코드를 보여줍니다. 대부분의 경우 이 클래스는 데이터베이스를 관리하고 표준 양방향 복제 집합을 시작하는 데 필요한 일반적인 작업을 멋지게 패키징한 것일 뿐입니다. 유용하고 명확성을 위해 여기에 포함시켰습니다.
저는 복제.변경 리스너
인터페이스입니다. 조금 특이할 수도 있습니다. 앞서 그 이유에 대해 언급했습니다. 이 링크를 클릭하면 블로그 게시물 에 대해 알아보세요.
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 |
패키지 com.카우치베이스.모바일; 가져오기 com.카우치베이스.lite.데이터베이스; 가져오기 com.카우치베이스.lite.자바 컨텍스트; 가져오기 com.카우치베이스.lite.관리자; 가져오기 com.카우치베이스.lite.auth.인증자; 가져오기 com.카우치베이스.lite.auth.인증자 팩토리; 가져오기 com.카우치베이스.lite.리플리케이터.복제; 가져오기 com.카우치베이스.lite.리플리케이터.복제 상태; 가져오기 자바.net.URL; 가져오기 자바.활용.ArrayList; 가져오기 자바.활용.목록; public 클래스 DBService 구현 복제.변경 리스너 { public 정적 final 문자열 데이터베이스 = "db"; 비공개 정적 final 문자열 DB_DIRECTORY = "데이터"; 비공개 관리자 관리자; 비공개 데이터베이스 데이터베이스; 비공개 복제 pushReplication = null; 비공개 복제 pullReplication = null; 비공개 부울 복제 활성 = false; 비공개 목록 stateListeners = new ArrayList(); 비공개 문자열 사용자 이름 = null; 비공개 문자열 비밀번호 = null; 비공개 DBService() { 시도 { 관리자 = new 관리자(new 자바 컨텍스트(DB_DIRECTORY), 관리자.기본_옵션); 데이터베이스 = 관리자.getDatabase(데이터베이스); } catch (예외 ex) { ex.프린트스택트레이스(); } } 비공개 정적 클래스 홀더 { 비공개 정적 DBService 인스턴스 = new DBService(); } public 인터페이스 복제 상태 리스너 { void onChange(부울 isActive); } public 정적 DBService getInstance() { 반환 홀더.인스턴스; } public 데이터베이스 getDatabase() { 반환 데이터베이스; } public void setCredentials(문자열 사용자 이름, 문자열 비밀번호) { 이.사용자 이름 = 사용자 이름; 이.비밀번호 = 비밀번호; } public void 토글복제(URL 게이트웨이, 부울 연속) { 만약 (복제 활성) { 중지복제(); } else { 시작 복제(게이트웨이, 연속); } } public void 시작 복제(URL 게이트웨이, 부울 연속) { 만약 (복제 활성) { 중지복제(); } pushReplication = 데이터베이스.createPushReplication(게이트웨이); pullReplication = 데이터베이스.createPullReplication(게이트웨이); pushReplication.setContinuous(연속); pullReplication.setContinuous(연속); 만약 (사용자 이름 != null) { 인증자 auth = 인증자 팩토리.기본 인증자 생성(사용자 이름, 비밀번호); pushReplication.setAuthenticator(auth); pullReplication.setAuthenticator(auth); } pushReplication.추가 변경 리스너(이); pullReplication.추가 변경 리스너(이); pushReplication.시작(); pullReplication.시작(); } public void 중지복제() { 만약 (!복제 활성) 반환; pushReplication.중지(); pullReplication.중지(); pushReplication = null; pullReplication = null; } public void 추가 복제 상태 리스너(복제 상태 리스너 리스너) { stateListeners.추가(리스너); } public void 제거복제상태 리스너(복제 상태 리스너 리스너) { stateListeners.제거(리스너); } // 복제.변경 리스너 오버라이드 public void 변경됨(복제.변경 이벤트 변경 이벤트) { 만약 (변경 이벤트.getError() != null) { 던지기 가능 마지막 오류 = 변경 이벤트.getError(); 대화 상자.디스플레이(마지막 오류.getMessage()); 반환; } 만약 (변경 이벤트.getTransition() == null) 반환; 복제 상태 dest = 변경 이벤트.getTransition().목적지 가져오기(); 복제 활성 = ((dest == 복제 상태.중지 || dest == 복제 상태.중지됨) ? false : true); stateListeners.forEach(리스너 -> 리스너.onChange(복제 활성)); } } |
동기화 게이트웨이 모니터 클래스
마지막으로 동기화 게이트웨이 모니터링을 위한 헬퍼 클래스에 대해 살펴보겠습니다. 이것도 조금씩 살펴보겠습니다.
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 |
패키지 com.카우치베이스.모바일; 가져오기 com.fasterxml.잭슨.데이터 바인드.JsonNode; 가져오기 okhttp3.*; 가져오기 자바.io.IOException; 가져오기 자바.net.소켓 예외; 가져오기 자바.활용.동시.시간 단위; 가져오기 정적 com.카우치베이스.모바일.런타임.매퍼; public 클래스 SGMonitor { 비공개 정적 final OkHttpClient 클라이언트 = new OkHttpClient.빌더() .읽기 시간 초과(1, 시간 단위.일수) .빌드(); 비공개 변경 피드 리스너 리스너; 비공개 HttpUrl.빌더 urlBuilder; 비공개 스레드 monitorThread; 비공개 문자열 이후 = "0"; 비공개 통화 통화; SGMonitor(문자열 URL, 문자열 활성 전용, 문자열 포함 문서, 문자열 이후, 문자열 스타일, 변경 피드 리스너 리스너) { 이.이후 = 이후; urlBuilder = HttpUrl.parse(URL).새로운 빌더() .추가 경로 세그먼트("_changes") .추가 쿼리 매개변수("active_only", 활성 전용) .추가 쿼리 매개변수("include_docs", 포함 문서) .추가 쿼리 매개변수("style", 스타일) .추가 쿼리 매개변수("since", 이후) .추가 쿼리 매개변수("피드", "longpoll") .추가 쿼리 매개변수("timeout", "0"); 이.리스너 = 리스너; } |
저는 Square의 OkHttp 라이브러리. 현재 카우치베이스 라이트는 내부적으로도 이 라이브러리를 사용합니다. OkHttp는 빌더 패턴을 사용합니다. 클래스 생성자에서 나머지 코드를 통해 사용할 빌더 인스턴스를 준비합니다. 모든 매개 변수의 의미에 대한 자세한 내용은 동기화 게이트웨이 문서.
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 |
public 인터페이스 변경 피드 리스너 { void onResponse(문자열 body); } public void 시작() { monitorThread = new 스레드(() -> { 동안 (!스레드.중단됨()) { 요청 요청 = new 요청.빌더() .URL(urlBuilder.빌드()) .빌드(); 통화 = 클라이언트.newCall(요청); 시도 (응답 응답 = 통화.실행()) { 만약 (!응답.성공()) throw new IOException("예기치 않은 코드" + 응답); 문자열 body = 응답.body().문자열(); JsonNode tree = 매퍼.readTree(body); 이후 = tree.get("last_seq").asText(); urlBuilder.설정 쿼리 매개변수("since", 이후); 리스너.onResponse(body); } catch (소켓 예외 ex) { 반환; } catch (IOException ex) { ex.프린트스택트레이스(); 대화 상자.디스플레이(ex); } } }); monitorThread.setDaemon(true); monitorThread.시작(); } |
그리고 시작
메서드에는 코드에서 가장 흥미로운 부분이 있습니다. 이 메서드는 백그라운드 스레드를 회전시킵니다. 스레드 설정 및 제어 코드 아래에서 연속 루프를 실행합니다. 이 루프는 동기식 네트워크 호출을 수행합니다. 오류 처리는 간단합니다. 문제가 발생하면 예외를 던지기만 하면 됩니다.
동기화 게이트웨이는 JSON 문자열로 응답합니다. 코드에서 응답을 분리하고 JSON을 파싱하여 JsonNode
객체입니다. 이 모든 것은 last_seq
값을 반환합니다.
다음에 보낼 내용을 추적하기 위해 변경사항 피드는 간단한 시퀀스 메커니즘에 의존합니다. 이를 불투명한 객체로 취급해야 합니다. 다음과 같은 값을 가져옵니다. last_seq
를 이전 응답에서 이후
매개변수를 다음 요청에 동일한 값으로 설정합니다.
제공하지 않는다고 해서 실제로 해가 되는 것은 없습니다. 이후
매개변수를 추가하세요. 동기화 게이트웨이는 이 매개변수가 누락된 경우 모든 변경 사항을 처음부터 다시 재생합니다. 그렇기 때문에 이 예제에서는 약간의 치트를 사용하여 항상 클래스 인스턴스를 이후
문자열 "0"으로 설정합니다.
실제 애플리케이션에서는 매번 변경 기록을 살펴보는 대신 앱이 처리한 마지막 시퀀스 문자열을 저장하는 방법이 필요할 수 있습니다.
나머지 코드는 몇 가지 짧은 메서드에 불과합니다.
1 2 3 4 5 6 7 8 9 |
public void 중지() { monitorThread.인터럽트(); 통화.취소(); } public 문자열 getSince() { 반환 이후; } } |
주요 수업은 여기까지입니다. 앱을 완성하기 위해 필요한 다른 수업도 있습니다.
다음 내용을 확인하세요. GitHub 리포지토리 를 클릭하여 모든 코드와 빌드 지침을 확인하세요.
앱에 대한 설명과 사용 방법을 다음에서 읽어보세요. 파트 1.
포스트 스크립트
더 많은 리소스는 다음에서 확인할 수 있습니다. 개발자 포털 트위터에서 팔로우하세요 카우치베이스 개발.
질문에 대한 답변을 게시할 수 있습니다. 포럼. 그리고 다음에도 적극적으로 참여합니다. 스택 오버플로.
질문, 의견, 보고 싶은 주제 등이 있으면 트위터에서 저에게 연락해 주세요. 호드그릴리