저자 Rahul Biswas
서브클래스가 수퍼클래스로부터 상태와 동작을 물려받을 수 있는 능력을 지칭하는 "상속(inheritance)"은 이전의 EJB(Enterprise JavaBeans) 기술에서는 지원이 되지 않았다. 또한 EJB 2.1 퍼시스턴스 모델(persistence model)에서는 엔티티가 다른 엔티티로부터 파생될 수 없었다. 하지만, EJB 3.0 스펙(JSR 220)의 한 부분을 차지하게 된 새로운 JPA(Java Persistence API)로 인해 상황이 바뀌고 있다. 즉, JPA는 상속을 지원함으로써 더 많은 모듈러(modular), 코히시브(cohesive), 확장(extensible) 코드를 작성할 수 있게 해줄 뿐 아니라 오브젝트가 다양한 형태를 취할 수 있는 능력인 다형성(polymorphism)을 활용할 수 있게 해준다.
본 테크 팁에서는 JPA에서 지원되는 몇 가지 상속 기능을 소개한다. 팁에는 예제 패키지가 첨부되어 있으며, 이 패키지는 팁에서 논의되는 몇 가지 기능을 예시한다. 팁의 보기들은 예제(패키지에 포함)를 위한 소스 코드에서 발췌한 것으로, 예제는 GlassFish라고 불리는 Java EE 5의 오픈 소스 레퍼런스 구현을 사용한다. GlassFish는 GlassFish 커뮤니티 다운로드 페이지에서 다운로드할 수 있다.
상속의 간단한 예
가령 여러분이 올해에는 조직의 효율성을 강화하기로 결정했다고 하자. 특히, 재무 회계 분야를 더욱 체계화하기로 하였다. 또, 여러분이 당좌(checking), 예금(savings), 신용카드(brokerage), 위탁(brokerage), 신용거래(margin) 등과 관련한 계정을 관리하고 있다고 가정해보자. 이 모두가 계정의 일종이므로 POJO(Plain Old Java Object)인 Account라는 이름의 클래스를 기초로 하여 이들을 모델링할 수 있다. 다음은 Account class의 코드 단편이다.

추상 엔티티로부터 상속하기
Account는 오브젝트로 직접 인스턴스화할 수 없는 추상 클래스라는 점에 유의할 것. 그럼에도 불구하고 JPA는 Account의 엔티티화를 허용하며, 따라서 Java Persistence Query Language로 작성된 다형성 쿼리(polymorphic queries)에서도 Account를 사용할 수 있다는 점이 특히 중요하다. 이 쿼리들을 통할 경우, 오직 계정 번호만을 이용하여 Account 서브클래스의 인스턴스를 로드할 수 있다. 또한, Account에는 보다 특정한 엔티티(아래에서 곧 다룰 예정임)에 의해 상속될 수 있는 여러 속성이 존재하므로 Account를 엔티티화할 경우 매핑 속성을 상속할 수 있게 된다. Account를 엔티티화하려면 먼저 클래스에 @Entity 주석을 표시하고, 클래스와 해당 서브클래스에 대한 매핑 전략을 확인하려면 클래스에 @Inheritance 주석을 표시한다. 다음은 @Entity와 @Inheritance 주석이 첨부된 Account class 단편이다.

아울러, acctNum 필드를 Account 엔티티에서 기본키(primary key)로 표시하는 @Id 주석에도 유의할 필요가 있다.
@Inheritance 주석 내의 전략 요소 값 SINGLE_TABLE은 기본 클래스 Account와 모든 해당 서브클래스가 단일 테이블로 매핑된다는 것을 의미한다.
Account를 엔티티화하지 않고도 그로부터 매핑 속성을 상속할 수 있는 방법이 있다. 바로 Account에 @MappedSuperclass 주석을 표시하는 것이다. 그러나 이 방법을 이용할 경우 Java Persistence Query Language 쿼리에서 Account를 사용할 수 없게 될 뿐 아니라, 사용자가 선택하는 상속 전략에 따라 기본 클래스 속성들이 보다 특정한 클래스를 위해 정의된 별도의 테이블에 매핑되는 결과를 초래할 수도 있다.
추상 비(非) 엔티티(매핑된 수퍼클래스)로부터 상속하기
당좌 계정과 예금 계정은 모두 은행 계정의 일종이므로, 이 두 종류의 계정에 대해 또 다른 추상 클래스 BankAccount를 정의할 수 있다. BankAccount는 인스턴스화될 필요가 없는 관계로 추상 클래스라 할 수 있으며(당좌 및 예금 계정을 모델링하는 서브클래스들은 인스턴스화되어야 함), 또한 Account class로부터 동작과 더불어 매핑 속성을 상속한다.

