2011년 6월 7일 화요일

iBATIS in Action (12/14)

12. iBATIS 확장하기

12.1. 플러그인 가능한 컴포넌트 설계 이해하기
플러그인 가능한 컴포넌트의 설계는 일반적으로 3개의 부분으로 구성된다.
 인터페이스
 구현체
 팩토리(Factory)
인터페이스는 어떤 기능을 원하는지 설명하며, 모든 구현체가 따라야 하는 계약서와 같은 것이다. 다시 말해서 인터페이스는 수행할 기능이 ‘무엇’인지 설명한다.
구현체는 기능들을 어떻게 수행하는지 묘사하는 특정한 행위이다. 이는 써드파티 프레임워크나 혹은 고급 캐싱 시스템이나 애플리케이션 서버와 같은 대규모 인프라스트럭처에 의존할 수도 있다.
팩토리는 몇몇 설정을 기반으로 해서 인터페이스에 구현체를 바인딩하는 역할을 한다. 이것의 기본적인 개념은 애플리케이션이 프레임워크에 대해 단 하나의 일관섬있는 인터페이스 이외의 다른 것에는 의존하지 않는다는 점을 명확히 하는 것이다. 애플리케이션이 세부적으로 봤을 때 다른 구현체에 많이 의존한다면 프레임워크가 자신의 소명을 제대로 하지 못한다는 것을 의미한다.


그림은 이 개념을 설명하고 있다. 화살표는 “~에 의존한다(depends upon)” 또는 적어도 “~를 알고 있다.(knows about)” 정도의 의미를 나타낸다.
앞에서 말했듯이, iBATIS는 iBATIS의 수많은 기능들을 확장할 수 있도록 플러그인 할 수 있는 컴포넌트 설계 방식을 지원한다. 하지만 이게 정확히 무슨 뜻일까? 보통 ‘플러그인’은 이전에 존재하지 않은 새로운 기능을 추가하거나 존재하는 기능을 다른 기능으로 대체하는 애플리케이션이나 프레임워크의 확장 기능이다. 대부분의 경우에 iBATIS의 확장은 이미 존재하는 기능을 대체하는 것과 관련돼 있다.
iBATIS는 계층적으로 설계되었으며, iBATIS는 모든 계층에 걸쳐서 개발자 스스로 기능을 확장할 수 있도록 지원하고 있다. 표에서는 iBATIS의 각 계층을 설명하고, 고수준의 관점에서 어떻게 확장할 수 있는지 간단히 설명했다.
확장 가능한 기능 확장 개요
TypeHandlerCallback 비표준 데이터베이스, 드라이버 그리고/또는 데이터 타입을 다루기 위한 사용자 정의 처리 로직을 구현한다.
CacheController 개발자가 만든 캐시 코드 또는 써드파티 캐싱 솔루션을 지원하기 위한 사용자 정의 CacheController를 구현한다.
DataSourceFactory 모든 표준적인 JDBC DataSource 구현체를 대체한다.
TransactionConfig 사용자의 환경에서 가장 잘 작동하는 사용자 정의 트랜잭션 관리자를 구현한다.
다음 절에서는 가장 일반적인 확장 타입인 TypeHandlerCallback을 시작으로 각각에 대해 좀 더 상세하게 알아볼 것이다.

12.2. 사용자 정의 타입 핸들러로 작업하기
우리 모두가 관계형 데이터 베이스 관리 시스템이 표준화되길 바라지만, 운 나쁘게도 표준화가 안 돼 있다. 모든 RDBMS 시스템들은 자체적인 SQL 확장 기능을 구현하고, 자신만의 데이터 타입을 가지고 있다. 비록 대부분의 관계형 데이터베이스가 BLOB이나 CLOB같은 좀 더 공통적인 데이터 타입을 지원하긴 하지만, 각각의 드라이버는 서로 다른 방법으로 이를 처리한다. 따라서 iBATIS가 단 하나의 타입 핸들러 구현체만을 사용하여 모든 데이터베이스를 지원하기는 어렵다. 이러한 문제를 해결하기 위해, iBATIS는 특정 타입을 다루는 방법을 사용자가 정의하도록 허용하는 사용자 정의 타입 핸들러를 지원한다. 사용자 정의 타입 핸들러를 사용하면 iBATIS가 관계형 데이터베이스 타입을 자바 타입으로 매핑하는 방법을 직접 지정할 수 있다. 또한 개발자는 iBATIS 내장 타입 핸들러조차도 오버라이드할 수 있다. 이번 절에서 그 방법을 알아보자.

