2011년 5월 31일 화요일

iBATIS in Action (8/14)

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 요소들과는 달리 어떤 값이나 상태도 평가하지 안는다. 요소는 대개 prepend 속성만을 사용하며, prepend 속성의 값은 요소의 결과가 되는 내용의 가장 앞에 오게 된다. 예제에서 prepend 속성의 값인 WHERE 내포하고 있는 동적 SQL 요소를 처리하면 생성되는 결과 SQL 가장 앞에 붙을 것이다.

부모 요소의 몸체에는 간단한 SQL 문법이나 다른 동적 SQL 요소가 온다. 예제에서 요소의 몸체에 내포된 요소를 있다. 여기서 우리가 관심을 가져야 부분은 Category 파라미터 클래스(parameterClass = “Category”) 가진 parentCategoryId 프로퍼티의 값이 null인지 아닌지 여부에 따라 WHERE 조건절에 적절한 SQL 구문을 넣는 것이다.

시점에서, 언제 prepend 속성의 값이 다른 SQL 구문 앞에 출력하게 되는지 알고 있어야 한다. 요소의 몸체에 아무런 텍스트도 나오지 않으면 prepend 값은 무시될 것이다. Prepend 속성 값이 앞에 출력되려면, 뒤에 붙은 SQL문이 있어야만 한다. 우리의 예제에서는 결과 SQL 항상 존재한다. 요소의 내용으로 아무것도 생성되지 않는 경우에는 prepend 속성의 WHERE 간단히 무시된다.

동적 SQL 이점 하나는 SQL 코드의 재사용성을 높여준다는 것이다. 예제에서 동적 SQL 사용하지 않고서 위와 같은 기능을 구현하려면 개의 select 구문을 작성해야 했을 것이다. 게다가 DAO계층에서 category 객체의 parentCategoryId 값을 먼저 확인하고, parentCategoryId값이 null인지 아닌지 여부에 따라 적당한 매핑 구문을 선택해서 호출해야 한다. 비록 이번 예제는 간단해서 동적 SQL 없더라도 별로 불편할 없지만, 여러 서로 다른 프로퍼티의 조합으로 인해 SQL구문이 기하급수적으로 증가할 가능성이 있을 경우에는 동적 SQL 진정한 가치가 분명하게 드러난다. 동적 SQL 사용하여 매핑 구문을 재사용하도록 하고 정적인 SQL 구문을 여러 작성하는 것은 피하도록 한다.

이제 동적 SQL 사용법과 강력함에 대해 이해했을 것이다. 모든 요소와 속성을 상세히 알아보자.

1.2. 동적 요소들과 친숙해지기

iBATIS 강력한 XML 요소들을 통해 동적 SQL문제를 처리한다. 요소들은 매핑 구문에 넘어온 파라미터 객체에 관한 다양한 조건들을 평가하는 역할을 한다. 존재하는 모든 요소들을 이해하고 그것들이 올바르게 SQL 출력하는 어떤 역할을 하는지 이해하고 있어야 한다. 다음 절에서는 요소들을 , 이항연산, 단항연산, 파라미터 그리고 5 카테고리로 나눈다. 각각의 그룹은 공통적인 특성을 공유하는 하나 이상의 관련 XML요소를 포함하고 있다. 그룹을 알아보기 전에 잠시 시간을 할애해서 모든 동적 SQL 요소가 공유하는 가지 공통적인 속성과 행위를 알아보자.

모든 동적 요소는 prepend, open, close 속성을 포함한다. Open close속성은 각각의 요소에서 똑같이 작동한다. 개의 속성은 동적으로 생성된 결과 내용의 시작이나 끝에 무조건 값은 붙인다. Prepend 속성은 요소를 제외하고 모든 요소에서 동일하게 작동한다. 요소는 내용물을 만들어낼 때는 항상 prepend 값을 앞에 붙인다. 요소에서 prepend 지정된 값의 처리를 막을 있는 방법은 없다. 다음은 가지 동적 SQL 요소의 실례를 보여준다.

----(1) 요소 시작

----(2) 간단한 요소

y=#y#

복잡한 요소

prepend="AND" open="(" close=")">

----(4) 내포된 요소

a=#x.a#

a=#x.b#

a=#x.c#

. (1)에서 시작 요소는 자식 요소의 첫번째 prepend 기능을 자동으로 무조건 제거한다. 요소가 true 된다면 (2) removeFirstPrepend 기능이 요소 때문에 자동으로 작동하게 된다. 동일한 레벨의 뒤따라오는 모든 요소들은 각각의 내용에 앞에 붙을 prepend 값을 가질 것이다. 요소는 (3)removeFirstPrepend 속성을 명시하고 있다. Open close 속성 값은 요소에서 출력된 내용의 앞뒤를 감싼다. (4)에서 번째 내포된 내용을 만드는 요소는 removeFirstPrepend 요구사항을 처리할 것이다. 처음으로 내용을 출력하는 요소는 prepend 속성의 OR 출력하지 않는다. 이로 인해 괄호 안에 정확한 SQL 생성된다.

