Developer: Java
  다운로드
Oracle TopLink 11g 미리보기
샘플 코드
  태그
java, adf, All

Java SE 6를 이용한 JPA, JAXB, 주석 처리 기능의 개선


저자 – Dustin Marx

주석 처리 기능, JAXB 2.0 등을 포함하는 Java SE 6를 이용하여 Java 기반 애플리케이션 개발 환경을 개선할 수 있습니다.

게시일: 2007년 9월

최근 발표된 Java SE(Standard Edition) 6는 Java 프로그래밍 언어와 플랫폼의 사용성과 편의성을 크게 개선한 버전입니다. 특히, 개발자들은 Java SE 6를 이용하여, 개발 과정에서 JAXB(Java Architecture for XML Binding)와 주석 처리(annotation processing)를 한층 쉽게 적용할 수 있게 되었습니다.

본 문서에서는 Java SE 6의 JAXB, 주석 처리 지원 기능을 이용하여 JPA 기반 애플리케이션을 보다 쉽게 개발하는 방법을 설명하고 있습니다. 이 기능은 특히 Java 코드에 친숙하지 않은 애플리케이션 개발자들에게 많은 도움이 될 것입니다. 또 설명 과정에서 Java SE 6의 몇 가지 새로운 기능이 함께 설명될 것입니다.

Java 6를 이용한 통합 환경의 구현

Java 6는 새로운 툴과 기능을 포함하고 있을 뿐 아니라, 기존의 기능을 한층 개선하여 표준 툴셋에 통합된 형태로 제공하고 있습니다. 아래 표에서 Java 6의 개선된 기능을 확인할 수 있습니다. 이 표에는 J2SE 5와 Java SE 6를 포함하는 Java Specification(JSR)뿐 아니라 다른 테크놀로지들을 포함하는 JSR이 함께 설명되고 있습니다.

기능 J2SE 5.0 ("Tiger") JSR 176 Java SE 6 ("Mustang") JSR 270
JAXB Reference Implementation JWSDP(Java Web Services Developer Pack)를 통해 별도 제공 JSR 222 ("Java Architecture for XML Binding 2.0")
Annotations Processing

JSR 175 ("A Metadata Facility for the Java Programming Language")

새로운 주석 기능의 처리를 위한 "apt" 툴 제공
JSR 269 ("Pluggable Annotation Processing API")
JPA JSR 220 (Enterprise JavaBeans 3.0)*

JAXB, 주석 처리의 두 가지에 대해 각각 표준 Java SE 6 툴셋과 통합된 별도의 툴이 제공되고 있습니다. 과거의 경우 JAXB Reference Implementation(RI)는 J2SE 다운로드와 별도로 Java Web Services Developer Pack(JWSDP)을 통해 다운로드해야만 했습니다. 마찬가지로, Java SE 6 이전 버전의 주석 처리는 Java 컴파일러와 별개인, 매우 복잡한 apt 툴(annotations processing tool)을 사용해야만 했습니다. 이제 Java SE 6에서는 JAXB Reference Implementation(xjc 컴파일러 포함)이 Java SE 6와 함께 제공됩니다. 또 사용 편의성, 기능 면에서 한층 개선된 주석 처리 프레임워크가 javac Java 컴파일러에 포함된 형태로 기본 제공되고 있습니다.

JPA는 본래 Enterprise JavaBeans(EJB) 3.0의 개선을 위한 Java EE(Enterprise Edition) 표준의 하나입니다. 하지만 Java Persistence API는 Java의 SE, EE 버전에서 모두 사용이 가능합니다. Java SE 6에서는 아직 Java SE에 JPA를 완전히 통합하고 있지 않습니다. 하지만 JPA 표준은 모든 JPA 프로바이더가 Java SE를 지원할 것을 명시적으로 요구하고 있습니다. 또 Java 7에 JSR 220 퍼시스턴스 메커니즘이 공식적으로 포함될 것이라는 소식이 들려오고 있습니다.

JPA, 주석 처리, JAXB의 연계

개발자들은 JPA 표준을 이용하여 Java 클래스를 엔티티(entity)로서 주석 처리(annotate)하고 데이터 저장소에 저장(persist)하는 한편 실제 Java 소스 코드에서 다른 메타데이터 정보를 제공할 수 있습니다. 또 JPA를 이용하여 외부 XML 파일에 오브젝트-관계형 매핑 데이터를 지정하는 것 또한 가능합니다. 개발자들은 XML 파일이 소스 내의 특정 주석을 무시하도록, 또는 XML 매핑에서 모든 소스 코드의 주석을 완전히 무시하도록 설정할 수 있습니다.

개발자들은 소스 내에 주석을 처리하는 방식(in-source annotation)을 선호합니다. 메타데이터가 메타데이터를 기술하는 코드와 같은 위치에 저장된다는 장점이 있기 때문입니다. 좀 더 큰 규모의 프로젝트에서는 애플리케이션 적용 팀이 개발 팀과 분리되어 있을 수 있습니다. 이런 경우라면 외부에 설정된 오브젝트-관계형 매핑 메타데이터를 사용하고 소스 내의 주석을 무시하는 것이 바람직할 수도 있습니다. 또는 스타일적인 측면을 우선적으로 고려(소스 코드가 아닌 외부에서 설정을 관리)하거나, 주석의 값을 자주 변경해야 하거나, 소스 코드를 재컴파일하지 않고도 설정 변경을 적용하고자 하는 경우 소스 내에 주석을 설정하지 않고 외부에 오브젝트-관계형 매핑을 설정할 수 있습니다.

개발자들이 소스 내에 주석을 삽입하는 방법을 선호하는 반면 외부적으로 오브젝트-관계형 데이터를 정의하는 것이 바람직한 경우를 위해, 소스 내에 정의된 주석을 외부 XML 포맷으로 변환해 주는 툴이 제공된다면 유용할 것입니다. JPA 표준은 인-소스 주석(in-source annotation)과 외부 XML 포맷을 이용한 메타데이터 정의를 모두 지원하며, 메타데이터 변환을 위한 소스/타겟 포맷을 명시적으로 정의하고 있습니다.

소스 코드의 주석을 XML로 변환하려면 먼저 주석과 그 값을 읽어 들여야 합니다. 바로 이를 위해 Java 6의 주석 처리(annotation processing) 기능이 활용됩니다. 주석과 그 값을 읽어 온 다음에는 JPA 표준의 XML Schema에 정의된 XML 포맷으로 주석의 내용을 작성해야 합니다. JAXB는 바로 이 과정을 담당합니다. JAXB Reference Implementation의 xjc 컴파일러는 JPA의 XML Schema와 호환하는 XML 포맷으로 데이터를 작성하는 Java 클래스를 생성합니다. 그림 1은 주석 처리 기능과 JAXB Reference Implementation을 이용하여 소스 내의 Java 주석을 외부 XML 데이터로 변환하는 방법을 단순화된 형태로 보여 주고 있습니다.

Figure 1
그림 1. Java 6 SE를 이용한 JPA 주석 처리 툴

JPA 표준은 XML 메타데이터가 소스 내의 특정 주석을 무시하거나, 또는 전체 주석을 무시할 수 있도록 허용하고 있습니다. 두 가지 방법 중 어떤 것을 사용할 것인지는 (소스 코드가 아닌) XML 파일에 정의됩니다.

Java Persistence API에 대한 간략한 소개

개념의 이해를 돕기 위해 JPA에 대해 몇 가지 기본적인 내용들을 설명할 필요가 있어 보입니다. (JPA에 대한 상세한 배경 정보가 필요하신 경우, OTN에서 제공되는 JPA 관련 리소스들을 참고하시기 바랍니다.) 오라클은 TopLink Essentials를 통해, 또는 GlashFish(Java EE 애플리케이션 서버의 오픈 서버 버전)과 번들된 형태로 JPA Reference Implementation을 제공하고 있습니다. GlassFish 퍼시스턴스 페이지에서 JPA에 대한 다른 정보들을 함께 참고하실 수 있습니다.

Java Persistence API는 엔티티 EJB의 개발 업무를 단순화하기 위한 목적에서 EJB 3.0 표준의 일부로서 구현되었습니다. EJB 3.0에서 JPA가 처음 소개되었을 때, JPA는 Java EE 또는 Java SE 환경에서 사용될 수 있다는 중요한 이점을 제공하였습니다. 이제 JPA 덕분에 Java SE, Java EE 환경에 공통적인 표준 기반 데이터베이스 액세스 접근법을 구현할 수 있게 된 것입니다.

EJB 3.0으로 오면서 EJB는 일반적인 Java 클래스로 구현되었으며, 따라서 별도의 인터페이스를 구현하거나 부모 클래스를 확장할 필요가 없게 되었습니다. 기존의 어떤 Java 클래스든 클래스를 주석 처리함으로써 Entity로 변환하는 것이 가능합니다. 이를 위해 인-소스 주석(in-source annotation) 또는 외부 XML 설정 파일이 사용되며, 두 가지가 동시에 사용되는 경우도 있습니다. 외부 XML 설정 파일을 사용하는 방법의 경우 XML 포맷의 deployment descriptor를 사용하던 EJB의 이전 버전과 유사성을 갖습니다.