12.2.1 사용자 정의타입 핸들러 구현하기
사용자 정의 타입 핸들러를 구현할 때는, 오직 기능의 일부분만 구현하면 된다. 이 기능은 TypeHandlerCallback이라고 불리는 매우 간단한 인터페이스에 정의돼 있다. 다음에서 인터페이스의 정의를 볼 수 있다.
public interface TypeHandlerCallback {
public void setParameter(
ParameterSetter setter, Object parameter)
throws SQLException;
public Object getResult(ResultGetter getter)
throws SQLException;
public Object valueOf(String s);
}

TypeHandlerCallback의 구현체를 순서대로 살펴보자. 예제를 살펴보기 위해서, Boolean 값을 ‘YES’ 와 ‘NO’(각각 true와 false를 나타낸다.) 두 단어로 나타내는 데이터베이스가 있다고 가정하자. 다음 표에서 이 예제를 볼 수 있다.
UserID Username PasswordHashcode Enabled
1 Asmith 1190B32A35FACBEF YES
2 Brobertson 35FACBEFAF35FAC2 YES
3 cjohnson AF35FAC21190B32A NO

이 테이블을 다음과 같은 클래스에 매핑한다고 생각해보자.
public class User {
private int id;
private String username;
private String passwordHashcode;
private boolean enabled;
// assume compliant JavaBeans properties
// (getters/setters) below
}

여기서 데이터 타입이 서로 일치하지 않는 것에 주목하라. Enabled 칼럼이 자바 클래스에서는 Boolean 타입이지만 데이터베이스에서 YES 와 NO 값을 저장하는 VARCHAR이다. Boolean 타입에 YES나 NO 값을 직접 지정할 수는 없다. 그러므로 이것을 변환할 필요가 있다. 사실 JDBC드라이버도 변환을 자동으로 수행하지만, 이 경우는 못한다고 가정해보자.
TypeHandlerCallback의 목적은 이러한 상황을 다루는 것이다. 이제 이를 수행하는 구현체를 작성해보자.

12.2.2 TypeHandlerCallback 생성하기
이미 본 것처럼, TypeHandlerCallback 인터페이스는 매우 간단하다. 우리가 할 일은 오직 인터페이스를 구현하는 클래스를 생성하는 것뿐이다. 멋지게 자신을 잘 나타내도록 클래스 이름을 지어주고 몇 가지 private 상수들도 생성하였다.
public class YesNoTypeHandlerCallback
implements TypeHandlerCallback {
private static final String YES = "YES";
private static final String NO = "NO";
public void setParameter(
ParameterSetter setter, Object parameter)
throws SQLException {
}
public Object getResult(ResultGetter getter)
throws SQLException {
}
public Object valueOf(String s) {
}
}

