Developer: Java
  DOWNLOADS
Oracle TopLink
Oracle JDeveloper 10g
  TAGS
jdeveloper, reporting, All

JPA 테스트 드라이브


저자 - Samudra Gupta

Java Persistence Architecture(JPA)의 활용과 구축에 관련한 사례 연구

게시일: 2006년 11월

EJB 3.0의 Summer 2006 릴리즈는 한층 단순화되고 한층 강력한 EJB 프레임워크를 제공하고 있습니다. 특히 기존의 EJB 2.x 디플로이먼트 디스크립터(deployment descriptor)와 분명히 차별화된 주석(annotation) 기능이 제공됩니다. J2SE 5.0에서 처음 소개된 주석은 클래스, 필드, 메소드, 매개변수, 로컬 변수, 컨스트럭터, 이뉴머레이터(enumerator), 패키지 등에서 사용되는 수정자(modifier)입니다. 주석 기능은 POJO(plain old java object) 기반 EJB 클래스, EJB 매니저 클래스의 종속성 인젝션(dependency injection), 다른 비즈니스 호출을 가로채는 인터셉터(interceptor), 그리고 혁신적으로 개선된 Java Persistence API(JPA) 등을 포함하는 다양한 EJB 3.0의 신기능에서 활용되고 있습니다.

JPA의 개념에 대한 이해를 돕기 위해, 실제 적용 사례를 검토해 보기로 하겠습니다. 최근, 필자는 사업자 등록 시스템(tax registration system)을 구현하는 프로젝트를 진행한 일이 있습니다. 다른 시스템의 경우와 마찬가지로, 이 프로젝트에는 나름의 애로 사항이 있었습니다. 특히 데이터 액세스와 오브젝트-관계형 매핑(ORM)의 구현에 어려움을 겪던 우리 팀은 시스템에 새로운 JPA를 시험 삼아 도입해 보기로 결정하였습니다.

프로젝트 과정에서 도출된 어려운 문제들이 아래와 같습니다:

  • 애플리케이션에서 사용되는 엔티티 간에 존재하는 복잡한 관계.
  • 애플리케이션이 관계형 데이터에 대한 복잡한 검색 기능을 지원해야 함.
  • 애플리케이션의 데이터 무결성을 보장해야 함.
  • 데이터를 저장하기 전에 검증 작업을 수행해야 함.
  • 벌크 작업이 요구됨.

데이터 모델

먼저 관계형 데이터 모델의 축소된 버전을 검토해 보기로 합시다. 이 모델만으로도 JPA의 개념을 이해하는 데에는 충분할 것입니다. 먼저 비즈니스 관점에서, 등록 신청자가 사업자 등록 신청을 제출합니다. 신청자는 0개 또는 그 이상의 파트너를 가질 수 있습니다. Applicant와 Partner는 각각 두 개의 주소, 즉 사업자 등록 주소(registered address)와 거래 주소(trading address)를 제공해야 합니다. 또 신청자는 과거에 부과된 벌금 내역을 신고하고 기술해야 합니다.

Figure 1

엔티티의 정의.각 테이블에 대한 매핑을 통해 정의된 엔티티가 아래와 같습니다:

엔티티 매핑된 테이블
Registration REGISTRATION
Party PARTY
Address ADDRESS
Penalty PENALTY
CaseOfficer CASE_OFFICER

표 1.
엔티티-테이블 매핑

엔티티를 데이터베이스 테이블과 컬럼에 매핑하는 작업은 그리 어렵지 않았습니다. Registration 엔티티의 단순화된 예가 아래와 같습니다. (이 엔티티에 관련된 추가적인 매핑 및 설정에 대해서는 뒷부분에서 설명합니다.)

@Entity
@Table(name="REGISTRATION")

public class Registration implements Serializable{

    @Id
    private int id;

    @Column(name="REFERENCE_NUBER")
    private String referenceNumber;

    

   ..........
    }