BankAccount에는 @Entity 주석이 없으며, 이는 추상 비(非) 엔티티 클래스로 정의된다. 이 때, 보다 특정한 클래스가 파생되기 때문에 BankAccount를 매핑된 수퍼클래스로 주석 표시할 수 있다.

아마도 여러분은 Java Persistence Query Language 쿼리에서 BankAccount를 사용할 수 없다면 왜 이런 방식을 이용해야 하는지 궁금해 할 지도 모른다. 실제로 BankAccount는 엔티티가 아니기 때문에 데이터베이스 테이블에 매핑되지 않는다. 어쨌든 본 예에서는 이런 기능들이 필요하지는 않다. 매핑된 수퍼클래스 방식은 BankAccount로부터 동작과 매핑 속성을 모두 상속할 수 있게 해준다. 한편 다형성 쿼리에서 BankAccount와 은행명을 사용할 필요가 있는 경우에는 클래스를 추상 엔티티로 정의해야 한다.
실질적인 은행 계정 엔티티로는 SavingsAccount와 CheckingAccount 두 종류가 있다. 다음은 이 클래스의 단편들이다.

bankName 속성은 InheritanceType.SINGLE_TABLE의 경우 ACCOUNT.BANKNAME으로, 그리고 InheritanceType.JOINED의 경우 SavingsAccount.BANKNAME으로 매핑된다.
마찬가지로, CheckingAccount에서는 bankName이 InheritanceType.SINGLE_TABLE의 경우 ACCOUNT.BANKNAME으로, 그리고 InheritanceType.JOINED의 경우에는 CheckingAccount.BANKNAME으로 매핑된다.
다형성 쿼리
JPA의 경우 다형성 쿼리를 지원하기 때문에 계정 번호(acctNum)를 이용하여 Account 엔티티의 실제 인스턴스를 찾을 수 있으며, 사용자는 엔티티 관리자의 'find by primary key' 메소드를 이용하거나 독자적으로 Java Persistence Query Language 쿼리를 작성할 수 있다. 다음은 쿼리 실행 문장의 예이다.
Account = em.find(Account.class, acctNum);
이 문장에서 em은 기본 키로 엔티티를 찾는데 사용되는 EntityManager 인스턴스인데, EntityManager는 퍼시스턴스 컨텍스트 내에서 엔티티의 수명을 관리하는 동시에 쿼리가 엔티티 상에서 실행될 수 있도록 하는 JPA 클래스이다.
쿼리는 계정 번호가 포함된 Account class와 acctNum을 이용하여 특정 계정 오브젝트 인스턴스를 찾는다. acctNum의 값이 예금 또는 당좌 계정의 번호와 일치할 경우 퍼시스턴스 프로바이더(persistence provider)에 의해 예금 또는 당좌 계정이 로드되는데, 이는 쿼리에서 Account class(SavingsAccount 또는 CheckingAccount 클래스가 아니라)를 전달하는 경우에도 마찬가지로 적용된다. 이것은 다형성의 한 가지 실례로서, Account가 쿼리될 경우에는 쿼리 기준을 충족하는 모든 서브클래스가 리턴된다. 또한 Account가 엔티티가 아니라 매핑된 수퍼클래스인 경우에는 위에 표시된 것처럼 쿼리를 실행할 수 없게 된다.
상속된 클래스에서 매핑 오버라이드하기
상속된 클래스에서, 매핑된 수퍼클래스에 지정한 매핑을 오버라이드할 수 있다. 실례로, 아래와 같이 CheckingAccount 클래스에서 매핑 속성 bankName을 오버라이드할 수 있다.

