2011년 5월 24일 화요일

iBATIS in Action (6/14)

1. 고급 쿼리 기법

1.1. iBATIS에서 XML 사용하기

iBATIS 프레임워크에서는 쿼리에 파라미터를 전달할 때나 혹은 쿼리 결과를 반환받을 XML 사용할 있다. 경우 모두 XML 사용할 필요도 없으면서 XML 사용하는 것은 그다지 권장할 것은 못된다. 대신 POJO 사용하는 것이 대부분의 경우에 훨씬 효율적이기 때문이다.

6.1.1 XML 파라미터

완전히 동일한 구조를 갖추고 있는 문자열이나 DOM 객체를 통해서 XML 매핑 구문에 파라미터로 넘겨줄 있다.

구조의 형태는 파라미터로 전달할 값이 파라미터의 이름을 의미하는 요소로 감싸져 있고 요소를 다시 parameter 요소로 감싸고 있는 것이다. 예를 보자.

3

예에서 매핑 구문은 값이 3이고 이름이 “accountId” 파라미터 개를 전달받게 된다. 여기 XML 문자열을 매핑 구문에 파라미터로 넘겨주는 예제가 있다.

String parameter = "3";

Account account = (Account) sqlMapClient.queryForObject(

"Account.getByXmlId",

parameter);

비슷한 방식으로 DOM 객체도 동일한 결과를 있게 iBATIS 넘겨줄 있다.

Document parameterDocument = DocumentBuilderFactory.newInstance()

.newDocumentBuilder().newDocument();

Element paramElement = parameterDocument

.createElement("parameterDocument");

Element accountIdElement = parameterDocument

.createElement("accountId");

accountIdElement.setTextContent("3");

paramElement.appendChild(accountIdElement);

parameterDocument.appendChild(paramElement);

Account account = (Account) sqlMapClient.queryForObject(

"Account.getByXmlId", parameterDocument);

6.1.2 XML 결과 생성하기

iBATIS 프레임워크는 매핑 구문에서 XML 결과를 생성할 수도 있다. XML 반환하는 매핑 구문을 실행하면 각각의 반환된 객체를 완전한 XML 문서로 얻을 있다.

기능을 사용하기 위해, 결과 클래스를 “xml” 지정한 매핑 구문을 생성한다. 여기 간단한 예제가 있다.

String xmlData = (String) sqlMap.queryForObject(

"Account.getByIdValueXml",

new Integer(1));

경우, 반환 받은 결과는 다음과 같다.

1

lmeadors

blah

XML 문서로 넘겨받은 결과가 개의 레코드만을 가지고 있다면 정말로 다루기 쉽다. 만일 여러 개의 객체로 받기를 원한다면 그렇게 수도 있다. 아래에 예제가 있다.

List xmlList = sqlMap.queryForList("Account.getAllXml", null);

경우 결과는 XML 문서의 리스트이다. 이렇게 되면 개의 XML문서로 합치길 원할 경우 문자열 처리 작업을 해야만 한다. 이것은 최적의 방법이 아니다.

문제를 피해가는 방법으로 iBATIS 결과를 XML 반환하지 않게 하는 것이다. 간단한 방법으로 단순한 컬렉션을 반환하는 iBATIS 매핑 구문을 사용하고 그로부터 XML 생성하는 것이다. 이를 수행하는 가지 간단한 방법으로(결과로 빈즈를 사용한다면)아래와 같이 XML 생성을 도와주는 메서드를 만드는 방법이 있다.

public String toXml(){

StringBuffer returnValue = new StringBuffer("");

returnValue.append("");

returnValue.append("" + getAccountId() +"");

returnValue.append("" + getUsername() + "");

returnValue.append("" + getPassword() + "");

returnValue.append("");

return returnValue.toString();

}

문제에 대한 다른 접근 방법으로 리플렉션을 사용하여 빈즈를 XML 변환하는 클래스를 생성하는 방법이 있다. 방법은 아주 간단하다. 아래에 방법을 적용할 사용할 있는 작은 유틸리티가 있다.