JPA 엔티티를 사용함으로써 얻을 수 있는 가장 중요한 혜택은, 마치 일반적인 Java 클래스를 코딩하는 것과 유사한 작업이 가능하다는 점입니다. 따라서 이제 더 이상 복잡한 라이프사이클 관리 방법론을 적용할 필요가 없습니다. 주석 기능을 이용하면 엔티티에 퍼시스턴스 기능을 적용하는 것이 가능합니다. 또 Data Transfer Object(DTO) 레이어를 추가로 구현할 필요가 없었으며, 엔티티를 재활용하고 서로 다른 레이어에 적용할 수 있었습니다. 덕분에 데이터의 이동성이 확보되었습니다.

폴리모피즘(Polymorphism)의 지원.데이터 모델을 검토하는 과정에서 PARTY 테이블이 Applicant와 Partner의 레코드를 동시에 저장하고 있음을 설명한 바 있습니다. 이 레코드들은 몇 가지 공통 속성을 공유하고 있지만, 또 서로 다른 속성을 가지기도 합니다.

우리는 이 모델을 상속 계위(inheritance hierarchy)로 모델링하기로 결정하였습니다. EJB 2.x에서는 하나의 Party 엔티티 빈만을 사용하여 코드 내에서 로직을 구현함으로써 party 타입의 Applicant, Partner 오브젝트를 생성해야 합니다. 반면 JPA에서는 엔티티 레벨에서 상속 계위를 정의하는 것이 가능합니다.

우리 팀은 Party라는 ‘abstract’ 엔티티와 Partner, Applicant라는 두 개의 ‘concrete’ 엔티티를 사용하여 상속 계위를 모델링 하기로 결정하였습니다:

@Entity
@Table(name="PARTY_DATA")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PARTY_TYPE")

public abstract class Party  implements Serializable{

    @Id
    protected int id;

    @Column(name="REG_ID")
    protected int regID;


   
    protected String name;

   .........

 }

이제 두 개의 ‘concrete’ 클래스, Partner와 Applicant는 ‘abstract’ 클래스인 Party로부터 상속합니다.

@Entity
@DiscriminatorValue("0")
public class Applicant extends Party{

    @Column(name="TAX_REF_NO")
    private String taxRefNumber;

    @Column(name="INCORP_DATE")
    private String incorporationDate;

  ........

}

party_type 컬럼이 0의 값을 갖는 경우, 퍼시스턴스 프로바이더는 Applicant 엔티티의 인스턴스를 반환합니다. 값이 1인 경우에는 Partner 엔티티의 인스턴스를 반환합니다.

관계의 구현.우리가 구현한 애플리케이션 데이터 모델의 PARTY 테이블은 REGISTRATION 테이블을 참조하는 외래 키 컬럼(reg_id)을 포함하고 있습니다. 이 구조에서 Party 엔티티는 관계 엔티티 또는 소스(source)의 소유자로 간주됩니다. 따라서 Party 엔티티에서 JOIN 컬럼을 정의해야 합니다. 반면 Registration 엔티티는 관계의 타겟(target)으로 간주됩니다.

대부분의 ManyToOne 관계는 양방향으로 설정되어 있을 가능성이 높습니다. 다시 말해, 두 엔티티 간에 OneToMany 관계가 존재할 가능성이 있습니다. 아래 표는 우리 팀이 정의한 관계를 보여 주고 있습니다:

>
Relationship Owning Side Multiplicity/Mapping
Registration->CaseOfficer CaseOfficer OneToOne
Registration->Party Party ManyToOne
Party->Address Address ManyToOne
Party->Penalty Penalty ManyToOne
Reverse Side of Relationship
Registration->CaseOfficer
OneToOne
Registration->Party
OneToMany
Party->Address
OneToMany
Party->Penalty
OneToMany

표 2.
관계의 정의

public class Registration  implements Serializable{
....

    @OneToMany(mappedBy = "registration")
    private Collection<Party> parties;

....
}