이것은 타입 핸들러의 골격만 가진 구현체다. 다음 절에서 살을 붙여 나갈 것이다.
- 파라미터를 설정하기
데이터베이스에 값을 보낼 때는 값이 YES나 NO여야 한다. 이 경우 null은 유효하지 않다. 자바 클래스에서 true나 false라는 강력한 타입의 boolean값을 가져올 것이다. 따라서 true를 YES값으로 false를 NO 값으로 변환할 필요가 있다. 이를 위해 다음처럼 간단한 메서드를 사용할 수 있다.
private String booleanToYesNo(Boolean b) {
if (b == null) {
throw new IllegalArgumentException (
"Could not convert null to a boolean value. " +
"Valid arguments are 'true' and 'false'.");
} else if (b.booleanValue()) {
return YES;
} else {
return NO;
}
}
이제 파라미터 값을 지정하기 전에 이 메서드를 사용하여 변환할 수 있다. 파라미터 지정은 쉽게 할 수 있다. TypeHandlerCallback 인터페이스의 setParameter() 메서드는 두 개의 파라미터를 받아들인다. 첫 번째로 받는 ParameterSetter를 사용하여 다양한 setter 메서드에 접근할 수 있다. 각각의 setter 메서드는 서로 다른 데이터 타입에 대한 처리를 한다. 예를 들면 setString() 메서드, setInt() 메서드, setDate()메서드 등이 있다. 여기서 이를 모두 나열하려면 목록이 너무 길다. 하지만 개발자들이 알고 있는 거의 모든 자바 데이터 타입들이 그에 관련된 set 메서드를 가지고 있다고 생각하면 된다. 이번 경우에는 데이터베이스 테이블의 데이터 타입이 VARCHAR이기 때문에 ParameterSetter의 setString() 메서드를 사용할 것이다.
두번째 파라미터는 데이터베이스에 전달하는 변환될 필요가 있는 값이다. 우리의 경우, User 클래스의 enabled 프로퍼티로부터 Boolean 값을 가져올 것이다. 다음은 위에서 작성한 편리한 booleanToYesNo() 메서드를 사용하는 setParameter()메서드의 코드이다.
public void setParameter(
ParameterSetter setter, Object parameter
) throws SQLException {
setter.setString(booleanToYesNo((Boolean) parameter));
}
메서드의 내용은 단순히 ParameterSetter를 사용하여 변환 메서드를 통해 변환한 문자 값을 파라미터로 지정한다. TypeHandlerCallback이 모든 타입을 지원하는 인터페이스이기 때문에 파라미터로 받은 값을 boolean으로 형 변환해야 한다.
참 간단하게 끝났다. 그렇지 않을가? 다음 절에서 살펴보겠지만, 결과를 가져오는 것도 이만큼 쉽다.
- 결과 가져오기
데이터베이스로부터 ‘YES’ 나 ‘NO’ 값을 가져올 때는 이 값을 true나 false의 Boolean 타입의 값으로 변환해야 한다. 이것은 파라미터를 지정하는 것과 정확히 반대로 하는 것이다. 그렇다면 같은 방식으로 접근해보자. 다음과 같이 문자열 타입을 Boolean 타입으로 변환하는 메서드를 만들어보자.
private Boolean yesNoToBoolean(String s) {
if (YES.equalsIgnoreCase(s)) {
return Boolean.TRUE;
} else if (NO.equalsIgnoreCase(s)) {
return Boolean.FALSE;
} else {
throw new IllegalArgumentException (
"Could not convert " + s +
" to a boolean value. " +
"Valid arguments are 'YES' and 'NO'.");
}
}
이제는 이 메서드를 사용하여 데이터베이스에서 가져온 String 타입의 결과를 Boolean 타입의 값으로 변환할 수 있게 되었다. TypeHandlerCallback의 getResult() 메서드에서 새로운 변환 메서드를 호출하면 된다. getResult() 메서드는 ResultGetter라는 오직 하나의 파라미터만 받는다. ResultGetter는 서로 다른 타입의 값을 가져오는 메서드를 포함하고 있다. 우리의 경우, String 타입의 값을 가져올 필요가 있다. 다음은 getResult() 구현체의 코드이다.
public Object getResult(ResultGetter getter)
throws SQLException {
return yesNoToBoolean(getter.getString());
}

이 경우 간단히 ResultGetter의 getString()을 호출해서 데이터베이스에서 문자열 값을 받는다. 그러고 나서는 그 반환받는 값을 간편한 변환 메서드(yesNoToBoolean())에 전달하면 이 메서드는 User 클래스의 enabled 프로퍼티에 값으로 지정할 수 있는 Boolean 값으로 변환해준다.

- Null 다루기 – valueOf() 메서드는 왜 존재할까?
iBATIS는 null 값 변환 기능을 통해서 객체 모델에서 null이 가능한 타입(nullable type)을 사용하지 않고도 데이터베이스의 null값이 가능한 칼럼을 처리할 수있다. 이는 개발자에게 객체 모델이나 데이터베이스에 대한 완전한 설계 결정권이 없을 때 유용하다. 하지만 데이터베이스와 객체 모델을 매핑해야만 한다. 예를 들어 자바 클래스에 int 타입의 프로퍼티가 있다면 int는 null 값을 허용하지 않는다. 이 프로퍼티를 nullable 타입의 데이터베이스 칼럼에 매핑해야만 한다면, 그 때 null 값을 대신해서 상수 값을 사용해야 한다. 때로 이를 ‘매직 넘버’라고 부르기도 한다. 그리고 일반적으로 그다지 좋지 못한 방법이기도 하다. 하지만 가끔 이 방법 외의 다른 해결책이 없을 때도 있다. 그리고 때로는 이 방법이 가장 알맞을 경우도 있다.
iBATIS는 XML 파일을 사용해서 설정하기 때문에 null 값 대체는 문자열로 지정한다. 다음 예를 보자.