번째 prepend 제거 기능은 모든 요소에서 자동으로 혹은 명시적 지정을 통해 사용할 있다. 요소는 이것을 자동으로 지원한다. 다른 요소들은 이를 removeFirstPrepend속성을 사용하여 명시함으로써 지원한다. 번째 prepend 제거 기능은 처음으로 내용을 생성하는 자식 요소의 prepend 기능을 제거해 버린다. 만약 처음으로 내용을 생성하는 자식 요소가 prepend 속성을 가지고 있지 않다면 removeFirstPrepend 기능을 수행하지 않은 것로 간주한다. 그래서 이후에 내용을 생성하는 자식요소들 prepend 속성이 있을 때까지 removeFirstPrepend 제거 기능은 유효하고 이후 처음으로 나오는 prepend 속성에 대해 작동한다.

공통 기능의 마지막 부분은 모든 요소를 서로 독립적으로 사용할 있다는 것이다. 이것은 요소 내부에서 모든 동적 SQL 요소를 포함시켜 사용할 필요가 없다는 의미이다. 요소로 시작해서 안에 요소를 넣는 것도 요소로 이들을 감싸는 것만큼 쉽게 있다. 이러한 기능을 제공하는 이유는 요소가 필요한 경우는 단지 요소의 open, close 혹은 prepend 속성값을 출력 내용에 붙여주기 위함일 뿐이기 때문이다.

그럼 이제부터 요소들의 카테고리 각각을 분석해보자.

8.2.1 요소

최상위 레벨에서만 사용할 있는 요소이다. 요소는 다른 요소 안에 들어갈 없다는 뜻이다. 요소를 이용해 동적 SQL 절들을 구분한다. 요소는 결과 내용에 공통 속성인 prepend, open 혹은 close 통해 뒤에 뭔가를 출력해 주기 위해서 사용한다.

이제 요소에서 사용할 있는 속성들을 모두 살펴보았다. 다음은 요소 사용법을 보여준다.

여기서 동적인 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) 요소 다음에 공백이 아닌 내용을 처음으로 출력하고 prepend 값이 지정돼 있는 요소일 경우.

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) 요소 다음에 공백이 아닌 내용을 처음으로 출력하고 prepend 값이 지정돼 있는 요소일 경우

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) 요소 다음에 공백이 아닌 내용을 처음으로 출력하고 prepend 값이 지정돼 있는 요소일 경우