오버라이드 후에 bankName은 ACCOUNT.BANKNAME 대신 ACCOUNT.BANK_NAME에 매핑된다.
상속 전략
Java Persistence API 는 서브클래스가 데이터베이스 테이블에 매핑되는 방식을 지시하는 세 가지의 상속 전략을 허용하는데, single table per class hierarchy, joined subclass, 그리고 single table per class가 바로 그것이다.
Single Table Per Class Hierarchy 전략
전략 값 "SINGLE_TABLE"에 의해 지정되는 이 전략은 클래스와 모든 서브클래스가 동일한 테이블에 매핑된다는 것을 의미한다. 기억하겠지만, 이는 앞서 ACCOUNT 클래스에서 살펴보았던 전략이다. single table per class hierarchy에서 인스턴스는 판별자(discriminator) 칼럼 내의 판별자 값에 의해 구분된다.
판별자 칼럼의 기본값 이름은 DTYPE이며, 칼럼의 기본값 유형은 STRING이다. 사용자가 판별자 값을 지정하지 않으면 공급자가 생성한 값이 기본값으로 지정된다. 판별자 칼럼 유형 STRING에 대한 기본값 판별자 이름으로 엔티티 이름이 사용되는데, 예를 들어 사용자가 Account에 대한 모든 기본값을 수락할 경우 판별자 칼럼 값은 "ACCOUNT"가 된다. 하지만 사용자는 @DiscriminatorColumn과 @DiscriminatorValue 주석을 이용하여 판별자 칼럼과 값에 대한 기본값을 오버라이드할 수 있다. 예:

이처럼 지정을 마치면 판별자 칼럼은 "DCOL"이라는 이름을 가지고, 당좌 계정을 나타내는 레코드들은 "Checking_Account"의 DCOL 칼럼 값을 가지게 된다.
다음은 ACCOUNT와 그 서브클래스를 위한 스펙을 토대로 생성되는 테이블의 칼럼 이름과 그 유형이다.

테이블에는 전체 클래스 계층의 모든 속성이 포함된다는 점에 유의할 것. 따라서 CheckingAccount 유형의 계정에 대해서는 CheckingAccount, BankAccount, Account 등의 인스턴스를 나타내는 레코드가 하나뿐이며, 이 레코드는 "Checking_Account"의 DTYPE 값을 가지게 된다(이는 @DiscriminatorValue 주석에 지정된 내용이다). 사용자의 퍼시스턴스 프로바이더가 메타 정보를 토대로 데이터베이스 테이블을 생성할 수 있는 경우에는 데이터베이스 스키마에 관해 전혀 신경을 쓸 필요가 없다.
Joined Subclass 전략
전략 값 "JOINED"에 의해 지정되는 joined subclass 전략에서 기본 클래스의 속성들은 하나의 테이블에 저장되며 서브클래스의 속성들은 별도의 테이블들에 저장된다. 따라서 ACCOUNT를 위한 코드에서 상속 전략을 "JOINED"로 바꾸면,

데이터베이스 테이블은 다음과 같이 된다.

Account의 서브클래스에서 기본 키가 지정되지 않았더라도 서브클래스를 나타내는 각각의 테이블은 기본 키를 가지게 되며, 기본 키는 Account entity -- acctNum에 대해 정의된 기본 키와 동일한 이름과 유형을 가진다. 서브클래스와 기본 클래스는 서브클래스 엔티티를 로드하기 위한 쿼리가 실행될 때 기본 키 상에서 결합된다. 단, joined subclass 전략에서 서브클래스를 나타내는 각 테이블이 어떻게 기본 클래스에 포함되지 않은 속성만을 가지게 되는지 유의할 것.
Single Table Per Class
single table per class에서 각 클래스는 별도의 테이블에 매핑되는데, 이 버전의 JPA 스펙의 경우 퍼시스턴스 프로바이더가 이 전략을 지원할 필요가 없다.
조인 칼럼 오버라이드하기
joined subclass 전략에서는 기본 클래스와 서브클래스 간의 결합이 기본 키에서 이루어진다는 것을 알 수 있다. 서브클래스의 기본 키는 기본 클래스와 동일한 유형과 이름을 가지며, 서브클래스에서 기본 키 조인 칼럼을 지정하고 기본값을 오버라이드하는 것이 가능하다. 신용카드 계정 엔티티에서 관련 예를 찾아 볼 수 있다.

