App Engine에 JDO 2.3 사용

JDO(자바 데이터 객체)는 자바에서 데이터베이스에 액세스하기 위한 표준 인터페이스로, 자바 클래스와 데이터베이스 테이블 간에 매핑을 제공합니다. JDO를 Datastore와 함께 사용하기 위한 오픈소스 플러그인이 있으며, 이 페이지에서는 해당 플러그인을 시작하는 방법에 대한 정보를 제공합니다.

경고: 대부분의 개발자는 낮은 수준의 Datastore API 또는 Datastore 전용으로 개발된 오픈소스 API 중 하나(예: Objectify)를 사용하는 것이 더 좋습니다. JDO는 기존 관계형 데이터베이스에 사용하도록 설계되었기 때문에 항목 그룹이나 상위 쿼리 등 관계형 데이터베이스와 차별화되는 Datastore의 몇 가지 측면을 명시적으로 나타낼 수 있는 방법이 없습니다. 이로 인해 이해하고 해결하기 어려운 미묘한 문제가 발생할 수 있습니다.

App Engine 자바 SDK에는 App Engine Datastore의 JDO 2.3 구현이 포함되어 있습니다. 이 구현은 JDO 2.3을 위한 오픈소스 참조 구현인 DataNucleus Access Platform의 버전 1.0을 기반으로 합니다.

참고: 이 페이지의 안내는 App Engine용 DataNucleus 플러그인의 버전 1.0을 사용하는 JDO 버전 2.3에 적용됩니다. App Engine은 이제 JDO 3.0을 실행할 수 있도록 허용하는 DataNucleus 2.x 플러그인을 제공합니다. 새 플러그인은 미소유 관계를 지원하며 다수의 새로운 API 및 기능을 제공합니다. 이 업그레이드는 이전 버전과 완전히 호환되지는 않습니다. JDO 3.0을 사용하여 애플리케이션을 다시 빌드하는 경우 코드를 업데이트하고 다시 테스트해야 합니다. 새 버전에 대한 자세한 내용은 JDO 3.0을 참조하세요. 업그레이드에 대한 자세한 내용은 App Engine에 JDO 3.0 사용을 참조하세요.

JDO 2.3 설정

JDO를 사용하여 Datastore에 액세스하려면 App Engine 앱에 다음이 필요합니다.

  • JDO 및 DataNucleus App Engine 플러그인 JAR은 앱의 war/WEB-INF/lib/ 디렉터리에 있어야 합니다.
  • jdoconfig.xml이라는 구성 파일은 JDO에 App Engine Datastore를 사용하라고 지시하는 구성과 함께 앱의 war/WEB-INF/classes/META-INF/ 디렉터리에 있어야 합니다.
  • 프로젝트의 빌드 프로세스가 컴파일된 데이터 클래스에서 컴파일 후 '보정' 단계를 수행하여 컴파일된 데이터를 JDO 구현에 연결해야 합니다.

JAR 복사

JDO 및 Datastore JAR은 App Engine 자바 SDK에 포함되어 있습니다. appengine-java-sdk/lib/user/orm/ 디렉터리에서 찾을 수 있습니다.

JAR을 애플리케이션의 war/WEB-INF/lib/ 디렉터리에 복사합니다.

appengine-api.jarwar/WEB-INF/lib/ 디렉터리에 있는지 확인합니다. 프로젝트를 만들 때 이미 복사했을 수도 있습니다. App Engine DataNucleus 플러그인은 이 JAR을 사용하여 Datastore에 액세스합니다.

jdoconfig.xml 파일 만들기

JDO 인터페이스에는 애플리케이션의 war/WEB-INF/classes/META-INF/ 디렉터리에 jdoconfig.xml이라는 구성 파일이 필요합니다. 이 위치에서 이 파일을 직접 만들거나 빌드 프로세스를 통해 이 파일을 소스 디렉터리에서 복사할 수 있습니다.

다음과 같은 내용으로 파일을 만듭니다.

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>
</jdoconfig>

Datastore 읽기 정책 및 호출 기한 설정

Datastore 쿼리 페이지에 설명된 대로 읽기 정책(strong consistency와 eventual consistency)과 호출 기한을 설정하여 Datastore의 동작을 맞춤설정할 수 있습니다. JDO에서는 jdoconfig.xml 파일의 <persistence-manager-factory> 요소에 원하는 값을 지정하여 이 작업을 수행합니다. 특정 PersistenceManager 인스턴스를 통해 실행되는 모든 호출은 PersistenceManagerFactory를 통해 관리자가 생성될 때 효과적인 구성 값을 사용합니다. 단일 Query 객체에 이 설정을 재정의할 수도 있습니다.