이러한 이유로, 실제 값으로 변환하는 일을 무언가가 수행해 줘야만 한다. iBATIS는 TypeHandlerCallback의 valueOf() 메서드에 이 변환 작업을 맡긴다. 위의 경우에서는 NO값을 boolean값 false로 변환해야 한다. 운 좋게도 이 작업은 보통 결과를 가져올 때 하는 변환과 매우 유사하다. 실제로 YesNoTypeHandlerCallback의 경우에는 두 가지가 완전히 동일하다. 따라서 이를 구현하면 다음과 같이 될 것이다.
public Object valueOf(String s) {
return yesNoToBoolean(s);
}
이게 전부다! 우리가 원하는 사용자 정의 타입 핸들러를 모두 완성했다! 다음에서 완전한 소스를 볼 수 있다.
public class YesNoTypeHandlerCallback
implements TypeHandlerCallback {
public static final String YES = "YES";
public static final String NO = "NO";
public void setParameter(
ParameterSetter setter, Object parameter
)
throws SQLException {
setter.setString(booleanToYesNo((Boolean)parameter));
}
public Object getResult(ResultGetter getter)
throws SQLException {
return yesNoToBoolean(getter.getString());
}
public Object valueOf(String s) {
return yesNoToBoolean(s);
}
private Boolean yesNoToBoolean(String s) {
if (YES.equalsIgnoreCase(s)) {
return Boolean.TRUE;
} else if (NO.equalsIgnoreCase(s)) {
return Boolean.FALSE;
} else {
throw new IllegalArgumentException (
"Could not convert " + s +
" to a boolean value. " +
"Valid arguments are 'YES' and 'NO'.");
}
}
private String booleanToYesNo(Boolean b) {
if (b == null) {
throw new IllegalArgumentException (
"Could not convert null to a boolean value. " +
"Valid arguments are 'true' and 'false'.");
} else if (b.booleanValue()) {
return YES;
} else {
return NO;
}
}
}

이제 TypeHandlerCallback 을 모두 완성하였다. 이를 사용하려면 등록을 해줘야 한다. 다음 절에서는 등록을 다룰 것이다.

12.2.3 TypeHandlerCallback을 등록해서 사용하기
TypeHandlerCallback을 사용하려면, 몇 가지 방법으로 사용할 위치와 시점을 명시해줘야 한다. 아래 세 가지 중에서 선택할 수 있다.
1. SqlMapConfig.xml 파일에 전역적으로 등록하기
2. 간단히 SqlMap.xml 파일에 지역적으로 등록하기
3. 단일 결과 매핑 혹은 파라미터 매핑에 등록하기
TypeHandlerCallback 을 전역으로 등록하려면 SqlMapConfig.xml 파일에 간단히 요소를 추가하면 된다. 다음은 요소의 완전한 예제이다.
callback="com.domain.package.YesNoTypeHandlerCallback"
javaType="boolean" jdbcType="VARCHAR" />

요소는 2개 또는 3개의 속성을 가지고 있다. 첫 번째는 TypeHandlerCallback 클래스 자체이다. 패키지 경로를 포함한 완전한 클래스 명을 간단히 명시하거나, 원한다면 타입 별칭을 사용하여 설정 파일을 좀 더 읽기 쉽게 할 수 있다. 두 번째는 TypeHandlerCallback이 다루어야 하는 자바 타입을 명시하는 javaType 속성이다. 마지막으로 세 번째 속성은 선택사항으로 TypeHandlerCallback 이 적용되는 JDBC (이를 테면 데이터베이스) 타입을 명시한다. 따라서 위의 경우에는, 자바 타입은 boolean이고 JDBC 타입은 VARCHAR로 지정하여 작업한다. 데이터 타입을 명시하지 않는다면 이 타입 핸들러는 기본적으로 모든 Boolean 타입에 적용될 것이다. 하지만 좀 더 상세하게 정의하면 어떤 타입 핸들러도 오버라이드 하지 않게 된다. 따라서 자바 타입과 JDBC 타입 모두에 가장 명확하게 일치하는 타입 핸들러 등록이 사용된다.
사용자 정의 타입 핸들러는 iBATIS의 확장 중에서 가장 일반적인 형태이다. 이는 대개 관계형 데이터베이스 시스템들이 폭넓게 비표준 기능과 데이터 타입을 지원하기 때문이다. 나머지 절에서는 많이는 안 쓰이지만 여전히 알아두면 유용한 다른 확장 기능 형태들을 공부할 것이다.