JPA 표준은 인-소스 주석, XML 설정 파일, 또는 두 가지 방법을 동시에 사용한 JPA 클래스의 주석 처리를 지원합니다. 본 문서는 인-소스 JPA 주석을 XML 파일로 변환하기 위한 주석 처리기(annotation processor)의 구현 과정을 설명하고 있습니다. 본 문서의 예제를 통해 인-소스 주석을 외부 설정 파일로 변환하는 방법뿐 아니라, Java 6의 강력하고 유연한 기능을 실제 환경에서 활용하는 방법이 설명됩니다. 특히, Java 6의 주석 처리 기능과 JAXB 2.0 Reference Implementation을 이용한 Java-to-XML 바인딩이 중점적으로 설명될 예정입니다.

Java에서의 주석 처리

주석(annotation) 기능은 Java 5 버전에서야 처음으로 소개되었지만 이미 Java SE, Java EE 또는 Java SE, Java EE를 지원하는 써드 파티 제품 및 프레임워크에서 가장 기본적인 언어 기능으로 활용되고 있을 뿐 아니라 주석은 매우 다재다능한 Java 메타데이터 메커니즘으로, 그 활용 방법은 이루 헤아릴 수 없을만큼 다양합니다.

Java 5에서의 주석 처리: 주석 기능의 최초 도입

주석 기능은 J2SE 5에서 Java 언어 소스 코드에 메타데이터를 직접 삽입하기 위한 표준적인 방법으로 처음 소개되었습니다. Java 주석의 개념과 문법은 Javadoc 스타일 태그 및 XDoclet 스타일 태그와 유사합니다. J2SE 5는 새로운 주석의 처리를 위한 별도의 툴로 Annotation Processing Tool(apt)를 제공하였습니다.

2004년, OTN은 Jason Hunter가 기고한 연재물("Making the Most of Java's Metadata")을 통해 Java 5에서 주석을 작성, 활용, 처리하는 방법을 설명하였습니다. 시리즈의 첫 번째 연재에서는 Java 주석의 개념을 소개하고 두 번째 연재에서는 커스텀 주석을 생성하는 방법을 설명하고 있습니다. 또 시리즈의 세 번째 아티클은 apt를 이용한 Java 5의 주석 처리에 대해 설명하고 있습니다.

J2SE 5의 주석 처리 기능 구현에 사용된 4 가지 패키지는 apt, declaration, type, util로 모두 com.sun.mirror를 통해 서브패키지로 제공되었습니다. 따라서 J2SE 5의 apt 툴은 모두 "com.sun.mirror"로 시작하는 패키지를 사용하고 있습니다.

Java 6에서의 주석 처리: javac 컴파일러와의 통합

Java SE 6는 주석 처리에 관련한 다양한 개선 기능을 구현하고 있습니다. 그 중에서도 가장 중요한 개선 사항으로 주석 처리 기능이 javac 컴파일러 내부에 통합되었다는 점을 들 수 있습니다. 개발자들은 이제 apt와 같은 별도의 툴을 사용하지 않고도 javac 컴파일러를 직접 활용하여 주석 처리를 수행할 수 있습니다.

"javac -help" 커맨드를 실행해 보면 J2SE 5의 javac과 비교하여 Java SE 6의 javac이 어떻게 달라졌는지 쉽게 확인할 수 있습니다. "help" 기능을 실행하여 확인할 수 있는 기능들 중 주석 처리와 관련하여 추가된 옵션이 아래 표와 같습니다.

Java 6 javac 주석 처리 옵션 설명
-processor 주석 처리를 실행할 대상 클래스를 명시적으로 지정할 때 사용합니다. 본 문서에서는 이 방법을 중점적으로 사용하고 있지만, Java 코드에서 주석 처리를 수행하면서 자동 발견 기능을 활용할 수도 있습니다.
-proc 일반적인 Java 소스 코드 컴파일을 수행하면서 주석 처리를 실행할 것인지, 또는 주석 처리 없이 컴파일을 수행할 것인지 선택하기 위한 옵션 플래그입니다.
-processorpath 주석 javfac 처리기가 주석을 처리할 클래스를 검색할 경로입니다.
-A<key>[=value] javac 커맨드에서 주석 처리기로 옵션을 전달하기 위한 옵션입니다. "value"는 사용될 수도, 사용되지 않을 수도 있습니다.
-XprintRounds 주석 처리기의 1회 실행(round)에 관련된 상세 정보를 제공하는 비표준적인 옵션입니다. 주석 처리기에 의해 처리되는 입력 파일, 각 실행 단계 별로 발견, 처리된 주석 등에 관련한 유용한 정보가 제공됩니다.
-XprintProcessorInfo 주석 처리기가 처리 요청을 접수한 주석의 목록을 표시합니다.

위 표에서 마지막 두 가지 옵션은 표준이 아니지만("-X"로 시작되는 이유가 여기에 있습니다), javac 컴파일러를 이용한 주석 처리 과정에서 매우 유용하게 활용됩니다. javac classpath 옵션(-cp 또는 -classpath) 역시 주석 처리 과정에서 함께 활용됩니다.

그 밖에 Java 6 javac 컴파일러에 대한 자세한 정보를 이곳에서 확인하실 수 있습니다. (위의 URL에서 "windows"를 "solaris"로 대체하면 Solaris에 관련한 javac 문서를 확인할 수 있습니다. 또 URL의 뒷 부분에 "#processing"을 덧붙이면 브라우저 화면이 주석 처리에 관련된 섹션으로 바로 이동합니다.)

Java SE 6 주석 처리에서 사용되는 메인 패키지는 javax.annotation.processing과 javax.lang.model(및 서브 패키지)입니다. javax.tools 패키지 또한 주석 처리를 위한 유용한 클래스들을 제공합니다.

javax.annotation.processing 패키지는 Java SE 6에 처음으로 추가되었으며 주석 처리기를 구현하는 과정에서 크게 도움이 됩니다. 그림 2는 단순화된 다이어그램을 통해 몇 가지 중요 인터페이스와 클래스 간의 관계를 보여 주고 있습니다.

Figure 2
그림 2. 주석 처리 인터페이스 및 클래스

Processor 인터페이스(javax.annotation.processing 패키지)는 주석 처리에서 사용되는 인터페이스입니다. 이 인터페이스를 직접 구현하는 대신, AbstractProcessor 클래스(동일한 javax.annotation.processing 패키지)를 확장하는 방법으로 주석 처리기를 구현하는 경우가 많습니다. Processor 인터페이스에 의해 사용되고 AbstractProcessor 클래스에서 확장되는 가장 중요한 메소드로 process 메소드가 있습니다. 주석 처리기는 이 메소드를 통해 처리에 필요한 주석을 제공합니다. 이 메소드는 또 현재 처리 작업에 대한 정보를 커스텀 프로세서로 전달하고 Processor 인터페이스(또는 AbstractProcessor)를 확장합니다.

그림 2는 본 문서의 예제를 위해 marx.apt.jpa 패키지의 일부로서 개발된 커스텀 클래스들을 보여 주고 있습니다. 이 클래스들은 코드 내부에 작성된 JPA 주석을 기반으로 오브젝트-관계형 매핑 XML 디스크립터 파일을 작성하기 위한 주석 처리기를 부분적으로 구현하고 있습니다.

아래 코드(Listing 1)는 JPA 관련 주석을 처리하는 Processor 인터페이스를 구현하기 위한 process 메소드의 구현 예를 보여 주고 있습니다. 아래 코드에는 process 메소드와 이 메소드를 둘러 싼 클래스 구조가 함께 소개되고 있습니다.

Listing 1

/**
 * Creates orm.xml file based on annotations in JPA-based code.
 */
@SupportedAnnotationTypes("javax.persistence.*")
@SupportedSourceVersion(RELEASE_6)
public class AnnotationEntityProcessor extends AbstractProcessor
{
   // . . . other code . . .

   /**
    * Process JPA-specific annotations in Java entity classes.
    * 
    * @param aAnnotations Matching annotations to be processed.
    * @param aRoundEnvironment Annotation processing round environment.
    * @return
    */
   @Override
   public boolean process( Set<? extends TypeElement> aAnnotations, 
                           RoundEnvironment aRoundEnvironment )
   {
      processingEnv.getMessager().printMessage(
         Diagnostic.Kind.NOTE,
         "Entered AnnotationEntityProcessor.process ...");

      Set<? extends Element> elements = aRoundEnvironment.getRootElements();
    
      for (Element element: elements)
      {
         handleRootElementAnnotationMirrors(element);
      }

      if (aRoundEnvironment.processingOver())
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.NOTE,
            "EntityProcessor processing completed." );

         preparePersistenceUnitMetadata();
         prepareFixedEntityMappings();

         writeMappingXmlFile();
      }

      return true;
   }

   // . . . other code . . .
}

대부분의 실제 주석 처리 작업은 handleElementAnnotationMirrors(Element) 메소드를 통해 구현되고 있지만, 그 밖에도 참고할 만한 점이 몇 가지 더 있습니다. 위 코드에서 클래스는 AbstractProcessor를 클래스를 확장하고 process 메소드를 무시(override)하는 형태로 구현되고 있습니다.