PersistenceManagerFactory에 읽기 정책을 설정하려면 datanucleus.appengine.datastoreReadConsistency이라는 속성을 포함합니다. 가능한 값은 EVENTUALSTRONG입니다. 지정하지 않으면 기본값은 STRONG입니다. 이 설정은 특정 항목 그룹 내의 상위 쿼리에만 적용됩니다. 상위 쿼리가 아닌 교차 그룹 쿼리는 적용되는 읽기 정책에 관계없이 항상 eventual consistency를 가집니다.

        <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />

읽기 및 쓰기에 대해 별도의 Datastore API 호출 기한을 설정할 수 있습니다. 읽기의 경우 JDO 표준 속성 javax.jdo.option.DatastoreReadTimeoutMillis를 사용합니다. 쓰기의 경우 javax.jdo.option.DatastoreWriteTimeoutMillis을 사용합니다. 값은 밀리초 단위의 시간입니다.

        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />

XG(교차 그룹) 트랜잭션을 사용하려면 다음 속성을 추가합니다.

        <property name="datanucleus.appengine.datastoreEnableXGTransactions" value="true" />

같은 jdoconfig.xml 파일에서 서로 다른 name 속성을 사용하는 <persistence-manager-factory> 요소 여러 개가 동일한 앱에서 서로 다른 구성으로 PersistenceManager 인스턴스를 사용하도록 할 수 있습니다. 예를 들어 다음 jdoconfig.xml 파일은 구성 세트 두 개를 설정합니다. 하나는 "transactions-optional"이고, 다른 하나는 "eventual-reads-short-deadlines"입니다.

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>
    </persistence-manager-factory>

    <persistence-manager-factory name="eventual-reads-short-deadlines">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="true"/>

        <property name="datanucleus.appengine.datastoreReadConsistency" value="EVENTUAL" />
        <property name="javax.jdo.option.DatastoreReadTimeoutMillis" value="5000" />
        <property name="javax.jdo.option.DatastoreWriteTimeoutMillis" value="10000" />
    </persistence-manager-factory>
</jdoconfig>

이름이 지정된 구성 세트로 PersistenceManager를 만드는 방법에 대한 내용은 아래의 PersistenceManager 인스턴스 가져오기를 참조하세요.

데이터 클래스 향상

JDO는 빌드 프로세스에서 컴파일 후 '보정' 단계를 사용하여 데이터 클래스를 JDO 구현에 연결합니다.

명령줄에서 다음 명령어를 사용하여 컴파일된 클래스에서 향상 단계를 수행할 수 있습니다.

java -cp classpath com.google.appengine.tools.enhancer.Enhance
class-files

classpath에는 appengine-java-sdk/lib/ 디렉터리에 있는 JAR appengine-tools-api.jar 및 모든 데이터 클래스가 포함되어야 합니다.

DataNucleus 바이트코드 보정기에 대한 자세한 내용은 DataNucleus 문서를 참조하세요.

PersistenceManager 인스턴스 가져오기

앱은 PersistenceManager 클래스의 인스턴스를 사용하여 JDO와 상호작용합니다. PersistenceManagerFactory 클래스의 인스턴스에서 메서드를 인스턴스화하고 호출하여 이 인스턴스를 가져옵니다. 팩토리는 JDO 구성을 사용하여 PersistenceManager 인스턴스를 만듭니다.

PersistenceManagerFactory 인스턴스는 초기화되는 데 시간이 다소 걸리므로 앱은 단일 인스턴스를 재사용해야 합니다. 이를 강제하기 위해 앱이 둘 이상의 PersistenceManagerFactory(구성 이름은 같음)를 인스턴스화하는 경우 예외가 발생합니다. PersistenceManagerFactory 인스턴스를 관리하는 쉬운 방법은 다음과 같이 정적 인스턴스가 있는 싱글톤 래퍼 클래스를 만드는 것입니다.

PMF.java

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManagerFactory;

public final class PMF {
    private static final PersistenceManagerFactory pmfInstance =
        JDOHelper.getPersistenceManagerFactory("transactions-optional");

    private PMF() {}

    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
}

팁: "transactions-optional"jdoconfig.xml 파일에 있는 구성 세트의 이름을 나타냅니다. 앱에서 여러 구성 세트를 사용하는 경우에는 원하는 JDOHelper.getPersistenceManagerFactory()를 호출하도록 이 코드를 확장해야 합니다. 코드는 각 PersistenceManagerFactory의 싱글톤 인스턴스를 캐시해야 합니다.

앱은 팩토리 인스턴스를 사용하여 Datastore에 액세스하는 요청마다 하나의 PersistenceManager 인스턴스를 만듭니다.

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

import PMF;