퍼시스턴스 프로바이더가 CreditCardAccount 인스턴스 로드를 위해 CreditCardAccount와 Account entities를 나타내는 테이블 간의 결합할 진행시킬 경우, 이제 결합은 CreditCardAccount의 cca_id(기본 키 칼럼)와 Account 칼럼의 acctNum 칼럼 사이에서 이루어지게 된다. 기본값 기본 키 조인 칼럼을 오버라이드하지 않을 경우, 결합은 엔티티를 나타내는 테이블의 acctNum 칼럼에서 이루어지게 된다.
상속 전략 오버라이드하기
엔티티가 자체 서브클래스에 대해 다양한 상속 전략을 지정하는 것이 가능한데, 관련 예를 BrokerageAccount 엔티티에서 찾아볼 수 있다.

Account entity에 의해 SINGLE_TABLE per class hierarchy 전략이 지정되는 관계로, Account, CheckingAccount, SavingsAccount, CreditCardAccount, BrokerageAccount 등이 동일한 데이터베이스 테이블에 저장된다. 단, BrokerageAccount에서 상속 전략이 "JOINED"로 변경되었기 때문에 MarginAccount 엔티티의 maxLoanAllowed 속성은 별도의 테이블에 저장된다. 퍼시스턴스 프로바이더가 굳이 이 특성을 지원할 필요가 없지만 Java API 스펙은 이를 허용하고 있다. 따라서 벤더들 간에 사용자 코드의 이식성을 유지하도록 하려면 이 특성의 사용을 피하는 것이 최선이다.
MarginAccount는 또 다른 실제 엔티티, BrokerageAccount에서 파생된다는 점에 유의할 것.
다형성 결합(Polymorphic Association)
JPA는 다형성 결합을 허용하며, 모든 계정은 소유자를 가지고 일반적으로 계정 소유자는 복수의 계정을 가진다. 또한 이 계정들이 동일한 종류일 필요는 없으며, 사용자는 AccountOwner의 멤버 변수가 각 계정과 같은 유형을 나타내기를 원치 않는다(그런 경우에는 확장성 또는 효율성이 저하됨). 다행히도 사용자는 Account 엔티티의 컬렉션을 이용하여 (POJO의 경우와 마찬가지로) AccountOwner 엔티티가 가지는 다양한 유형의 계정을 모투 표현할 수 있다. 사용자는 Account 오브젝트의 컬렉션을 가질 수는 있지만 Account를 직접 인스턴스화할 수는 없다는 점에 유의할 것. 또한 계정 소유자는 다양한 종류의 계정으로 구성된 컬렉션을 가질 수 있다. 다형성 결합(polymorphic associations)을 가질 수 있다는 말은 Account의 컬렉션 내에 보다 분명한 계정 클래스 인스턴스를 가질 수 있다는 것을 의미한다.

매핑된 수퍼클래스는 퍼시스턴트 관계의 타깃이 될 수 없으므로, Account를 매핑된 수퍼클래스로 만들었다면 AccountOwner와 Account 간에 1 대 다(多)의 관계는 형성될 수 없다.
비(非) 엔티티 클래스로부터 상속하기
통상적으로 사용자는 비 엔티티의 동작을 상속하기 위해 그로부터 엔티티를 상속하게 되며, 비 엔티티로부터 상속하는 속성은 존속(persisted)되지 않는다. 예를 들어 BaseAccount 클래스는 원금액과 이자율이 주어졌을 때 잔액 계산을 윈한 기본 기능을 제공하는데, 사용자는 이 동작을 상속하기 위해 BaseAccount로부터 Account를 확장한다.