public abstract class Party  implements Serializable{
....
    @ManyToOne
    @JoinColumn(name="REG_ID")
    private Registration registration;
....

참고:mappedBy 엘리먼트는 join 컬럼이 관계 상의 다른 엔티티에 정의되어 있음을 의미합니다.

다음으로, JPA 표준과 퍼시스턴스 프로바이더에 의해 정의된 관계의 동작 방식을 고려할 필요가 있습니다. 관련된 데이터를 어떤 방법으로 가져 오게 될 까요? EAGER와 LAZY 중 어떤 FETCH 타입이 사용될까요? 우리는 JPA 표준에 정의된 디폴트 FETCH 타입을 검토한 후, 확인된 결과를 포함시키기 위해 표 2에 컬럼을 추가하였습니다:

Relationship Owning Side Multiplicity/Mapping Default FETCH Type
Registration->CaseOfficer CaseOfficer OneToOne EAGER
Party->Registration Party ManyToOne EAGER
Address->Party Address ManyToOne EAGER
Penalty->Party Penalty ManyToOne EAGER




Reverse Side of Relationship
Registration->Party
OneToMany LAZY
Party->Address
OneToMany LAZY
Party->Penalty
OneToMany LAZY

표 3. 디폴트 FETCH 타입의 설정

비즈니스 요구사항을 검토하는 과정에서, 우리는 Registration 상세 정보를 가져올 때마다 이에 관련한 Party의 상세 정보를 항상 표시해야 함을 확인하였습니다. FETCH 타입을 LAZY로 설정한 상태에서는, 데이터베이스에서 데이터를 가져오기 위해 반복적으로 호출 작업을 실행해야 할 것입니다. 이는 Registration->Party 관계의 FETCH 타입을 EAGER로 변경한다면 개선된 성능을 얻을 수 있음을 의미합니다. 이 설정을 사용하면, 퍼시스턴스 프로바이더는 하나의 SQL 구문을 통해 모든 관련 데이터를 반환할 수 있을 것입니다.

마찬가지로, Party의 상세 정보를 화면에 표시할 때마다 관련된 Address 정보를 표시해야 한다는 요구사항이 있었습니다. 따라서 Party->Address 관계에서도 FETCH 타입을 EAGER로 설정하는 것이 좋을 것입니다.

반면 Party->Penalty 관계의 FETCH 타입은 LAZY로 변경하였습니다. 사용자가 별도로 요청하지 않는 이상 벌금의 상세 정보를 표시할 필요는 없기 때문입니다. 각각 n 개의 Penalty를 갖는 m개의 Party에 대해 EAGER FETCH 타입을 사용하는 경우, m 번의 Penalty 로딩 작업이 발생하여 불필요한 성능 저하 현상을 유발하게 될 것입니다.

public class Registration  implements Serializable{

    @OneToMany(mappedBy = "registration", fetch = FetchType.EAGER)
    private Collection<Party> parties;

 .....
}

public abstract class Party implements Serializable{

       @OneToMany (mappedBy = "party", fetch = FetchType.EAGER)
    private Collection<Address> addresses;

    @OneToMany (mappedBy = "party", fetch=FetchType.LAZY)
    private Collection<Penalty> penalties;