12.3. CacheController 다루기
iBATIS는 많은 수의 내장 캐싱 구현체를 포함하고 있다. 이전 장에서 이미 다루었지만 다시 기억을 되살리기 위해 표에서 다양한 캐시 구현체를 요약해 보았다.
클래스 상세 설명
LruCacheController Least Recently Used(LRU) 캐시는 캐시된 항목들을 언제 마지막으로 접근했는지를 기준으로 감시한다. 새로운 항목을 캐싱하기 위해 공간이 필요하면 최근에 가장 오랫동안 사용하지 않은 캐시 항목을 삭제한다.
FifoCacheController First-In, First-Out (FIFO) 캐시는 캐시에서 가장 오래된 항목을 삭제해서 새로운 항목을 위한 공간을 만든다.
MemoryCacheController Memory 캐시는 자바 메모리 모델과 가비지 컬렉터를 사용하여 캐시된 항목을 삭제할지 여부를 결정한다.
OSCacheController OpenSymphony 캐시를 통해 OSCache라는 매우 고급 써드파티 캐싱 솔루션을 사용할 수 있다. OSCache 는 그 자체에서 다양한 캐시 모델을 제공해 주며 분산 캐싱 같은 고급 기능들도 제공한다.

iBATIS는 CacheController라는 인터페이스를 제공하여 사용자 정의 캐싱 솔루션이나 기존에 존재하는 써드파티 캐싱 솔루션을 플러그인으로 만들 수 있도록 하고 있다.
CacheController 인터페이스는 상당히 단순하며 다음과 같다.
public interface CacheController {
public void configure(Properties props);
public void putObject(CacheModel cacheModel,
Object key, Object object);
public Object getObject(CacheModel cacheModel,
Object key);
public Object removeObject(CacheModel cacheModel,
Object key);
public void flush(CacheModel cacheModel);
}

아래 몇 절에서 캐시를 구현하는 예제를 살펴볼 것이다. 이 예제는 전사적인 애플리케이션에서 사용할 수 있는 캐시 컨트롤러 작성법을 가르치려는 것이 아니다. 그저 Map을 사용한 간단한 캐싱 방법을 살펴볼 것이다. 고급 기능을 가진 써드파티 캐시를 플러그인 하는 것을 만드는 것이 좀 더 일반적일 것이다. 여기서 예로 든 캐시를 실제 애플리케이션의 캐싱 기법으로 사용하는 것은 추천하지 않는다.

12.3.1 CacheController 생성하기
CacheController 구현은 설정으로 시작한다. 설정은 configure() 메서드를 구현해서 처리한다. 이 메서드는 자바의 Properties 인스턴스를 파리미터로 받는다. Properties 인스턴스에는 관련 설정 정보를 저장한다. 예제 캐시에서는 설정 프로퍼티는 필요없다. 하지만 객체를 저장할 Map은 필요할 것이다. 이제 예제 CacheController 의 구현을 시작해보자.
public class MapCacheController {
private Map cache = new HashMap();
public void configure(Properties props) {
// There is no configuration necessary, and therefore
// this cache will depend upon external
// flush policies (e.g. time interval)
}
// other methods implied …
}

좋다. 이제 캐시 모델의 골격을 완성했으니, 나머지를 채워보자.

12.3.2 CacheController의 저장, 가져오기, 삭제하기
현 시점에서는 캐시에 객체를 어떻게 추가할지에 대해 생각해 볼 수 있다. iBATIS Cache Model이 모든 키를 관리하고 다양한 매핑 구문 호출과 결과셋을 어떻게 구분할지를 결정한다. 따라서 캐시에 객체를 저장할 때는 키와 객체를 선택한 캐시 구현체에 전달하는 것만 해주면 된다.
여기 저장, 가져오기 그리고 삭제를 처리하는 메서드의 예가 있다.
public void putObject(CacheModel cacheModel, Object key,
Object object) {
cache.put (key, object);
}
public Object getObject(CacheModel cacheModel,
Object key) {
return cache.get(key);
}
public Object removeObject(CacheModel cacheModel,
Object key) {
return cache.remove(key);
}