또 주석 처리를 담당하는 클래스가 별도의 주석을 자체적으로 가지고 있다는 점도 흥미롭습니다. @SupportedAnnotationTypes("javax.persistence.*") 주석과 @SupportedSourceVersion(RELEASE_6) 주석은 AnnotationEntityProcessor 클래스에 대한 메타데이터 정보를 제공하며, 생성되는 주석 처리 클래스의 자체적인 주석으로서 활용됩니다. 본 문서의 예제에서, 첫 번째 주석은 이 주석 처리기가 모든 JPA 관련 주석을 처리한다고 정의하고 있습니다. javax.persistence.*는 주석 처리기의 적용 대상을 JPA 관련 주석으로 제한하고 있습니다(이는 패키지내에 JPA 관련 주석만이 정의되어 있기 때문입니다). 두 번째 주석은, 그 이름이 암시하는 바와 같이, 처리된 소스 코드가 지원하는 Java 버전(위의 경우 Java SE 6)를 정의합니다.

또 세 번째 주석으로 @Override가 사용되고 있습니다. 이 주석은 J2SE 5에서부터 지원되기 시작했으며, 부모 클래스의 메소드를 무시(override)하도록 설정할 때 유용하게 활용됩니다. 위의 경우, AnnotationEntityProcessor의 process 메소드가 AbstractProcessor의 process 메소드를 무시하도록 보장하는데 사용되고 있습니다.

AbstractProcess 클래스는 모든 자식 클래스의 ProcessingEnvironment(동일 패키지)에 대한 액세스를 제공하고 있습니다. ProcessingEnvironment는 주석 처리를 위한 커스텀 파일 핸들러(Filer), 주석 처리를 위한 커스텀 메신저(Messager), 그리고 주석 처리 유틸리티를 제공하는 Elements 인터페이스(javax.lang.model.util 패키지)에 대한 액세스 등의 다양한 아이템을 주석 처리기와 연결하기 위한 후크(hook)로서 사용됩니다. 위의 코드는 AbstractProcessor가 제공하는 processEnv 필드를 이용하여 Messager(getMessager 메소드)에 접근하고 로그에 정보를 저장(printMessage 메소드)하는 방법을 예시하고 있습니다.

Listing 1의 코드는 또 주석 처리 과정에서 전달된 정보를 이용하여 주석 실행(round)의 루트 엘리먼트에 접근하고 있습니다(getRootElements() 메소드). 이 경우, 루트 엘리먼트는 엔티티 Java 클래스로 정의됩니다.

JPA 주석의 처리

본 문서의 예제에서는 엔티티로 정의된 Java 클래스에서 사용되는 주석을 처리하고, 이 엔티티 클래스에 대한 메타데이터의 O-R 매핑을 수행합니다. OTN에서 Java Persistence API 주석의 전체 목록을 확인하실 수 있습니다. Chapter 8("Metadata Annotations")과 Chapter 9("Metadata for Object/Relational Mapping")에서 JPA 표준(EJB 3.0 표준은 Core EJB 3.0 표준에서 파생된 것으로 JPA에 초점을 맞추고 있습니다)에서 사용될 수 있는 주석에 대한 정보가 제공됩니다. 마지막으로, 이 주석(인터페이스)의 패키지를 위한 javadoc 기반 API 문서를 통해 각 주석 인터페이스에서 JPA 주석을 사용하는 방법이 예시되고 있습니다. Javadoc 문서는 이곳에서 확인할 수 있습니다.

Listing 1의 코드는 자체적인 주석–@SupportedAnnotationTypes("javax.persistence.*")–을 통해 이 지수거 처리기가 어떤 유형의 주석을 지원하는지 정의하고 있습니다. @SupportedAnnotationTypes 주석을 이용하여 각각의 JPA 주석을 개별적으로 처리할 수도 있지만, "*" 기호를 와일드 카드 문자로 사용하면 더 간단합니다.

JPA 주석 중 가장 눈여겨 볼 만한 대상이 바로 @Entity입니다. @Entity는 일반 Java 클래스를 저장가능한(persistable) 엔티티로 정의하는데 사용되는 주석입니다. 아래 코드(Listing 2)는 이 속성을 가진 엔티티로서 정의된 클래스를 위한 Java 입력 소스 파일을 처리하는 한 가지 방법을 예시하고 있습니다. 아래는 Listing 1에서 호출된 handleRootElementAnnotationMirrors 메소드의 코드 중 일부분입니다.

Listing 2

/**
    * Process the provided Element for its JPA-related annotations.
    * It is expected that the Element provided will be one of the "root elements"
    * encountered when JPA classes are processed by the annotation processor.
    * This will ensure that annotations such as @Entity and @NamedNativeQuery
    * will be at this level.
    * 
    * @param aElement A "root" element (JPA-decorated class).
    */
   private void handleRootElementAnnotationMirrors(Element aElement)
   {
      processingEnv.getMessager().printMessage(
         Diagnostic.Kind.NOTE,
         "Entered handleElementAnnotationMirrors("
            + aElement.getSimpleName() + ") method..." );
      final String simpleName = aElement.getSimpleName().toString();
      List<? extends AnnotationMirror> annotationMirrors = 
         aElement.getAnnotationMirrors();
      marx.jpa.persistence.jaxb.Entity entity = this.of.createEntity();

      for (AnnotationMirror mirror: annotationMirrors)
      {
         final String annotationType = mirror.getAnnotationType().toString();

         if ( annotationType.equals(javax.persistence.Entity.class.getName()) )
         {
            populateEntity(entity, mirror, simpleName);
            furtherPopulateEntity( entity, aElement );
         }
         else if ( annotationType.equals(
            javax.persistence.NamedNativeQueries.class.getName()) )
         {
            createNativeNamedQuery(mirror);
         }
         else if ( annotationType.equals(
            javax.persistence.DiscriminatorColumn.class.getName()) )
         {
            addDiscriminatorColumnToEntity(entity, mirror);
         }
         else if ( annotationType.equals(
            javax.persistence.DiscriminatorValue.class.getName()) )
         {
            addDiscriminatorValueToEntity(entity, mirror);
         }
         else if ( annotationType.equals(
            javax.persistence.Inheritance.class.getName()) )
         {
            addInheritanceToEntity(entity, mirror);
         }
         else if ( annotationType.equals(
            javax.persistence.SequenceGenerator.class.getName()) )
         {
            addSequenceGeneratorToEntity(entity, mirror);
         }
         else if (    annotationType.equals(
                         javax.persistence.PersistenceContext.class.getName()) 
                   || annotationType.equals(
                         javax.persistence.PersistenceContexts.class.getName())
                   || annotationType.equals(
                         javax.persistence.PersistenceUnit.class.getName()) 
                   || annotationType.equals(
                         javax.persistence.PersistenceUnits.class.getName()) )
         {
            processingEnv.getMessager().printMessage(
               Diagnostic.Kind.MANDATORY_WARNING,
               "AnnotationType " + annotationType +
                  " should map to the persistence.xml file instead of orm.xml.");
         }
         else    // Flag any JPA annotations available but not handled
         {
            processingEnv.getMessager().printMessage(
               Diagnostic.Kind.MANDATORY_WARNING,
               "AnnotationType " + annotationType + " not handled currently." );
         }
      }  // end of for loop over annotationMirrors

      // An @Entity annotation was encountered and is ready to be added to
      // the JAXB EntityMappings element as a sub-element.
      if ( (entity.getName() != null) && (!entity.getName().isEmpty()) )
      {
         this.em.getEntity().add(entity);
      }
   }

Listing 2를 통해 AnnotationMirror 인터페이스의 구현 방법을 부분적으로나마 확인할 수 있습니다. (인터페이스의 Javadoc 코멘트를 참고하시기 바랍니다) 이 코드는 AnnotationMirror로부터 주석 타입을 가져오고 여러 가지 주석 타입에 따라 적절한 처리 방식을 선택하고 있습니다.

위 코드에는 JAXB 관련 코드도 부분적으로 사용되고 있습니다("entity"라는 이름의 로컬 Entity 변수에 관련된 코드). JAXB 코드에 관련된 부분은 뒷부분에서 설명하기로 하겠습니다. 코드의 뒷부분에서 Java 6의 String 클래스에 새로 추가된 isEmpty() 메소드가 사용되고 있음을 주목하시기 바랍니다. 이 메소드를 이용하면 String의 "empty" 여부를 별도로 확인할 필요가 없습니다.

Listing 2에서 마지막으로 사용된 "else if" 구문은 JPA 기반 애플리케이션의 persistence.xml 파일과 연관된 주석이 루트 엘리먼트에 존재하는지 확인합니다. 이 주석들은 orm.xml 또는 다른 이름의 O-R 매핑 파일이 아닌 persistence.xml 파일에 기록되므로, 주석 처리기는 이 주석에 대해 아무런 작업을 수행할 필요가 없습니다. (@SupportedAnnotationTypes 처리기 레벨 주석을 사용하여) 처리하고자 하는(다시 말해, orm.xml 파일의 항목들에 대응되는) javax.persistence 주석을 일일이 명시할 수도 있지만 이렇게 하려면 매우 긴 목록을 작성해야 할 것입니다. 그보다는 javax.persistence.*로 설정하여 모든 JPA 관련 주석을 처리하고, orm.xml 파일에 적용되지 않은 (상대적으로 소량의) 주석들을 필터링하는 것이 훨씬 간단할 것입니다.