 .....
}

‘Lazy Loading’의 구현.’lazy loading’ 방식을 고려하는 경우, 퍼시스턴스 컨텍스트(persistence context)의 범위를 함께 고려해야 합니다. EXTENDED 퍼시스턴스 컨텍스트와 ‘TRANSACTION scoped’ 퍼시스턴스 컨텍스트의 둘 중 하나를 선택할 수 있습니다. EXTENDED 퍼시스턴스 컨텍스트는 서로 다른 트랜잭션 사이에서도 계속적으로 유지되며, ‘stateful’ 세션 빈과 유사하게 동작합니다.

우리가 개발하는 애플리케이션은 많은 인터액션을 필요로 하지 않으며, 따라서 퍼시스턴스 컨텍스트가 서로 다른 트랜잭션 간에 유지될 필요는 없었습니다. 그래서 우리는 ‘TRANSACTION scoped’ 퍼시스턴스 컨텍스트를 사용하기로 결정했습니다. 하지만 여기에서 ‘lazy loading’에 관련한 문제가 발생했습니다. 엔티티를 가져오고 트랜잭션이 종료되고 나면, 엔티티는 ‘detach’ 상태가 됩니다. 우리의 애플리케이션에서 ‘lazy loading’ 방식으로 가져온 관계 데이터를 로드하려고 시도하면 예기치 못한 동작이 발생하게 될 것입니다.

생활환경조사원(CaseWorker)이 등록 데이터를 읽어올 때에는 벌금(Penalty) 레코드를 디스플레이할 필요가 없으며, 이러한 경우가 조회 작업의 대부분을 차지합니다. 하지만 매니저(Manager)가 같은 데이터를 읽어올 때에는 벌금 레코드를 디스플레이 해야 합니다. 대부분의 경우 벌금 레코드를 디스플레이할 필요가 없다는 점을 감안한다면, 관계의 FETCH 타입을 EAGER로 바꾸는 것이 의미가 없을 것입니다. 그 대신, 매니저가 시스템을 사용하는 것이 감지되었을 때에는 트리거 방식으로 관계 데이터의 ‘lazy loading’을 시도하게 할 수 있습니다. 이와 같이 함으로써 엔티티가 ‘detach’되는 경우에도 관계 데이터를 사용하는 것이 가능하고, 향후 재접속이 가능함을 보장할 수 있습니다. 아래 코드의 예를 통해 그 개념을 이해하실 수 있습니다:

Registration registration = em.find(Registration.class, regID);

     Collection<Party> parties = registration.getParties();
     for (Iterator<Party> iterator = parties.iterator(); iterator.hasNext();) {
         Party party = iterator.next();
         party.getPenalties().size();

     }
     return registration;
		

위의 예에서는, Party 엔티티의 penalties 컬렉션의 size() 메소드를 호출하기만 했습니다. 이 작업만으로도 ‘lazy loading’을 트리거하고 (Registration 엔티티가 ‘detach’되어 있는 경우라도) 모든 컬렉션을 다시 재입력하는 것이 가능합니다. (또는 JP-QL이 제공하는 FETCH JOIN 기능을 사용할 수도 있습니다. 여기에 대해서는 뒷부분에서 설명합니다.)

관계와 퍼시스턴스

다음으로, 데이터 퍼시스턴스의 컨텍스트에서 관계가 어떤 방식으로 동작하였는지 고려해 볼 필요가 있습니다. 중요한 것은, 관계 데이터를 변경하는 경우 변경 작업을 오브젝트 레벨에서 수행하고 변경 정보의 퍼시스턴스가 퍼시스턴스 프로바이더에 의해 보장될 수 있게 해야 한다는 것입니다. JPA에서는 CASCADE 타입을 이용하여 퍼시스턴스 동작(persistence behavior)을 컨트롤할 수 있습니다.

JPA는 네 가지 CASCADE 타입을 정의하고 있습니다:

  • PERSIST: 엔티티의 소유에 대한 퍼시스턴스와, 관련 데이터의 퍼시스턴스가 확보됨.
  • MERGE: 'detach’된 엔티티가 다시 액티브 퍼시스턴스 컨텍스트에 병합되고, 관련 데이터 또한 병합됨.
  • REMOVE: 엔티티가 제거될 때, 관련 데이터 또한 함께 제거됨.
  • ALL: 위의 세 가지 조건이 모두 적용됨.

엔티티의 생성.우리는 새로운 부모 엔티티(parent entity)를 생성할 때마다 관련된 자식 엔티티(child entity)의 퍼시스턴스가 자동으로 확보될 수 있기를 원했습니다. 이런 방법은 코딩 작업을 간단하게 만들어 줍니다. 관계 데이터를 정확히 설정하기만 하면 각 엔티티별로 일일이 persist() 작업을 호출할 필요가 없기 때문입니다.

따라서 PERSIST 캐스케이드 타입을 사용하는 것이 가장 매력적인 대안으로 보였습니다. 우리는 PERSIST 캐스케이드 타입을 사용할 수 있도록 관계 정보를 재수정했습니다.

엔티티의업데이트.트랜잭션 내에서 데이터를 조회한 뒤, 트랜잭션의 외부에서 엔티티를 수정하고 변경 사항을 저장하는 경우를 자주 볼 수 있습니다. 우리 팀이 개발하는 애플리케이션에서는, 사용자가 기존 등록 정보를 조회한 뒤 신청자의 주소를 변경하는 상황을 고려해야 했습니다. 특정 트랜잭션 내에서 기존에 존재하는 Registration 엔티티와 관련 데이터를 가져온 경우, 트랜잭션이 종료하면 데이터는 프리젠테이션 레이어로 바로 전달됩니다. 이 시점에서 Registration 및 관련 엔티티 인스턴스들은 퍼시스턴스 컨텍스트로부터 ‘detach’된 상태가 됩니다.

JPA에서 ‘detach’된 엔티티에 변경 사항을 영구 저장하려면 EntityManager의 merge() 작업을 사용해야 합니다. 또, 변경 사항을 관계 데이터에 추가로 적용하기 위해서는, 모든 관계 정의에 CASCADE 타입 MERGE와 그 밖에 관계 매핑 설정에 정의된 다른 CASCADE 타입이 포함되어 있어야 합니다.

이러한 작업을 모두 완료한 뒤, 우리는 모든 관계 정의에 CASCADE 타입이 올바르게 정의되었는지를 확인했습니다.

엔티티의 제거. 다음으로, 특정 엔티티를 삭제 또는 제거했을 때 어떤 문제가 있을 수 있는지 확인하기로 했습니다. 예를 들어 Registration을 삭제하는 경우, Registration과 관련된 모든 Party 엔티티들을 안전하게 삭제할 수 있을 것입니다. 하지만 그 역은 성립되지 않습니다. 여기서 중요한 것은, 관계에 대한 remove() 작업을 캐스케이드 처리함으로써 엔티티가 예기치 않게 삭제되는 것을 방지하는 것입니다. 다음 섹션에서도 설명하겠지만, 참조 무결성 제약 때문에 이러한 작업은 성공할 수 없습니다.

우리는 Party와 Address, 또는 Party와 Penalty 간에 OnetoMany 기반의 명확한 부모-자식 관계가 성립되어 있는 경우, 관계의 부모 쪽에서만 CASCADE 타입의 REMOVE를 실행하는 것이 안전하다는 결론을 내렸습니다. 그런 다음 이렇게 얻어진 기준에 의해 관계를 재정의하였습니다.


public abstract class Party implements Serializable{

   @OneToMany (mappedBy = "party", fetch = FetchType.EAGER, cascade = 
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) private Collection<Address> addresses; @OneToMany (mappedBy = "party", fetch=FetchType.LAZY, cascade =
{CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}) private Collection<Penalty> penalties; ..... }

관계의 관리

JPA 표준은 관계(relationship)의 관리 책임이 전적으로 프로그래머에게 있다고 규정하고 있습니다. 퍼시스턴스 프로바이더는 관계 데이터의 상태에 대해 아무 것도 가정하지 않으며, 관계의 관리에 관련한 아무런 작업을 수행하지 않습니다.

이러한 점을 감안하여, 우리는 관계 관리 전략을 재검토하고 문제가 될 수 있는 부분이 무엇인지 확인했습니다. 우리가 확인한 사항이 다음과 같습니다:

  • 부모/자식 간의 관계를 설정하는 과정에서 부모가 데이터베이스에 존재하지 않는 경우(예를 들어 다른 사용자에 의해 부모가 삭제된 경우), 데이터 무결성의 문제가 발생합니다.
  • 자식 레코드를 삭제하지 않은 상태에서 부모 레코드를 먼저 삭제하려 시도하는 경우, 참조 무결성 위반이 발생합니다.

우리는 다음과 같은 코딩 가이드라인을 정의하였습니다:

  • 특정 엔티티와 관련된 엔티티들을 트랜잭션 내부에서 가져온 다음 트랜잭션 외부에서 관계를 변경하고 변경 사항을 새로운 트랜잭션에서 저장하려 시도하는
    경우, 부모 엔티티를 다시 가져오는(re-fetch) 것이 최선의 방법이다.
  • 자식 레코드를 삭제하지 않은 상태에서 부모 레코드의 삭제를 시도하는 경우, 부모를 삭제하기 전에 모든 자식에 대한 외래 키 필드를 NULL로 설정해야 한다.

CaseWorker와 Registration의 OneToOne 관계를 생각해 봅시다. 특정 Registration을 삭제하는 과정에서 CaseWorker는 삭제되지 않습니다. 따라서 Registration을 삭제하기 전에 reg_id 외래 키를 NULL로 설정해야 합니다.

@Stateless
public class RegManager {
.....

public void deleteReg(int regId){
        Registration reg = em.find(Registration.class, regId);
        CaseOfficer officer =reg.getCaseOfficer();
        officer.setRegistration(null);
        em.remove(reg);
    }
}

데이터 무결성

사용자가 Registration 레코드를 조회하는 동안, 다른 사용자가 같은 애플리케이션에서 변경 작업을 수행하는 경우를 가정해 봅시다. 이때 첫 번째 사용자가 애플리케이션에 추가적으로 변경 작업을 수행하고, 기존의 데이터에 덮어쓰기를 시도할 수 있습니다.

이러한 문제를 해결하기 위해, 우리는 ‘옵티미스틱 락킹(optimistic locking)’을 사용하기로 결정하였습니다. JPA에서는 엔티티에 버저닝 컬럼(versioning column)을 사용하는 것을 허용합니다. 우리는 이 컬럼을 사용해서 옵티미스틱 락킹 기능을 구현하였습니다.

public class Registration  implements Serializable{


    @Version