어떻게 각 메서드들에서 캐시를 제어하는 CacheModel 인스턴스에 접근할 수 있는지 주의해서 보라. 이를 통해 CacheModel 에 있는 필요한 모든 프로퍼티에 접근할 수 있다. Key 파라미터는 매핑 구문에 전달된 파라미터들을 비교하는 iBATIS의 특별한 클래스인 CacheKey의 인스턴스이다. 거의 모든 부분에서 key 객체를 어떤 방식으로도 관리할 필요는 없다. putObject()의 경우에는 object 파라미터가 캐싱할 객체의 컬렉션을 저장하고 있다.
CacheModel 에 필요한 마지막 메서드는 flush() 메서드이다. 이 메서드는 간단히 전체 캐시를 지워버린다.
public void flush(CacheModel cacheModel) {
cache.clear();
}

바로 이것이, 한 마디로 말하자면 완전한 CacheController 구현체이다.
이제 CacheController를 어떻게 사용하는지 공부할 차례다.

12.3.3 CacheController를 등록해서 사용하기
다른 모든 iBATIS 설정들과 마찬가지로 CacheModel 과 CacheController도 XML 설정 파일에서 설정한다. CacheModel 사용을 시작하는 가장 쉬운 방법은, 먼저 새로운 클래스에 대한 타입 별칭을 선언하는 것이다. 이를 통해 나중에 타이핑 횟수를 줄일 수 있다.
type="com.domain.package.MapCacheController"/>

이제 타이핑 횟수를 줄였으니, 다른 캐시 모델 타입에서 했던 것처럼, 캐시 컨트롤러 타입을 정의에 적용할 수 있다. 예를 보자.







이로써 사용자 정의 캐시 구현체를 완성했다. 하지만 이것이 단순히 예제임을 기억하라.
만약 스스로 캐시 구현체를 작성하면 실제 애플리케이션을 작성하는 것보다 더 많은 시간을 들여야 할 것이다. 되도록 다른 캐시 구현체를 iBATIS에 플러그인 하는 것이 좋다.

12.4. 지원되지 않는 DataSource 설정하기
iBATIS는 JNDI(애플리케이션 서버에서 관리하는 DataSource), Apache DBCP 그리고 SimpleDataSource 라고 부르는 내장 DataSource 구현체를 포함하여 거의 대부분의 일반적인 DataSource 대체재들을 지원한다. 하지만 개발자들이 DataSource 구현을 추가할 수도 있는 기능을 지원한다.
새로운 DataSource 구현체를 설정하고자 한다면, iBATIS에 DataSource 인스턴스를 iBATIS 프레임워크에 공급해 주는 팩토리를 만들어줘야 한다. 이 팩토리 클래스는 DataSourceFactory 인터페이스를 구현해야 하며, 이는 아래와 같다.
public interface DataSourceFactory {
public void initialize(Map map);
public DataSource getDataSource();
}

DataSourceFactory 는 하나의 DataSource를 초기화하고 다른 하나는 DataSource에 접근하는 오직 두 개의 메서드만을 가지고 있다. Initialize() 메서드는 JDBC 드라이버 이름, 데이터베이스 URL, 사용자명과 비밀번호와 같은 설정 정보들을 포함하는 Map 인스턴스를 제공한다.
getDataSource() 메서드는 간단히 설정된 DataSource를 반환하면 된다. 이는 매우 간단한 인터페이스이며, 이를 구현할 때의 복잡도는 개발자가 제공하는 DataSource 구현체의 복잡도에 달려 있다. 아래는 iBATIS 소스 코드에서 가져온 예제이다. 이는 SimpleDataSource 구현체의 DataSourceFactory 이다. 보다시피, 정말로 ‘간단’하다.
public class SimpleDataSourceFactory
implements DataSourceFactory {
private DataSource dataSource;
public void initialize(Map map) {
dataSource = new SimpleDataSource(map);
}
public DataSource getDataSource() {
return dataSource;
}
}

전에 말했다시피, 더 복잡한 DataSource 의 구현은 더 많은 작업을 필요로 한다. 하지만 지나치게 걱정할 정도까지는 아니다.
iBATIS 확장하기에서 마지막으로 다룰 주제는 사용자 정의 트랜잭션 관리이다.