Listing 2의 마지막 "else" 블록은 이 툴에 의해 처리되어야 하지만 (어떤 이유에서든) 처리되지 않는 주석들을 쉽게 확인하는 방법을 예시하고 있습니다. 이 단계까지 코드가 실행되었다면, 해당 processor 클래스의 @SupportedAnnotationTypes 주석에 의해 이 처리기가 특정 주석을 지원하도록 설정한 상태이지만, handleRootElementAnnotationMirrors 메소드는 이 주석을 예상하고 처리하도록 작성되지 않았다고 판단할 수 있습니다. 이러한 문제는 이 주석을 처리할 코드를 아직 작성하지 않았거나 향후 JPA 버전에 새로운 주석이 추가되었을 때 발생할 수 있습니다.

Listing 2의 handleRootElementAnnotationMirrors 메소드는 여러 가지 다른 메소드들을 호출하여 JPA O-R 매핑 파일을 작성하기 위한 JAXB 오브젝트를 구현합니다. 여기에서는 메소드의 예제가 아주 상세하게 설명되고 있지 않지만, 대부분의 코드는 일반적인 패턴을 따르며 소스 내의 주석 정보를 추출한 뒤 JAXB 오브젝트에 저장하는 방식으로 구현됩니다. (JAXB 오브젝트는 나중에 ORM XML 파일을 작성하는데 이용됩니다.)

The Power of AnnotationMirror

Listing 2에서 호출된 메소드 중 하나로 populateEntity 메소드가 있습니다. 이 메소드의 코드가 Listing 3과 같습니다.

Listing 3.

/**
    * Accept an Entity object that corresponds to an Entity XML element and
    * populate the given Entity element with its name as acquired from the
    * in-source @Entity annotation or use entity class' name as default name if
    * it is not explicitly specified in the annotation.
    * 
    * @param aEntity XML Entity element for which entity name should be supplied.
    * @param aMirror In-code Entity's annotation mirror.
    * @param aSimpleName Name of class holding @Entity annotation.
    */
   private void populateEntity( marx.jpa.persistence.jaxb.Entity aEntity,
                                final AnnotationMirror aMirror,
                                final String aSimpleName )
   {
      processingEnv.getMessager().printMessage(
         Diagnostic.Kind.NOTE,
         "Entered createEntity(Entity,AnnotationMirror,String)..." );

      try
      {
         Map<? extends ExecutableElement, ? extends AnnotationValue> mirrorMap = 
            aMirror.getElementValues();

         String entityName = aSimpleName; // Use element's name by default

         for (Map.Entry mirrorEntry : mirrorMap.entrySet())
         {
            String mirrorKey = mirrorEntry.getKey().toString();

            // The name() attribute of the Entity annotation will only be
            // available if it was explicitly set when that annotation was used.
            // If it was not explicitly set, this attribute will not be available.
            if ( mirrorKey.equals(ANNOTATION_KEY_NAME) )
            {
               // The string-formatted entity name includes double quotes
               // that must be removed before storing names in XML.
               entityName = trimDoubleQuotes(mirrorEntry.getValue().toString());
            }
         }

         aEntity.setName(entityName);
      }
      catch (Exception ex)
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR,
            "createEntity: " + ex.getMessage() );
      }
   }

Listing 3의 populateEntity 메소드는 JAXB에서 생성한 클래스를 받아 들이며, 궁극적으로 O-R XML 매핑 파일에 Entity 엘리먼트를 작성하는 목적으로 사용됩니다. 이 메소드는 위의 코드에서 "full scope"를 가집니다. 이는 processor 클래스에서도 Entity 주석 인터페이스(javax.persistence.Entity)에 접근해야 하기 때문입니다. JAXB에서 생성된 Entity 클래스의 스코프 처리를 통해, Entity 엘리먼트의 작성을 위해 JAXB에서 생성된 클래스와 JPA Entity 주석을 쉽게 구분할 수 있습니다.

Listing 3은 또 orm.xml 파일의 작성을 위해 JPA 주석을 반복적으로 처리하는 패턴을 구현하고 있습니다. populateEntity 메소드에 전달된 "annotation mirror"는 getElementValues() 메소드를 통해 Map<? extends ExecutableElement, ? extends AnnotationValue>의 형태로 그 엘리먼트를 제공합니다. 이 Map의 "key" 부분은 "? extends ExecutableElement"를 통해 알 수 있듯 ExecutableElement 인터페이스를 의미합니다. ExecutableElement는 주석 타입 엘리먼트의 파싱을 위해 사용됩니다. 반환된 Map의 "value" 부분은 "? extends AnnotationValue"를 통해 알 수 있듯 주석의 값을 의미합니다. 따라서 AnnotationMirror.getElementValues() 메소드는 주석 엘리먼트와 Map에서의 주석의 값을 반환할 때 유용하게 활용될 수 있습니다. 이 테크닉은 본 문서의 JPA 주석 처리기 코드 전반에 걸쳐, 주석과 그 값을 가져와 XML 매핑 파일에 기록하는 목적으로 활용됩니다.

앞에서 부분적으로 예시된 것처럼, Java SE 6의 강력한 주석 모델링 기능을 이용하여 소스 코드의 모든 JPA 주석을 주석 처리기에 전달할 수 있습니다. 다음으로는, JAXB를 이용하여 소스로부터 가져온 주석을 XML 매핑 파일 형태로 작성하는 방법을 알아보기로 하겠습니다.

주석 처리에 관련한 추가 정보

J2SE 5와 apt 툴을 이용한 주석 처리에 관련된 여러 가지 좋은 자료들을 참고하실 수 있습니다. 앞에서 언급한 것처럼, OTN은 "Making the Most of Java's Metadata" 시리즈를 통해 일련의 아티클을 연재하였고 세 번째 문서("Advanced Processing")는 apt를 이용한 Java 5 주석 처리를 주제로 다루고 있습니다.

또 Java SE 6 Javadoc 기반 API 문서에서 Java SE 6를 이용한 주석 처리에 사용되는 중요한 클래스들의 정보를 확인하실 수 있습니다. 특히 Processor(javax.annotation.processing 패키지) 인터페이스에 대한 Javadoc 기반 API 문서를 통해 Java SE 6의 인터페이스와 주석 처리 방법에 대한 자세한 설명이 제공되고 있습니다.

마지막으로 Java SE 6 배포본의 JDK 디렉토리 밑에 위치한 sample/javac/processing/src 디렉토리에는 예제 주석 처리기 클래스가 포함되어 있습니다. 배포본에 포함된 주석 처리기는 Visitor 디자인 패턴을 처리기에 적용하고 ElementScanner6 클래스를 활용하는 방법을 예시하고 있습니다.

Java 6와 Java Architecture for XML Binding (JAXB)

JJAXB가 공개된 것은 몇 년 전의 일이지만, Java SE 6에 와서야 Java에 JAXB Reference Implementation이 처음으로 통합되었습니다. 기존에는 Java Web Services Developer Pack(JWSDP)이 JAXB Reference Implementation의 1차적인 제공 매체로 활용되었으며, 그러다 JAXB RI(reference implementation)를 별도로 다운로드할 수 있도록 지원되기 시작했습니다. 이제는 Java SE 6에 JAXB RI가 기본 포함되어 있으므로 JAXB를 별도로 다운로드할 필요가 없습니다.

Java SE 6에 포함된 JAXB 2.0 Reference Implementation을 검증하는 가장 쉬운 방법은 -version 옵션을 사용하여 XJC 컴파일러를 실행해 보는 것입니다. 이를 위해 xjc 컴파일러가 위치한 디렉토리(<JDK_1.6.x_HOME>/bin)를 경로에 포함시키거나 직접 이 디렉토리로 이동하여 커맨드를 실행합니다: xjc -version. 필자의 컴퓨터에서는 다음과 같은 버전 메시지가 확인되었습니다:

xjc version "JAXB 2.0 in JDK 1.6" 
JavaTM Architecture for XML Binding(JAXB) Reference Implementation, (build JAXB 2.0 in JDK 1.6)

위의 실행 결과에서 확인할 수 있듯, JAXB 2.0 Reference Implementation(RI)은 Sun Microsystems의 웹 사이트에서 제공되는 JDK 1.6과 직접적으로 연관되어 있습니다.

JAXB RI를 Java SE 배포본과 함께 제공함으로써 여러 가지 이점을 기대할 수 있습니다. Java SE 6 이전 버전에서는 JAXB RI를 Java 애플리케이션에서 사용할 수 있도록 다운로드하고 설치하기 위해 번거로운 과정을 거쳐야만 했습니다. 먼저 Java SE 배포본과 별도로 JAXB RI를 다운로드하고 압축을 푼 후, JAXB 환경 변수를 올바르게 설정해야 합니다. 그런 다음, JAXB에 의해 생성된 클래스 및 런타임 바인딩을 사용하는 모든 Java 애플리케이션에서 컴파일 및 런타임에 JAXB 라이브러리를 포함시켜야 합니다. Java SE 6에서는 Java SE 6의 설치 과정에서 JAXB RI가 함께 설치, 설정되기 때문에 이러한 복잡한 과정을 거칠 필요가 전혀 없습니다.