Open(선택적)

Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. Open 값은 prepend 속성 값보다는 뒤에 붙는다. 예를 들어 prepend=”OR” 이고 open=” (“ 라면 조합된 결과는 “OR (“ 것이다.

Close(선택적)

Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다.

removeFirstPrepend(선택적)

값으로 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다.

모든 속성은 아래에 있는 모든 요소에서 사용 가능하다.

: 파라미터 객체가 존재하는지 평가

: 파라미터가 존재하지 않는지 평가

다음은 select 구문에서 파라미터 요소를 사용하는 방법을 보여준다.

예제에서 다시 간단한 select 구문을 생성하고, 이번에는 productType 파라미터를 기반으로 해서 결과 값을 걸러내는 WHERE 절을 선택적으로 생성해준다.

8.2.5 요소

요소는 컬렉션이나 배열로 프로퍼티를 받아서, 값들로부터 SQL 반복적인 부분을 생성한다. 리스트의 값들을 conjunction 속성에 지정된 값을 문자열 사이 사이에 넣는 방식으로 분리하이 SQL 구문의 일부가 되도록 출력한다. Open 속성 값은 출력된 리스트의 부분에 붙어서 나온다. Close 속성의 값은 출력된 리스트의 뒷부분에 붙어서 나온다. 표에서 요소의 속성을 있다.

Property(필수)

리스트(컬렉션 혹은 배열) 포함하고 있는 파라미터의 프로퍼티

Prepend(선택적)

값은 요소 내용의 가장 앞에 출력된다. 다음의 경우에는 prepend 값이 출력되지 않고 무시된다. (a) 요소의 결과 내용이 공백인 경우, (b) 요소가 처음으로 내용을 만들어 냈고 removeFirstPrepend 속성이 true 요소에 내포되어 있는 경우, (c) 요소 다음에 공백이 아닌 내용을 처음으로 출력하고 prepend 값이 지정돼 있는 요소일 경우.

Open(선택적)

Open 값은 요소 내용에 접두사로 붙이기 위해 사용한다. Open 값은 요소의 결과 내용이 공백일 때는 접두사로 붙지 않는다. Open 값은 prepend 속성값보다는 뒤에 붙는다. 예를 들어 prepend=”OR” 이고 open=”(“라면 조합된 결과는 “OR (“ 것이다.

Close(선택적)

Close 값은 요소 결과 내용의 뒤에 덧붙이는데 사용한다. 값은 요소 내용이 공백이라면 덧붙여지지 않을 것이다.

Conjunction(선택적)

속성의 값은 목록의 사이 사이에 출력되어 SQL 문장을 구성한다.

removeFirstPrepend(선택적)

값으로 처음으로 내용을 출력하는 자식 요소의 prepend 속성 값을 출력할지 여부를 결정한다.

아래는 요소를 이용해서 SQL 구문에서 복잡한 WHERE 조건절을 구성하는 방법을 보여준다.

예제에서는 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) 리스트이다. 간단한 리스트를 직접 사용하기 때문에 요소를 사용할 것이다. 요소는 문자열의 리스트를 반복적으로 돌면서 categoryId, name, description 검색하는 조건을 생성한다. 값을 OR 지정한 conjunction 속성을 요소에 추가해야 함을 잊어서는 된다. Conjunction 속성은 단어 검색 조건을 함께 연결한다. 여기서 알아둬야 점이 있는데, 매핑 구문의 이름은 매핑 구문에 포함된 SQL 수행하는 기능을 제대로 설명해 있게 정해야 한다는 것이다. 경우에는 매핑 구문의 이름을 searchProductList 라고 정했다. 매핑 구문의 이름을 읽었을 구문의 목적을 즉시 명확하게 파악할 있어야 한다.

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 조금 복잡해진다. 가장 상위 레벨의 요소로 사용했음을 주의해서 보라. 어떠한 prepend, open 이나 close 값도 필요하지 않기 때문에 부모 요소로 사용할 필요는 없다. 단순히 내용을 표시하거나 표시하지 않거나 것이다. 요소는 LEFT JOIN 포함할지 여부를 결정한다.

시작 select 요소 (1)에서 파라미터 클래스 타입은 productSearch, 그리고 결과 클래스 타입은 product라는 별칭을 사용하여 정의하였다. SQL 부분 (2) 매핑 구문에서 생성할 있는 가장 작은 단위의 SQL이다. 예를 들면 사용자가 모든 것을 반환하도록 검색하고자 하는 경우에 SQL 사용한다. 요소 (3) 조인이 필요한지를 결정하기 위해 부분 밖에서 사용되었다. 우리는 “WHERE”라는 prepend 값을 가지는 시작 dynamic 요소 (4) 사용한다. 결과가 되는 내용물이 없다면 prepend 속성에 명시된 “WHERE” 값은 무시될 것이다. 다시 기억을 상기시켜보자면, dynamic 요소는 요소안의 내용에서 가장 처음으로 내용을 만들어내는 요소의 prepend 값을 무조건 제거한다. 요소(5) 주의해서 봐야한다. 이는 요소 (4) 짝을 이루어서 작동하여 특정 카테고리에 관한 검색 조건을 구성하는데 필요한 SQL 컴포넌트를 만들어 낸다. iBATIS 관습적인 문제 때문에, prepend 속성이 필요하다. Prepend 속성이 없는 첫번째 자식 요소에서는 내용이 생성되더라도 prepend 제거되었다고 간주하지 않기 때문에, 다음 번에 나온 prepend 요소가 무시되게 되는 현상이 일어난다. 해당 요소에 prepend 속성이 필요 없다 하더라도, 규칙에 따라 prepend 속성을 항상 명시해 주도록 한다. 요소는 productName, productDescription, itemName 그리고 itemDescription 프로퍼티가 ‘ ‘(빈값) 혹은 NULL인지 여부를 결정하는 평가를 수행한다.

그렇다면 괴물 같은 구문을 어떻게 호출할까?

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개로 줄여준다. 글을 쓰는 시점에는 , , , , , 요소들이 새로운 요소로 제안돼 있는 상태이다. 대부분의 경우에 요소들이 JSTL 같은 이름의 태그들과 동일한 기능을 수행할 것이다. 추가적으로 prepend, open, close 그리고 removeFirstPrepend 속성들을 포함한다는 점만 다르다.

8.6.2 표현식 (Expression Language)

새로운 동적 SQL XML 요소가 더욱 일반적으로 사용할 있게 변경되기 때문에 간단한 표현식이 필요하다. iBATIS 팀은 자바의 J2EE표현식(Expression Language, EL) 모델로 하여 표현식을 만들기로 결정하였다. 이를 사용하면 번의 평가에서 여러 개의 조건을 분석하는 기능이 지원된다. 현재 iBATIS 동적 SQL 요소들은 “and” 혹은 “or” 같은 논리 연산을 지원하지 못한다. 일반화된 동적 SQL 요소들과 강력한 표현식을 조합하면 복잡한 동적 SQL 요구사항들을 쉽게 처리할 있다.