12.5. 사용자 정의 트랜잭션 관리
iBATIS 는 앞의 장들에서 보았듯이, 여러 가지 트랜잭션을 제공한다. 하지만 요즘에는 애플리케이션 서버가 매우 다양하고 트랜잭션 관리방법도 다양하기 때문에 항상 사용자 정의 구현을 할 수 있도록 하고 있다. 외부에서 보기엔 트랜잭션이 간단하고 시작, 커밋, 롤백, 종료라는 몇 안되는 기능만 제공하는 것 같다. 하지만 내부를 들여다 보면 트랜잭션은 매우 복잡하고 표준을 어기기 쉬운 애플리케이션 서버의 기능들 중 하나이다. 따라서 iBATIS는 개발자가 스스로 사용자 정의 트랜잭션 관리 시스템을 만들 수 있도록 하고 있다. 만약 트랜잭션 관리 시스템을 만들어 본 경험이 있다면, 이 말을 듣고 나서 등골이 오싹해졌을것이다. 트랜잭션 관리자를 제대로 구현하는 것은 굉장히 어려운 일이다. 그런 까닭에 우리는 여기서 실제 구현에 착수하는 수고는 하지 않을 것이다. 대신, 인터페이스에 대해 자세히 공부해서 머리를 쥐어짜며 구현해야 할 것을 약간이나마 쉽게 시작할 수 있도록 할 것이다. 예제를 원한다면 iBATIS가 세가지 구현체 – JDBC, JTA, EXTERNAL 을 제공해 주니 그것을 보라. 혹시나 이전 장에서 다룬 내용을 잊었을까 하여 표에 이에 관해 요약해 두었다.
구현체 설명
JdbcTransactionConfig JDBC Connection API 가 제공하는 트랜잭션 기능을 사용
JtaTransactionConfig 전역 트랜잭션을 시작하거나 이미 존재하는 것에 합류
ExternalTransactionConfig 커밋과 롤백을 수행하지 않는 구현체, 다른 외부 트랜잭션 관리자가 커밋과 롤백을 하도록 한다.

대부분의 경우에는 표의 관리자 중 하나로 문제가 해결될 것이다. 어쨌든 애플리케이션 서버나 트랜잭션 관리자가 비표준일 경우(또는 버그가 많을 경우) 에 대비해, iBATIS는 자체적인 트랜잭션 관리 어댑터를 구축할 수 있는 인터페이스-TransactionConfig와 Transaction을 제공한다. 다른 구현체들 중 하나의 Transaction 클래스를 재사용할 수 있는 상황이 아니라면, 일반적으로 두 인터페이스를 모두 구현해야만 완전한 구현체를 얻을 수 있다.

12.5.1 TransactionConfig 인터페이스 이해하기
TransactionConfig 인터페이스는 일종의 팩토리이다. 하지만 대부분은 구현체의 트랜잭션 기능을 설정하는 역할을 한다. 이 인터페이스는 아래와 같다.
public interface TransactionConfig {
public void initialize(Properties props)
throws SQLException, TransactionException;
public Transaction newTransaction(int
transactionIsolation)
throws SQLException, TransactionException;
public int getMaximumConcurrentTransactions();
public void setMaximumConcurrentTransactions(int max);
public DataSource getDataSource();
public void setDataSource(DataSource ds);
}

첫 번째 메서드는 initialize() 이다. 프레임워크의 다른 확장 가능한 부분에서 보았듯이, 이 메서드는 트랜잭션 기능을 설정하는 데 사용한다. 이 메서드는 유일한 파라미터로 Properties 인스턴스를 받는다. 이 인스턴스는 설정 옵션을 몇 개라도 포함하고 있을 수 있다. 예를 들면 JTA 구현체는 JNDI 트리에서 가져오는 UserTransaction 인스턴스를 필요로 한다. 따라서 JTA 구현체에 전달되는 프로퍼티 중 하나는 이 구현체가 필요로 하는 UserTransaction의 JNDI 경로를 나타낸다.
다음은 newTransaction() 메서드이다. 이것은 트랜잭션의 새로운 인스턴스를 생성하는 팩토리 메서드이다. 이 메서드는 트랜잭션이 취해야 하는 트랜잭션 격리 레벨을 나타내는 int 파라미터를 받는다. (운 나쁘게도 타입이 안전한 열거형 이어야만 한다.). 사용 가능한 격리 레벨은 JDBC Connection 클래스에 상수로 정의돼 있으며 다음과 같다.
 TRANSACTION_READ_UNCOMMITTED
 TRANSACTION_READ_COMMITTED
 TRANSACTION_REPEATABLE_READ
 TRANSACTION_SERIALIZABLE
 TRANSACTION_NONE