다만 JAXB RI의 새로운 버전을 사용하려는 경우 Java SE 6와는 별도로 다운로드해야 함을 참고하시기 바랍니다. 본 문서가 작성되는 시점에는 JAXB RI 2.1.4가 별도 다운로드 가능한 상태이며, Java 6 SE에는 JAXB RI 2.0만이 포함되어 있습니다. 또는 Reference Implementation이 아닌 다른 버전의 JAXB를 구할 수도 있습니다. 다른 버전의 JAXB의 예로 TopLink의 JAXB 임플리멘테이션(TopLink 11g Preview는 JAXB 2와 호환하는 임플리멘테이션을 포함하고 있습니다.), 그리고 Apache의 JaxMe 2 등을 들 수 있습니다.

JAXB 2.0의 이점

Java SE 6에 포함된 JAXB 2.0은 기존 버전과 비교했을 때 여러 가지 부가적인 이점을 제공하고 있습니다. 여기에서 JAXB 2.0이 JAXB 1.0과 비교했을 때 갖는 상대적인 이점에 대해 간단하게 설명한 후, 바로 위에서 처리한 소스 내의 주석에 JAXB를 적용하여 XML 파일을 작성하는 방법을 예시하도록 하겠습니다.

JAXB 2.0 RI가 Java 6에 함께 포함되어 있다는 이점을 제외했을 때, JAXB 2.0 RI가 JAXB 1.0 RI과 비교했을 때 갖는 가장 중요한 장점은 JAXB에 의해 생성된 클래스 구조가 훨씬 간단하다는 것입니다. JAXB 2.0 RI 이전 버전에서는 모델링된 각각의 XML 엘리먼트별로 서로 다른 인터페이스와 임플리멘테이션 클래스를 JAXB xjc 컴파일러에서 생성해야 했습니다. JAXB 2.0 RI는 인터페이스와 임플리멘테이션을 "value class"로 대체함으로써 생성해야 하는 클래스의 수를 크게 줄이고 있습니다.

JAXB 2.0에서는 bgm.ser 파일을 고려할 필요가 없으며, 소스 XML Schema 파일로부터 모델링된 구조별로 단 하나의 클래스만이 적용됩니다. XML Schema 구조에 매핑되는 단순한 Java 클래스들을 제외했을 때, JAXB 2.0 RI에 의해 생성되는 클래스는 ObjectFactory 클래스(JAXB 1.0에도 포함)와 package-info.java 뿐입니다. 별도의 매핑 또는 커스터마이즈가 요구되지 않고 Reference Implementation이 사용되는 경우에는 jaxb.properties 파일을 사용할 필요가 없습니다.

JAXB 2.0과 JAXB 1.0의 또 다른 차이점으로, JAXB 2.0에 JAXB 관련 주석이 처음으로 사용되고 있다는 점을 들 수 있습니다. JAXB 1.0은 J2SE 5보다 이전에 공개되었으며, 따라서 기존 버전의 JAXB가 주석 기능을 지원하지 않는 것은 놀라운 일이 아닙니다. JAXB 2.0에서 사용되는 클래스의 수가 JAXB 1.0에 비해 훨씬 적은 것도 새로 추가된 주석에 힘입은 바가 큽니다. JAXB 2.0에서 지원되는 주석 기능을 이용하면 외부 바인딩 설정 파일을 이용하는 것보다 생성된 Java 클래스를 커스터마이즈하기가 쉽습니다. (본 문서의 예제에서는 JPA XML Schema를 위해 생성된 java 클래스를 커스터마이즈할 필요가 전혀 없으므로 이러한 방법들이 사용되지 않습니다.)

Java 클래스의 JAXB 생성과 관련하여 JAXB 2.0이 제공하는 이점 중 하나는, orm_1_0.xsd schema에서 생성된 클래스를 사용하기 위해 별도로 커스터마이즈 작업을 수행할 필요가 전혀 없다는 것입니다. JAXB 1.0 RI 환경이었다면 커스터마이즈 바인딩 파일을 이용하여 스키마에서 "class"로 명명되었고 Java의 Object.getClass 메소드와 충돌하는 속성들을 어떻게 처리해야 하는지 xjc 컴파일러에 알려 주어야 했을 것입니다.

JAXB 2.0 RI는 "class" 속성들을 처리하기 위해 생성된 메소드들의 이름을 자동으로 설정(예: "clazz")하여 get/set 메소드가 동일한 이름을 갖는 Object의 메소드와 충돌하지 않도록 합니다. JAXB 2.0 RI가 생성한 클래스들은 "clazz" 속성을 @XmlAttribute(name="class", true) 주석과 연계하여 "class"라는 이름을 갖는 스키마 속성에 매핑합니다. 따라서, JAXB 2.0 RI를 이용하면 Java 오브젝트와 기존 XML Schema의 커스터마이즈된 바인딩을 설정하기 용이할 뿐 아니라, 커스터마이즈가 필요할 때 xjc 컴파일러가 자동으로 필요한 작업을 수행해 준다는 장점을 제공합니다. JAXB 1.0 RI 환경에서는 Java 바인딩을 수작업으로 커스터마이즈 해야 할 뿐 아니라 생성된 코드 상의 주석이 아닌 커스터마이즈 파일을 이용해서(또는 소스 XML Schema를 변경하면서) 작업을 수행해야 합니다.

JAXB 2.0의 마지막 개선 사항으로 Java 클래스로부터의 XML Schema 생성을 지원한다는 점을 들 수 있습니다. JAXB 2.0 이전 환경에서, JAXB는 소스 XML Schema 또는 이와 유사한 XML 정의 문서(DTD, RelaxNG 등)으로부터 Java 클래스를 생성하는 용도로만 사용되었습니다. 하지만 JAXB 2.0이 발표되면서 그 반대 방향으로 자동 생성 기능을 활성화할 수 있게 되었습니다. 다시 말해 기존 Java 클래스로부터 XML Schema를 생성하는 것이 가능합니다. 본 문서의 예제에서는 기존 XML Schema에 매핑하는 Java 클래스를 생성하는 기능만으로 충분하므로(이 기능은 JAXB의 초기 버전부터 제공되어 왔던 것입니다) XML Schema 자동 생성 기능에 대해 더 이상 자세히 다루지는 않겠습니다.

주석 처리기에서 JAXB 2.0의 활용 사례

커스터마이즈 작업이 필요하지 않은 경우라면 JAXB Reference Implementation은 매우 간단한 방법으로 활용됩니다. 하지만 실제로는 부분적인 커스터마이즈 작업이 요구되는 경우가 많으며 JAXB는 이러한 환경에서 매우 뛰어난 기능을 발휘합니다. 본 문서의 예제에서는 Java 클래스의 생성을 위해 사용된 XML Schema의 엘리먼트들은 JAXB에서 생성된 Java 클래스의 명명 작업을 용이하게 할 수 있게끔 명명되고 있습니다. 예제에서는 별다른 문제 없이 JAXB의 기본 명명 규칙을 적용할 수 있으므로 커스터마이즈 방법론을 적용하지 않기로 합니다.

소스 XML Schema로부터 Java 클래스를 생성하는 작업은 매우 간단합니다. 그 첫 번째 단계로 JAXB에서 생성된 클래스들을 필요로 하는 XML Schema가 무엇인지 확인해야 합니다. 예제에서는 JPA 애플리케이션을 위한 orm.xml O-R 매핑 파일을 생성하는 것이 궁극적인 목적이므로, 매핑 파일의 구조를 설명하는 XML Schema가 필요합니다. 이 파일을 찾을 수 있는 경로는 여러 가지가 있습니다. 가장 무난한 선택 중 하나가 JPA 표준을 직접 참조하는 것입니다. XML Schema는 EJB 스펙 "Java Persistence API" 문서의 Section 10.2에서 확인할 수 있습니다. 또는 XSD 파일(orm_1_0.xsd) 형태의 스키마를 http://java.sun.com/xml/ns/persistence/ 페이지에서 다운로드할 수도 있습니다.

JAXB RI는 클래스가 생성된 XML Schema에 바인딩될 수 있는 Java 클래스 생성을 위한 "바인딩 컴파일러"를 제공합니다. JAXB RI의 "xjc -help" 커맨드를 이용하여 바인딩 컴파일러 사용 방법을 참고할 수 있습니다. 예제 애플리케이션에서는 세 가지, 즉 바인딩 Java 클래스를 생성할 XML Schema, 생성된 클래스들이 포함될 Java 패키지, 그리고 생성된 java 클래스를 저장할 디렉토리만을 알고 있으면 됩니다.