요약
새로운 JPA는 상속, 다형성 쿼리, 다형성 결합을 통해 더 많은 모듈러(modular), 코히시브(cohesive), 확장(extensible) 코드를 작성할 수 있게 해준다. 이는 처음부터 애플리케이션 데이터 모델을 생성하는 개발자와 표준화된 퍼시스턴스 API로 이동하기 위해 현재의 퍼시스턴스 구현을 마이그레이트하고자 하는 개발자 모두에게 유용하다.
예제 코드 실행하기
본 팁에 첨부된 예제 코드를 실행하려면 다음 단계를 수행한다
- GlassFish를 아직 구하지 못했다면 GlassFish 커뮤니티 다운로드 페이지에서 다운로드하여 설치한다.
- 아래의 환경 변수를 설정한다:
GLASSFISH_HOME: GlassFish의 설치 장소를 표시해야 한다.
ANT_HOME. ant의 설치 장소를 표시해야 한다. ant는 다운로드한 GlassFish 번들에 함께 포함되어 있다. (Windows에서는 lib\ant 서브디렉토리에 위치함.)
JAVA_HOME. 사용자 시스템에서의 JDK 5.0 위치를 표시해야 한다.
PATH환경 변수에$JAVA_HOME/bin,$ANT_HOME/bin,$GLASSFISH_HOME/bin을 추가한다.
- 예제 패키지를 다운로드하여 압축을 푼다. 이 때, 새로 압축이 풀린 디렉토리는
<sample_install_dir>/ttjun2006inheritance로 표시되어야 하는데, 여기서<sample_install_dir>은 예제 패키지가 설치된 디렉토리이다.ttjun2006inheritance아래의inherit디렉토리에는 예제를 위한 소스 파일과 기타 지원 파일이 포함되어 있다.
inherit디렉토리로 이동하여build.properties파일을 적절히 편집한다. 예를 들어 admin 호스트가 원격인 경우에는 admin.host의 값을 기본값(localhost)에서 해당 원격 호스트로 변경하면 된다. 아울러,javaee.server.passwordfile의 위치가 올바른지 확인한다.
- GlassFish를 시작한다.
$GLASSFISH_HOME/bin/asadmin start-domain domain1 - 데이터베이스 서버를 시작하고, 상속 디렉토리에서 아래 명령어를 입력한다.
ant dbsetup다음과 유사한 출력이 표시되어야 한다.

- 예제 애플리케이션을 구축하고 배포한다. 상속 디렉토리에서 아래 명령어를 입력한다.
다음과 유사한 출력이 표시되어야 한다.ant all
- 애플리케이션을 시작하고, 브라우저를 http://<host>:8080/sample/index.html로 연다. <host>에는 각자의 호스트 이름을 입력한다(가령, localhost). 애플리케이션의 홈페이지가 표시되어야 한다.

신규 계정을 생성하고 계정 검색을 시도해본다. 애플리케이션의 기능들은 JPA에서 상속을 지원하는 경우에 한하여 사용이 가능하다.
글쓴이 소개
Rahul Biswas는 썬 Java Performance Engineering 그룹 소속으로, Java Persistence에 대한 일반 성능 벤치마크 개발과 GlassFish에서의 퍼시스턴스 구현의 성능 개선 작업에 참여하고 있다.
"Java EE" 카테고리의 다른 글
- Attach API (댓글 18개 / 트랙백 1개) 2007/09/03
- JSP 2.0 EXPRESSION LANGUAGE (댓글 1개 / 트랙백 0개) 2004/02/05
- 환경 엔트리를 이용해서 배포의 사용자 정의하기 (댓글 2개 / 트랙백 0개) 2003/12/24
- JAX-WS를 이용한 웹 서비스 개발 (댓글 1개 / 트랙백 0개) 2006/01/18
- EJB 2.1로 메시지 구동 빈 이용하기 (댓글 1개 / 트랙백 0개) 2005/05/18
- EclipseLink를 사용하여 JPA에서 반복 불가능한 읽기 방지 (댓글 0개 / 트랙백 1개) 2008/07/09
- 컴포넌트 시스템과 클래스 로더 경계 (댓글 1개 / 트랙백 0개) 2004/10/05
- JAX-WS Dispatch 및 Provider API를 이용한 문서 처리 (댓글 4개 / 트랙백 0개) 2006/09/15
- POJO를 Persistent Entity로 변환하기 (댓글 1개 / 트랙백 0개) 2005/12/27
- SAAJ 소개 (댓글 1개 / 트랙백 0개) 2005/06/08
댓글을 달아 주세요
좋은 정보 감사해요~
2007/09/19 04:16