public class XmlReflector {

private Class sourceClass;

private BeanInfo beanInfo;

private String name;

XmlReflector(Class sourceClass, String name) throws Exception {

this.sourceClass = sourceClass;

this.name = name;

beanInfo = Introspector.getBeanInfo(sourceClass);

}

public String convertToXml(Object o) throws Exception {

StringBuffer returnValue = new StringBuffer("");

if (o.getClass().isAssignableFrom(sourceClass)) {

PropertyDescriptor[] pd = beanInfo.getPropertyDescriptors();

if (pd.length > 0){

returnValue.append("<" + name + ">");

for (int i = 0; i < pd.length; i++) {

returnValue.append(getProp(o, pd[i]));

}

returnValue.append("");

} else {

returnValue.append("<" + name + "/>");

}

} else {

throw new ClassCastException("Class " + o.getClass().getName() +

" is not compatible with " + sourceClass.getName());

}

return returnValue.toString();

}

private String getProp(Object o, PropertyDescriptor pd)

throws Exception {

StringBuffer propValue = new StringBuffer("");

Method m = pd.getReadMethod();

Object ret = m.invoke(o);

if(null == ret){

propValue.append("<" + pd.getName() + "/>");

}else{

propValue.append("<" + pd.getName() + ">");

propValue.append(ret.toString());

propValue.append("");

}

return propValue.toString();

}

}

예제는 빈즈를 파라미터로 받아서 XML 문서가 아닌 XML 구성 요소 조각으로 변환한다. 아래에 예제가 있다.

XmlReflector xr = new XmlReflector(Account.class, "account");

xmlList = sqlMap.queryForList("Account.getAll", null);

StringBuffer sb = new StringBuffer(

"");

for (int i = 0; i < xmlList.size(); i++) {

sb.append(xr.convertToXml(xmlList.get(i)));

}

sb.append("");

기법을 사용해서 대용량의 레코드를 처리하는 것은 메모리 효율면에서 매우 좋지 못하다. 객체 리스트와 XML 문서를 생성하기 위한 문자열 버퍼를 메모리상에 저장하기 때문이다. 6.3절에서 예제를 다시 살펴보고 대용량의 결과를 처리하는 더욱 효율적인 방법을 알아볼 것이다.

1.2. 매핑 구문을 객체와 연관시키기

6.2.1 복잡한 컬렉션(collection)

4장에서는 SELECT 구문을 이용해 데이터 베이스로부터 데이터를 가져오는 방법을 살펴보았다. 예제에서는 여러 테이블들을 조인할 때조차도 결과로는 단일 객체 타입만을 다루었다. 복잡한 객체가 필요할 때에도 iBATIS 사용할 있다.

이러한 기능은 여러분이 만드는 애플리케이션의 모델이 데이터 모델처럼 보였으면 유용하게 사용할 있다. iBATIS 사용하여 연관된 객체들에 관한 데이터 모델을 정의하고, iBATIS 객체들을 한꺼번에 읽어들일 있다. 예를 들어 Account 레코드가 Order 레코드와 연관돼 있고 Order 레코드가 OrderItem 레코드와 연관돼 있을 , 이러한 관계를 정의하고 Account 요청하면 모든 Order 객체와 모든 OrderItem 객체들 또한 가져올 있다.

이러한 일을 있게 SQL Map 정의하는 방법은 아래와 같다.

PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"

"http://ibatis.apache.org/dtd/sql-map-2.dtd">

----------------------- (1)

class="org.apache.mapper2.examples.bean.AccountInfo">

column="accountId" />

select="Ch6.getOrderInfoList"

column="accountId" />

----------------------- (2)

class="org.apache.mapper2.examples.bean.OrderInfo">

select="Ch6.getOrderItemList" />

----------------------- (3)

class="org.apache.mapper2.examples.bean.OrderItem">

