SimpleDB에 대한 이 소개 기사의 전반부 기사에서 필자는 Amazon의 고유 API를 사용하여 CRUD 스타일 레이싱 애플리케이션을 모델링하는 방법에 대해 설명했다. 당신은 데이터 유형에 대한 Amazon의 문자열 전용 접근방식에 대한 대부분의 Java 개발자들의 두드러진 독창성을 별개로 하고 약간 회의적인 시각으로 Amazon API를 바라봤을 것이다. 결국 관계형 데이터베이스를 활용하기 위한 API가 이제 충분히 고려된 표준이며 더욱 중요한 점은 이러한 API가 익숙하다는 것이다.
오늘날 다수의 관계형 프레임워크가 JPA(Java Persistence API)를 암암리에 구현하고 있다. 이를 통해 거의 모든 유형의 Java 애플리케이션에 대한 도메인 오브젝트 모델링이 RDBMS의 범위에서 편리하고 익숙해진다. 제대로 작동하는 접근방식을 이미 터득한 경우에는 새로운 접근방식을 배우는 데 거부감이 드는 것이 당연하기 때문에 SimpleDB의 경우도 마찬가지이지만 그럴 필요가 없다.
SimpleDB에 대한 소개 기사의 이 후반부 기사에서는 JPA 스펙을 준수하도록 Part 1의 레이싱 애플리케이션을 리팩토링하는 방법에 대해 설명한다. 그런 다음 해당 애플리케이션을 SimpleJPA에 이식하여 이 혁신적인 오픈 소스 플랫폼이 NoSQL 도메인 모델링, 클라우드 기반 스토리지에 좀 더 쉽게 적응할 수 있도록 하는 방법을 찾는다.
오늘날 다수의 Java 개발자가 데이터 지속성을 위해 Hibernate(및 Spring)를 활용하고 있다. Hibernate는 오픈 소스의 성공을 주도했을 뿐만 아니라 ORM 분야를 영구적으로 바꾸었다. Hibernate 이전에는 Java 개발자가 EJB 엔티티 Bean의 문제를 처리해야 했으며 그 전에 기본적으로 자체 ORM을 수행하거나 IBM®과 같은 벤더로부터 ORM을 구입했다. Hibernate는 많은 사용자가 오늘날 당연하게 여기는 POJO 기반 모델링 플랫폼을 채택하여 이러한 복잡성을 제거하고 비용 지출을 없앴다.
JPA(Java Persistence API)는 데이터 모델링에 POJO를 활용한다는 Hibernate의 혁신성에 대한 인기에 부응하여 작성되었다.
오늘날 EJB 3.0은 JPA를 구현하며 Google App Engine도 마찬가지이다. Hibernate EntityManager를 사용한다고 가정하면 Hibernate 자체도 JPA 구현이다.
POJO를 사용한 데이터 중심 애플리케이션 모델링을 통해 Java 개발자가 얼마나 편해졌는지를 생각하면 SimpleDB와 같은 데이터 저장소도 비슷한 혜택을 제공하는 것이 합리적이다. 결국 SimpleDB도 데이터베이스와 비슷한 유형이 아닌가?
SimpleJPA를 사용하려면 Racer 및 Runner 오브젝트에 대해 약간의 작업을 수행하여 JPA 스펙에 대한 정보를 제공해야 한다.
다행히도 JPA의 기본사항은 매우 단순하다. 일반적인 POJO에 어노테이션을 지정하면 EntityManager 구현이 나머지 사항을 처리하기 때문에 XML이 필요하지 않다.
JPA에서 사용하는 주요 어노테이션 중 두 가지는 POJO를 지속적인 것으로 지정하는 @Entity와 ID 키를 표시하는 @Id이다.
레이싱 애플리케이션을 JPA로 변환하기 위해서는 관계 관리에 사용되는 두 개의 어노테이션(@OneToMany와 @ManyToOne)도 필요하다.
이 기사의 전반부에서는 러너와 레이스를 지속하는 방법에 대해 설명했다. 이러한 엔티티를 표시하기 위해 오브젝트를 사용한 적은 없지만 Amazon의 원시 API를 사용하여 두 엔티티의 특성을 지속시켰다. 레이스와 레이스의 러너 사이에서 단순한 관계를 모델링하려고 한다면 Listing 1과 같이 할 수 있다.
Listing 1. 단순 Race 오브젝트
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
//setters and getters left out...
}
|
Listing 1에서 필자는 네 개의 특성을 가진 Race
오브젝트를 지정했다(마지막 특성은 러너의 Collection임).
그런 다음 필자는 각 러너의 이름(여기서는 최대한 단순하게 유지)을 보유하는 단순 Runner 오브젝트(Listing 2에 표시됨)와
SSN을 참여 중인 Race 인스턴스와 함께 작성할 수 있다.
Listing 2. Race와 관련된 단순 Runner
public class Runner {
private String name;
private String ssn;
private Race race;
//setters and getters left out...
}
|
Listings 1과 2에서 알 수 있듯이 필자는 러너와 레이스 사이에 다대일 관계를 논리적으로 모델링했다. 실제 상황에서는 다대다 링크를 작성하는 것이 더 적절할 것이다(러너는 일반적으로 둘 이상의 레이스에 참여함). 하지만 단순성을 유지한다. 또한 우선은 생성자, setter 및 getter를 남겨두었다. 이들에 대해서는 나중에 살펴볼 것이다.
SimpleJPA를 위해 이러한 두 오브젝트를 준비하는 것은 그다지 어렵지 않다. 먼저 @Entity
어노테이션을 각각에 추가하여 이들을 지속 가능하게 만들겠다는 필자의 의도를 나타내야 한다.
또한 필자는 @OneToMany를 Race 오브젝트에 사용하고
@ManyToOne을 Runner 오브젝트에 사용하여 관계를 적절하게 나타내야 한다.
@Entity 어노테이션은 클래스 레벨에서 첨부되고 관계 어노테이션은
getter 레벨에서 첨부된다. 모든 사항은 Listings 3 및 4에 설명되어 있다.
Listing 3. JPA 어노테이션된 Race
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
Listing 3에서 필자는 getRunners 메소드에 @OneToMany 어노테이션을 지정했다.
또한 Runner 엔티티에서 race 특성을 사용하여 관계를 찾을 수 있도록 지정했다.
Listing 4에서 필자는 Runner 오브젝트에서 getRace 메소드에 비슷하게 어노테이션을 지정할 것이다.
Listing 4. JPA 어노테이션된 Runner
@Entity
public class Runner {
private String name;
private String ssn;
private Race race;
@ManyToOne
public Race getRace() {
return race;
}
//other setters and getters left out...
}
|
관계형인지 여부에 관계없이 대부분의 데이터 저장소에는 데이터 사이에서 고유성을 나타내는 방법이 필요하다.
따라서 이러한 두 오브젝트를 데이터 저장소에서 지속적인 것으로 만들려면 적어도 ID를 이들 오브젝트에 추가해야 한다.
Listing 5에서 필자는 BigInteger 유형의 id 특성을 Race 도메인 오브젝트에 추가했다.
Runner에 대해서도 동일한 작업을 수행할 것이다.
Listing 5. Race에 ID 추가하기
@Entity
public class Race {
private String name;
private String location;
private double distance;
private List<Runner> runners;
private BigInteger id;
@Id
public BigInteger getId() {
return id;
}
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
Listing 5에 있는 @Id 어노테이션은 ID 관리 방법에 대한 정보는 제공하지 않는다.
프로그램에서는 예를 들어, EntityManager를 사용하는 대신 필자가 수동으로 해당 정보를 제공한다고 가정한다.
지금까지는 SimpleDB와 관련된 사항은 수행하지 않았다. Race 및 Runner 오브젝트에는 특유하게 JPA 어노테이션이 지정되어 이들 오브젝트는 JPA 구현에서 지원하는 모든 데이터 저장소에서 지속될 수 있다.
옵션에는 Oracle, DB2, MySQL과 지금쯤은 짐작하고 있을 SimpleDB가 포함된다.
SimpleJPA는 Amazon의 SimpleDB에 대한 JPA의 오픈 소스 구현이다. SimpleJPA는 전체 JPA 스펙을 지원하지는 않지만(예를 들어, JPA 쿼리에서는 결합을 수행할 수 없음) 풍부한 서브세트를 지원하기 때문에 살펴볼 만한 가치가 있다.
SimpleJPA 사용 시 가장 큰 장점은 필자가 전반부 기사에서 다룬 사전 편집순 문제를 원만하게 처리한다는 것이다.
SimpleJPA는 숫자 유형에 의존하는 오브젝트에 대해 문자열 변환 및 후속 패딩(필요한 경우)을 수행한다.
대부분의 경우 이는 String 유형을 반영하기 위해 사용자가 도메인 모델을 변경하지 않아도 된다는 것을 의미한다.
(이 규칙에는 한 가지 예외가 있으며 이 예외에 대해서는 잠시 후에 설명한다.)
SimpleJPA는 JPA 구현이기 때문에 편리하게 JPA 준수 도메인 오브젝트를 함께 사용할 수 있다.
SimpleJPA를 사용하기 위해서는 String ID를 사용하기만 하면 된다. 즉, id 특성이 java.lang.String이어야 한다.
작업을 더 간편하기 하기 위헤 SimpleJPA는 도메인 오브젝트의 ID 특성과 날짜 속성created 및 updated를 관리하는 기본 클래스 IdedTimestampedBase를 제공한다.
(기본적으로 SimpleDB는 고유의 Id를 생성한다.)
Race 및 Runner 클래스가 SimpleJPA를
준수하도록 하기 위해 SimpleJPA의 편리한 기본 클래스를 확장하거나 각 클래스의 id 특성을 BigInteger에서 String으로 변경할 수 있다.
필자는 Listing 6과 같이 첫 번째 옵션을 선택했다.
Listing 6. SimpleJPA의 IdedTimestampedBase 기본 클래스를 사용하도록 Race 변경하기
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private double distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
필자는 Runner에 대한 동일한 코드를 표시하지 않지만 사용자는 자유롭게 살펴봐도 된다. 단, IdedTimestampedBase를 확장하고 id 특성을 Runner에서 제거한다.
Race 및 Runner의 ID를 업데이트하는 것이 레이싱 애플리케이션이 SimpleJPA를 준수하도록 만드는 첫 번째 단계이다.
그런 다음 Integer 및 BigDecimal과 같은 오브젝트의 원시 데이터 유형(예: double, int 및 float)을 교환해야 한다.
Race의 distance 특성부터 시작한다.
SimpleJPA의 현재 릴리스에서는 BigDecimal이 Double보다 신뢰성이 높은 것으로 판명되었기 때문에 Listing 7과 같이 Race의 distance 특성을 BigDecimal로 변경했다.
Listing 7. distance를 BigDecimal로 변경하기
@Entity
public class Race extends IdedTimestampedBase{
private String name;
private String location;
private BigDecimal distance;
private List<Runner> runners;
@OneToMany(mappedBy = "race")
public List<Runner> getRunners() {
return runners;
}
//other setters and getters left out...
}
|
이제 Runner와 Race는 둘 다 SimpleJPA 구현을 통해 지속될 준비가 되었다.
SimpleJPA를 사용하여 SimpleDB에 대해 도메인 오브젝트를 조작하는 것은 JPA 구현을 사용하여 일반적인 관계형 데이터베이스에 대해 도메인 오브젝트를 조작하는 것과 다르지 않다.
JPA를 사용하여 애플리케이션 개발을 수행한 적이 있으면 놀라울 것이 하나도 없다. 유일하게 새로운 사항은 SimpleJPA의 EntityManagerFactoryImpl을 구현하는 것이며
이를 수행하려면 SimpleDB 도메인에 대한 접두어 이름과 Amazon Web Services 신임 정보가 필요하다. (또다른 옵션은 클래스 경로에 대한 신임 정보가 포함된 특성 파일을 제공하는 것이다.)
SimpleJPA의 EntityManagerFactoryImpl의 인스턴스 작성 시 지정하는 접두어 이름을 사용하면 대시와 도메인 오브젝트의 이름이 뒤따라 오는 접두어로 시작하는 SimpleDB 도메인이 생성된다.
따라서 내 접두어에 "b50"을 지정하는 경우에는 필자가 SimpleDB에서 Race 항목을 지정하면 도메인은 "b50-Race"가 된다.
SimpleDB의 EntityManagerFactoryImpl의 인스턴스를 작성한 후에는 모든 항목이 해당 인터페이스에 의해 구동된다.
Listing 8과 같이 EntityManagerFactoryImpl에서 확보하는 EntityManager 인스턴스가 필요할 것이다.
Listing 8. EntityManager 확보하기
Map<String,String> props = new HashMap<String,String>();
props.put("accessKey","...");
props.put("secretKey","..");
EntityManagerFactoryImpl factory =
new EntityManagerFactoryImpl("b50", props);
EntityManager em = factory.createEntityManager();
|
EntityManager를 처리하면, 원하는 대로 도메인 오브젝트를 조작할 수 있다.
예를 들어, 다음과 같이 Race 인스턴스를 작성할 수 있다.
Listing 9. Race 작성하기
Race race = new Race();
race.setName("Charlottesville Marathon");
race.setLocation("Charlottesville, VA");
race.setDistance(new BigDecimal(26.2));
em.persist(race);
|
Listing 9에서 SimpleJPA는 모든 HTTP 요청을 처리하여 클라우드에서 Race를 작성한다.
SimpleJPA를 사용한다는 것은 Listing 10과 같이 JPA 쿼리를 사용하여 레이스를 검색할 수도 있다는 것을 의미한다.
(사용자는 이러한 쿼리를 사용하여 결합을 수행할 수는 없지만 필자는 여전히 숫자를 사용하여 검색을 수행할 수 있음을 기억한다.)
Listing 10. 거리별 레이스 찾기
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race race : races){
System.out.println(race);
}
|
SimpleJPA의 기본적인 숫자에서 문자열로의 변환은 특히 유용하다. 예를 들어, SimpleJPA에서 쿼리 인쇄를 사용할 수 있게 하면 SimpleDB에 대해 발생되는 쿼리를 알 수 있다.
제출되는 쿼리는 Listing 11에 표시되어 있다. distance가 인코딩되는 방식에 주목한다.
Listing 11. SimpleJPA는 숫자를 효과적으로 처리함
amazonQuery: Domain=b50-Race, query=select * from `b50-Race` where `distance` = '0922337203685477583419999999999999928946' |
자동 패딩 및 인코딩을 사용하면 작업이 훨씬 편리해진다. 그렇지 않은가?
SimpleDB는 쿼리에서 도메인 결합을 허용하지 않지만 여전히 여러 도메인에 걸쳐 관련된 항목은
가지고 있을 수 있다. Part 1에서도 설명했듯이
단순히 관련 오브젝트의 키를 다른 오브젝트에 저장한 후 필요할 때 해당 오브젝트를 검색할 수 있다. SimpleJPA에서도 동일하게 수행된다.
예를 들어, 앞서 JPA 어노테이션을 사용하여 Runner를 Race에 링크하는 방법에 대해 설명했다.
따라서 필자는 Listing 12와 같이 Runner의 인스턴스를 작성하고 기존 race 인스턴스를 해당 인스턴스에 추가한 후
Runner 인스턴스를 지속시킬 수 있다.
Listing 12. SimpleJPA와의 관계
Runner runner = new Runner();
runner.setName("Mark Smith");
runner.setSsn("555-55-5555");
runner.setRace(race);
race.addRunner(runner);
em.persist(runner);
em.persist(race); //update the race now that it has a runner
|
Listing 12에서 Race 인스턴스를 업데이트하여 해당 인스턴스에 Runner 인스턴스가 추가되었다는 사실을 반영해야 한다는 것에 유의한다.
단순히 Runner를 Runner의 내부 Collection에 추가하는 addRunner 메소드를 Race에 추가했다는 것에도 주목한다.
또다시 거리별로 레이스를 검색하는 경우에는 Listing 13과 같은 러너 Listing도 확보할 수 있다.
Listing 13. 관계에 대한 추가 사항
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
System.out.println(race);
List<Runner> runners = race.getRunners();
for(Runner rnr : runners){
System.out.println(rnr);
}
}
|
EntityManager 인스턴스를 사용하면 Listing 14와 같이 remove
메소드를 통해 엔티티를 삭제할 수 있다.
Listing 14. 클래스 인스턴스 제거하기
Query query = em.createQuery("select o from Race o where o.distance = :dist");
query.setParameter("dist", new BigDecimal(26.2));
List<Race> races = query.getResultList();
for(Race races : race){
em.remove(race);
}
|
Listing 14에서 Race 인스턴스를 제거하는 동안 관련된 Runners는 제거되지 않는다.
(물론 필자는 JPA의 EntityListeners 어노테이션을 사용하여 이를 처리할 수 있으며 이는 필자가 제거 이벤트를 사용하여 Runner 인스턴스를 제거할 수 있음을 의미한다.)
SimpleDB에 대한 이 간단한 둘러보기에서는 Amazon Web Services API와 SimpleJPA를 사용하여 비관계형 데이터 저장소에서 오브젝트를 조작하는 방법에 대해 살펴봤다. SimpleJPA는 JPA(Java Persistence API)의 서브세트를 구현하여 더 쉽게 오브젝트가 SimpleDB에서 지속성을 유지할 수 있도록 한다. 살펴본 바와 같이 SimpleJPA를 사용할 때 한 가지 편리한 점은 원시 유형을 SimpleDB가 인식하는 문자열 오브젝트로 자동으로 변환한다는 것이다. 또한 SimpleJPA는 SimpleDB의 비결합 규칙을 자동으로 처리하여 관계 모델링 수행을 용이하게 한다. SimpleJPA의 확장 리스너 인터페이스도 논리적 데이터 무결성 규칙을 구현할 수 있도록 하며 이는 관계형 데이터 저장소의 경우를 통해 예상하고 있었을 것이다.
SimpleJPA에 대한 결론은 SimpleJPA가 신속하고 편리하게 적은 비용으로 상당한 확장성을 제공하는 데 도움이 될 수 있다는 것이다. SimpleJPA를 사용하면 비관계형 클라우드 기반 스토리지 환경에서 Hibernate와 같은 프레임워크에 대한 수년 동안의 작업 경험을 통해 이미 알고 있는 지식을 활용할 수 있다.
교육
- Java development 2.0: 이 developerWorks 시리즈에서 Gaelyk(2009년 12월), Google App Engine(2009년 8월) 및 CouchDB(2008년 11월)를 포함하여 Java 개발 환경을 재정의하는 기술과 도구에 대해 살펴보자.
- "NoSQL Patterns"(Ricky Ho, Pragmatic Programming Techniques, 2009년 11월): NoSQL 데이터베이스의 개요 및 목록을 확인할 수 있고 NoSQL 데이터 저장소의 일반적인 아키텍처에 대한 자세한 설명을 볼 수 있다.
- "Amazon Web Services를 사용한 클라우드 컴퓨팅, Part 5: SimpleDB를 통해 클라우드의 데이터세트 처리하기"(Prabhakar Chaganti, developerWorks, 2009년 2월): 기본적인 Amazon SimpleDB 개념에 대해 알아보고 SDB와 상호작용하는 데 필요한 오픈 소스 Python 라이브러리인 boto에서 제공하는 일부 기능을 살펴보자.
- "Amazon Web Services를 사용한 클라우드 컴퓨팅, Part 1: 소개"(Prabhakar Chaganti, IBM developerWorks, 2008년 7월): 신뢰할 수 있는 확장 가능한 애플리케이션을 만들고 빌드하는 데 필요한 Amazone Web Services의 뛰어난 기능에 대해 살펴보자.
- Google App Engine for Java: Part 3: 영속성과 관계"(Richard Hightower, developerWorks, 2009년 8월): Rick Hightower가 Google App Engine의 현재 Java 기반 지속성 프레임워크의 단점과 일부 대안에 대해 설명한다.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
-
SimpleJPA: Amazon의 SimpleDB에 대한 JPA(Java Persistence API) 구현이다. 달리 말하면 클라우드에 있는 Amazon의 데이터베이스에 대한 ORM(Object-Relational Mapping)이다.
토론
- My developerWorks 커뮤니티에 참여하자.
개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.