이 각각은 JDBC Connection API에 문서화돼 있다. 그리고 7장에서 더 상세히 알아볼 수 있다. 여기서 꼭 알아둬야 할 점은 여러분이 만든 트랜잭션 관리자 구현이 격리 레벨을 하나 혹은 그 이상 지원하지 못한다면, 개발자가 알 수 있도록 예외를 확실하게 던져야 한다는 것이다. 그렇지 않으면 예기치 않은 결과가 발생하여 개발자들이 디버깅하기 힘들어지게 될 수도 있다.
다음에 볼 메서드 쌍은 getDataSource() 와 setDataSource() 이다. 이 두 메서드는 TransactionConfig 인스턴스와 DataSource 에 관련된 자바빈즈 프로퍼티를 묘사한다. 보통은 DataSource 에 무슨 특별한 일을 할 필요는 없다. 하지만 이 메서드들을 제공함으로써 만일 필요한 경우에 추가적인 행위로 꾸며주는 것이 가능해진다. 많은 트랜잭션 관리자 구현체들은 자기가 제공하는 DataSource와 Connection 객체를 감싸서 그 각각에 트랜잭션 관련 기능을 추가한다.
마지막으로 알아볼 두 개의 메서드는 프레임워크가 동시에 지원할 수 있는 트랜잭션의 최대 개수를 설정할 수 있는 자바빈즈 프로퍼티를 구성한다. 여러분이 만든 구현체는 설정이 가능할 수도 불가능할 수도 있다. 하지만 하나 확실히 해야 할 점은, 시스템이 처리할 수 있는 것보다 더 큰 숫자를 설정하면 적절한 예외를 던져야 한다는 것이다.

12.5.2 Transaction 인터페이스 이해하기
이전 절에서 언급된 TransactionConfig 클래스의 newTransaction() 이라는 팩토리 메서드를 다시 살펴보자. 이 메서드가 반환하는 값은 Transaction 인스턴스이다. Transaction 인터페이스는 iBATIS 프레임워크 내에서 트랜잭션을 지원하는 데 필요한 기능들을 나타낸다. 상당히 전형적인 기능들이며 예전에 트랜잭션 관련 작업을 해봤다면 모두다 익숙한 것들이다. Transaction 인터페이스는 다음과 같다.
public interface Transaction {
public void commit() throws SQLException,
TransactionException;
public void rollback() throws SQLException,
TransactionException;
public void close() throws SQLException,
TransactionException;
public Connection getConnection()
throws SQLException, TransactionException;
}

이 인터페이스에는 특별한 것이 없다. 만약 트랜잭션에 대한 경험이 있다면 친숙하게 느껴질 것이다. 예상했겠지만 commit() 메서드는 작업 단위에 포함된 모든 변경 사항을 영구적으로 반영하는 수단이다. 반면에 rollback() 메서드는 마지막 커밋 이후 작업 단위에 발생한 모든 변경 사항을 취소하고 되돌리는 수단이다. Close() 메서드는 트랜잭션에 할당되거나 예약된 모든 자원을 해제하는 역할을 담당한다.
마지막 메서드인 getConnection()은 아마도 뜻밖일 것이다. 설계에 따르면 iBATIS는 JDBC API에 비해 상위 레벨의 프레임워크이다. 대충 말하자면 JDBC에서 Connection은 곧 트랜잭션이다. 좀 더 정확히 말하면, JDBC 커넥션 차원에서 트랜잭션을 관리하고 제어하고 이해해야 한다. 이러한 까닭에 대부분의 트랜잭션 구현체는 Connection 인스턴스에 묶여 있다. 이는 유용한 것이다. 왜냐하면 iBATIS는 트랜잭션에 관련해서 현재 커넥션에 접근해야 하기 때문이다.

댓글 없음:

댓글 쓰기