    private int version;
.....
}

퍼시스턴스 프로바이더는 데이터베이스에 저장된 버전 컬럼 값과 메모리 상의 버전 컬럼 값을 비교합니다. 두 값이 동일하지 않은 경우, 퍼시스턴스 프로바이더는 익셉션을 발생시킵니다.

검증 (Validation)

신청자는 최소한 하나의 주소를 가지며, 이 주소는 최소한 하나의 라인과 우편 번호를 가집니다. 이러한 비즈니스 규칙은 Party 엔티티와 Address 엔티티에 적용됩니다. 또 각 주소 라인의 문자 수는 100개 이하이어야 합니다. 이 규칙은 Address 엔티티에 대한 검증 기능을 통해 구현되어야 합니다.

우리는 Session Bean 레이어에 타입 검증 기능을 구현하기로 결정하였습니다. (Session Bean 레이어는 대부분의 워크플로우/프로세스 관련 로직이 코딩되는 곳입니다.) 또 이 검증 기능은 엔티티 내부에서 구현되는 것으로 결정되었습니다. JPA를 이용하면, 엔티티의 특정 라이프사이클 이벤트에 임의의 메소드를 연계시킬 수 있습니다.

아래 코드는 Address 라인의 문자 수가 100 개 이하인지 검증하고, Address 엔티티가 저장되기 전에 (@PrePersist 주석을 사용하여) 이 메소드를 호출합니다. 작업이 실패한 경우, 메소드는 호출자에게 (RuntimeException 클래스로부터 확장된) 비즈니스 익셉션을 발생시킵니다. 이 익셉션은 사용자에게 메시지를 전달하는데 사용될 수도 있습니다.

public class Address  implements Serializable{
.....
    @PrePersist
   public void validate() 
       if(addressLine1!=null && addressLine1.length()>1000){
           throw new ValidationException("Address Line 1 is longer than 1000 chars.");
       }
   }

검색

우리가 구현하는 애플리케이션에는 특정 Registration 및 관련된 Party 등에 대한 상세 정보를 검색하는 기능이 요구되었습니다. 효과적인 검색 기능을 제공하기 위해서는 여러 가지 문제를 해결해야 합니다. 효율적인 쿼리의 작성, 대량의 결과 셋 브라우징을 위한 페이지네이션(pagination) 구현 등이 그 예입니다. JPA는 데이터 액세스의 구현을 위해 엔티티에 Java Persistence Query Language(JP-QL)를 사용할 것을 규정하고 있습니다. JP-QL은 EJB 2.x의 EJB QL에 비해 훨씬 개선된 기능을 제공합니다. 우리는 JP-QL을 사용하여 효과적인 데이터 액세스 메커니즘을 구현할 수 있었습니다.

쿼리

JPA는 다이내믹 쿼리(dynamic query)와 정적 쿼리(static query)의 두 가지 옵션을 제공합니다. 이 정적 쿼리(또는 named query라 부르기도 합니다)는 매개변수를 지원하며, 매개변수의 값은 런타임에 할당됩니다. 우리가 작성하려는 쿼리의 범위가 매우 잘 정의되어 있는 상태이기 때문에, 우리는 네임드 쿼리와 매개변수를 이용하기로 결정하였습니다. 프로바이더가 변환된 SQL 쿼리를 캐시에 저장하여 재활용할 수 있다는 점에서, 네임드 쿼리는 효율성 면에서도 뛰어납니다.

우리의 애플리케이션에서는 네임드 쿼리를 다음과 같은 방법으로 적용하였습니다: 사용자가 애플리케이션 레퍼런스 넘버를 입력하여 Registration 상세 정보를 조회하려 시도하는 경우, Registration 엔티티에 대해 아래와 같은 네임드 쿼리가 전달됩니다:

@Entity
@Table(name="REGISTRATION")
@NamedQuery(name="findByRegNumber", query = "SELECT r FROM REGISTRATION r WHERE r.appRefNumber=?1")
public class Registration implements Serializable{
.....
}

검색과 관련하여 특별히 우리의 관심을 끈 애플리케이션 요구 사항이 한 가지 있었습니다. 바로 모든 Party의 전체 Penalty 합계를 조회하기 위한 리포트용 쿼리입니다. Party 중에는 Penalty 기록이 존재하지 않는 경우가 있습니다. 따라서 단순한 JOIN 작업을 사용하는 경우에는 Penalty가 없는 Party를 조회할 수 없습니다. 이 문제를 해결하기 위해, 우리는 JP-QL의 OUTER JOIN 기능을 사용하였습니다. 또 GROUP BY 구문을 사용하여 Penalty의 합계를 계산하였습니다. Party 엔티티에 대해 추가된 새로운 네임드 쿼리가 아래와 같습니다:


@Entity
@Table(name="PARTY_DATA")
@Inheritance(strategy= InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="PARTY_TYPE")

@NamedQueries({@NamedQuery(name="generateReport", 
                           query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount)) 
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""), @NamedQuery(name="bulkInactive", query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1")}) public abstract class Party { ..... }

위의 “generateReport” 네임드 쿼리에서 쿼리 내부에 새로운 ReportDTO 오브젝트 인스턴스를 생성하고 있음을 참고하시기 바랍니다. 이러한 기능은 JPA의 매우 강력한 기능으로서 활용 가능합니다.

벌크(Bulk) 작업의 처리

우리가 개발하는 애플리케이션에서는 Officer가 Registration을 조회한 후 ‘inactive' 상태로 변경할 수 있습니다. 여기서 해당 Registration에 관련된 모든 Party 또한 ‘inactive’로 설정해야 할 것입니다. 이를 위해 PARTY 테이블의 Status 컬럼을 0으로 설정합니다. 성능 개선을 위해, 우리는 각 Party 별로 SQL을 실행하지 않고 배치 업데이트를 실행하는 방법을 선택하였습니다.

JPA는 벌크 작업의 처리를 위한 기능을 제공합니다:

@NamedQuery(name="bulkInactive", query="UPDATE PARTY p SET p.status=0 where p.registrationID=?1")
public abstract class Party implements Serializable{
.....
}

참고:Bulk 작업은 SQL을 데이터베이스에 직접 실행합니다. 따라서 변경 사항을 반영하는 형태로 퍼시스턴스 컨텍스트가 업데이트되지 않습니다. 단일 트랜잭션의 범위를 넘어서는 ‘extended’ 퍼시스턴스 컨텍스트를 사용하는 경우, 캐시된 엔티티의 데이터가 올바르게 업데이트 되어 있지 않을 수도 있습니다.

선택적인 데이터 디스플레이.

또 한 가지 까다로운 요구사항이 있었습니다. 고객은 선택적인 데이터 디스플레이 기능을 원했습니다. 예를 들어, Manager가 Registration을 검색하는 경우 관련된 Party 레코드의 모든 Penalty 정보를 디스플레이해야 했습니다. 반면 일반 CaseWorker가 검색을 수행할 때에는 Penalty 정보가 디스플레이되지 않습니다.

Party와 Penalty는 OneToMany의 관계를 갖습니다. 앞에서 설명한 것처럼 디폴트 FETCH 타입은 LAZY로 정의됩니다. 하지만 선택적인 검색 결과 디스플레이 요구사항을 만족하기 위해서는, 하나의 SQL 쿼리를 통해 모든 Penalty 정보를 가져옴으로써 반복적인 SQL 호출을 피하는 것이 바람직했습니다.

이를 위해 JP-QL의 FETCH Join 기능이 사용되었습니다. 디폴트로 사용되는 LAZY FETCH 타입을 무시하려면 FETCH Join을 사용하면 됩니다. 하지만 이 기능이 빈번하게 사용된다면 FETCH 타입을 EAGER로 변경하는 것이 더 효율적일 것입니다.


@NamedQueries({@NamedQuery(name="generateReport",
                           query=" SELECT NEW com.ssg.article.ReportDTO(p.name, SUM(pen.amount)) 
FROM Party p LEFT JOIN p.penalties pen GROUP BY p.name""), @NamedQuery(name="bulkInactive", query="UPDATE PARTY P SET p.status=0 where p.registrationID=?1"), @NamedQuery(name="getItEarly", query="SELECT p FROM Party p JOIN FETCH p.penalties")}) public abstract class Party { ..... }

결론

JPA는 전반적인 퍼시스턴스 코딩 작업을 단순화하는 효과를 제공합니다. 우리는 이번 프로젝트를 통해 JPA의 다양한 기능과 뛰어난 효율성을 확인할 수 있었습니다. JPA의 쿼리 인터페이스와 개선된 쿼리 언어를 통해 복잡한 관계형 시나리오에 쉽게 대응하는 것이 가능했습니다. 또 JPA의 상속 기능을 이용하여 퍼시스턴스 레벨의 논리적 도메인 모델을 관리하고, 서로 다른 레이어에 대해 동일한 엔티티를 재활용할 수 있었습니다. 다양한 혜택을 제공하는 JPA는 미래를 위한 확실한 선택이 될 것입니다.


Samudra Gupta는 영국에서 활동 중인 Java/J2EE 컨설턴트입니다. 그는 지난 9 년 간 공공, 유통, 국가 보안 등의 분야에서 Java/J2EE 애플리케이션을 구현한 경험을 보유하고 있습니다. Samudra는 의 저자로, Javaboutique, Javaworld, Java Developers Journal 등의 전문지에 활발한 기고 활동을 펼치고 있습니다. 프로그래밍을 하지 않을 때에는, 그는 브리지 게임과 10 핀 볼링 게임을 즐겨 합니다.
E-mail this page
Printer View Printer View