MongoDB는 JSON(JavaScript Object Notation) 스타일 문서를 저장하고 검색하기 위한 문서 지향 데이터베이스이다. 인덱싱, 복제 및 샤딩 기능으로 늘어난 MongoDB는 강력하고 확장 가능한 NoSQL 경쟁자로 부상했다(참고자료 참조).
공식 Java 드라이버는 MongoDB와 상호작용하기 위해 사용 가능하다. 드라이버는 데이터 저장소에서 문서를 표현하기 위해 Map 구현인 BasicDBObject를 제공한다. Map 표현이 편리하다고 하더라도, 특히 JSON에서 왔다갔다하며 일련화할 때 문서를 Java 클래스 계층으로 표현할 수 있는 것도 장점을 가진다. 예를 들어, Java 도메인 모델에서부터 왔다갔다하며 문서를 맵핑하면 MongoDB로 스키마 제거 개발의 혜택을 누리는 동시에 Java 계층에서 유형 안전을 강제 실행할 수 있다. 그리고 많은 Java 프레임워크는 POJO(plain old Java objects)의 사용을 가정하거나, 이를 더 능히 처리할 수 있다.
Morphia는 MongoDB에서 문서로 저장된 POJO를 지속하고 검색하고 삭제하며 쿼리할 수 있는 Apache 라이센스 부여된 Google Code 프로젝트이다. Morphia는 Mongo Java 드라이버와 관련된 랩퍼와 어노테이션 세트를 제공하여 이를 완수한다. Morphia는 JPA(Java Persistence API) 또는 JDO(Java Data Objects) 구현과 같은 오브젝트 지향적 맵퍼와 개념적으로 유사하다. 이 기사에서 MongoDB로 맵핑된 Java 도메인 모델로 Morphia를 사용하는 방법을 보여줄 것이다. 전체 샘플 코드를 얻으려면 다운로드를 참조한다.
Morphia의 기능을 시연하기 위해 간소화된 도메인 모델을 사용할 것이다. 가상적인 웹 애플리케이션인 BandManager는 뮤지컬 연극에 대한 데이터를 공급한다. 다시 말해서, 이는 구성원, 배급사, 백카탈로그, 장르 및 기타 등등이다. 필자는 다음 그림 1과 같이 이 도메인 모델을 표현하기 위해 Band, Song, Distributor 및 ContactInfo 클래스를 정의할 것이다.
그림 1. BandManager의 클래스
그림 1의 UML(Unified Modeling Language) 다이어그램은 도메인 모델 클래스 계층 구조를 보여준다. 왼쪽의 직사각형은 Band 클래스를 표현한다. 오른쪽에서 누적된 것은 ContactInfo, Distributor 및 Song 클래스를 각각 표현하는 직사각형들이다. Band에서 ContactInfo로 향하는 화살표는 ContactInfo 측에서 1을 표시하여, 두 클래스 간에 일대일 관계를 표시한다. Band에서 Distributor로 연결하는 선은 Band 측에 0..*을 표시하고 Distributor 측에 1을 표시하여, Band에 하나의 Distributor가 있고 Distributor가 Band를 많이 표현하는 것을 표시한다. 마지막으로 Band에서 Song으로의 화살표는 Song 측에 catalog 0..1을 표시하여, Band가 Song과 일대다 관계가 있고 이 관계는 catalog라고 하는 것을 표시한다.
이러한 클래스에 어노테이션을 작성한 다음에 Morphia의 Datastore 인터페이스를 사용하여 MongoDB에서 문서로 이를 저장할 것이다.
다음 목록 1은 Band 클래스에 어노테이션을 작성한 방법을 보여준다.
목록 1. Band.java
@Entity("bands")
public class Band {
@Id
ObjectId id;
String name;
String genre;
@Reference
Distributor distributor;
@Reference("catalog")
List<Song> songs = new ArrayList<Song>();
@Embedded
List<String> members = new ArrayList<String>();
@Embedded("info")
ContactInfo info;
|
@Entity 어노테이션이 필요하다. 이는 클래스가 전용 콜렉션에서 문서로 지속되는 것을 선언한다. @Entity 어노테이션인 bands로 공급된 값은 콜렉션의 이름이 지정되는 방법을 정의한다. Morphia는 기본값으로 클래스 이름을 사용하여 콜렉션의 이름을 지정한다. 예를 들어, bands 값을 남겨둔다면, 콜렉션은 데이터베이스에서 Band라고 불릴 것이다.
@Id 어노테이션은 Morphia에 어느 필드를 문서 ID로 사용하는지 지시한다. @Id 어노테이션이 있는 필드가 널인 오브젝트를 지속하려고 시도하면, Morphia는 ID 값을 자동생성한다.
Morphia는 @Transient 어노테이션으로 표시되는 경우를 제외하고, Morphia가 직면하는 어느 어노테이션이 없는 필드나 지속하도록 시도한다. 예를 들어, name 및 genre 특성은 문서에서 string으로 저장되어, name 및 genre가 핵심이 될 것이다.
distributor, songs, members 및 info 특성은 다른 오브젝트를 참조한다. 곧 확인하는 대로, @Reference로 어노테이션이 있는 경우를 제외하고 멤버 오브젝트가 임베드되어 있다고 가정한다. 이는 콜렉션의 상위 문서에서 하위로 나타날 것이다. 예를 들어, members List는 지속할 때에 다음과 같이 될 것이다.
"members" : [ "Jim", "Joe", "Frank", "Tom"] |
info 특성은 또 다른 임베드된 오브젝트이다. 이 경우에, 필자는 info의 값으로 @Embedded 어노테이션을 명시적으로 설정하고 있다. 이는 문서에서 하위의 기본 이름 지정을 대체하며, 그렇지 않으면 이는 contactInfo라고 할 것이다. 예를 들면, 다음과 같다.
"info" : { "city" : "Brooklyn", "phoneNumber" : "718-555-5555" }
|
@Reference 어노테이션을 사용하면 오브젝트가 또 다른 콜렉션에서 문서로 참조되는 것을 표시한다. 오브젝트가 Mongo 콜렉션에서부터 로드되면, Morphia는 이러한 참조를 따라 오브젝트 그래프를 빌드한다. 예를 들어, distributor 특성은 지속된 문서에서 다음과 같이 표시된다.
"distributor" : { "$ref" : "distributors", "$id" : ObjectId("4cf7ba6fd8d6daa68a510e8b") }
|
@Embedded 어노테이션을 사용하는 것처럼 @Reference는 기본 이름 지정을 대체하기 위해 값을 취할 수 있다. 이 경우에 필자는 문서에서 songs의 List를 catalog라고 부르고 있다.
이제 Song, Distributor 및 ContactInfo의 클래스 정의를 살펴보자. 다음 목록 2는 Song의 정의를 보여준다.
목록 2. Song.java
@Entity("songs")
public class Song {
@Id
ObjectId id;
String name;
|
다음 목록 3은 Distributor 정의를 보여준다.
목록 3. Distributor.java
@Entity("distributors")
public class Distributor {
@Id
ObjectId id;
String name;
@Reference
List<Band> bands = new ArrayList<Band>();
|
다음 목록 4는 ContactInfo의 정의를 보여준다
목록 4. ContactInfo.java
public class ContactInfo {
public ContactInfo() {
}
String city;
String phoneNumber;
|
ContactInfo 클래스는 @Entity 어노테이션이 없다. 필자는 ContactInfo의 전용 콜렉션이 필요하지 않기 때문에 의도적인 것이다. 인스턴스는 band 문서에 항상 임베드될 것이다.
이제 도메인 모델을 정의하였고 어노테이션을 작성하였으므로, Morphia의 Datastore를 사용하여 엔티티를 저장하고 로드하며 삭제하는 방법을 알려줄 것이다.
Datastore 인터페이스 — Mongo Java 드라이버를 아우르는 랩퍼—가 MongoDB에서 엔티티를 관리하기 위해 사용된다. Datastore가 설치를 위해 Mongo 인스턴스가 필요하기 때문에, 기존 Mongo 인스턴스를 재사용하거나 환경에 대해 이를 적절하게 구성할 수 있다. 여기에 다음과 같이 로컬 MongoDB 인스턴스로 연결하는 Datastore를 인스턴스화하는 예제가 있다.
Mongo mongo = new Mongo("localhost");
Datastore datastore = new Morphia().createDatastore(mongo, "bandmanager");
|
그 다음에 다음과 같이 Band의 인스턴스를 작성할 것이다.
Band band = new Band();
band.setName("Love Burger");
band.getMembers().add("Jim");
band.getMembers().add("Joe");
band.getMembers().add("Frank");
band.getMembers().add("Tom");
band.setGenre("Rock");
|
이제 Band 인스턴스가 있으니, 이를 지속하기 위해 다음과 같이 datastore를 사용할 수 있다.
datastore.save(band); |
band는 이제 bandmanager db에서 bands라고 하는 콜렉션에 저장되어야 한다. Mongo 명령행 인터페이스 클라이언트를 사용하여 확실히 보장하기 위해 다음을 살펴볼 수 있다(행들은 이 기사의 페이지 너비에 맞추기 위해 이 예제와 다른 예제에 나뉘어져 있음).
> db.bands.find();
{ "_id" : ObjectId("4cf7cbf9e4b3ae2526d72587"), "className" :
"com.bandmanager.model.Band", "name" : "Love Burger", "genre" : "Rock",
"members" : [ "Jim", "Joe", "Frank", "Tom" ] }
|
훌륭하다! 거기에 나와 있다. className 필드를 제외하고 모두 예상대로인 것처럼 보인다. Morphia는 자동으로 이 필드를 작성하여 MongoDB에서 오브젝트의 유형을 기록한다. 이는 컴파일 시(예를 들어, 콜랙션에서 혼합 유형으로 오브젝트를 로드하는 시점) 필수적으로 알려지지 않은 오브젝트 유형을 판별하기 위해 주로 사용된다. 이러한 점이 마음에 들지 않고 그 기능이 필요하지 않다는 것을 안다면, 다음과 같이 noClassnameStored 값을 @Entity 어노테이션으로 추가하여 지속되지 않도록 className을 사용 안함으로 할 수 있다.
@Entity(value="bands",noClassnameStored=true) |
이제 Band를 로드하고, 이는 필자가 지속한 band에 상응하는 것을 확인하게 될 것이다.
assert(band.equals(datastore.get(Band.class, band.getId()))); |
Datastore의 get() 메소드를 통해 ID를 사용하여 엔티티를 로드할 수 있다. 오브젝트를 로드하기 위해 콜렉션을 지정하거나 쿼리 문자열을 정의할 필요가 없다. Datastore에 로드하려는 클래스가 어느 것이고 그 ID가 무엇인지 알려주기만 하면 된다. Morphia가 나머지를 수행한다.
이제 Band의 협업하는 오브젝트를 살펴볼 차례이다. 다음과 같이 Song을 일부 정의하여 시작한 다음에, 방금 작성한 Band 인스턴스로 이를 추가할 것이다.
Song song1 = new Song("Stairway");
Song song2 = new Song("Free Bird");
datastore.save(song1);
datastore.save(song2);
|
Mongo에서 songs 콜렉션을 확인하면 다음과 같은 내용이 표시되어야 한다.
> db.songs.find();
{ "_id" : ObjectId("4cf7d249c25eae25028ae5be"), "className" :
"com.bandmanager.model.Song", "name" : "Stairway" }
{ "_id" : ObjectId("4cf7d249c25eae25038ae5be"), "className" :
"com. bandmanager.model.Song", "name" : "Free Bird" }
|
Song이 아직 band에서 참조되지 않는 점을 참고한다. 다음과 같이 이를 band로 추가하고 발생하는 내용을 확인한다.
band.getSongs().add(song1); band.getSongs().add(song2); datastore.save(band); |
이제 bands 콜렉션을 쿼리하면 이와 같은 내용이 표시되어야 한다.
{ "_id" : ObjectId("4cf7d249c25eae25018ae5be"), "name" : "Love Burger", "genre" : "Rock",
"catalog" : [
{
"$ref" : "songs",
"$id" : ObjectId("4cf7d249c25eae25028ae5be")
},
{
"$ref" : "songs",
"$id" : ObjectId("4cf7d249c25eae25038ae5be")
}
], "members" : [ "Jim", "Joe", "Frank", "Tom"] }
|
songs 콜렉션이 어떻게 두 개의 DBRef로 catalog는 이름의 배열로 저장하는지 참고한다.
현재 제한사항은 다른 오브젝트가 이를 참조할 수 있기 전에 참조된 오브젝트가 저장되어야 한다는 점이다. 이러한 이유 때문에 필자는 song1과 song2를 band로 추가하기 전에 저장했다.
이제 다음과 같이 song2를 삭제할 것이다.
datastore.delete(song2); |
songs 콜렉션을 쿼리하면 song2가 누락되었다는 점을 표시해야 한다. 하지만 band를 살펴보면 song이 여전히 존재하는 것을 확인할 것이다. 더 안 좋은 것은 다음과 같이 band 엔티티를 로드하려고 시도하면 예외를 초래한다.
Caused by: com.google.code.morphia.mapping.MappingException: The
reference({ "$ref" : "songs", "$id" : "4cf7d249c25eae25038ae5be" }) could not be
fetched for com.bandmanager.model.Band.songs
|
지금으로서는 이 오류를 방지하기 위해 이를 삭제하기 전에 수동으로 song으로 참조를 제거해야 한다.
ID로 엔티티를 로드하는 것은 여기까지만 할 것이다. 궁극적으로 필자가 원하는 엔티티를 위해 Mongo를 쿼리 가능하도록 하려고 한다.
ID로 band를 로드하는 것이 아니라 이름으로 이를 쿼리할 것이다. 다음과 같이 원하는 결과를 얻기 위해 Query 오브젝트를 작성하고 필터를 지정하여 이를 수행한다.
Query query = datastore.createQuery(Band.class).filter("name = ","Love Burger");
|
쿼리하려는 클래스, Band 및 createQuery() 메소드로의 필터를 지정한다. 한 번 쿼리를 정의했으니, 다음과 같이 결과에 액세스하기 위해 asList() 메소드를 사용할 수 있다.
Band band = (Band) query.asList().get(0); |
Morphia의 필터 연산자는 MongoDB 쿼리에서 사용되는 쿼리 운영자로 긴밀하게 맵핑한다. 예를 들어, 위의 쿼리에서 사용한 = 연산자는 MongoDB에서 $eq 운영자와 유사하다. 필터 연산자에 대한 전체 상세정보는 Morphia 온라인 문서에서 확인할 수 있다(참고자료 참조).
쿼리를 필터하기 위한 대체로서, Morphia는 쿼리를 빌드하기 위해 능숙한 인터페이스를 제공한다. 예를 들어, 다음 능숙한 인터페이스 쿼리는 이전의 필터 쿼리와 동일하다.
Query query = datastore.createQuery(Band.class).field("name").equal("Love Burger");
|
임베드된 오브젝트를 쿼리하기 위해 "점 표기법"을 사용할 수 있다. 여기에 다음과 같이 Brooklyn을 기반으로 하는 모든 밴드를 선택하기 위해 능숙한 인터페이스와 점 표기법을 사용하는 쿼리가 있다.
Query query = datastore.createQuery(Band.class).field("info.city").equal("Brooklyn");
|
더 나아가 쿼리의 결과 세트를 정의할 수 있다. 다음과 같이 밴드를 이름으로 정렬하고 100으로 결과를 제한하기 위해 이전 쿼리를 수정할 것이다.
Query query =
datastore.createQuery(Band.class).field("info.city").equal
("Brooklyn").order("name").limit(100);
|
콜렉션이 성장하면서 쿼리 성능이 저하될 것이라는 점을 확인할 것이다. 관계형 데이터베이스 표와 마찬가지로 Mongo 콜렉션은 합리적인 쿼리 성능을 보장하기 위해 적절하게 인덱스되어야 한다.
@Indexed 어노테이션으로 특성의 어노테이션을 작성하는 것은 인덱스를 필드로 적용한다. 여기에서 다음과 같이 Band의 genre 특성에서 genreName이라고 하는 오름차순 인덱스를 작성한다.
@Indexed(value = IndexDirection.ASC, name = "genreName") String genre; |
인덱스를 적용하기 위해, Morphia는 어느 클래스가 맵핑될 것이라는 점을 알아야 한다. 독자는 인덱스가 적용되도록 보장하기 위해 Morphia를 조금 다르게 인스턴스화해야 한다. 이를 다음과 같이 수행할 수 있다.
Morphia morphia = new Morphia();
morphia.mapPackage("com.bandmanager.model");
datastore = morphia.createDatastore(mongo, "bandmanager");
datastore.ensureIndexes();
|
최종 ensureIndexes() 호출은 데이터 저장소에 이미 존재하지 않는 필수 인덱스를 작성하도록 지시한다.
인덱스는 또한 중복이 콜렉션으로 삽입되는 것을 방지하는 데 사용할 수도 있다. 예를 들어, band의 이름에 대해 @Indexed 어노테이션에서 unique 특성을 설정하여, 다음과 같이 주어진 이름으로 하나의 band만 콜렉션에 존재하도록 보장할 수 있다.
@Indexed(value = IndexDirection.ASC, name = "bandName", unique = true) String name; |
그 이후에 동일한 이름으로 된 band가 삭제될 것이다.
Morphia는 MongoDB와 상호작용하기 위한 강력한 도구이다. 이를 통해 MongoDB 문서로 유형에 안전하고 자연스러운 액세스를 허용한다. 이 기사는 Morphia로 작업하는 주요 분야를 다루었지만 일부 기능은 제외했다. 데이터 액세스 오브젝트(DAO) 지원, 유효성 검증 및 수동 맵핑 기능에 대한 정보는 Morphia Google Code 프로젝트를 확인하기를 바란다.
| 설명 | 이름 | 크기 | 다운로드 방식 |
|---|---|---|---|
| Sample code for this article | j-morphia.zip | 17.2KB | HTTP |
교육
-
Morphia: Google Code에서 Morphia 프로젝트를 방문하여 Morphia에 대해 자세히 알아보자.
-
MongoDB: MongoDB에 대해 자세히 알아보자.
-
"Java
development 2.0: MongoDB: (적절한) RDBMS 이동 기능을 제공하는 NoSQL 데이터 저장소"(Andrew Glover저, developerWorks, 2010년 9월): Java 언어 드라이버로 MongoDB의 사용에 대한 훌륭한 소개 기사이다.
-
팟캐스트: Andrew
Glover가 10gen의 Eliot Horowitz CTO인 MongoDB의 작성자를 인터뷰한다
(developerWorks, 2010년 9월).
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
토론
-
Morphia Google Group: 다른 Morphia 사용자의 도움을 받아보자.
- developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