JAXB에서 생성된 모든 클래스들을 marx.jpa.persistence.jaxb라는 이름의 Java 패키지에 포함시키고, 이 클래스들을 C:\jpaJaxb\src 디렉토리에 저장한다고 가정하면, 다음과 같이 JAXB 바인딩 컴파일러 커맨드를 실행해야 합니다: xjc -p marx.jpa.persistence.jaxb -d C:\jpaJaxb\src orm_1_0.xsd JAXB RI의 바인딩 컴파일러는 "xjc" 실행파일을 통해 호출됩니다. 생성된 Java 클래스가 포함되는 패키지는 -p 옵션으로, 클래스를 포함하는 파일들을 저장하는 디렉토리는 -d 옵션으로 지정합니다. Java 클래스 생성의 기준이 되는 XML Schema는 xjc 바인딩 컴파일러의 마지막 매개변수로서 제공됩니다.

xjc 바인딩 컴파일러 커맨드가 명시된 스키마를 발견하지 못하는 경우 다음과 같은 에러가 발생합니다: "The system cannot find the file specified: unknown location." 또 XML Schema가 제공되지 않고 있다면 다음과 같은 에러를 확인하게 됩니다: "Grammar is not specified."

아래 표는 JAXB 1.0 RI, JAXB 2.0 RI 바인딩 컴파일러 커맨드가 -verbose 옵션 또는 -suppress 옵션을 설정하지 않은 상태에서 실행되는 경우의 출력 결과를 보여 주고 있습니다. 실행 결과를 통해 어떤 파일이 JAXB RI xjc 바인더 컴파일러에 의해 생성되었는지 확인할 수 있습니다. 표에서 JAXB 2.0 RI에 의해 생성되는 파일의 수보다 JAXB 1.0 RI에 의해 생성되는 파일의 수가 더 많음을 알 수 있습니다. 표의 하단에서는 생성된 파일들의 기본 크기를 보여 주고 있으며, 파일의 크기는 파일의 수보다 더 큰 차이를 보임을 확인할 수 있습니다.

  JAXB 1.0 RI (1.0.6) JAXB 2.0 RI (in JDK 1.6)

xjc Generated Files for orm_1_0.xsd file.

(all files created in sub-directory structure specified with -p option)

parsing a schema...

compiling a schema...

impl\AssociationOverrideImpl.java

impl\AttributeOverrideImpl.java

impl\AttributesImpl.java

impl\BasicImpl.java

impl\CascadeTypeImpl.java

impl\ColumnImpl.java

impl\ColumnResultImpl.java

impl\DiscriminatorColumnImpl.java

impl\EmbeddableAttributesImpl.java

impl\EmbeddableImpl.java

impl\EmbeddedIdImpl.java

impl\EmbeddedImpl.java

impl\EmptyTypeImpl.java

impl\EntityImpl.java

impl\EntityListenerImpl.java

impl\EntityListenersImpl.java

impl\EntityMappingsImpl.java

impl\EntityMappingsTypeImpl.java

impl\EntityResultImpl.java

impl\FieldResultImpl.java

impl\GeneratedValueImpl.java

impl\IdClassImpl.java

impl\IdImpl.java

impl\InheritanceImpl.java

impl\JAXBVersion.java

impl\JoinColumnImpl.java

impl\JoinTableImpl.java

impl\LobImpl.java

impl\ManyToManyImpl.java

impl\ManyToOneImpl.java

impl\MapKeyImpl.java

impl\MappedSuperclassImpl.java

impl\NamedNativeQueryImpl.java

impl\NamedQueryImpl.java

impl\OneToManyImpl.java

impl\OneToOneImpl.java

impl\PersistenceUnitDefaultsImpl.java

impl\PersistenceUnitMetadataImpl.java

impl\PostLoadImpl.java

impl\PostPersistImpl.java

impl\PostRemoveImpl.java

impl\PostUpdateImpl.java

impl\PrePersistImpl.java

impl\PreRemoveImpl.java

impl\PreUpdateImpl.java

impl\PrimaryKeyJoinColumnImpl.java

impl\QueryHintImpl.java

impl\SecondaryTableImpl.java

impl\SequenceGeneratorImpl.java

impl\SqlResultSetMappingImpl.java

impl\TableGeneratorImpl.java

impl\TableImpl.java

impl\TransientImpl.java

impl\UniqueConstraintImpl.java

impl\VersionImpl.java

impl\runtime\ValidatorImpl.java

impl\runtime\Util.java

impl\runtime\UnmarshallableObject.java

impl\runtime\ValidatingUnmarshaller.java

impl\runtime\XMLSerializable.java

impl\runtime\InterningUnmarshallerHandler.java

impl\runtime\SAXUnmarshallerHandler.java

impl\runtime\IdentityHashSet.java

impl\runtime\ContentHandlerAdaptor.java

impl\runtime\UnmarshallerImpl.java

impl\runtime\ValidatableObject.java

impl\runtime\GrammarInfo.java

impl\runtime\UnmarshallingEventHandlerAdaptor.java

impl\runtime\PrefixCallback.java

impl\runtime\UnmarshallingEventHandler.java

impl\runtime\SAXUnmarshallerHandlerImpl.java

impl\runtime\DefaultJAXBContextImpl.java

impl\runtime\Discarder.java

impl\runtime\ValidationContext.java

impl\runtime\GrammarInfoImpl.java

impl\runtime\NamespaceContext2.java

impl\runtime\AbstractUnmarshallingEventHandlerImpl.java

impl\runtime\XMLSerializer.java

impl\runtime\ErrorHandlerAdaptor.java

impl\runtime\GrammarInfoFacade.java

impl\runtime\MSVValidator.java

impl\runtime\MarshallerImpl.java

impl\runtime\UnmarshallingContext.java

impl\runtime\SAXMarshaller.java

impl\runtime\NamespaceContextImpl.java

AssociationOverride.java

AttributeOverride.java

Attributes.java

Basic.java

CascadeType.java

Column.java

ColumnResult.java

DiscriminatorColumn.java

Embeddable.java

EmbeddableAttributes.java

Embedded.java

EmbeddedId.java

EmptyType.java

Entity.java

EntityListener.java

EntityListeners.java

EntityMappings.java

EntityMappingsType.java

EntityResult.java

FieldResult.java

GeneratedValue.java

Id.java

IdClass.java

Inheritance.java

JoinColumn.java

JoinTable.java

Lob.java

ManyToMany.java

ManyToOne.java

MapKey.java

MappedSuperclass.java

NamedNativeQuery.java

NamedQuery.java

ObjectFactory.java

OneToMany.java

OneToOne.java

PersistenceUnitDefaults.java

PersistenceUnitMetadata.java

PostLoad.java

PostPersist.java

PostRemove.java

PostUpdate.java

PrePersist.java

PreRemove.java

PreUpdate.java

PrimaryKeyJoinColumn.java

QueryHint.java

SecondaryTable.java

SequenceGenerator.java

SqlResultSetMapping.java

Table.java

TableGenerator.java

Transient.java

UniqueConstraint.java

Version.java

bgm.ser

jaxb.properties

parsing a schema...

compiling a schema...

Embeddable.java

EmbeddableAttributes.java

Embedded.java

EmbeddedId.java

EmptyType.java

Entity.java

EntityListener.java

EntityListeners.java

EntityMappings.java

EntityResult.java

EnumType.java

FetchType.java

FieldResult.java

GeneratedValue.java

GenerationType.java

Id.java

IdClass.java

Inheritance.java

InheritanceType.java

JoinColumn.java

JoinTable.java

Lob.java

ManyToMany.java

ManyToOne.java

MapKey.java

MappedSuperclass.java

NamedNativeQuery.java

NamedQuery.java

ObjectFactory.java

OneToMany.java

OneToOne.java

PersistenceUnitDefaults.java

PersistenceUnitMetadata.java

PostLoad.java

PostPersist.java

PostRemove.java

PostUpdate.java

PrePersist.java

PreRemove.java

PreUpdate.java

PrimaryKeyJoinColumn.java

QueryHint.java

SecondaryTable.java

SequenceGenerator.java

SqlResultSetMapping.java

Table.java

TableGenerator.java

TemporalType.java

Transient.java

UniqueConstraint.java

Version.java

package-info.java

Total Generated Files

142

(57 at root level of package,

55 in "impl" subdirectory,

30 in impl/runtime subdirectory)

62

Total Size (All Files)

2.22 MB

279 KB

JAXB 2.0 RI에 의해 생성된 클래스는 그 숫자와 크기 면에서 JAXB 1.0에 의해 생성된 클래스보다 훨씬 적습니다. 생성된 모든 클래스가 -p 옵션을 통해 제공되는 패키지 구조와 매치되는 서브디렉토리 구조(marx\jpa\persistence\jaxb\)에 저장됨을 참고하시기 바랍니다. (패키지와 매치되는 서브디렉토리 구조에 대한 출력 부분은 화면 제약 상 생략하였습니다.)

Java 클래스들은 (-p 옵션, 또는 -p가 설정되지 않은 경우 XSD 디폴트 설정에 의해) -d 옵션으로 정의된 디렉토리 하부의 패키지 이름과 매치되도록 적절한 서브디렉토리 구조에 생성됩니다. 이 시점에서 IDE, 텍스트 편집기로 Java 클래스를 열어 JAXB RI xjc 컴파일러가 어떤 내용을 생성했는지 확인할 수 있습니다.

