1. 동적인 SQL 사용하기
1.1. 동적인 WHERE 조건절 다루기
다음 예제에서 장바구니 어플리케이션의 Category 테이블을 조회한다. parentCategoryId 칼럼은 자기 자신을 참조하는 칼럼이다. 다시 말하면 parentCategoryId는 그림에서 보듯이 동일한 Category 테이블의 categoryId를 참조한다.
요구사항은 간단하다. 전달받은 Category객체의 parentCategoryId 프로퍼티가 null 이면 이것은 가장 상위 레벨의 카테고리를 모두 다 조회한다는 의미이다.
parentCategoryId 프로퍼티가 null이 아닌 숫자 값이라면 부모 category 의 자식 category 객체를 모두 조회한다는 것을 뜻한다. 부모 category란 parentCategoryId 의 값을 의미한다.
SQL 에서는 =기호를 null 값 비교에 사용할 수 있다. NULL값인지 여부는 IS 키워드로 확인해야 한다. 우리는 NULL인지 혹은 NULL이 아닌지를 비교하기 위해 동일한 SQL 구문을 사용하고자 한다. 이를 하나의 매핑 구문으로 처리하기 위해서 동적 SQL 을 사용할 것이다. 동적 SQL의 뼈대를 알아보기 위해 다음의 매핑 구문을 사용할 것이다.
…
…
동적 SQL의 뼈대는 언제나 부모 요소로 시작한다. 부모 요소는 동적 SQL 요소 중 어떤 것이라도 될 수 있다. 위 코드의 경우 부모 요소로
부모 요소의 몸체에는 간단한 SQL 문법이나 다른 동적 SQL 요소가 온다. 이 예제에서
이 시점에서, 언제 prepend 속성의 값이 다른 SQL 구문 앞에 출력하게 되는지 알고 있어야 한다.
동적 SQL의 이점 중 하나는 SQL 코드의 재사용성을 높여준다는 것이다. 이 예제에서 동적 SQL을 사용하지 않고서 위와 같은 기능을 구현하려면 두 개의 select 구문을 작성해야 했을 것이다. 게다가 DAO계층에서 category 객체의 parentCategoryId 값을 먼저 확인하고, 이 parentCategoryId값이 null인지 아닌지 여부에 따라 적당한 매핑 구문을 선택해서 호출해야 한다. 비록 이번 예제는 간단해서 동적 SQL이 없더라도 별로 불편할 게 없지만, 여러 서로 다른 프로퍼티의 조합으로 인해 SQL구문이 기하급수적으로 증가할 가능성이 있을 경우에는 동적 SQL의 진정한 가치가 분명하게 드러난다. 동적 SQL을 사용하여 매핑 구문을 재사용하도록 하고 정적인 SQL 구문을 여러 개 작성하는 것은 피하도록 한다.
이제 동적 SQL의 사용법과 강력함에 대해 이해했을 것이다. 모든 요소와 속성을 좀 더 상세히 알아보자.
1.2. 동적 요소들과 친숙해지기
iBATIS는 강력한 XML 요소들을 통해 동적 SQL문제를 처리한다. 이 요소들은 매핑 구문에 넘어온 파라미터 객체에 관한 다양한 조건들을 평가하는 역할을 한다. 존재하는 모든 요소들을 이해하고 그것들이 올바르게 SQL을 출력하는 데 어떤 역할을 하는지 이해하고 있어야 한다. 다음 절에서는 요소들을
모든 동적 요소는 prepend, open, close 속성을 포함한다. Open과 close속성은 각각의 요소에서 똑같이 작동한다. 그 두 개의 속성은 동적으로 생성된 결과 내용의 시작이나 끝에 무조건 값은 붙인다. Prepend 속성은
…
…
y=#y#
prepend="AND" open="(" close=")">
a=#x.a#
a=#x.b#
a=#x.c#
…
…
. (1)에서 시작
첫 번째 prepend 제거 기능은 모든 요소에서 자동으로 혹은 명시적 지정을 통해 사용할 수 있다.
공통 기능의 마지막 부분은 모든 요소를 서로 독립적으로 사용할 수 있다는 것이다. 이것은
그럼 이제부터 요소들의 카테고리 각각을 분석해보자.
8.2.1 요소
이제 요소에서 사용할 수 있는 속성들을 모두 살펴보았다. 다음은
…
…
여기서 동적인 SQL을 사용해서, parentCategoryId 프로퍼티를 검사하여 SQL을 생성하는 select 구문의 WHERE절을 구성하였다.
8.2.2 이항연산 요소
이항연산 요소는 파라미터 프로퍼티의 값을 다른 값이나 다른 파라미터 프로퍼티 값과 비교한다. 요소의 내용은 비교 결과가 true일 때 사용된다. 모든 이항연산 요소는 property, compareProperty, compareValue 속성을 공통으로 가지고 있다. Property 속성은 비교할 기본값을 제공한다. compareProperty 와 compareValue 는 비교 대상이 되는 값을 제공한다. compareProperty 속성에는 기본값에 대해 비교할 값을 가지고 있는 파라미터 객체의 프로퍼티를 명시한다. compareValue 에는 기본값에 대해 비교할 정적인 값을 명시한다. 요소의 이름은 값의 비교 방법을 의미한다. 요소의 속성은 표에서 볼 수 있다.
Property (필수) | compareValue 나 compareProperty 와 비교하는데 사용할 파라미터 프로퍼티 |
Prepend (선택적) | 이 값은 보통 요소 내용의 가장 앞에 출력된다. 다음의 경우에는 prepend의 값이 출력되지 않고 무시된다. (a) 요소의 결과 내용이 공백인 경우, (b)요소가 처음으로 내용을 만들어 냈고 removeFirstPrepend 속성이 true 인 요소에 내포되어 있는 경우, (c) |
Open (선택적) | Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. Open 값은 prepend 속성 값보다는 뒤에 붙는다. 예를 들어 prepend=”OR” 이고 open=”(“ 라면 조합된 결과는 “OR(“가 될 것이다. |
Close(선택적) | Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 이 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다. |
removeFirstPrepend( 선택적) | 이 값은 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다. |
compareProperty(compareValue가 명시되지 않았다면 필수) | compareProperty는 property 속성에 의해 명명된 프로퍼티와 비교할 파라미터 객체의 프로퍼티를 명시한다. |
compareValue(compareProperty가 명시되지 않았다면 필수) | Property 속성에 의해 명명된 프로퍼티와 비교할 정적인 비교값 |
모든 이항연산 동적 요소는 위의 속성들을 공유하고, 요소 이름 자체는 다음의 표에서 볼 수 있다.
| Property 속성값이 compareProperty 값이나 compareValue 값과 같은지 검사 |
| Property 속성값이 compareProperty 값이나 compareValue 값과 같지 않은지 검사 |
| Property 속성값이 compareProperty 값이나 compareValue 값보다 큰지 검사 |
| Property 속성값이 compareProperty 값이나 compareValue 값보다 크거나 같은지 검사 |
| Property 속성값이 compareProperty 값이나 compareValue 값보다 작은지 검사 |
| Property 속성값이 compareProperty 값이나 compareValue 값보다 작거나 같은지 검사 |
위 표는 좋은 참조가 되겠지만 예제가 충분치 않다. 아래에서 어떻게 이 요소들을 함께 사용하는지 보자.
…
…
위 에서 select 구문을 생성하고 사용할 shippingType 의 값이 100보다 작은 standard 타입인지, 100보다 큰 freight 타입인지 결정하기 위해 weight 프로퍼티를 검사한다.
8.2.3 단항연산 요소
단항연산 동적 요소는 빈즈 프로퍼티의 상태를 검사만하고 다른 값에 대한 비교는 수행하지 않는다. 상태의 결과가 true 라면 내용이 포함된다. 모든 단항연산 요소는 property 속성을 공유한다. Property 속성에는 상태를 검사할 파라미터 객체의 프로퍼티 이름을 지정해준다.요소의 이름은 어떤 상태를 검사할지를 의미한다. 표는 단항연산 요소의 속성을 보여준다.
Property(필수) | 상태 비교를 위해 사용되는 파라미터의 변수명 |
Prepend(필수) | 이 값은 요소 내용의 가장 앞에 출력된다. 다음의 경우에는 prepend 의 값이 출력되지 않고 무시된다. (a) 요소의 결과 내용이 공백인 경우, (b) 요소가 처음으로 내용을 만들어 냈고 removeFirstPrepend 속성이 true 인 요소에 내포되어 있는 경우, (c) |
Open(선택적) | Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. open값은 prepend 속성값보다는 뒤에 붙는다. 예를 들어, prepend=”OR” 이고 open=”(“ 라면 조합된 결과는 “OR (“가 될 것이다. |
Close(선택적) | Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 이 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다. |
removeFirstPrepend(선택적) | 이 값으로 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다. |
위 표의 모든 속성은 아래의 표에 있는 모든 단항연산 동적 SQL 요소에서 사용 가능하다.
| 명시된 프로퍼티가 파라미터에 존재하는지 검사. 빈즈에서는 프로퍼티를 찾고 Map에서는 키를 찾는다. |
| 명시된 프로퍼티가 파라미터에 존재하지 않는지 검사. 빈즈에서는 프로퍼티를 찾고 Map에서는 키를 찾는다. |
| 명시된 프로퍼티가 null 인지 검사. 빈즈에서는 프로퍼티를 찾고 Map에서는 key를 찾는다. 키가 존재하지 않으면 true를 반환한다. |
| 명시된 프로퍼티가 null이 아닌 다른 값인지를 검사. 빈즈에서는 프로퍼티를 찾고 Map에서는 키를 찾는다. 키가 존재하지 않는다면 false를 반환한다. |
| 명시된 프로퍼티가 null 이거나 빈 문자열(“”), 빈 컬렉션이나 빈 String, valueOf() 인지를 검사 |
| 명시된 프로퍼티가 null이 아니거나 빈 문자열(“”), 빈 컬렉션, 빈 String, valueOf() 가 아닌지 검사 |
아래는 단항연산 동적 SQL 요소를 어떻게 사용하는지 보여준다.
…
…
여기서는 간단한 select 매핑 구문을 생성하고, 동적 SQL 요소를 사용하여 productType 프로퍼티의 값에 따라 선택적으로 결과를 걸러서 가져온다.
8.2.4 파라미터 요소
파라미터 없이 매핑 구문을 정의하는 것도 가능하다. 파라미터 요소는 파라미터가 매핑 구문으로 전달되는지를 검사하기 위해 만들어졌다. 표는 요소의 속성을 보여준다.
Prepend(선택적) | 이 값은 요소 내용의 가장 앞에 출력된다. 다음의 경우에는 prepend 값이 출력되지 않고 무시된다. (a) 요소의 결과 내용이 공백인 경우, (b) 요소가 처음으로 내용을 만들어 냈고 removeFirstPrepend 속성이 true인 요소에 내포되어 있는 경우, (c) |
Open(선택적) | Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. Open 값은 prepend 속성 값보다는 뒤에 붙는다. 예를 들어 prepend=”OR” 이고 open=” (“ 라면 조합된 결과는 “OR (“ 가 될 것이다. |
Close(선택적) | Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 이 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다. |
removeFirstPrepend(선택적) | 이 값으로 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다. |
모든 속성은 아래에 있는 모든 요소에서 사용 가능하다.
다음은 select 구문에서 파라미터 요소를 사용하는 방법을 보여준다.
…
…
이 예제에서 다시 간단한 select 구문을 생성하고, 이번에는 productType 파라미터를 기반으로 해서 결과 값을 걸러내는 WHERE 절을 선택적으로 생성해준다.
8.2.5 요소
Property(필수) | 리스트(컬렉션 혹은 배열)을 포함하고 있는 파라미터의 프로퍼티 |
Prepend(선택적) | 이 값은 요소 내용의 가장 앞에 출력된다. 다음의 경우에는 prepend 의 값이 출력되지 않고 무시된다. (a) 요소의 결과 내용이 공백인 경우, (b) 요소가 처음으로 내용을 만들어 냈고 removeFirstPrepend 속성이 true 인 요소에 내포되어 있는 경우, (c) |
Open(선택적) | Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. Open 값은 prepend 속성값보다는 뒤에 붙는다. 예를 들어 prepend=”OR” 이고 open=”(“라면 조합된 결과는 “OR (“가 될 것이다. |
Close(선택적) | Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 이 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다. |
Conjunction(선택적) | 이 속성의 값은 값 목록의 사이 사이에 출력되어 SQL 문장을 구성한다. |
removeFirstPrepend(선택적) | 이 값으로 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다. |
아래는
…
…
이 예제에서는 select 문을 만들고, 그 다음에 제품 타입(product type) 목록을 반복적으로 돌아서, 제품 타입에 따라 결과를 필터링하는 좀 더 복잡한 조건절을 생성한다.
1.3. 모두 적용한 간단한 예제
동적 SQL의 기본 지식을 모두 배웠다. 간단한 검색 기능을 구현하여 애플리케이션에서 사용해보자. 이 예제에서는 JGameStore 라는 애플리케이션을 사용할 것이다. 이 애플리케이션은 14장에서 정식으로 소개할 예정이다. 이 애플리케이션을 구축하면서 동적 SQL의 개념을 이해하고 조합해서 사용하는데 도움이 될 수 있도록, 간결한 접근 방법을 적용할 것이다.
예제를 보기전에, 우리가 사용할 프로세스를 살펴보자. 프로세스 그 자체는 상당히 간단하다. 그리고 애플리케이션이 성숙해감에 따라 각 처리 과정들은 서로 다른 순서와 다양한 수준의 노력을 요할 수 있다. 초반에 해줘야 하는 작업이 약간 더 있다. 먼저 기반을 다져놔야 하기 때문이다. 일단 기반을 다지고 나면 그 위에 무언가를 구축하는 것은 별로 복잡하지 않다.
프로세스는 몇 가지 기본적인 단계로 구성된다.
1. 데이터를 가져오고 표시하는 방법을 지정한다.
2. 어떤 데이터베이스 구조를 사용할 지 결정한다.
3. 정적인 형태로 SQL을 작성한다.
4. 정적인 SQL에 동적 SQL 요소를 적용한다.
상당히 간단한 페이지이지만, 모두 알다시피 항상 눈에 보이는 것보다 더 많은 일이 뒤에서 이뤄지게 마련이다. 다음 절에서 위의 작업을 수행하는 코드를 살펴볼 것이다.
8.3.1 데이터를 가져오고 표시하는 방법을 정의하기
JGameStore 애플리케이션의 각 페이지에 간단한 검색 필드와 검색 버튼을 구현할 것이다. 검색 필드는 공백으로 각 단어를 구분한다. 예를 들어 ‘Adventure Deus’ 를 입력했다면 ‘Adventure’ 와 ‘Deus’ 라는 두 개의 단어로 분리될 것이다. 각각의 단어를 제품의 카테고리 변호 (CategoryId), 제품명(name), 그리고 설명(description) 내에서 찾을 수 있는지 검사할 것이다. 일단 검색 버튼을 클릭하면 결과로 나온 모든 제품들을 한 페이지당 네 개씩 출력한다.
8.3.2 데이터베이스 구조 결정하기
테이블 구조를 어떻게 정의할 지 알아보자. 우리는 categoryId, name, 그리고 description에 대해 검색을 한다. 따라서 Product 테이블이 이러한 요구사항을 충족시킬 수 있는 정도가 되게끔만 만들자. Product 테이블은 검색하고 출력할 필요가 있는 모든 제품 정보를 포함하고 있다.
8.3.3 정적인 형태로 SQL 작성하기
처음 시작할 때는 검색 결과에서 표시해줄 모든 필드를 다 가져오는 정적인 SQL 을 만든다. 다음에서 제품 정보를 출력하는데 사용할 정적인 쿼리를 볼 수 있다.
SELECT
PRODUCTID,
NAME,
DESCRIPTION,
IMAGE,
CATEGORYID
FROM PRODUCT
WHERE
lower(name) like 'adventure%' OR
lower(categoryid) like 'adventure%' OR
lower(description) like 'adventure%' OR
lower(name) like 'Deus%' OR
lower(categoryid) like 'deus%' OR
lower(description) like 'deus%'
이제 입력과 출력 그리고 데이터 베이스 테이블을 모두 갖추었다. SQL 구문의 모형을 만들때가 된 것이다. 앞서 언급한 정보를 가지고 있다면 SQL을 생성하는 것은 지극히 간단하다. 우리에게 필요한 제품 목록은 SELECT 구문을 통해 받을 수 있다. Name, categoryId 그리고 description 과 제공받은 단어를 비교하는 검색 조건은 WHERE 절에 나온다.
8.3.4 동적인 SQL 요소를 정적인 SQL에 적용하기
모의 SQL 구문을 살펴보면 SQL 구문을 상세히 분석하고 어디에서 동적인 요소를 사용할지 결정할 수 있다. SELECT 절은 정적이기 때문에 여기에선 아무런 동적인 요소도 필요 없다. WHERE 절은 동적으로 변경해줄 필요가 있다.
동적 요소들로 매핑 구문에 전달받은 파라미터를 활용할 수 있다. 따라서 어떤 파라미터들이 있고, 이 파라미터들에 동적 요소를 어떻게 적용할 지 생각해 볼 필요가 있다. 매핑 구문이 받는 파라미터는 String(java.lang.String) 의 리스트이다. 간단한 리스트를 직접 사용하기 때문에
sqlMap.queryForPaginatedList(
“searchProductList”, parameterObject, PAGE_SIZE);
마지막 요구 사항으로 한 번에 네 개의 레코드만을 반환해야 한다. 이를 위해 pageSize 파라미터를 받는 queryForPaginatedList() 메서드를 사용해서 매핑 구문을 호출하여 이 요구 사항을 처리할 것이다. 이렇게 하면 반환되는 레코드의 개수를 제어할 수 있다.
간단한 예제를 전체적으로 둘러보면서 동적 SQL 작성 계획을 세우고 개발하는 방법을 공부해 보았다. 동적 SQL 은 여러가지 선택사항이 필요한 한 가지 목적에만 사용해야 한다는 것을 기억하라. 여러 가지 목적을 달성하기 위해 동적 SQL을 사용해서는 안 된다. 예제에서 동적 SQL의 목적은 사용자가 어떻게 제품을 선택할지를 처리하는 것이다. 명심해야할 것은 동적 SQL은 복잡한 것을 간단하게 만들려고 나온 것이지, 간단한 것을 더 복잡하게 만들려고 나온 것이 아니라는 점이다. 다음으로 동적 SQL의 좀 더 상세한 사용법을 살펴보자.
1.4. 고급 동적 SQL 기법
8.4.1 결과 데이터 정의하기
일반적인 말로 우리가 출력하고자 하는 것을 정의해보자. 우리의 장바구니 애플리케이션은 사용자가 지정한 검색 조건에 따라 출력할 제품 목록을 필요로 한다. 출력은 categories, products, 그리고 manufacturers 를 포함한 선택 조건에 기초하여 결정될 것이다. 제품은 페이지당 4개씩 표시되어야만 한다. 동적 SQL은 페이지 처리된 제품 목록을 생성하고 사용자가 입력하는 복잡하고 다양한 값을 처리할 수 있어야만 한다.
그렇다면 검색 페이지는 데이터베이스와 어떤 식으로 작동할까? 아래에서 이 물음에 대한 답변의 일부를 볼 수 있다.
public class ProductSearchCriteria {
private String[] categoryIds;
private String productName;
private String productDescription;
private String itemName;
private String itemDescription;
…
// setters and getters
}
본래, 검색 페이지의 각 필드는 간단한 자바빈즈인 검색 기준 클래스의 프로퍼티에 매핑된다.
8.4.2 정적인 형태로 SQL 작성하기
이제 입력과 출력을 모두 정의했으니, 데이터베이스 쿼리 툴에서 실행할 수 있는 간단한 정적 SQL을 사용하여 SQL을 작성할 수 있다. 다음에서 개발의 시작점이 되면서 동적 SQL을 구성하는 기초가 될 정적 SQL 구문을 볼 수 있다.
SELECT
p.PRODUCTID AS PRODUCTID,
p.NAME AS NAME,
p.DESCRIPTION AS DESCRIPTION,
p.IMAGE AS IMAGE,
p.CATEGORYID AS CATEGORYID
FROM Product p
INNER JOIN Category c ON
c.categoryId=p.categoryId
INNER JOIN Item i ON
i.productId = p.productId
WHERE
c.categoryId IN ('ACTADV')
AND
p.name LIKE '007'
AND
p.description LIKE '007'
AND
i.name LIKE 'PS2'
AND
i.description LIKE 'PS2'
우리는 이제 필요한 입력이 무엇이고, 어떤 테이블이 필요한지 알고 있으므로 모의로 정적 SQL 쿼리를 만들어 볼 수 있다. 우리의 정적 SQL 예제에서 WHERE 조건절이 조합 가능한 모든 조건들을 모두 다 포함하여 작성되었음을 알 수 있다. 무엇을 동적으로 만들어야 하는지 알려면 정적 SQL을 모든 사항을 고려해서 말들어 보는 것이 중요하다. 동적 SQL을 조합하는 부분을 보자.
8.4.3 동적 SQL 요소를 정적 SQL에 적용하기
모의로 정적 SQL을 모두 만들었으니, 이제 동적 요소들을 적용해보자. 다듬에서 볼 수 있듯이 동적 SQL 은 조금 더 복잡해진다. 가장 상위 레벨의 요소로
시작 select 요소 (1)에서 파라미터 클래스 타입은 productSearch, 그리고 결과 클래스 타입은 product라는 별칭을 사용하여 정의하였다. 이 SQL 부분 (2)은 이 매핑 구문에서 생성할 수 있는 가장 작은 단위의 SQL이다. 예를 들면 사용자가 모든 것을 반환하도록 검색하고자 하는 경우에 이 SQL을 사용한다.
그렇다면 저 괴물 같은 구문을 어떻게 호출할까?
queryForPaginatedList (
“product.searchProductsWithProductSearch”,
productSearch, PAGE_SIZE);
앞의 예제에서 우리의 마지막 요구사항은 한 번에 네 개의 레코드만을 반환해야 한다는 것이었다. 이를 위해 페이지 크기 파라미터를 받는 queryForPaginatedList 메서드를 사용하여 구문을 호출한다. 범위를 지정하는 queryForPaginatedList 를 사용하는 방법에 대해서는 8.3.4절의 예제를 보라.
복잡한 예제를 보며 다소 복잡한 동적 SQL을 어떻게 수행하는지 공부하였다. 여기서는 동적 SQL이 사용자가 입력한 검색 기준에 따라 제품 목록을 제공한다는 단 하나의 목적만을 수행하도록 하였다. 비록 이 예제가 조금 복잡해 보이긴 하지만, 모든 것을 자바 코드로만 작성하는 것이 훨씬 더 어렵다는 것은 명백하다. 이제 당신 자신의 동적 SQL을 구성할수 있는 준비가 되었으리라 믿는다.
iBATIS의 동적 SQL을 동적 SQL을 처리하는 다른 솔루션들과 비교하는 방법을 이해하는것도 중요하다. 동적 SQL을 수행하는 몇몇 다른 방법들을 간단히 알아보자.
1.5. 동적 SQL에 대안이 되는 접근법
동적 SQL은 결코 새로운 개념이 아니다. 조건에 따라 생성되는 SQL 쿼리는 항상 발생하는 복잡한 요구사항이다. 과거에는 동적 SQL을 효율적으로 수행하기 위해서 저장 프로시저를 사용하여 호환성이 없는 데이터베이스 내부적인 접근 방법으로 처리하였다. 다른 방식으로는 좀 더 견고한 프로그래밍 언어로 SQL을 구성하여 드라이버를 통해 데이터베이스에 쿼리를 전달해 주었다. 하지만 이는 성능을 떨어뜨린다. 두 경우 모두, 간단한 SQL 문자열을 구성하는 방법이 비대하고 점점 더 복잡해진다. 이미 여러분이 iBATIS를 사용하고 있다면, 이번 절은 여러분이 잊고 있던 것을 간단히 다시 상기시켜줄 것이다. iBATIS가 처음이라면 이는 신선한 비교가 될 것이다. 당신의 배경지식에 관계없이, 이번 절에서 당신의 업무에 몇 가지 신선한 통찰력을 제공해 주고, 여러분이 겪고 있는 복잡한 일의 대부분을 iBATIS가 줄여 줄 수 있다는 점을 알려주고 싶다.
우리는 자바코드 예제와 저장 프로시저 예제에서 동일한 SQL 구문을 공유할 것이다. 다음은 코딩하기에 적당히 간단하고, 그다지 복잡하지 않은 select 쿼리 구문이다. 각각의 접근 방법이 어떻게 동적 SQL의 문제를 해결하는지 보여주고 나서, 그 방법이 iBATIS가 사용하는 방법과 어떻게 다른지 간단하게 요약해서 보여줄 것이다.
SELECT *
FROM Category
WHERE
categoryId IN ('ACTADV','SPORTS','STRATEGY') AND
name LIKE ('N%')
8.5.1 자바 코드 사용하기
자바로 코드를 작성하는 것은 매우 멋진 일이다. 하지만 자바의 SQL을 혼합할 때는 명확성을 잃지 않는 방법으로 코드를 세심하게 다루어야 한다. 요구 사항이 점점 복잡하게 늘어남에 따라, 코드의 부분 부분들이 모두 어디 있는지 추적하기가 어려워진다. 동적 SQL 구문을 조합하기 위해 JDBC를 사용하고 데이터베이스에 구문을 전달하는 다소 복잡한 예제를 살펴보자. 다음은 SQL 구문을 구성하는데 사용할 검색 기준을 보여준다.
public class CategorySearchCriteria implements Serializable {
private String firstLetter;
private List categoryIds;
…
// setters and getters
}
위 예제의 다소 복잡한 SQL 구문은 CategorySearchCriteris 의 categoryIds 프로퍼티로부터 정확히 몇 개인지는 정해지지 않은 카테고리 아이디들을 가져올 것이다. 이 프로퍼티는 IN 구문을 생성하는 데 사용될 것이다. firstLetter 프로퍼티의 값은 알파벳 문자 하나이다. 이는 카테고리 이름의 첫 번째 문자로 검색을 수행하는 데 사용된다. 이 예제에서 우리의 관심사는 JDBC의 상호작용과 동적 SQL 솔루션들간의 비교에 있다. 그래서 우리는 그 관심사 외에는 어느 것도 설명하지 않을 것이다.
다음은 오직 자바코드를 사용하여 동적 SQL을 구성하는 방법을 보여준다.
public class CategorySearchDao {
…
public List searchCategory(
CategorySearchCriteria categorySearchCriteria) {
List retVal = new ArrayList();
try {
Connection conn =
ConnectionPool.getConnection("MyConnectionPool");
PreparedStatement ps = null;
ResultSet rs = null;
List valueList = new ArrayList();
StringBuffer sql = new StringBuffer("");
sql.append("SELECT * ");
sql.append("FROM Category ");
if(categorySearchCriteria.getCategoryIds() != null
&& categorySearchCriteria.getCategoryIds().size() > 0) {
Iterator categoryIdIt =
categorySearchCriteria.getCategoryIds().iterator();
sql.append("WHERE ");
sql.append("categoryId IN (");
if(categoryIdIt.hasNext()) {
Object value = categoryIdIt.next();
valueList.add(value);
sql.append("?");
}
while(categoryIdIt.hasNext()) {
Object value = categoryIdIt.next();
valueList.add(value);
sql.append(",?");
}
sql.append(") ");
}
if(categorySearchCriteria.getFirstLetter() != null
&&
!categorySearchCriteria.getFirstLetter().trim().equals(""))
{
if(valueList.size() != 0) {
sql.append("AND ");
}
sql.append("name LIKE (?)");
valueList.add(categorySearchCriteria.getFirstLetter()
+ "%");
}
ps = conn.prepareStatement(sql.toString());
Iterator valueListIt =
valueList.iterator();
int indexCount = 1;
while(valueListIt.hasNext()) {
ps.setObject(indexCount,valueListIt.next());
indexCount++;
}
rs = ps.executeQuery();
while(rs.next()) {
Category category = new Category();
category.setCategoryId(rs.getInt("categoryId"));
category.setTitle(rs.getString("title"));
category.setDescription(rs.getString("description"));
category.setParentCategoryId(
rs.getInt("parentCategoryId"));
category.setSequence(rs.getInt("sequence"));
retVal.add(category);
}
} catch (SQLException ex) {
logger.error(ex.getMessage(), ex.fillInStackTrace());
} finally {
if (rs != null)
try { rs.close(); }
catch (SQLException ex)
{logger.error(ex.getMessage(), ex.fillInStackTrace());}
if (ps != null)
try { ps.close(); }
catch (SQLException ex)
{logger.error(ex.getMessage(), ex.fillInStackTrace());}
if (conn != null)
try { conn.close(); }
catch (SQLException ex)
{logger.error(ex.getMessage(), ex.fillInStackTrace());}
}
return retVal;
}
…
}
자바 코드를 사용할 때는 커넥션 획득, 파라미터 준비, 결과 셋 처리 그리고 객체에 값 지정하기 등의 기본적이고 반복적인 작업들을 개발자가 스스로 몇 번이고 계속해서 처리해야만 한다. 공통적인 작업들을 관리하는 것과 함께, 이 예제에서는 IN 구문을 생성하는 약간의 복잡성을 첨가하여 처리하였다. 이를 처리하기 위해서 파라미터의 List를 생성하고 값을 저장하였다. 모든 코드를 다 작성하였지만, 우리가 실제로 원하는 기능을 다 갖추고 있는 것은 아니다. 아마도 PreparedStatement 가 단순히 setObject 메서드를 사용하여 값을 설정하고 있음을 알아챘을 것이다. 가장 좋은 방법은 정확한 타입을 명시해 주는 것이다. 하지만 그렇게 하면 이 코드가 불필요하게 너무 길어지게 된다.
결국에 이 코드는 어릴 적 잡지에서 큰 그림 안에 꼭꼭 감춰둔 숨은 그림을 찾는 놀이를 연상시킨다. 여기서의 도전 과제는 ‘숨은 SQL’ 찾기이다. 이 예제에서는 JDBC를 직접 사용하는 것이 매우 좋지 않음을 보여주고 있다. 이제 저장 프로시져를 사용하여 동적 SQL 을 다루는 것을 살펴보자.
8.5.2 저장 프로시저 사용하기
저장 프로시저는 수많은 작업에서 진정한 구원자의 역할을 할 수도 있으며, 우리는 이 저장 프로시저의 능력에 감사해야 한다. 동적 SQL을 위해 저장 프로시저를 사용할 때에도 자바 동적 SQL을 코딩할 때와 동일한 문제를 종종 겪게 될 것이다. 다음은 오라클의 PL/SQL로 동적 SQL 구축을 다루는 저장 프로시저를 어떻게 만드는지 보여준다.
create or replace package category_pkg
as
type ref_cursor is ref cursor;
function get_category(
categoryid varchar default null,
name category.name%type default null)
return ref_cursor;
end;
/
create or replace package body category_pkg
as
function get_category(
categoryid varchar default null,
name category.name%TYPE default null)
return ref_cursor
is
return_cursor ref_cursor;
sqltext varchar(4000);
first char(1) default 'Y';
begin
sqltext :=
'select c.categoryid, c.title, c.description, ' ||
'c.sequence ' ||
' from category c ';
if ( categoryid is not null ) then
if ( first = 'Y' ) then
sqltext := sqltext ||
'where c.categoryid in (' ||
categoryid || ') ';
first := 'N';
end if;
end if;
if ( name is not null ) then
if ( first = 'Y' ) then
sqltext := sqltext || 'where ';
else
sqltext := sqltext || 'and ';
end if;
sqltext := sqltext || 'c.name like ''' ||
name || '%''' ;
first := 'N';
end if;
open return_cursor for sqltext;
return return_cursor;
end get_category;
end;
/
위의 예제는 저장 프로시저의 유용한 규칙을 명백하게 위반하고 있다. SQL 주입을 막고 성능을 향상시켜줄 수 있는 바인딩을 사용하지 않고 있다. 이는 복잡성을 줄이기는커녕 오히려 증대시켰다는 의미이다. 복잡성을 줄이는 것이 우리 삶에서 항상 지켜야 하는 규범일까? 물론 아니다. 하지만 저장 프로시저에서 동적 SQL을 사용할 경우에는 복잡성을 줄여야 할 필요를 많이 느끼게 될 것이다.
저장 프로시저를 사용하는 두 가지 중요한 이유는 보안과 성능 때문이다. 하지만 둘 중 어느 것도 동적 SQL을 사용할 경우에는 적용되지 않는다. 자바나 저장 프로시저 쪽에서 SQL 파라미터를 사용하면 서로 동등한 수준의 성능과 안정성을 보여준다. 주 관심사를 가독성이나 유지보수에 둔다면 약간은 실망하게 될 것이다. 위 예제의 프로시저는 자바 예제보다 더 가독성이 높은데, 이는 SQL 파라미터를 사용하여 보안을 강화하지 않았기 때문이다. 유지보수성의 경우에는 좀 더 복잡한 문제가 있다. 저장 프로시저의 유지보수성은 애플리케이션을 배포할 때 DDL 스크립트를 배포하는 데이터베이스 관리자에게 달려있다. 자바 예제의 경우에는 SQL이 개발자의 영역에 속하고 애플리케이션의 다른 코드와 함께 배포된다.
저장 프로시저는 어떤 데이터베이스를 사용하고, 내장된 언어가 복잡한 업무에 얼마나 잘 부합하는가에 따라 그 효율이 들쑥날쑥하다. 동적 SQL에 저장 프로시저를 사용하면 자바 예제에서 본 것과 동일한 복잡성에 직면하게 된다. 보안성과 성능이 떨어지고 배포하기도 어려워진다. 알아둬야 할 또 다른 것이 있는데, 예제에서 저장 프로시저를 호출하는 자바 코드는 포함시키지 않았다는 것이다. 평가를 내려보면, 자바를 직접 사용하는 것과 저장 프로시저를 사용하는 것만으로는 선뜻 해결책을 선택하기가 어렵다. 그래서 iBATIS가 나왔다.
8.5.3 iBATIS와 비교하기
자바를 직접 사용하거나 저장 프로시저를 사용하여 동적 SQL 처리를 살펴보고 나서, 성능과 안정성 그리고 생산성을 높여 줄 수 있는 무엇인가가 있으면 좋겠다는 생각을 하게 되었을 것이다. 다음은 이전에 본것과 동일한 동적 SQL이다. 하지만 동적 SQL 구성에 iBATIS SQL Maps 프레임워크를 사용하였다.
저게 바로 매핑 구문이다. 그리고 다음은 구문을 호출하는 코드이다.
queryForList (“getCategories”, searchObject);
약 14줄 정도의 코드를 볼 수 있다. 자바나 저장 프로시저를 사용하면 동일한 작업을 하는데 그 몇 배의 코드가 필요하다. iBATIS가 내부적으로 PreparedStatement를 사용하기 때문에 SQL 주입을 막아주어 보안성이 높아지고, SQL 파라미터를 통해 성능도 향상된다. 간단한 XML 파일에 SQL 을 넣고 자바 코드와 같은 곳에 파일을 두기 때문에 SQL 을 관리하는 것도 쉬워지고, 애플리케이션 배포할 때 함께 배포할 수 있다. iBATIS를 자바코드를 직접 사용하는 것이나 저장 프로시저와 비교해서 분석해 보면, iBATIS가 논리적으로 승자가 될 것이라는 점에 이론의 여지가 없을 것이다.
1.6. 동적 SQL의 미래
iBATIS는 이미 미래를 생각하면서 동적 SQL 기능을 개선시켜 나가고 있다. 우리가 이번장에서 배운 거의 모든 것들은 미래의 동적 SQL 에서도 계속 유효할 것이다. 그와 함께, iBATIS가 동적 SQL 기능을 어떤 식으로 발전시킬지 알고 있어야 한다.
동적 SQL XML 요소들의 초창기 개념은 iBATIS 버전 1.x에서 개발되었다. 동적 SQL 요소는 대부분 Struts(자세한 사항은 Ted Husted 의 Struts in Action[Manning, 2002]에서 볼 수 있다.)에서 개념을 빌려와서 만든 것이다. 자바 커뮤니티에서 표준화가 진행됨에 따라, iBATIS는 좀 더 자바 표준에 가까운 개념을 빌려와서 이를 iBATIS와 잘 융합시켜 사용할 수 있도록 탐색전을 벌였다. 개선이 필요한 두 가지 영역이 나타났는데, 간결하면서 더욱 강력한 XML 요소셋과 XML요소들과 함께 사용할 간결한 표현식이 바로 그것이다. iBATIS에서 동적 SQL의 어느 부분을 개선하게 될 지 잠시 알아보자.
8.6.1 간단해진 조건 요소
현재 iBATIS는 강력한 16개의 XML 요소를 사용하여 동적 SQL을 구성하는 것을 자랑으로 삼고 있다. 이 요소들은 모두 다 매우 특별한 기능들을 수행한다. 좀 더 일반적인 목적의 조건 XML 요소들을 제공하려는 노력의 일환으로 iBATIS 개발팀은 현재 존재하는 것들에 더하여 간결한 요소들을 만들려고 한다. 새로운 동적 SQL 요소의 목표는 최종적으로 옛날 요소들을 완전히 대체하는 것이다. 새로운 동적 SQL 요소는 JSTL 을 모델로 삼고 있다. 이는 현재 존재하는 16개의 요소를 단 6개로 줄여준다. 이 글을 쓰는 시점에는
8.6.2 표현식 (Expression Language)
새로운 동적 SQL XML 요소가 더욱 일반적으로 사용할 수 있게 변경되기 때문에 간단한 표현식이 필요하다. iBATIS 팀은 자바의 J2EE표현식(Expression Language, EL) 을 모델로 하여 표현식을 만들기로 결정하였다. 이를 사용하면 한 번의 값 평가에서 여러 개의 조건을 분석하는 기능이 더 잘 지원된다. 현재 iBATIS의 동적 SQL 요소들은 “and” 혹은 “or” 같은 논리 연산을 지원하지 못한다. 일반화된 동적 SQL 요소들과 강력한 표현식을 조합하면 복잡한 동적 SQL의 요구사항들을 더 쉽게 처리할 수 있다.
댓글 없음:
댓글 쓰기