03 realm 쓰기 & 질의
TRANSCRIPT
![Page 1: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/1.jpg)
0
Realm쓰기 & 질의
Made by 이종찬
![Page 2: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/2.jpg)
1
01주차 Realm introrealm을 안드로이드 스튜디오에 설치하기
02주차 모델 & 관계지원하는 필드 타입과 어노테이션 & 관계 설정하는 법
03주차 쓰기 & 질의트랜잭션을 통한 삽입 & 질의의 종류
![Page 3: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/3.jpg)
2
01Transaction
![Page 4: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/4.jpg)
3
01 TransactionRealm 객체를 사용하기 전 Transaction에 대해서 알아보자.
1 Transaction이란?
1 데이터 베이스에서 읽거나 쓰기 위해 실행하는 여러 개의쿼리(질의)를 끊기지 않고 전부 수행하기 위한 것.-> 완전성(integrity)를 보장
2 Atomicity(원자성), Consistency(일관성), Isolation(고립성), Durability(영구성)을 보장한다.
Realm은 MVCC 아키텍처를 사용하기 때문에 쓰기 트랜잭션 실행되도읽기는 막히지 않는다.
-> 읽기는 언제든지 객체를 접근 할 수 있다.-> 추가, 수정, 삭제는 트랜잭션 안에서 해야한다.
![Page 5: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/5.jpg)
4
MVCC
다중 버전 동시성 제어(multiversion concurrency control)
A가 읽기에 대한 처리를 트랜잭션을 통해 수행하면 동일 데이터를다른 사람 B는 A가 끝날 때까지 접근을 할 수 없었다.
하지만 MVCC는 A가 트랜잭션을 할 때 동일한 데이터를 복사해 두어B도 동시에 접근을 가능하게 만들었다.
쉽게 말해서 [동시에 여러명이 접근이 가능하게 했다] 라는 것이다.
![Page 6: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/6.jpg)
5
01 TransactionRealm에서 Transaction을 관리하는 법을 알아보자.
Transaction관리하기
1 수동으로 관리하기
// Realm 인스턴스를 얻습니다Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();// 객체를 여기에서 추가하거나 갱신합니다
realm.commitTransaction();// commit이 호출되면 모든 작업이 정상적으로 완료한 것입니다.
realm.cancelTransaction();// 오류가 났을 때는 cancel을 호출하여 모든 작업을 취소할 수 있습니다.
![Page 7: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/7.jpg)
6
01 TransactionRealm에서 Transaction을 관리하는 법을 알아보자.
Transaction관리하기
2 자동으로 관리하기
// Realm 인스턴스를 얻습니다Realm realm = Realm.getDefaultInstance();
realm.executeTransaction(new Realm.Transaction() {@Overridepublic void execute(Realm realm) {
User user = realm.createObject(User.class);user.setName("John");user.setEmail("[email protected]");
}});
Transaction 블록을 사용하여 간편하게 Transaction을 관리할 수 있다.
![Page 8: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/8.jpg)
7
01 TransactionRealm에서 Transaction을 관리하는 법을 알아보자.
Transaction관리하기
3 비동기 트랜잭션(Async)
realm.executeTransactionAsync(new Realm.Transaction() {@Overridepublic void execute(Realm bgRealm) {
// 수행할 내용.}
}, new Realm.Transaction.OnSuccess() {@Overridepublic void onSuccess() {
// 트랜잭션이 성공하면 호출되는 콜백 함수.}
}, new Realm.Transaction.OnError() {@Overridepublic void onError(Throwable error) {// 트랜잭션이 실패하면 호출되는 콜백 함수}
});
![Page 9: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/9.jpg)
8
동기 & 비동기
동기 (Synchronous: 동시에 일어나는)- 요청과 그 결과가 동시에 일어난다는 약속- 장점 : 설계가 간단하고 직관적이다.- 단점 : 결과가 주어질 때까지 아무것도 못하고 대기해야 한다.
비동기 (Asynchronous: 동시에 일어나지 않는)- 요청과 그 결과가 동시에 일어나지 않을 것이라는 약속이다. -> 요청한 그 자리에서 바로 결과가 주어지는 것이 아니라,
이따가 준다고 약속하는 것이다.- 장점 : 결과가 주어지지 않아도 그 시간 동안 다른 작업을 할 수 있다.- 단점 : 설계가 복잡하다.
![Page 10: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/10.jpg)
9
01 TransactionRealm에서 Transaction을 관리하는 법을 알아보자.
Transaction관리하기
3 비동기 트랜잭션(Async)
APIPublic RealmAsyncTask executeTransactionAsync(Realm.Transaction transaction);Public RealmAsyncTask executeTransactionAsync(Realm.Transaction transaction,
Realm.Transaction.OnSuccess onSuccsess);Public RealmAsyncTask executeTransactionAsync(Realm.Transaction transaction,
Realm.Transaction.OnError onError);Public RealmAsyncTask executeTransactionAsync(Realm.Transaction transaction,
Realm.Transaction.OnSuccess onSuccsess,Realm.Transaction.OnError onError);
백그라운드 쓰레드에서 쓰기 작업을 하는 중인데 UI 쓰레드가 쓰기 작업을하려고 하면 ANR에러가 발생할 수 있다.-> 때문에 비동기 트랜잭션을 사용한다.
OnSuccess와 OnError 콜백은 선택 사항으로 사용하면 된다.콜백은 Looper에 의해 제어되고 Looper 스레드에서만 허용된다.
![Page 11: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/11.jpg)
10
01 TransactionRealm에서 Transaction을 관리하는 법을 알아보자.
Transaction관리하기
3 비동기 트랜잭션(Async)
비동기 트랜잭션은 RealmAsyncTask 객체로서 저장할 수 있다.
RealmAsyncTask transaction = realm.executeTransactionAsync(new Realm.Transaction() {// 수행할 내용.
});
객체로 표현하는 이유는 트랜잭션이 끝나기 전에 액티비티나 프레그먼트를 끝내야할 때 대기중인 트랜잭션을 취소하기 위해서도 사용하기 때문이다.Ex) 1. 현재 대기중인 트랜잭션이 있다고 가정하자.
2. 이 트랜잭션을 수행한 다음에는 액티비티의 TextView를 갱신한다.3. 하지만 이 트랜잭션이 수행되기 전에 액티비티를 종료하였다.4. 만약 트랜잭션이 그대로 수행된다면 존재하지 않는 메모리에 TextView에 대한
데이터 갱신 작업을 수행해야 하는 것이다. 앱이 충돌할 수 있다.
때문에 액티비티를 종료하기 전에 확인 작업을 해주어야 한다.public void onStop () {
if (transaction != null && !transaction.isCancelled()) {transaction.cancel();
}}
![Page 12: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/12.jpg)
11
02쓰기(write)
![Page 13: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/13.jpg)
12
02 쓰기Realm 객체를 생성하는 법을 알아보자.
객체 생성하기
1 Realm을 통해 객체 생성하기
RealmObject들은 Realm과 긴밀하게 연결되어 있기 때문에Realm을 통해 인스턴스가 생성되어야 한다.
realm.beginTransaction();User user = realm.createObject(User.class); // 새 객체 만들기user.setName("John");user.setEmail("[email protected]");realm.commitTransaction();
Realm에 의해 관리되는 객체의 데이터를 수정할 경우 Transaction 안에서 해줘야한다는 단점이 있을 수 있다.(내 생각)
![Page 14: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/14.jpg)
13
02 쓰기Realm 객체를 생성하는 법을 알아보자.
객체 생성하기
2 객체를 생성 후 Realm에 등록하기
Realm을 통해서만 객체를 생성할 때는 불편한 점이 있을 수 있다.그때는 Realm.copyToRealm() 메소드를 사용하여 등록하면 된다.
User user = new User("John");user.setEmail("[email protected]");
realm.beginTransaction();User realmUser = realm.copyToRealm(user);realm.commitTransaction();
< 중요 > copyToRealm을 사용할 때 반환되는 객체를 Realm이 관리한다. -> 원본 객체(user)의 내용을 바꿔도 Realm에 있는 데이터가 바뀌는 것이 아니다.-> Realm이 관리하는 객체(realmUser)를 바꿔야 Realm에 있는 데이터가 바뀐다.
![Page 15: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/15.jpg)
14
02 쓰기객체를 생성하고 쓰기를 할 때 주의할 점을 알아보자.
주의 할 점
createObject를 사용하여 객체를 처음 생성할 경우 Realm은 RealmObject안에 있는 필드의 내용을 default value(쓰레기값)으로 설정을 하고 객체를 만든다.이때 객체의 필드에 PrimaryKey가 등록되어 있으면 충돌이 일어 날 수 있다.-> default value는 고정값이기 때문이다.
이를 해결하기 위해서는 비관리 객체를 생성하고 PrimaryKey에 대한 값을 설정한다음 copyToRealm을 이용해서 Realm에 등록해 주어야 한다.
User user = realm.createObject(User.class); // 필드들에 대한 값을 default value로 설정한다.// 이때 PrimaryKey에 대한 충돌이 날 수 있다.
User user = new User("John");uesr.setId(value);
realm.beginTransaction();User realmUser = realm.copyToRealm(user);realm.commitTransaction();// 이렇게 해주면 충돌을 피할 수 있다.
1 PrimaryKey 충돌
![Page 16: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/16.jpg)
15
02 쓰기객체를 생성하고 쓰기를 할 때 주의할 점을 알아보자.
주의 할 점
Realm은 문자열이나 바이트 배열의 개별 요소를 갱신하는 것은 불가능하다.특정 요소를 갱신하기 위해선 아래처럼 해야한다.
realm.executeTransaction(new Realm.Transaction() {@Overridepublic void execute(Realm realm) {
bytes[] bytes = realmObject.binary;bytes[4] = 'a';realmObject.binary = bytes;
}});
2 문자열과 바이트 배열 갱신하기
Realm이 MVCC 아키텍처에 따라 기존의 데이터를 변경하는 것을 막아 다른 스레드와프로세스가 안정된 상태의 데이터를 읽기 위함이다.
![Page 17: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/17.jpg)
16
03질의(Query)
![Page 18: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/18.jpg)
17
03 질의(Query)Realm의 기본적인 사용법(1)
질의의 시작은 realm.where를 시작으로 하며 끝은 find(All)을 통해 끝난다.
Realm의 모든 질의는 바로 처리되지 않고 속성에 접근할 때에만 데이터를 읽는다.
API(Realm.class) Public RealmQuery<E> where(java.lang.Class<E> class);
(RealmQuery.class) Public RealmResults<E> findAll();(RealmQuery.class) Public RealmResults<E> findAllAsync();(RealmQuery.class) Public RealmResults<E> findAllSorted(String fieldName);
(RealmQuery.class) Public E findFirst();
![Page 19: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/19.jpg)
18
03 질의(Query)Realm의 기본적인 사용법(1)
// Build the query looking at all users:RealmQuery<User> query = realm.where(User.class);
// 질의 조건을 추가합니다query.equalTo("name", "John");query.or().equalTo("name", "Peter");
// 질의를 수행합니다RealmResults<User> result1 = query.findAll();
질의를 통해 나온 결과가 RealmResults에 저장되어 나온다.이때 객체는 복사되지 않고, 레퍼런스가 저장되어 있는 RealmResult를 통해서직접적으로 다룰 수 있다.
RealmResults는 AbstractList를 상속받았기 때문에각 객체는 인덱스를 통해서 접근이 가능하다.
또한 질의에 맞는 결과가 없더라도 null을 반환하지 않고size()메소드가 0을 반환한다.
각 객체의 내용을 수정하거나 삭제할 경우 Transaction을 이용해야 한다.
![Page 20: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/20.jpg)
19
03 질의(Query)Realm의 기본적인 사용법(2) – Fluent interface
// 같은 일들을 한번에 합니다 ("Fluent interface"):RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();
RealmQuery<User> query = realm.where(User.class);
query.equalTo("name", "John");query.or().equalTo("name", "Peter");
RealmResults<User> result1 = query.findAll();
![Page 21: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/21.jpg)
20
03 질의(Query)Realm의 Query engine에 대해서 알아보자.
Fluent interface
Realm은 Query Engine으로 Fluent interface를 사용하였다.Fluent interface란 메소드 체이닝(method chaining) 기법으로return 값으로 자기 자신 또는 다른 객체를 지정 함으로서메소드를 연속적으로 호출 할 수 있도록 하는 기법이다.
가장 쉽게 볼 수 있는 것은 자바스크립트(또는 Jquery)에서 볼 수 있다.document.getElementById("demo").innerHTML…
안드로이드에서 쉽게 볼 수 있는 곳은 Toast 부분에서이다.Toast.makeText(…).show(); (또는 스낵바)
Toast의 makeText는 return 값으로 Toast 객체를 반환한다.-> 즉 자기 자신을 반환하는 것이다. 때문에 자기 자신의 메소드를
다시 호출 할 수 있어서 메소드 체이닝이 가능하게 된다.
Realm의 Query도 이러한 구조를 띄고 있다.
![Page 22: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/22.jpg)
21
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
1 조건 관련 질의대부분 이름만으로 설명이 된다.• between(), greaterThan(), lessThan(), greaterThanOrEqualTo(), lessThanOrEqualTo()
-> (필드 이름, 숫자 (, 숫자))• equalTo(), notEqualTo()
-> (필드 이름, 값)• contains(), beginsWith(), endsWith()
-> (필드 이름, 문자)• isNull(), isNotNull()
-> (필드 이름)• isEmpty(), isNotEmpty()
-> (필드 이름)
equalTo(), contains(), beginsWith(), endsWith()문자에 대한 질의 중 이 4개에 대해서는(필드 이름, 문자, case)를 통해서 대소문자를 구분의 유무를 정할 수 있다.
-> CASE.SENSITIVE , CASE.INSENSITIVE
![Page 23: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/23.jpg)
22
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
2 논리 연산자• Realm의 Query는 암묵적으로 and 연산을 가지고 있으며 or연산이필요할 때는 or연산만 명시해주면 된다.
• 그룹 조건의 연산 순서를 명시하기 위해서는 왼쪽 괄호로 beginGroup()을사용하고 오른쪽 괄호로 endGroup()을 사용한다.
• not()으로 부정 조건을 만들 수 있다. 하위 조건을 부정하기 위해서는beginGroup과 endGroup을 함께 사용해야 한다.
RealmResults<User> r = realm.where(User.class).greaterThan("age", 10) // 암묵적인 AND.beginGroup()
.equalTo("name", "Peter")
.or()
.contains("name", "Jo").endGroup().findAll();
![Page 24: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/24.jpg)
23
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
3 결과 정렬하기RealmResults<User> result = realm.where(User.class).findAll();result = result.sort("age"); // 오름차순으로 정렬result = result.sort("age", Sort.DESCENDING); // 내림차순으로 정렬
API (RealmResults.class)
Public RealmResults<E> sort(String fieldname);Public RealmResults<E> sort(String fieldname, Sort sortOrder);Public RealmResults<E> sort(String[] fieldname, Sort[] sortOrder);Public RealmResults<E> sort(String fieldname1, Sort sortOrder1,
String fieldname2, Sort sortOrder2);
Sort.Enum - Sort.ASCENDING, Sort.DESCENDING
![Page 25: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/25.jpg)
24
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
4 집합(arregation)
RealmResults<User> results = realm.where(User.class).findAll();
long sum = results.sum("age").longValue();long min = results.min("age").longValue();long max = results.max("age").longValue();double average = results.average("age");
long matches = results.size();
API (RealmResults.class)숫자가 저장되어 있는 필드만 지원한다.
Public Number sum(String fieldname);Public Number max(String fieldname);Public Number min(String fieldname);Public double average(String fieldname);
![Page 26: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/26.jpg)
25
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
5 삭제(Deletion)
final RealmResults<Dog> results = realm.where(Dog.class).findAll();// 데이터에 대한 모든 변경은 트랜잭션에서 이루어져야 합니다realm.executeTransaction(new Realm.Transaction() {
@Overridepublic void execute(Realm realm) {
// 하나 맞는 데이터를 삭제합니다results.deleteFirstFromRealm();results.deleteLastFromRealm();
// 하나의 객체를 삭제합니다Dog dog = results.get(5);dog.deleteFromRealm();
// 전체 맞는 데이터를 삭제합니다results.deleteAllFromRealm();
}});
![Page 27: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/27.jpg)
26
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
// RealmResults에서 맨 처음 객체를 삭제하거나 맨 마지막 객체를 삭제합니다.results.deleteFirstFromRealm();results.deleteLastFromRealm();
// 하나의 객체를 삭제합니다Dog dog = results.get(5);dog.deleteFromRealm();
1) 하나의 데이터를 삭제하는 방법
이렇게 할 경우 Realm에서는 실제로는 삭제가 되었지만, RealmResults에서는 삭제가 되지 않았을 수도 있다.-> 후에 RealmResults에서 삭제한 데이터가 다시 검색될 수도 있다.
// RealmResults에서 삭제를 직접한다.(realm 문서 – 질의 – 반복자 참고)results.deleteFromRealm(5);
5 삭제(Deletion)
![Page 28: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/28.jpg)
27
03 질의(Query)Realm의 Query의 몇 가지 종류를 알아보자.
Query
API (RealmResults.class)
Public Boolean deleteAllFromRealm();Public Boolean deleteFristFromRealm();Public Boolean deleteLastFromRealm();Public void deleteFromRealm(int location);
API (RealmObject.class)
Public void deleteFromRealm();
5 삭제(Deletion)
2) 결과에 맞는 여러 개의 데이터를 삭제하는 방법
// RealmResults에 있는 모든 데이터를 삭제한다.results.deleteAllFromRealm();
![Page 29: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/29.jpg)
28
03 질의(Query)Realm의 비동기 질의에 대해서 알아보자.
비동기 질의
Realm의 대부분의 질의는 UI스레드에서 동기적으로 처리할 수 있을 정도로충분히 빠르다.
하지만 복잡한 질의나 대규모 데이터 집합을 질의하는 경우, 백그라운드에서질의 하는 것이 더 나은 경우들이 있다.
RealmResults<User> result = realm.where(User.class).equalTo("name", "John").or().equalTo("name", "Peter").findAllAsync();
// findAll 이였던 부분을 findAllAsync로만 바꿔주면 된다.
질의는 블록되지 않고 즉시 RealmResults를 반환하는 것을 유의해야 한다.-> 표준 자바의 Future와 흡사한 구조이다. (Thread와 관련 있는 것으로 생각된다)
질의는 백그라운드에서 계속되며 한번 완료되면 반환된RealmResults 인스턴스를 갱신한다.
![Page 30: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/30.jpg)
29
03 질의(Query)Realm의 비동기 질의에 대해서 알아보자.
비동기 질의
1) 백그라운드에서 질의가 끝났는지 확인할 수 있다.
if (result.isLoaded()) {// 결과는 지금 사용할 수 있습니다
}
2) 비동기 질의를 강제적으로 동기적으로 로딩 할 수 있다.
동기적으로 획득한 RealmResults의 isLoaded()를 호출하면 항상 true를 반환한다.
result.load() // 주의하세요. 이 코드는 현재 스레드를 반환될 때까지 멈추게 합니다
3) 루퍼 스레드에서만 비동기 질의를 사용할 수 있다.비동기 질의는 결과를 일관되게 전달하기 위해 Handler를 사용한다.Looper 없이 스레드 내에서 비동기 질의를 하게 되면ILLegalStateException을 발생시킨다.
![Page 31: 03 realm 쓰기 & 질의](https://reader034.vdocuments.net/reader034/viewer/2022050914/586f89531a28ab54768b5f73/html5/thumbnails/31.jpg)
30
감사합니다.
정보 출처 : realm.ioppt템플릿 : 홍양홍삼님 블로그