JAXB Runtime

지금까지 설명한 것처럼, xjc 바인딩 컴파일러를 이용하면 XML에 대해 직접 작업을 수행하지 않고도 orm_1_0.xsd XML Schema 파일과 호환하는 XML 읽기/쓰기 작업을 지원하는 Java 클래스를 생성할 수 있습니다. 본 문서의 예제 애플리케이션에서는 XML의 쓰기 작업만을 수행하므로 JAXB 기반 오브젝트를 XML로 마샬링(marshaling)하는 작업만을 수행하면 됩니다. XML의 읽기 작업이 필요한 경우라면, JAXB에 의해 생성된 클래스로부터 인스턴스화(instantiate)된 오브젝트로 XML을 언마샬링(unmarshaling)하는 작업이 필요했을 것입니다.

JAXB RI는 JAXB 오브젝트 데이터를 XML로 마샬링 하거나 XML 데이터를 JAXB 오브젝트로 언마샬링하는데 사용할 수 있는 런타임 라이브러리들을 지원합니다. 이 라이브러리들은 Java SE 6에 기본 포함되어 있으며, 따라서 JAXB의 클래스 생성을 위해 RI를 사용할 때 별도의 JAR 파일을 포함시킬 필요가 없습니다.

JAXB Java 오브젝트를 XML로 마샬링

Listing 4는 JAXB Java 오브젝트의 데이터를 XML로 마샬링하는 방법을 예시하고 있습니다. 아래의 코드는 주석 처리기에서 확장한 AbstractProcessor에 의해 제공되는 Filer를 사용하고 있습니다. Filer는 마샬링된 JAXB 데이터를 orm.xml이라는 이름의 XML 파일에 기록하는데 사용됩니다.

Listing 4

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.PropertyException;

   /**
    * Write out XML file that is consistent with JPA in-source annotations found
    * in the JPA-based code.
    * 
    * This method only has a few lines of code, but they are all significant.
    * The example here demonstrates writing of an XML file using JAXB
    * marshalling.  It also demonstrates use of the parent class
    * (AbstractProcessor class) and access to that parent class' handle to a
    * ProcessingEnvironment (processingEnv).  Through access to this
    * ProcessingEnvironment, the code gains access to a Filer that it uses
    * to write out the JAXB-generated XML file to the file system.
    */
   private void writeMappingXmlFile()
   {
      processingEnv.getMessager().printMessage(
         Diagnostic.Kind.NOTE,
         "Entered writeMappingXmlFile() method..." );

      try
      {
         JavaFileManager.Location location = StandardLocation.CLASS_OUTPUT;

         /*
            A few interesting observations for Filer.createResource()
            1) Files created with this method are not available for further
               annotation processing.  This is okay in this context because
               we are writing out an XML file and would have no reason to
               attempt to process that XML for Java annotations.
            2) Limitation of using Filer here is that the supported standard
               locations may not really be where we wish to write this XML
               file.  If so, using a different mechanism for writing a file
               might be preferred because the main benefit of Filer is using it
               in conjunction with the annotation processor and we don't need
               that benefit in this case.
            3) The second argument to createResource() is for package name.
               This XML file we are writing is really not a source or class
               file and packaging concepts do not apply.  Therefore, as
               recommended in the Javadoc for this method, we are passing the
               empty string to that argument.
            4) The final argument is the name of the actual file to which the
               XML will be written.  We only need one file to be specified here.
            5) Filer.createResource uses "new" Java support for "..." (ellipses
               and variable arguments - Java 5)
         */
         FileObject fo =
            processingEnv.getFiler().createResource(location, "", "orm.xml");
         OutputStream os = fo.openOutputStream();
      
         this.marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, 
                                     Boolean.TRUE);
         this.marshaller.marshal(this.em, os /* System.err */);
         
         os.close();
      }
      catch (PropertyException propEx)
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR,
            "PropertyException trying to write XML mapping file:"
               + propEx.getMessage() );
      }
      catch (JAXBException jaxbEx)
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR,
            "JAXBException trying to write XML mapping file:"
               + jaxbEx.getMessage() );
      }
      catch (FilerException filerEx)  // must appear before parent IOException
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR,
            "Problem with Processing Environment Filer: "
               + filerEx.getMessage() );
      }
      catch (IOException ioEx)
      {
         processingEnv.getMessager().printMessage(
            Diagnostic.Kind.ERROR,
            "Problem opening file to write ORM XML."
               + ioEx.getMessage() );
      }
   }

위 코드의 코멘트들을 통해 여러 가지 흥미로운 사실들을 확인할 수 있습니다. JAXB 마샬링에 관련하여 가장 눈 여겨 보아야 할 부분이 아래와 같습니다:

         this.marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, 
                                     Boolean.TRUE);
         this.marshaller.marshal(this.em, os /* System.err */);

JAXB_FORMATTED_OUTPUT 설정은 JAXB 런타임 마샬러(runtime marshaller)가 "올바른" 포맷 및 들여쓰기를 사용하여 XML을 기록하도록 요구하고 있습니다. 두 번째 라인은 마샬링된 XML을 Filer로부터 가져온 출력 스트림에 기록합니다. 커멘트 처리된 라인에서 확인할 수 있듯, XML을 System.err(또는 System.out)로 출력하거나 파일로 리다이렉트할 수도 있습니다.

Listing 5는 Listing 4에서 사용된 마샬러의 설정을 보여 주고 있습니다.

Listing 5

         ClassLoader cl = Class.forName("marx.jpa.persistence.jaxb.ObjectFactory").getClassLoader();
         jc = JAXBContext.newInstance("marx.jpa.persistence.jaxb", cl);
         this.marshaller = jc.createMarshaller();
         this.of = new ObjectFactory();
         this.em = of.createEntityMappings();

Listing 5의 코드는 JAXBContext의 새로운 인스턴스를 가져오고, 다시 이 인스턴스를 사용해서 Listing 4에서 사용된 Marshaller를 가져옵니다.

Listing 5는 또 ObjectFactory의 createEntityMappings() 메소드를 호출하여 orm.xml 파일의 루트 엘리먼트를 생성하는 방법을 보여 주고 있습니다. ObjectFactory는 XML 구조에 직접 매핑되는 Java 클래스와 함께 xjc 바인딩 컴파일러에 의해 생성됩니다. Listing 5에서 볼 수 있듯, ObjectFactory는 마샬링되었을 때 작성되는 XML로 매핑되는 오브젝트로의 액세스를 위해 사용됩니다.

JAXB에 관련한 추가 정보

"The Java EE 5 Tutorial"은 JAXB 2.0에 대한 훌륭한 개요 정보를 제공하고 있습니다. 이 튜토리얼은 Java SE(Standard Edition)가 아닌 Java EE(Enterprise Edition)에 초점을 맞추고 있지만, JAXB에 관련된 대부분의 개념과 원칙은 EE, SE에 동일하게 적용됩니다. "Unofficial JAXB Guide" 역시 JAXB의 활용에 관한 유용한 팁을 제공합니다.

JAP 주석 처리기의 실행

Java SE 6의 주석 처리 기능은 javac 커맨드에 포함되어 있으므로, 예제에서 새로 생성한 주석 처리기(annotation processor)를 매우 간단한 방법으로 실행할 수 있습니다. 본 문서의 앞 부분에서는 주석 처리를 위해 javac이 지원하는 옵션을 설명한 바 있습니다.

JPA 주석을 포함하는 Java 엔티티 클래스에 대해 예제의 주석 처리기를 실행하는 방법이 아래와 같습니다:

javac -processor marx.apt.jpa.AnnotationEntityProcessor
   -proc:only
   -processorpath C:\otnJava6\classes;C:\TopLink_11.1.1.0_070502_preview\lib\java\api\persistence.jar
   -cp C:\TopLink_11.1.1.0_070502_preview\lib\java\api\persistence.jar
   -AxmlOverrideAnnotations=true
   -AuseUpperCaseColumnNames=true
   -Xlint:unchecked
   *.java

각 옵션에 대한 가독성을 높이기 위해, 각각의 javac 매개변수를 서로 다른 라인에 배치했습니다. -process 옵션은 주석 처리 클래스의 "fully scoped name(marx.apt.jpa.AnnotationEntityProcessor)"을 설정하기 위해 사용합니다. -proc:only 옵션은 주석 처리만을 실행하고 Java 클래스의 컴파일 작업은 수행하지 않음을 의미합니다. 위의 예에서, javac 커맨드는 Java JPA 클래스가 위치한 디렉토리에서 실행되고 있습니다. 따라서 "*.java"만으로도 어떤 클래스를 처리해야 하는지 명시할 수 있습니다.

필요한 경우 위의 실행 코드를 변경하는 것도 가능합니다. 예를 들어 -J"-verbose:class" 옵션은 새로 실행된 Java 인터프리터가 주석 처리 실행 과정에서 로드된 각 Java 클래스에 대한 정보를 출력하도록 하는데 사용됩니다. 마찬가지로, java(애플리케이션 런처) 커맨드와 관련된 다른 많은 옵션들을 javac(Java 컴파일러) 커맨드에서 -J 옵션과 함께 사용할 수 있습니다.

