 |
|
난이도 : 중급 Ryan Senior, 컨설턴트, Source Allies, Inc. Travis Klotz, 컨설턴트, Source Allies, Inc. Jim Majure , 컨설턴트, Source Allies, Inc.
옮긴이: 박찬욱 dwkorea@kr.ibm.com
2008 년 9 월 30 일
2부로 된 이번 연재 중 Part 1에서는 현대적인 객체 관계형 매핑(ORM: object-relational mapping) 도구를 사용해 일관되고 간결한 도메인 모델과 영속성 티어(persistence tier)를 만들 수 있는 기본을 다뤘습니다. Part 2에서 필자는 도메인 모델의 행동으로 기본 도메인 엔티티와 좀 더 발전된 기능인 제네릭 DAO(Generic DAO)를 설명합니다. 또한 도메인 모델을 사용하는 데이터 검색(data-retrieval) 성능을 향상시킬 수 있는 전략을 공유합니다.
소개
이 연재의 Part 1에서는 하이버네이트와 객체 관계형 매핑(ORM) 도구의 일반적인 모범 사례를 몇 가지 다뤘다. 공통적인 기본 도메인 클래스와 인터페이스, 집중화된 감사(auditing), 제네릭 데이터 접근 객체(generic DAO)를 통해 애플리케이션은 훨씬 더 간결하고, 유지보수가 쉬운 도메인 모델과 영속성 단(tier)을 갖게 될 수 있다.
Part 1에서 적용한 개념은 코드 재사용에 새로운 기회를 열어 줄 수 있다. 도메인 모델의 행동을 통합하기 위해 상속(inheritance)과 다형성(polymorphism)을 사용할 수 있는 방법을 보여주면서 시작할 것이다. 다음으로 Part 1에서 얘기한 제네릭 DAO를 구축할 것이다. 일단 제네릭 DAO로 통합해 사용하기 시작하면, 애플리케이션에 곳곳에 공통으로 나타나는 잠재적인 연산을 발견하게 된다. 제네릭 DAO로 데이터의 페이지 처리와 쿼리를 통합해 코드를 줄일 수 있는 방법을 함께 살펴 볼 것이다. 도메인 모델을 사용해 성능을 향상시킬 수 있는 전략으로 이번 글을 마치게 된다. 이러한 전략 없이 부정확하게 구성한 도메인 모델의 연관은 필요보다 많은 수천 개의 쿼리가 실행되는 원인이 되며, 불필요한 기록을 받아옴으로써 엄청난 자원을 허비할 수 있다.
모델 다시 보기: ORM select 행동 위임하기
데이터베이스 테이블은 그 자체로 행동을 가지고 있지 않으므로 서비스나 뷰 레이어에 도메인-모델 엔티티의 행동을 넣으려고 시도하게 된다. ‘객체는 행동과 데이터를 갖는다’라는 객체 지향의 근본적인 규칙을 위반하기 때문에 이 방법은 바람직하지 못하다. 행동이 객체에서 제거되고, 이 행동을 서비스에 넣으면, 객체는 단순히 데이터만을 유지하게 된다. 그리고 서비스나 뷰 레이어에 엔티티의 행동을 넣는 것은 애플리케이션 도처에 엔티티의 핵심 로직을 뿔뿔이 흩어놓는 것으로 유지보수 문제의 원인이 된다. 하이버네이트와 같은 도구를 이용하면 객체에 데이터와 함께 모델의 행동을 훨씬 더 쉽게 넣을 수 있으며, 일반적으로 좀 더 도메인 지향 모델로 자연스럽게 관심을 갖게 된다.
Part 1에서 사용했던 종업원 예제를 계속해서 보자. 그림 1은 임금 기능을 정의한 객체 모델을 보여준다.
그림 1. pay-rate 계산에 대한 객체 모델
데이터베이스에 PayRate 테이블과 관계를 맺고 있는 Employee 테이블이 있다고 가정하자. pay-rate 테이블은 Hourly와 Salary 두 개의 값을 받을 수 있는 employeeType으로 명명된 칼럼을 갖고 있다. 시간제 종업원의 임금 계산과 연봉제 종업원의 임금 계산이 다르기 때문에, 종업원의 임금을 계산하는 데 사용하는 알고리즘은 이 칼럼의 값에 의존한다. 연봉제 종업원은 일하는 시간에 관계 없이 매주 같은 임금을 받는다. 시간제로 일하는 종업원은 일한 시간에 따라 임금이 지불되며, 초과 근무에 대한 계산이 추가될 것이다.
도메인 엔티티에 임금 계산 코드를 넣으면 도메인 모델을 좀 더 응집력 있게 이끌어 내며, 전체 티어에 걸쳐 로직이 분산되지 않는다. pay-rate 예제는 먼저 판별자(discriminator)를 필요로 하는 단일 테이블 상속을 사용한다. 테이블에 있는 판별자 칼럼은 데이터베이스에 있는 데이터를 대표하는 객체를 생성할 때 객체 타입을 하이버네이트에 알려준다. 이번 예에서는 employeeType 칼럼이 판별자 역할을 한다. 다음으로 엔티티의 기본 클래스로 활동하는 상위 클래스를 반드시 정의해야 한다. 이번 예제에서는 PayRate 추상 클래스가 상위 클래스가 된다. Listing 1은 상위 클래스의 클래스 선언부와 애노테이션 선언부를 보여준다.
Listing 1. 상위 클래스에 대한 애노테이션과 클래스 선언
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn( name="employeeType", discriminatorType=DiscriminatorType.STRING )
public abstract class PayRate extends BaseEntity{//...}
|
마지막으로 각 타입에 맞는 하위 클래스의 구현체를 생성할 필요가 있다. Listing 2는 연봉제 종업원과 시간제 종업원에 대한 하위 클래스를 정의한다.
Listing 2. 하위 클래스 정의
@Entity
@DiscriminatorValue("Salary")
public class SalaryPayRate extends PayRate {//...}
@Entity
@DiscriminatorValue("Hourly")
public class HourlyPayRate extends PayRate {//...}
|
PayRate 테이블을 쿼리할 때, 하이버네이트는 employeeType="Salary"면 SalaryPayRate 인스턴스를, employeeType="Hourly"면 HourlyPayRate 인스턴스를 자동으로 생성한다. 이제 애플리케이션 코드는 임금 계산 알고리즘이 정확하게 사용되는 보장을 받으면서 createPayCheck 메서드를 호출할 수 있다. 하이버네이트는 이 클래스들에 대한 일종의 팩토리 역할을 하며, 정확한 시기에 정확한 인스턴스를 생성해낸다.
또한 하이버네이트는 쿼리를 작성할 때 다형성을 이해한다. 연봉제 종업원을 전부 찾는 쿼리를 작성할 때, 하이버네이트는 원하는 결과를 얻어오기 위한 쿼리에 employeeType="Salary" 줄을 덧붙인다. Listing 3은 다형성 쿼리 예제를 보여준다. 이 쿼리에 딱히 다르다거나 특이한 점이 없다는 것을 주의하자.
Listing 3. 다형성 쿼리
Criteria crit = session.createCriteria(PayRate.class);
crit.add(Restictions.eq("jobRole", "Programmer");
...
Criteria salaryCrit = session.createCriteria(SalaryPayRate.class);
salaryCrit.add(Restrictions.eq("yearlyRate",50000.00));
|
때때로 엔티티의 판별자는 단순한 칼럼만큼 직관적이지 못하다. 어쩌면 판별자 로직에서 고려해야 하는 칼럼이 하나 이상이거나, 판별자 로직이 칼럼 A와 X 로직이 일치할 때는 기본적으로 다르게 고려해야 한다. 또한 인스턴스를 생성해야 할지를 결정하는 방식을 정의할 수도 있다. 칼럼 값이 null인 경우에는 특정 클래스의 인스턴스를 생성하고, 만약 값을 가지고 있다면 다른 인스턴스를 생성하도록 하는 방법을 예로 들 수 있다. 이러한 전략 유형은 일반적으로 데이터 모델을 리팩터링해야 한다는 신호가 되기도 하지만, 레거시 데이터 모델에서는 선택의 여지가 없을지도 모른다.
정보 은닉을 위한 다형성
모델에서 엔티티의 조회 행동에서 하이버네이트를 사용하는 것은 좋지만, 종종 여러 다른 엔티티마다 서로 다른 데이터 집합을 필요로 하기도 한다. 필드들이 특정 하위 클래스에만 있고, 다른 하위 클래스들에는 없는 것이 적합하다면, 상위 클래스에서 필드를 제거하고, 해당 필드를 하위 클래스가 소유하도록 밀어 내릴 수 있다. 이는 앞으로 코드 운영자가 인스턴스가 소유하고 있지 않은 필드를 무심코 사용해버리는 실수를 예방해 준다.
예를 들어 연봉제 종업원의 임금을 계산하는 데는 필요 없지만, 시간제 종업원의 임금을 계산하는 데 필요한 몇 가지 정보가 있다고 하자. 초과 근무 수당은 연봉제 종업원에게는 맞지 않기 때문에 이 정보를 PayRate에서 HourlyRate 클래스로 밀어 내릴 수 있다. 반면에 yearlyRate 필드는 HourlyRate 클래스가 가지고 있지 않기 때문에 SalaryRate 클래스로 밀어 내릴 수 있다.
Listing 4처럼 PayRate에는 시간제와 연봉제 종업원 모두 공통으로 갖는 필드를 정의한다.
Listing 4. PayRate 클래스
public abstract class PayRate extends BaseDomainEntity{
public String getJobRole(){//...}
@OneToMany(cascade = CascadeType.ALL)
@OrderBy("payPeriodBeginDate")
public List<PayCheck> getPayChecks(){//...}
@OneToOne(cascade = CascadeType.ALL)
public Employee getAssociatedEmployee(){//...}
}
|
다음으로 Listing 5처럼 SalaryRate 클래스는 PayRate 필드를 상속받으며, yearlyRate 필드를 추가한다.
Listing 5. SalaryRate 클래스
@Entity
@DiscriminatorValue("Salary")
public class SalaryPayRate extends PayRate {
private double yearlyRate;
//...
}
|
고급 제네릭 DAO
제네릭 DAO를 사용하는 애플리케이션이 점점 더 많아짐에 따라, 데이터 접근 단(data access tier)에서 더 많은 공통점을 찾을 수 있게 된다. 공통되는 행동을 추상화해 클래스의 모든 사용자가 이 기능에 접근할 수 있도록 제네릭 DAO에 포함시킴으로써 코드 중복을 줄일 수 있다.
빈번하게 발생하는 공통적인 연산 두 가지는 조회 인자 기반 쿼리와 데이터베이스 수준의 페이지 처리가 있다.
쿼리하기
거의 모든 영속성 엔티티는 여러 타입의 쿼리 기능을 필요로 한다. 전형적으로 쿼리는 검색하려는 객체에 있는 다수의 필드에 대한 조회 텍스트 입력을 통해 조건을 정한다(때로는 와일드 카드 기능을 사용하기도 한다). 쿼리하는 한 가지 접근 방법은 영속성 객체의 인스턴스를 만들고, 조회 기준(criteria)을 사용해 필드의 데이터를 가져오는 방법이다. 하이버네이트는 example 쿼리를 통해 이 접근 방법을 지원한다.
그림 2는 example을 사용하는 쿼리(QBE: Query-By-Example) 기능을 지원하도록 확장된 BaseDao 인터페이스를 보여준다.
그림 2. 확장된 BaseDao 인터페이스
이 인터페이스의 구현을 보여주기 전에, DAO를 구현해 어떻게 사용하는지를 먼저 생각해보자. 특정 영속성 엔티티에 대한 쿼리를 실행하는 처리 과정을 설명하는 다음 코드를 잘 살펴보자.
Employee example = employeeDao.getNewInstance();
example.setLastName("Smith");
List<Employee> results = employeeDao.getByExample(example);
|
웹 애플리케이션 예제에서 조회 매개변수는 대개 조회 폼(search form)에 바인딩되는 객체(backing object for a search form)가 되며, 쿼리 결과는 사용자 인터페이스로 건네진다. Listing 6은 하이버네이트를 사용해 getAllByExample() 메서드를 구현하는 방법을 보여준다.
Listing 6. getAllByExample 구현
List<B> getAllByExample(B example) {
Criteria criteria = getCurrentSession().createCriteria(getQueryClass());
Example hibExample = Example.create(example);
return criteria.add(hibExample).list();
}
|
이 접근 방법의 강점은 코드 양을 상당히 줄일 수 있다는 것이다. 제네릭 DAO 사용으로 DAO 코드가 줄 뿐만 아니라 쿼리하고 화면에 보여주는 데이터 타입에 대한 제네릭 UI 연결 코드(plumbing code)를 만들 수도 있다. UI에서 데이터를 수집하고, 이 데이터를 사용해 쿼리를 실행해 사용자에게 결과를 보여줘야 할 필요가 있음은 간단하게 알 수 있다.
페이지 매기기(Pagination)
데이터베이스 쿼리가 대규모 결과 집합을 만들어낼 때는 일반적으로 결과 집합을 페이지로 나눠, 사용자가 페이지 사이를 이동할 수 있도록 해야 한다. 페이지 처리에는 두 가지 접근 방법이 있다. 첫 번째 방법은 쿼리를 실행해 데이터베이스에서 전체 결과 집합을 받아온 다음 결과를 간단하게 한 페이지로 사용자에게 보여주는 것이다. 이 접근 방법은 전체 결과 집합을 변환해 사용자 세션(session)에 저장해야 하는 비용을 발생시킨다.
다른 접근 방법은 페이지 처리를 모두 데이터베이스에서 하도록 하는 방법이다. 이 방법은 자원 소비를 줄여서 효과적이지만, 좀 더 복잡한 프로그래밍 모델이 필요하다. 다행히도 제네릭 DAO를 확장해 일반화한 제네릭 기능을 유용하게 이용할 수 있다. 그림 3에 나오는 BaseDao 인터페이스는 한 가지 기능을 더 확장했다.
그림 3. 페이지를 매기기 위해 확장한 BaseDao 인터페이스
그림 3은 받아와야 하는 데이터의 페이지를 식별하는 PageInfo 클래스를 소개한다. getPageAll 메서드 구현은 Listing 7에서 볼 수 있다.
Listing 7. getPageAll 구현
public List<B> getPageAll(PageInfo pageinfo) {
return getCurrentSession().createCriteria(getQueryClass()).
setFirstResult(pageinfo.getFirstRow()).
setMaxResults(pageinfo.getMaxResults()).list();
}
|
이번 글에 포함되어 있는 예제 코드는 제네릭 DAO에 추가한 기능성보다 더 많은 기능을 보여준다(다운로드).
데이터 검색 성능 개선하기
하이버네이트 애플리케이션의 성능을 조정할 때는 하이버네이트가 엔티티 연관을 다루는 방법을 세부적으로 조정하는 데 가장 많은 개발 시간을 들이게 된다. 우리는 애플리케이션으로 가져오는 데이터 양을 최소화하기를 원하면서, 동시에 실행돼야 하는 쿼리 양을 최소화하기를 바란다. 하이버네이트는 관계를 다루는 방법으로 lazy 조회(lazy fetching)와 eager 조회(eager fetching), 두 가지 주요한 방법을 제시한다.
lazy 조회
lazy 조회는 데이터베이스에서 쿼리해 오는 데이터 양을 최소화한다. 객체를 불러올 때 연관에서 모든 데이터를 쿼리해 오는 대신에 lazy로 표시된 연관에 대한 쿼리를 연관이 실제로 사용될 때까지 지연시킨다. 예를 들어, Employee 객체가 Address 객체와 연관을 맺고 있다고 하자. 이 두 객체 사이의 연관이 lazy이므로 Employee를 불러온다고 해서 하이버네이트가 매번 자동으로 Address 객체를 불러오지 않는다. 대신 Employee 객체를 불러올 때, 하이버네이트는 Address의 프록시 인스턴스를 생성한다. 이 프록시에 처음으로 접근할 때 프록시는 하이버네이트 세션에 Address 객체를 쿼리해 올 것을 요청하며, 앞으로 오는 쿼리 인스턴스에 대한 직접적인 모든 호출을 연기한다. Address 객체는 Employee 객체 생명 주기 동안에 절대로 사용되는 경우가 없다고 하면, Address 쿼리는 절대로 실행될 필요가 없다. 이 특징 덕에 확실히 데이터가 필요할 때만 하이버네이트가 객체를 불러오게 된다.
lazy 조회는 대부분 항상 기본 연관-조회 전략으로 사용된다. 실제 세계의 객체 모델은 많은 수의 엄청나게 복잡한 객체 연관을 갖게 된다. 그렇기 때문에 lazy 조회를 사용하지 않으면 결과적으로 단일 객체를 불러올 때마다 데이터베이스에 있는 전체 데이터를 세션으로 불러오는 상황을 초래한다. lazy를 이용하도록 한 판단은 간단하지만, 연관 유형에 따라 조금씩 변화를 줄 수 있다. 컬렉션-기반 연관(OneToMany와 ManyToMany)은 기본적으로 lazy를 사용하도록 판단이 되어 있기 때문에, 아무런 환경 설정을 하지 않아도 lazy의 효과를 얻을 수 있다. 단일-객체 연관(OneToOne과 ManyToOne)은 기본적으로 lazy가 아니다. 단일-객체 연관에서 lazy로 판단해 사용하려면 다음처럼 연관에 애노테이션을 지정하면 된다.
@ManyToOne(fetch=FetchType.LAZY)
public void getAddress() {
|
 |
lazy 매핑과 HBM 포맷
lazy 매핑을 구현하는 것은 매핑을 정의하기 위해 애노테이션 대신 하이버네이트의 HBM 포맷을 사용할 때와는 약간 다르다. 버전 3.0 이후로 HBM 파일을 사용해 정의된 어떤 연관에 대해서든 기본 조회 전략은 lazy 조회다. 단일 객체 연관을 하는 데도 추가 설정이 전혀 필요 없다.
|
|
lazy 조회를 사용할 때 문제
lazy 조회가 대부분의 애플리케이션에서 가장 우선되는 조회 전략이지만, 몇 가지 귀찮은 문제가 있다. 가장 중요한 문제는 LazyInitializationException이다. lazy 연관은 최초에 부모 객체를 불러온 다음 한참 후에 연관 사이에 이동을 할 수 있다(그리고 연관된 엔티티를 받아올 수 있다). 그렇지만 연관의 데이터를 쿼리하려면, 하이버네이트 세션이 이용할 수 있어야 하고, 풀(pool)에서 데이터베이스 커넥션을 첨부할 수 있어야 한다. 객체를 받아온 후에 하이버네이트 세션과 연관된 데이터베이스 커넥션이 닫혀 있지만, 그 전에 연관에 접근한 적이 없었다면, 이 시점에서 에러가 발생할 수 있다. 쿼리가 lazy 연관으로 실행되고, 데이터베이스가 연관되어 있는 커넥션이 없기 때문에 하이버네이트는 LazyInitializationException을 던진다. 이 문제를 다루는 데는 많은 전략을 이용할 수 있지만, 이 주제는 이 글의 범위 밖이다(관련된 내용은 참고자료 링크에서 "Open Sessions in View"를 보라).
고려해야 할 또 다른 문제는 lazy 연관을 이동할 때 얼마나 많은 쿼리가 실행되는가이다. lazy 연관의 인스턴스에서 처음으로 이동할 때 쿼리가 실행된다. 단일 객체의 인스턴스에서는 별로 문제가 되지 않지만, 객체 목록에 걸친 반복은 지나치게 많은 쿼리가 실행되는 원인이 될 수 있다. 열 명의 종업원과 연관을 맺고 있는 주소를 출력해주는 프로그램의 예제를 고려해보자. 기본적으로 lazy 조회를 사용하는 경우 쿼리가 열한 개 실행된다. 종업원 명단을 가져오는 데 한 번, 그리고 각 종업원의 주소를 받아오는 데 한 번씩 실행된다. 이 방법은 아주 심각한 성능 문제를 일으킬지도 모른다. 이 문제는 명칭이 주어졌을 정도로 많이 발생하는 문제다. 하이버네이트에서는 이 문제를 n+1 Selects 문제라고 부른다(Martin Fowler는 ripple loading이라 부른다).
eager 조회
eager 조회는 원래 lazy 조회의 정반대다. 객체를 불러올 때, eager로 표시되어 있는 모든 연관도 바로 불러온다. 단일 객체를 불러올 때는 lazy 연관을 사용할 때와 동일한 쿼리가 실행된다. 그러나 쿼리가 바로 실행되기 때문에, LazyInitializationException이 발생할 가능성이 없다. eager를 사용하도록 한 판단도 n+1 Selects 문제에 도움이 될 수 있다. Employee 객체의 Address 연관이 eager이고, 쿼리가 열 개의 Employee 인스턴스를 받아오도록 실행하는 경우, 쿼리가 두 개 실행된다. 하나는 Employee 인스턴스에 대한 쿼리이고, 또 다른 하나는 Employee 객체와 연관을 맺고 있는 Address 인스턴스 목록에 대한 쿼리다. 그러고 나서 하이버네이트는 간결하게 이 목록을 함께 통합한다.
기본적으로 대부분의 eager 조회는 좋은 생각이 아니다. 너무 자주 eager 조회를 사용하면, 엄청나게 큰 불필요한 데이터 트리를 불러오기 십상이다. 그러나 가끔 반드시 필요한 연관인 경우에는 eager를 사용하도록 한 판단이 더 적합하기도 하다. eager 조회가 가능한 건 lazy 조회가 가능한 것과 본질적으로 동일한 처리 흐름이다. 애노테이션으로 간단하게 지정하면 된다.
@ManyToOne(fetch=FetchType.EAGER)
public void getAddress() {//...}
|
모범 사례와 연관 쿼리하기
하이버네이트는 lazy 조회의 긍정적인 속성을 계속 유지하면서 lazy 조회를 사용할 때 일어나는 문제를 처리하는 우아한 해결책을 가지고 있다. 하이버네이트에서는 쿼리 정의에서 연관에 대한 기본 조회 전략을 재정의할 수 있다. 일반적인 객체 트리(object tree)를 이동(traversal)하는 동안 lazy 조회를 사용할 수 있지만, 연관이 쿼리 실행 시점에 필요한 내용을 모두 알아야 하는 것처럼 특별한 경우에 연관을 eager하도록 표시하는 쿼리 매개변수를 설정할 수 있다. 예를 들어, Employee 예제 프로그램에서 주소 라벨 용지를 만들기 위해 지급 주기 동안 특정 시간만큼 일한 모든 employee를 받아오도록 쿼리를 작성해야 할지도 모른다. Listing 8에 있는 쿼리는 이 목적을 달성한다.
Listing 8. 기본 조회 전략 재정의하기
Criteria addressCrit = empCrit.createCriteria("payChecks")
.add(Restrictions.eq("hoursWorked", hoursParam));
empCrit.setFetchMode("address",FetchMode.JOIN);
return empCrit.list();
|
Listing 8에 있는 쿼리는 보통 criteria 쿼리를 구성하면서 시작하지만, 흥미로운 부분은 setFetchMode를 호출하는 부분이다. 이 코드는 employee의 address 연관에 대한 기본 lazy 조회 전략을 재정의한다. FetchMode.JOIN은 조인을 사용해 단일 쿼리에서 모든 데이터를 가져오는 검색 쿼리의 일부에 Address 인스턴스를 포함해 받아온다. 대부분의 경우에 이 방법이 우리가 원하는 상황에 딱 들어 맞고, 다른 어떤 전략보다 좋은 성능을 낼 것이다.
그렇지만 FetchMode.JOIN에는 몇 가지 주의할 점이 있다. 컬렉션 기반 연관을 사용할 때 쿼리는 기본 조회 전략에서 반환하는 행의 수와는 다른 행의 수를 반환할 수 있다. 반환되는 단일 부모 객체가 반환되는 다수의 자식 객체를 가질 수 있기 때문이다. 하이버네이트는 적절하게 결과 집합을 파싱하고, 적절한 객체 목록을 반환해 실행 시점에서 이 모든 내용을 일관되게 감춰준다. 그러나 setFirstResult나 setMaxResults가 해당 쿼리에서 사용된 경우 이 행동은 문제가 될 수도 있다. 일반적으로 하이버네이트는 이 특징을 구현하는 데 데이터베이스 고유의 SQL 문을 사용하지만 정제하지 않은 SQL 쿼리는 부정확한 행 수를 반환하기 때문에 데이터베이스 고유의 기술로 개발할 수는 없다. 대신 데이터베이스에서 전체 데이터 집합을 가져와, 하이버네이트가 요청을 이행하는 데 필요로 하는 데이터를 직접 뽑아낸다. 결과적으로 n+1 Selects 문제를 고쳐보고자 했던 단순한 성능 미세 조정이, 결국에는 사용하지 않는 수천 개의 행을 애플리케이션 계층으로 가져오는 원인이 됐다.
하이버네이트는 쿼리에서 사용할 수 있는 두 번째 조회 모드 설정을 제공한다. FetchMode.SELECT는 연관에 설정된 eager 계산(evaluation) 대신 lazy 연산을 사용하도록 재정의하면 된다. 그러나 쿼리 작성 시점에서 기본으로 eager를 사용하도록 하는 판단 기술(잠깐 사이에 바로 실행되는 쿼리)을 사용하도록 연관을 재정의하면 절대로 안 된다.
배치 처리 lazy 로딩
n+1 Selects 문제의 또 다른 해결책은 lazy 로딩 요청의 배치 처리를 통한 lazy와 eager 조회의 혼성 접근 방법이다. 이 접근 방법은 여전히 데이터를 lazy 로딩하지만, 한 번에 한 연관을 lazy 로딩하는 대신 여러 연관을 함께 불러온다. 많은 ORM 프레임워크에서 이 기초적인 아이디어를 구현했다. 하이버네이트는 이 설정을 BatchSize라, TopLink는 배치 읽기(batch reading)라 부른다.
바로 앞 예제를 사용해 Employee 인스턴스를 받아오는 예제를 생각해보자. Employee와 연관을 맺고 있는 Address 인스턴스는 lazy 로딩하며, 배치 크기가 5로 되어 있다. 쿼리 하나가 Employee 인스턴스를 받아오는 데 쓰이며, Address 인스턴스에 대해서는 어떤 쿼리도 값을 받아오지 않는다. 그 뒤에 특정 Address 인스턴스에 접근할 때 하이버네이트는 lazy 로딩으로 표시된 해당 Address 인스턴스와 다음 네 개의 인스턴스를 받아온다. Employee 인스턴스와 연관을 맺고 있는 모든 Address 인스턴스에 접근했다고 가정해보면, 결과적으로 모든 Address 인스턴스를 받아오는 데 쿼리는 열 개가 아니라 두 개만 실행된다.
이 전략을 사용하려면 Listing 9에서 볼 수 있듯이 Address 클래스에 @BatchSize 애노테이션이 설정될 필요가 있다.
Listing 9. @BatchSize 애노테이션
@Entity
@Table(name="ADDRESS")
@BatchSize(size=5)
public class Address extends AuditableEntity {//...}
|
Listing 9에서는 Address에 애노테이션을 붙였지, Employee 클래스에 붙인 게 아님에 주의하자. 일단 애노테이션이 정해진 위치에 있으면, Address 인스턴스는 자동으로 한 번에 다섯 개씩 가져온다. 또는 lazy 로드에서 이용할 수 있는 기록(record)이 다섯 개보다 더 적은 경우에는 5보다 더 적은 기록을 가져온다. 또한 @BatchSize 애노테이션을 컬렉션에 붙여 엔티티의 컬렉션을 불러오는 데 배치를 적용할 수도 있다.
결론
이번 연재에서는 영속성 티어 내에서 공통으로 발생하는 문제에 대한 다양한 접근 방법을 개략적으로 소개했다. 하이버네이트와 도메인 모델을 적용하고, 주 키를 기본 클래스로 리팩터링하며, 모델의 조회 전략을 변경하는 것처럼 간단한 해결 방법을 통해 유지보수에 훨씬 유리한 애플리케이션을 만들었다. 하이버네이트와 같은 프레임워크를 통해 상속이나 다형성처럼 일반적인 객체 지향 개념을 데이터베이스로 전파했을 때 표현이 더 풍부하고, 재사용이 가능한 도메인 모델이 나올 수 있다. 여기서 간단히 소개한 많은 모범 사례를 독자들이 개발하고 있는 영역(context)과 도메인에 적용할 수 있도록 모델링할 수 있기를 바란다.
다운로드 하십시오 | 설명 | 이름 | 크기 | 다운로드 방식 |
|---|
| 이 글의 예제 코드 | PatternsOfPersistenceCode.zip | 16KB | HTTP |
|---|
참고자료 교육
제품 및 기술 얻기
토론
필자소개  | 
|  | Ryan Senior는 Source Allies, Inc.의 컨설턴트로 보험, 금융, 제조, 건강 관리 분야에서 7년의 경험을 가지고 있다. Ryan Senior는 엔터프라이즈 자바 소프트웨어 설계와 개발을 전문으로 한다. |
 | 
|  | Travis Klotz는 Source Allies, Inc.의 컨설턴트다. Travis Klotz는 교육, 보험과 엔지니어링 분야에서 10년간의 소프트웨어 개발 경험이 있다. 오픈 소스 프레임워크를 사용하는 엔터프라이즈 소프트웨어 개발을 전문 분야로 하며, 자동화된 빌드 관리 시스템을 조직이 개발하고 배포할 수 있도록 돕는다. |
 | 
|  | Jim Majure는 Source Allies, Inc.의 컨설턴트다. 과학, 금융과 보험 분야에서 20년 넘게 소프트웨어를 개발한 경험을 가지고 있다. 현재 전문 분야는 엔터프라이즈 자바 소프트웨어 설계와 개발이다. |
기사에 대한 평가
 |
| 이 문서 북마킹 하기
|
|