// ...
    PersistenceManager pm = PMF.get().getPersistenceManager();

PersistenceManager를 사용하여 데이터 객체를 저장, 업데이트, 삭제하고 Datastore 쿼리를 수행합니다.

PersistenceManager 인스턴스를 마쳤으면 close() 메서드를 호출해야 합니다. close() 메서드를 호출한 후에 PersistenceManager 인스턴스를 사용하면 오류가 발생합니다.

    try {
        // ... do stuff with pm ...
    } finally {
        pm.close();
    }

JDO 2.3의 지원되지 않는 기능

JDO 인터페이스의 다음 기능은 App Engine 구현에서 지원되지 않습니다.

  • 미소유 관계. 명시적 Key 값을 사용하여 미소유 관계를 구현할 수 있습니다. 미소유 관계의 JDO 구문이 이후 출시 버전에서 지원될 수도 있습니다.
  • 다대다 소유 관계
  • 'join' 쿼리. 상위 종류를 대상으로 쿼리를 실행할 때 필터에 하위 항목의 필드를 사용할 수 없습니다. 쿼리에서 키를 사용하여 상위 요소의 관계 필드를 직접 테스트할 수는 있습니다.
  • JDOQL 그룹화 및 기타 집계 쿼리
  • 다형성 쿼리. 클래스 쿼리를 실행하여 서브클래스의 인스턴스를 가져올 수 없습니다. 각 클래스는 Datastore에서 별도의 항목 종류로 표시됩니다.
  • @PersistenceCapable 주석의 IdentityType.DATASTORE. IdentityType.APPLICATION만 지원됩니다.
  • 현재는 상위 요소와 하위 요소가 같은 클래스일 때 일대다 소유 관계를 방지하는 버그가 있기 때문에 트리 구조를 모델링하기가 어렵습니다. 이 문제는 이후 출시 버전에서 수정될 예정입니다. 상위 요소나 하위 요소에 대해 명시적 Key 값을 저장하여 이 문제를 해결할 수 있습니다.

트랜잭션 중지 및 기존 JDO 앱 포팅

사용이 권장되는 JDO 구성은 datanucleus.appengine.autoCreateDatastoreTxns라는 속성을 true로 설정합니다. 이는 Datastore 트랜잭션을 애플리케이션 코드에서 관리되는 JDO 트랜잭션에 연결할 것을 JDO 구현에 지시하는 App Engine 고유 속성입니다. 처음부터 새로 앱을 빌드하는 경우 이 속성을 사용하는 것이 좋습니다. 하지만 기존의 JDO 기반 애플리케이션을 App Engine에서 실행하려는 경우 이 속성 값을 false로 설정하는 대체 지속성 구성을 사용하려 할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<jdoconfig xmlns="http://java.sun.com/xml/ns/jdo/jdoconfig"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://java.sun.com/xml/ns/jdo/jdoconfig">

    <persistence-manager-factory name="transactions-optional">
        <property name="javax.jdo.PersistenceManagerFactoryClass"
            value="org.datanucleus.store.appengine.jdo.DatastoreJDOPersistenceManagerFactory"/>
        <property name="javax.jdo.option.ConnectionURL" value="appengine"/>
        <property name="javax.jdo.option.NontransactionalRead" value="true"/>
        <property name="javax.jdo.option.NontransactionalWrite" value="true"/>
        <property name="javax.jdo.option.RetainValues" value="true"/>
        <property name="datanucleus.appengine.autoCreateDatastoreTxns" value="false"/>
    </persistence-manager-factory>
</jdoconfig>

이것이 유용할 수 있는 이유를 이해하려면 트랜잭션 내에서 같은 항목 그룹에 속하는 객체만 사용할 수 있다는 점을 기억해야 합니다. 기존 데이터베이스를 사용하여 빌드한 애플리케이션은 일반적으로 글로벌 트랜잭션을 사용할 수 있다고 가정하기 때문에 트랜잭션 내의 모든 레코드 집합을 업데이트할 수 있습니다. App Engine Datastore는 글로벌 트랜잭션을 지원하지 않기 때문에 코드가 글로벌 트랜잭션을 사용할 수 있다고 가정할 경우 App Engine에서 예외가 발생합니다. 매우 커질 수 있는 코드베이스를 살펴보고 모든 트랜잭션 관리 코드를 삭제하는 대신 간단히 Datastore 트랜잭션을 사용 중지할 수 있습니다. 이렇게 해도 다중 레코드 수정의 원자성에 대한 코드의 가정을 처리하는 데 아무런 도움도 되지 않지만 앱이 제대로 작동되므로 트랜잭션 코드를 한 번에 리팩토링하는 대신 필요에 따라 증분 방식으로 리팩토링할 수 있게 됩니다.