문서의 앞부분에서 설명한 것처럼 -XprintProcessorInfo 또는 -XprintRounds 옵션을 주석 처리 기능과 함께 javac에 전달하여 자세한 처리 정보를 확인할 수 있습니다. javac 또는 주석 처리기의 "verbose" 모드는 새로운 주석 처리기의 개발, 디버깅 과정에서 유용하게 활용됩니다.

마지막으로, -A 옵션은 javac 컴파일러 커맨드를 통해 주석 처리기에 관련된 옵션을 전달할 때 사용됩니다. 예를 들어, 예제 JPA 주석 처리기는 사용자가 소스 코드 상의 주석을 완전히 무시하고 "-AxmlOverrideAnnotations=true"를 javac 라인에 추가함으로써 외부 XML 매핑만을 사용하도록 설정하는 기능을 지원합니다. 이 옵션은 주석 처리기 제공자의 특화된 기능에 의해 사용될 가능성이 더 높지만, 예제의 processor 클래스에서 매개변수를 전달하기 위한 편리한 방법으로 활용이 가능합니다.

Java SE 6의 와일드카드 주석을 사용하면 동일한 디렉토리의 다수 JAR 파일에 대한 경로 설정을 한 번에 완료할 수 있습니다. 위에서 사용된 classpath에서는 각각의 JAR 파일이 서로 다른 위치에 존재하므로 개별적으로 호출하는 것이 더 간단합니다. 하지만 동일한 디렉토리에 많은 JAR 파일이 위치하는 경우라면 JAR 와일드카드를 이용해서 많은 시간을 절약할 수 있을 것입니다.

결과

JPA 클래스에 주석 처리기를 실행하면, 기존 Java 소스 코드와 동일한 메타데이터를 포함하는 orm.xml 파일이 생성됩니다. 간단한 JPA 기반 엔티티 클래스에 대해 주석 처리기를 실행하여 얻어진 orm.xml 파일의 예가 아래와 같습니다.

Listing 6

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" version="1.0">
    <persistence-unit-metadata>
        <xml-mapping-metadata-complete/>
    </persistence-unit-metadata>
    <named-native-query name="findAllMovies">
        <query>SELECT * FROM MOVIE</query>
    </named-native-query>
    <named-native-query name="oracle-getDbDate">
        <query>SELECT sysdate FROM dual</query>
    </named-native-query>
    <entity name="Director" class="marx.Director">
        <discriminator-value>DIRECTOR</discriminator-value>
    </entity>
    <entity name="Genre" class="marx.Genre">
        <attributes>
            <id name="label">
                <column name="LABEL"/>
            </id>
            <many-to-many target-entity="marx.Movie"
                          name="movies" mapped-by="genres"/>
        </attributes>
    </entity>
    <entity name="Movie" class="marx.Movie">
        <attributes>
            <id name="id">
                <column name="ID"/>
            </id>
            <many-to-many target-entity="marx.Genre" name="genres">
                <join-table name="genre_movie">
                    <join-column referenced-column-name="id" name="movie_id"/>
                    <inverse-join-column referenced-column-name="label"
                                         name="genre_label"/>
                </join-table>
            </many-to-many>
            <transient name="somethingNotPersisted"/>
        </attributes>
    </entity>
    <entity name="Person" class="marx.Person">
        <inheritance strategy="SINGLE_TABLE"/>
        <discriminator-column name="ROLE" length="31"
                              discriminator-type="STRING"/>
        <sequence-generator sequence-name="PERSON_SEQ"
                            name="PERSON_SEQ"
                            initial-value="1"
                            allocation-size="1"/>
        <attributes>
            <id name="id">
                <column name="ID"/>
                <generated-value strategy="SEQUENCE" generator="PERSON_SEQ"/>
            </id>
            <basic name="gender">
                <enumerated>STRING</enumerated>
            </basic>
        </attributes>
    </entity>
    <entity name="Rating" class="marx.Rating">
        <attributes>
            <id name="label">
                <column name="LABEL"/>
            </id>
        </attributes>
    </entity>
    <entity name="Studio" class="marx.Studio">
        <attributes>
            <id name="id">
                <column name="ID"/>
            </id>
        </attributes>
    </entity>
</entity-mappings>

위의 XML 파일에서 몇 가지 흥미로운 사실을 발견할 수 있습니다. 빈 태그 는 모든 JPA 관련 소스 내 주석을 무시하고 이 XML 파일의 메타데이터를 사용하도록 정의하고 있습니다. 이 태그는 주석 처리기를 실행하면서 "-AxmlOverrideAnnotations=true" 플래그를 설정한 경우 생성됩니다.

또 위의 코드는 주석 처리 기능을 이용하여 소스 내의 주석으로부터 orm.xml 파일(또는 다른 매핑 파일을) 생성하는 또 다른 방법을 예시하고 있습니다. 위에서 주석 처리기는 모든 <named-native-query..> 엔트리를 루트 <entity-mappings..> 엘리먼트에 직접적으로 포함된 서브엘리먼트로서 기록하고 있습니다. JPA O-R 매핑의 XML Schema는 이와 같은 레벨, 또는 특정 엔트리에 대한 주석의 기록을 지원합니다. 하지만, 네임드 네이티브 쿼리(named native query)들이 개별 유닛이 아닌 퍼시스턴스 유닛을 그 범위(scope)로 하고 있으므로, XML 파일에 쿼리들을 위치시키는 것이 더 바람직할 것입니다. Java 코드가 네임드 네이티브 쿼리를 개별 Java 엔티티 클래스에 기록하고 있는 반면, 예제 주석 처리기는 (좀 더 합리적인 방법으로) 이 쿼리들을 생성된 XML 파일로 이동시키고 있습니다.

XML 포맷의 JPA 애플리케이션 메타데이터는 소스 내에 처리된 주석과 비교했을 때 여러 가지 이점을 제공합니다. 애플리케이션의 적용(deploy) 담당자가 개발(develop) 담당자와 구분되어 있는 경우, 적용 담당자는 Java 코드를 변경하지 않고 외부 XML 파일만을 변경하면 되므로 편리할 것입니다. 개발자는 여전히 개발 과정에서 소스 내의 주석을 활용할 수 있으며, 적용 담당자는 이 주석을 XML로 변환하고 XML 파일만을 관리하면 됩니다. 또 JPA 베스트 프랙티스의 적용이 보다 용이하다는 장점이 있습니다. 예를 들어, ORM 파일을 이용하여 네이티브 네임드 쿼리를 보다 적절한 위치로 가져가는 것이 가능합니다.

주석 처리와 관련된 이슈 및 개선 사항

본 문서의 주석 처리기(annotation processor)는 Java SE 6의 주석 처리 방법과 JAXB의 단순화된 활용 방법을 예시하는 것을 목적으로 구현되었습니다. 또 소스 내의 Java 주석을 외부 XML 설정 파일로 변환하는 개념에 대해 설명하는 것이 본 문서의 두 번째 목적이라 할 수 있습니다.

본 문서에서 소개된 코드는 여러 가지 한계를 안고 있습니다. 먼저, JPA에 관련된 모든 주석이 처리되지 않고 있습니다. 두 번째로 특정 주석에 관련하여 모든 가능한 값, 속성이 처리되지 않고 있습니다. 하지만 앞에서 설명된 것과 동일한 방법을 사용하여 주석 및 주석 처리 기능을 쉽게 추가할 수 있을 것입니다.

본 문서에서 설명된 주석 처리기는 O-R 매핑과 관련된 JPA 주석(특히 외부 설정된 orm.xml 파일)만을 다루고 있습니다. JPA 외부 XML 설정의 또 다른 예로 (Java SE JPA 기반 애플리케이션에서 요구되는) persistence.xml 파일을 생각해 볼 수 있을 것입니다. 본 문서에서 제안, 구현된 주석 처리기는 persistence.xml 파일을 지원하지 않지만, Sahoo의 블로그에서 J2SE5의 apt 툴과 JAXB 2.0을 이용한 persistence.xml 파일 생성 방법을 확인할 수 있습니다. (Sahoo의 블로그를 참고하십시오.) Java SE 6에서도 apt는 여전히 지원되므로, 이 블로그에서 예시되는 주석 처리기도 Java SE 6 환경에서 여전히 사용 가능합니다.

결론

Java SE 6는 JAXB 개발 및 주석 처리와 관련하여 Java 개발자의 편의성을 크게 개선하고 있습니다. Java 6의 JAXB 지원 기능 및 정교한 주석 처리 기능을 활용하여 소스 내의 JPA 주석을 외부 XML 설정 파일로 쉽게 변환할 수 있습니다. 지금까지, 본 문서를 통해 JAXB의 가용성 및 주석 처리에 관련한 개선 사항을 이용하여 소스 코드 주석 또는 외부 XML 설정 파일을 기반으로 JPA 기반 애플리케이션을 적용하는 방법에 대해 설명하였습니다.


Dustin Marx는 Raytheon Company의 수석 소프트웨어 엔지니어 겸 아키텍트입니다.
E-mail this page
Printer View Printer View