결과 (ResultAccountInfoMap (1), ResultOrderInfoMap (2), ResultOrderItemMap (3) 보면 처음 개는 매핑 프로퍼티 중의 하나로 select 속성을 사용함을 있다.

속성을 사용하면 iBATIS 프로퍼티가 select 속성의 값으로 지정된 다른 매핑 구문의 실행 결과로 설정된다고 간주하게 된다. 예를 들어 getAccountInfoList 구문 (4) 실행할 결과 ResultAccountInfoMap 포함하고 있다. 이것은 iBATIS에게 orderList 포로퍼티의 값을 얻어 오기 위해 accountId 칼럼 값을 파라미터로 넘겨서 “Ch6.getOrderInfoList” 매핑 구문을 실행하고(5) 반환 결과 데이터를 orderList 프로퍼티에 저장하라는 의미가 된다. 비슷한 방식으로 결과 ResultOrderInfoMap(2) orderItemList 프로퍼티의 값을 가져오기 위해 getOrderItemList(6) 매핑 구문을 실행하게 된다.

기능이 가진 편리함에도 불구하고 접근 방식은 가지 문제점을 가지고 있다. 첫째로 방식은 대용량의 메모리를 필요로 수도 있다. 번째로 아래에서 다룰 “N+1 Select 문제 알려진 현상으로 인해서 데이터베이스 I/O 문제를 일으킬 있다. iBATIS 프레임워크는 문제에 대한 해결책을 가지고 있다. 하지만 가지 문제를 한꺼번에 해결할 수는 없다.

- 데이터 베이스 I/O
iBATIS
제공하는 캐싱 매커니즘을 언제 어떻게 사용할지는 10장에서 살펴본다.
연관된 데이터를 사용할 부딪히게 데이터베이스 I/O 문제를 살펴보기 위해 1000개의 계좌(Account) 있고 각각 1000개의 주문(Order) 가지고 있으며, 각각의 주문이 25개의 주문 항목(OrderItem) 가지고 있다고 생각해보자. 모든 것을 메모리에 적재하려 든다면 이는 1000000 이상의 SQL 문장을 실행하고 2500 개의 자바 객체를 생성하게 된다. 이런 일을 하면 틀림없이 시스템 관리자에게 소리 듣게 것이다.

- “N+1 Select” 문제 살펴보기
“N+1 Select”
문제는 부모 레코드 리스트에 연관된 자식 레코드를 읽어들일 발생하게 된다. 부모 레코드를 가져오기 위해 개의 쿼리를 실행하면 부모가 어떤 “N” 만큼 있게되고 부모 레코드에 연관된 자식 레코드를 얻기 위해 “N”번의 쿼리를 실행해야 한다. 따라서 “N+1 Select” 라는 결과를 얻는다.

- 문제에 대한 해결책
적재 지연(6.2.2 절에서 알아볼 것이다.) 적재 처리 과정을 관리하기 좋은 수준으로 작게 쪼갬으로써 메모리 문제를 완화시켜준다. 그러나 여전히 데이터 베이스 I/O 문제가 남아있다.
데이터를 방식으로 연관 시킬 계좌를 주문에, 주문을 주문 항목에 연관시키는 것은 과연 좋은 예제일까? 주문과 주문 항목을 연관 짓는 것은 적절하다. 하지만 계좌를 주문과 연관시키는 것은 그다지 실질적인 것은 된다.
하지만 예제는 우리에게 친숙하고 이해할 있는 개념을 사용하여 여기서 배우고자 하는 기법을 보여줄 것이기에 당분간은 예제를 사용할 것이다.

6.2.2 적재 지연(lazy loading)

먼저 알아볼 해결책은 적재 지연이다. 적재 지연은 모든 연관된 데이터가 즉시 필요하지는 않은 경우에 유용하다. 예를 들어 우리의 애플리케이션이 모든 계좌를 보여주는 페이지에서 호출되고, 판매 대리인이 계좌를 클릭하면 계좌의 모든 주문을 보게되고, 마지막으로 특정 주문의 모든 세부 내역을 보기 위해 주문을 클릭하게 된다고 하자. 이러한 경우 우리에게 필요한 것은 시점에서 개의 리스트 뿐이다. 이것이 적재 지연의 적절한 사용 예이다.

적재 지연을 사용하려면 SqlMapConfig.xml 파일을 편집하여 요소의 lazyLoadingEnabled 속성을 ‘true’ 바꿔 이를 활성화 시켜야한다. cglib 확장판 적재 지연을 사용하고자 한다면 cglib 다운로드하여 애플리케이션의 클래스패스에 추가하고 요소의 enhancementEnabled 속성도 ‘true’ 변경해 주면 된다. 가지 주의할 점으로 이것은 전역적인 설정이므로 속성들을 활성화하면 SQL Map 모든 매핑 구문들이 적재 지연을 사용하게 된다는 점이다.

6.2.3 N+1 Select 문제 피해가기

“N+1 Select” 문제를 피해갈 있는 방법은 가지가 있다. 가지는 iBATIS groupBy 속성을 사용하는 것이고 다른 한가지는 로우 핸들러(Row Handler) 라는 사용자 정의 컴포넌트를 사용하는 것이다.

groupBy 속성을 사용하는 것은 바로 전에 알아본 기법과 매우 유사하다. 간단히 말해서 결과 맵을 사용하여 연관 관계를 정의하고 최상위 결과 맵을 매핑 구문과 결합시키면 된다. 아래 예제는 위에 나온 적재 지연 예제와 완전히 동일한 구조를 생성하지만 서버에서는 하나의 SQL 문장만이 실행된다.

가지의 결과 맵이 연관돼 있는데 하나는 계좌, 하나는 주문 그리고 나머지는 주문 항목을 위한 것이다.

계좌를 위한 결과 맵은 가지 기능을 한다.

l 계과 객체들의 프로퍼티를 매핑한다.

l iBATIS 에게 어떤 프로퍼티가 새로운 계좌를 생성하는데 필요한지 알려준다.

l iBATIS 에게 연관된 다음 객체 묶음을 어떻게 매핑할지 알려주는데, 경우에 연관된 객체 묶음이란 해당 계좌에 연관된 주문 객체의 묶음이다.

주의해야 매우 중요한 사항이 하나 있는데, groupBy 속성은 프로퍼티의 이름을 지정하는 것이지 테이블 칼럼 이름을 지정하는 것이 아니다.

주문을 위한 결과 맵도 동일한 가지 기능을 가지고 있다.

l 주문 데이터를 주문 객체에 매핑한다.

l 어떤 프로퍼티가 새로운 주문을 가리키는지 지정한다.

l 자식 레코드들을 가져오기 위해 어떤 결과 맵을 사용할지 지정한다.

마지막으로 주문 항목 결과 맵은 그냥 일반적인 결과 맵으로 오직 주문 항목을 객체로 매핑시키는 역할만을 한다. 아래는 이에 대한 매핑 예제이다.

ß-------- (1) 계좌 데이터를 위한 결과맵 선언

class="AccountInfo"

groupBy="account.accountId" >

column="accountId" />

resultMap="Ch6.ResultOrderInfoNMap" />

ß--------(2) 주문 데이터를 위한 결과맵 선언

class="OrderInfo"

groupBy="order.orderId" >

resultMap="Ch6.ResultOrderItemNMap" />

ß--------(3) 주문항목을 위한 결과맵 선언

class="OrderItem">

column="orderId" />

column="orderItemId" />

요소에 묶기

resultMap="ResultAccountInfoNMap">

select

account.accountId as accountid,

orders.orderid as orderid,

orderitem.orderitemid as orderitemid

from account

join orders on account.accountId = orders.accountId

join orderitem on orders.orderId = orderitem.orderId

order by accountId, orderid, orderitemid

getAccountInfoListN 매핑 구문을 호출하여 (4) 앞의 예제에서 보여준 것과 동일한 결과 데이터를 얻게 된다. 하지만 경우에는 오직 개의 SQL 문만을 실행하기 때문에 예제보다 훨씬 빠르다. 이것은 getAccoutnInfoListN 매핑 구문 (4) 실행되면 결과는 groupBy 속성을 사용하는 resultAccountInfoNMap 결과 (1) 통해서 매핑을 수행하여 생성된다. 속성은 iBATIS 에게 account.accountId 프로퍼티가 변경될 때에만 새로운 AccountInfo 인스턴스를 생성하면 된다고 지시한다. 살펴보면 orderList 프로퍼티가 ResultOrderInfoNMap 결과 (2) 매핑이 되어 있기 때문에 쿼리 결과로 나온 레코드를 처리해 가면서 주문 리스트도 생성하게 된다. ResultOrderInfoNMap 결과 맵도 또한 groupBy 속성을 사용하기 때문에 orderItemList 프로퍼티의 ResultOrderItemNMap 결과 (3) 이용하여 orderItemList 생성하는 과정을 반복할 것이다.

1.3. 상속

6.3.1 상속 매핑하기

iBATIS 구별자(discriminator) 라고 불리는 특별한 매핑을 사용하여 상속 구조를 지원한다. 구별자를 사용하는 것으로 데이터베이스의 값에 기초하여 객체를 생성할 클래스의 타입을 판단할 있다. 구별자는 결과맵의 일부이고 switch 구문처럼 작동한다. 예를 들면:

구별자는 이렇게 읽을 있다.

- 칼럼 “TYPE” “Book” 이라는 값을 포함한다면, “book” 이라는 결과맵을 사용하라. 반면에 칼럼 “TYPE” “Newspaper” 라는 값을 포함한다면, “news” 라는 결과맵을 사용하라.

하위 맵은 이름으로 참조하는 일반적인 결과맵이다. 구별자가 하위 하나와 일치하는 값을 찾지 못한다면, 상위 결과맵이 적용된다. 하지만 적절한 값이 있다면 오직 하위 맵만이 적용된다. 부모에 정의된 결과 매핑은 다음 예제에서 보듯이 하위 맵이 부모 맵을 명시적으로 확장하지 않는 적용되지 않는다.

결과맵의 extends 속성은 참조하는 결과맵에서 모든 결과 매핑을 효과적으로 복사한다. 어쨌든 이것은 클래스 구조에 대한 어떠한 것도 의미하지 않는다. iBATIS 객체 관계 매핑 프레임워크가 아니라는 것을 기억해라. 차이점은 iBATIS 클래스와 데이터베이스 테이블 간의 매핑을 알거나 다루지 않는다는 것이다.

1.4. 잡다한 다른 활용법들

6.4.1 statement 타입과 DDL 사용하기

타입은 다른 타입들 (, , ,

select distinct

p.productId as productId,

o.accountId as accountId,

m.manufacturerId as manufacturerId

from product p

join manufacturer m

on p.manufacturerId = m.manufacturerId

join orderitem oi

on oi.productId = p.productId

join orders o

on oi.orderId = o.orderId

order by 1,2,3


AccountManufacturerProduct
클래스는 단순한 클래스로 가지 프로퍼티(account, manufacturer, product) 가지고 있다. 결과 (AmpRHExample) 데이터를 평평하게 바라보는 방식으로 생성할 처럼 프로퍼티의 값을 자동으로 설정해준다.
다음에는 로우 핸들러가 객체들을 받아서 객체를 만날때마다 이를 productId, accountId, manufacturerId 따라 객체로 분류해 넣는다. 각각의 account 혹은 product 처음으로 받아들일 해당 객체를 account 리스트나 product 리스트에 각각 추가한다. 만약 선택된 객체가 이미 저장되어 있을 경우, 이미 (Map상에) 존재하는 객체가 데이터베이스에서 방금 가져온 객체를 대체하게 된다.
마지막으로 특정 account, manufacturer 혹은 product 단일 인스턴스를 완전히 생성한 뒤에 이를 적합한 객체에 추가하게 된다. 이는 다음에서 있다.

public class AMPRowHandler implements RowHandler {

private Map accountMap

= new HashMap();

private Map manufacturerMap

= new HashMap();

private Map productMap

= new HashMap();

private List productAccountList

= new ArrayList();

private List accountManufacturerList

= new ArrayList();

public void handleRow(Object valueObject) {

AccountManufacturerProduct amp;

amp = (AccountManufacturerProduct)valueObject;

Account currentAccount = amp.getAccount();

Manufacturer currentMfgr = amp.getManufacturer();

AccountManufacturers am;

ProductAccounts pa;

Product currentProduct = amp.getProduct();

if (null == accountMap.get(currentAccount.getAccountId())) {

// this is the first time we have seen this account

am = new AccountManufacturers();

am.setAccount(currentAccount);

accountMap.put(currentAccount.getAccountId(), am);

accountManufacturerList.add(am);

} else {

// Use the accoutn from the account map

am = accountMap.get(currentAccount.getAccountId());

currentAccount = am.getAccount();

}

// am is now the current account / manufacturerlist

if (null ==

manufacturerMap.get(currentMfgr.getManufacturerId())) {

// we have not seen this manufacturer yet

manufacturerMap.put(

currentMfgr.getManufacturerId(),

currentMfgr);

} else {

// we already have this manufacturer loaded, reuse it

currentMfgr = manufacturerMap.get(

currentMfgr.getManufacturerId());

}

am.getManufacturerList().add(currentMfgr);

if (null == productMap.get(currentProduct.getProductId())) {

// this is a new product

pa = new ProductAccounts();

pa.setProduct(currentProduct);

productMap.put(currentProduct.getProductId(), pa);

productAccountList.add(pa);

} else {

// this prodcut has been loaded already

pa = productMap.get(currentProduct.getProductId());

}

// pa is now the current product's product / account list

pa.getAccountList().add(currentAccount);

am.getManufacturerList().add(currentMfgr);

}

public List getProductAccountList() {

return productAccountList;

}

public List getAccountManufacturerList() {

return accountManufacturerList;

}

public Map getProductMap() {

return productMap;

}

public Map getAccountMap() {

return accountMap;

}

}
코드가 복잡해 보이긴 하지만, 코드가 하는 역시 굉장히 복잡하기 때문에 어쩔 없다.

댓글 없음:

댓글 쓰기