2011년 6월 7일 화요일

iBATIS in Action (11/14)

11. DAO 더 살펴보기

11.1. SQL Maps 가 아닌 DAO 구현체
10장에서 DAO의 인터페이스를 정의했고 그 인터페이스의 구현체를 기반으로 하여 SQL Map을 구성하였다. 다음 두 절에서는 그 인터페이스를 하이버네이트와 JDBC를 이용하여 구현할 것이다. 이를 통해 DAO 패턴을 사용하면 애플리케이션에서 iBATIS가 아닌 다른 데이터 접근 방식을 사용하도록 변경하는 것이 얼마나 쉬운지 보여줄 것이다.

11.1.1 하이버네이트 DAO 구현체
하이버네이트 DAO 구현체는 SQL Maps 로 구현하는 것과 상당히 다르지만 DAO 인터페이스 덕분에 DAO 인터페이스를 사용하는 애플리케이션 코드의 입장에서는 완전히 동일한 방식으로 구현체를 사용할 수 있다.

- DAO 컨텍스트 정의하기
다음은 dao.xml 파일에서 하이버네이트를 사용하는 DAO 컨텍스트를 설정하는 XML의 코드 조각이다.


value="org.postgresql.Driver" />
value="jdbc:postgresql:ibatisdemo" />
value="ibatis" />
value="ibatis" />
value="5" />
value=
"net.sf.hibernate.dialect.PostgreSQLDialect" />

value=
"${DaoHomeRes}/hibernate/Account.hbm.xml" />

implementation=
"${DaoHome}.hibernate.AccountDaoImpl"/>

10장 (10.2.2 절)에서 이야기했듯이 HIBERNATE 트랜잭션 관리자를 사용하려면 보통 때는 hibernate.properties 파일에 작성하는 프로퍼티들을 요소에서 프로퍼티로 등록해야 한다.
우리는 우리의 소스 트리를 명확하게 유지하고 싶었기 때문에, Account 빈즈용 하이버네이트 매핑 파일(Account.hbm.xml)을 Account 빈즈와 동일한 패키지에 두지 않았다. 대신 매핑 파일의 위치를 “map.” 프로퍼티를 사용하여 설정하고, 이 프로퍼티들을 하이버네이트 설정 정보에 추가하였다. 기억하라 DAO 의 핵심은 데이터 접근에 관한 구현을 인터페이스로부터 분리하는 것이다.

- Account 테이블 매핑하기
다음의 매핑 파일은 매우 간단하다. 왜냐하면 빈즈의 프로퍼티를 프로퍼티와 동일한 이름의 테이블 칼럼에 직접 매핑하고, 연관된 엔티티도 없기 때문이다.

PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd">

name="org.apache.mapper2.examples.bean.Account"
table="Account">


name="sequence">account_accountid_seq














하이버네이트를 사용해 본 경험이 있다면, 위의 내용이 상당히 간단한 테이블 매핑이라서 명확하게 이해될 것이다. 예전에 하이버네이트를 사용해본 적이 없다면 그렇게 쉽게 이해되지는 않을 것이다. 이 설정 파일은 Account 빈즈의 프로퍼티를 데이터베이스의 Account 테이블에 있는 칼럼에 매핑한다. 또한 하이버네이트가 새로 삽입할 데이터베이스 레코드의 빈즈에 사용할 id 프로퍼티를 어떻게 생성해야 하는지도 설정한다.

실제 DAO 구현체
Account 테이블을 여러 서로 다른 클래스의 객체에 매핑하기 때문에, DAO 구현체의 자바 소스 코드가 조금 더 장황하다. 다음에서 보듯이 하이버네이트에서는 이게 조금 더 어렵다.
public class AccountDaoImpl
extends HibernateDaoTemplate
implements AccountDao {
private static final Log log =
LogFactory.getLog(AccountDaoImpl.class);
public AccountDaoImpl(DaoManager daoManager) {
super(daoManager);
if(log.isDebugEnabled()){
log.debug("Creating instance of " + getClass());
}
}
public Integer insert(Account account) {
try {
getSession().save(account);
} catch (HibernateException e) {
log.error(e);
throw new DaoException(e);
}
return account.getAccountId();
}
public int update(Account account) {
try {
getSession().save(account);
} catch (HibernateException e) {
log.error(e);
throw new DaoException(e);
}
return 1;
}
public int delete(Account account) {
try {
getSession().delete(account);
} catch (HibernateException e) {
log.error(e);
throw new DaoException(e);
}
return 1;
}
public int delete(Integer accountId) {
Account account = new Account();
account.setAccountId(accountId);
return delete(account);
}
public List getAccountListByExample(
Account acct) {
List accountList;
Session session = this.getSession();
Criteria criteria =
session.createCriteria(Account.class);
if (!nullOrEmpty(acct.getCity())) {
criteria.add(
Expression.like("city", acct.getCity())
);
}
If (!nullOrEmpty(acct.getAccountId())) {
criteria.add(
Expression.eq("accountId", acct.getAccountId())
);
}
try {
accountList = criteria.list();
} catch (HibernateException e) {
log.error(
"Exception getting list: " +
e.getLocalizedMessage(), e);
throw new DaoException(e);
}
return (List)accountList;
}

Account account
)
{
List accountList =
getAccountListByExample(account);


for (Account acctToAdd : accountList) {


map.put("accountId", acctToAdd.getAccountId());
map.put("address1", acctToAdd.getAddress1());
map.put("address2", acctToAdd.getAddress2());
map.put("city", acctToAdd.getCity());
map.put("country", acctToAdd.getCountry());
map.put("firstName", acctToAdd.getFirstName());
map.put("lastName", acctToAdd.getLastName());
map.put("password", acctToAdd.getPassword());
map.put("postalCode", acctToAdd.getPostalCode());
map.put("state", acctToAdd.getState());
map.put("username", acctToAdd.getUsername());
mapList.add(map);
}
return mapList;
}
public List getIdDescriptionListByExample(
Account exAcct
) {
List acctList =
getAccountListByExample(exAcct);
List idDescriptionList =
new ArrayList();
for (Account acct : acctList) {
idDescriptionList.add(
new IdDescription(
acct.getAccountId(),
acct.getFirstName() + " " + acct.getLastName()
)
);
}
return idDescriptionList;
}
public Account getById(Integer accountId) {
Session session = this.getSession();
try {
return (Account) session.get(
Account.class, accountId);
} catch (HibernateException e) {
log.error(e);
throw new DaoException(e);
}
}
public Account getById(Account account) {
return getById(account.getAccountId());
}
}

SQL Map 구현체에서보다 양이 더 많다. Map 객체나 IdDescription 객체의 List를 반환하는 것을 살펴보기 시작하면 좀 흥미가 생길 것이다. 하이버네이트는 데이터베이스 테이블을 자바 클래스로 매핑하도록 설계되었기 때문에 동일한 테이블을 다른 클래스에 매핑하는 것은 약간 어려워진다.
다음에 볼 DAO 구현체 예제에서는 어떠한 매핑 툴도 사용하지 않고 JDBC를 직접 사용하여 DAO를 구성해 볼 것이다.

11.1.2 JDBC DAO 구현체
지금까지 사용한 인터페이스로 구축하는 마지막 DAO 구현체는 JDBC를 직접 사용할 것이다. 단순히 JDBC를 기반으로 구현할 때의 가장 큰 장점은 설정을 (하이버네이트나 iBATIS에 비해) 가장 적게 하고 유연성은 높아진다는 점이다.
다음에서 dao.xml 설정 파일을 볼 수 있다.


value="SIMPLE"/>
value="org.postgresql.Driver" />
value="jdbc:postgresql:ibatisdemo" />
value="ibatis" />
value="ibatis" />
value="true" />

implementation="${DaoHome}.jdbc.AccountDaoImpl"/>


이 정도면 설정이 끝난다. 나머지는 모두 다음에 나오는 소스 코드에 있다. DAO의 JDBC 구현체의 코드 줄 수는 하이버네이트 코드에 비해 두 배가 넘고 SQL Maps 에 비해서는 거의 일곱 배에 달한다. 설정 파일을 포함한다면, ‘코드 줄 수’의 통계는 아래 표처럼 될 것이다. 결과적으로 설정 정보를 제거했지만, 전체 코드 줄 수로는 결국 두 배 정도되는 노력을 더 들여야 한다. JDBC를 이용한 애플리케이션 개발이 어리석은 방법이라고 말하는 것은 아니다. 여기서는 단지 유연성과 최소한의 설정을 원한다면 더 많은 코드를 작성해야 한다는 점을 말하고자 하는 것이다.
구현방식 설정 코드 합계
iBATIS 118+8=126 53 179
하이버네이트 23+20=43 141 184
JDBC 18 370 388

- JDBC DAO 구현체 살펴보기
JDBC DAO 구현체는 양이 많기 때문에, 여기서 클래스의 전체 내용을 보지는 않을 것이다. 대신 DAO를 구축할 때 사용할 수 있는 몇 가지 요령들을 중심으로 다룰 것이다.
처음으로 할 일은 iBATIS가 제공하는 JdbcDaoTemplate 클래스를 상속하여 DAO를 구성하는 것이다. 그렇게 하면 조금은 여유를 부려도 괜찮다. 이 클래스가 커넥션을 관리해주기 때문에 우리가 작성할 코드에서 커넥션을 닫아줄 필요가 없다.
이게 사소해 보이는가? 대규모 시스템에서 커넥션 관리에 실패하면 몇 시간 안에(혹은 몇 분 안에라도) 시스템을 운영 불가능한 상태로 만들어버릴 수 있다. 이 때문에 다음에 볼 팁을 통해 코드량을 줄이면서, 리소스를 안전하게 사용할 수 있도록 도와주는 메서드를 만들었다.
private boolean closeStatement(Statement statement) {
boolean returnValue = true;
if(null != statement){
try {
statement.close();
} catch (SQLException e) {
log.error("Exception closing statement", e);
returnValue = false;
}
}
return returnValue;
}
Statement 를 닫을 때 SQLException 예외가 발생할 수 있기 때문에, 이 메서드는 Statement를 닫고 SQLException을 DaoException으로 포장한다. 그래서 위 코드를 모두 쓰지 않고 closeStatement()를 간단히 호출하고 원래의 예외를 기록한 뒤 DaoException을 던진다. 위와 동일한 이유로 ResultSet 객체를 닫는 위와 유사한 메서드를 하나 더 만든다.
아래에 작성한 메서드들을 사용하면 ResultSet 객체에서 데이터 구조를 손쉽게 가져올 수 있다.
private Account extractAccount(ResultSet rs
) throws SQLException {
Account accountToAdd = new Account();
accountToAdd.setAccountId(rs.getInt("accountId"));
accountToAdd.setAddress1(rs.getString("address1"));
accountToAdd.setAddress2(rs.getString("address2"));
accountToAdd.setCity(rs.getString("city"));
accountToAdd.setCountry(rs.getString("country"));
accountToAdd.setFirstName(rs.getString("firstname"));
accountToAdd.setLastName(rs.getString("lastname"));
accountToAdd.setPassword(rs.getString("password"));
accountToAdd.setPostalCode(rs.getString("postalcode"));
accountToAdd.setState(rs.getString("state"));
accountToAdd.setUsername(rs.getString("username"));
return accountToAdd;
}

) throws SQLException {


acct.put("accountId", rs.getInt("accountId"));
acct.put("address1", rs.getString("address1"));
acct.put("address2", rs.getString("address2"));
acct.put("city", rs.getString("city"));
acct.put("country", rs.getString("country"));
acct.put("firstName", rs.getString("firstname"));
acct.put("lastName", rs.getString("lastname"));
acct.put("password", rs.getString("password"));
acct.put("postalCode", rs.getString("postalcode"));
acct.put("state", rs.getString("state"));
acct.put("username", rs.getString("username"));
return acct;
}
private IdDescription accountAsIdDesc(ResultSet rs
) throws SQLException {
return new IdDescription(
new Integer(rs.getInt("id")),
rs.getString("description"));
}

위의 작업을 다양한 곳에서 수행하기 때문에, 매핑을 단순화하는 메서드를 만들면 나중에 실제 메서드를 생성할 때 오류 발생과 시간 낭비를 줄일 수 있다.
다음 메서드는 DAO 인터페이스에서 “query-by-example” 메서드에서 사용할 PreparedStatement 를 생성한다. 아래에 나오는 코드는 여기에서 가장 복잡한 부분이다. 이것을 보고 나면 SQL Maps 구현체가 훨씬 더 매력적으로 보이기 시작할 것이다. 다른 도우미 메서드들은 간단하지만 코드가 긴 반면, 아래 메서드는 코드도 길며 오류를 발생시키기 일쑤이고 테스트하기도 어렵다. 꼭 만들어야 하지만 짜맞추기 좋진 않은 코드이다.
private PreparedStatement prepareQBEStatement(
Account account,
Connection connection,
PreparedStatement ps,
String baseSql
) throws SQLException {
StringBuffer sqlBase = new StringBuffer(baseSql);
StringBuffer sqlWhere = new StringBuffer("");

String city = account.getCity();
if (!nullOrEmpty(city)) {
sqlWhere.append(" city like ?");
params.add(account.getCity());
}
Integer accountId = account.getAccountId();
if (!nullOrZero(accountId)) {
if sqlWhere.length() > 0) {
sqlWhere.append(" and");
}
sqlWhere.append(" accountId = ?");
params.add(account.getAccountId());
}
if (sqlWhere.length() > 0) {
sqlWhere.insert(0, " where");
sqlBase.append(sqlWhere);
}
ps = connection.prepareStatement(sqlBase.toString());
for (int i = 0; i < params.size(); i++) {
ps.setObject(i+1, params.get(i));
}
return ps;
}

이제 모든 도우미 메서드를 적당히 살펴보았다. 이제부터는 public 인터페이스 구축을 시작 할 수 있다. Insert 메서드가 가장 복잡한데, 데이터를 가져오는 쿼리와 삽입 작업 둘 다 필요하기 때문이다.
public Integer insert(Account account) {
Connection connection = this.getConnection();
Statement statement = null;
PreparedStatement ps = null;
ResultSet rs = null;
Integer key = null;
if (null != connection) {
try{
statement = connection.createStatement();
rs = statement.executeQuery(sqlGetSequenceId);
if (rs.next()) {
key = new Integer(rs.getInt(1));
account.setAccountId(key);
if (log.isDebugEnabled()) {
log.debug("Key for inserted record is " + key);
}
}
ps = connection.prepareStatement(sqlInsert);
int i = 1;
ps.setObject(i++, account.getAccountId());
ps.setObject(i++, account.getUsername());
ps.setObject(i++, account.getPassword());
ps.setObject(i++, account.getFirstName());
ps.setObject(i++, account.getLastName());
ps.setObject(i++, account.getAddress1());
ps.setObject(i++, account.getAddress2());
ps.setObject(i++, account.getCity());
ps.setObject(i++, account.getState());
ps.setObject(i++, account.getPostalCode());
ps.setObject(i++, account.getCountry());
ps.executeUpdate();
} catch (SQLException e) {
log.error("Error inserting data", e);
throw new DaoException(e);
} finally {
closeStatement(ps);
closeResources(statement, rs);
}
}
return key;
}

여기서 삽입할 레코드의 새로운 id를 발급받고서 파라미터로 받은 빈즈에 그 값을 지정한다. 그리고는 이 빈즈를 데이터베이스에 삽입한다. 이 코드는 따라가면서 보기에는 매우 단순하며 단지 iBATIS나 하이버네이트 버전에 비해서 코드가 약간 길 뿐이다.
다른 메서드도 이것과 같이 간단하다. 따라서 더 이상 지면을 낭비하지는 않겠다.

11.2. 다른 데이터 소스로 DAO 패턴 사용하기

DAO 패턴은 Gateway 패턴과 매우 유사하다. Gateway 패턴은 LDAP이나 웹 서비스 등의 많은 다른 데이터 소스들에 적합한 패턴이다.
Gateway 패턴이란 말이 익숙지 않은가? 이 패턴은 그 하는 일 때문에 때때로 래퍼(Wrapper)라고 불리기도 한다. 이 패턴은 API를 감싸서(wrap) 그림에서 처럼 객체를 단순하게 보이도록 만들어준다.이 그림에서는 WebServiceGateway 인터페이스가 내부적인 구현을 감춰준다.
이 말이 익숙하게 들리는가? 아니 익숙하게 들릴 것이다. 바로 DAO 패턴의 배경이 되는 개념이기 때문이다. DAO 패턴은 트랜잭션과 커넥션 풀 그리고 다른 데이터베이스에 종속적인 사항들에 특화된 Gateway 이다.

11.2.1 예제: LDAP으로 DAO 사용하기
LDAP은 계층적인 데이터를 저장하는데 너무도 훌륭한 도구이다. 또한 네트워크 관리자들이 사용자들을 추적하고 구성원이나 다른 유사 데이터를 그룹화하는 데도 사용한다. 예를 들면 Novel Directory Services(NDS) 나 마이크로 소트트사의 ActiveDirectory 는 둘 다 LDAP을 기반으로 하며 LDAP API를 제공한다.
DAO 패턴을 사용하여 LDAP을 사용하는 디렉터리에 접근하는 것은 애플리케이션에서 JNDI 프로그래밍의 느낌을 계속 유지할 수 있는 훌륭한 한 방법이다. 특수 목적의 작은 클래스 집합을 만들어서 가볍고 테스트하기 좋은 JNDI 컴포넌트를 구축할 수 있다. 그러고나서는 데이터 소스는 외부에 노출시키지 않은 채로 DAO 구현체에 붙이면 된다.

- LDAP 용어 이해하기
LDAP 디렉터리 기반의 DAO 구현을 구축하는 완전한 예제를 시작하기 전에, 몇 가지 용어를 먼저 알아보자. LDAP은 의도적으로 모호하게 만들어졌다. LDAP은 계층적인 구조의 데이터 저장소에 접근하는 매우 유연하고 일반화된 목적을 수행하는 프로토콜이 되도록 설계되었기 때문이다.
LDAP 디렉터리의 기초가 되는 요소는 엔트리(entry)라고 부른다. 엔트리는 데이터(속성이라고 부른다.)나 다른 엔트리 혹은 그 둘 다를 포함할 수 있다. 모든 엔트리는 정확히 하나의 부모를 가지고 있으며 DN(Distinguished Name)으로 유일하게 구분 지을 수 있다. DN은 전체 디렉토리에서 중복이 있을 수 없다. 엔트리의 데이터 요소들은 엔트리가 나타내는 하나 혹은 그 이상의 객체 클래스로 정의한다.
예를 들어 자바 애플리케이션으로 일반적인 LDAP 엔트리들을 관리하는 연락처 관리자를 만들고자 한다면, 엔트리를 나타내는 다음과 같은 형태의 빈즈를 생성할 것이다.
public class Contact {
private String userId;
private String mail;
private String description;
private String firstName;
private String lastName;
// Getters and Setters to make properties...
}
이 객체를 LDAP 디렉터리에 저장하는 한 방법으로 간단히 자바 객체를 디렉터리로 직렬화해 넣는 것이 있다. 우리의 예제에서는 두 가지 이유 때문에 그렇게 하지 않을 것이다. 하나는 우리의 디렉터리를 잠재적으로 자바가 아닌 다른 시스템과 상호 교환할 수 있게 만들기 위해서이다. 다른 이유는 LDAP 기반 쿼리의 장점을 누리기 위해서이다. 데이터베이스를 사용할 때는 원래 그 시스템이 의도된 방식으로 사용하는 것이 좋다.
- 자바에서 LDAP으로 매핑하기
전에 말한 바와 같이, 모든 LDAP 디렉터리 엔트리는 하나 혹은 그 이상의 객체 클래스를 나타낸다. 이 객체 클래스들은 속성들의 묶음을 정의한다. 이 속성과 자바의 Map 인터페이스가 유사하기 때문에 간단히 Map을 사용하여 JNDI 속성 구조를 숨겨주는 Map 기반의 DAO를 만드는 것은 정말 일도 아니다. 하지만 이번 절에서는 이전 절의 빈즈를 사용하여 이 빈즈를 표의 매핑을 통해 LDAP의 intOrgPerson 엔트리에 매핑할 것이다.

빈즈 프로퍼티 LDAP 속성
userId Uid
Mail Mail
Description Description
lastName Sn
firstName givenName
이 매핑은 DAO 구현체 안에서 Attribute 객체로부터 빈즈를 생성하거나, 혹은 그 반대로 빈즈로부터 Attributes 객체를 생성해내는 메서드를 사용해서 이루어진다. 리플렉션을 기반으로 하여 위 매핑을 처리하는 메커니즘을 만들어내는 것도 가능하긴 하지만, 여기서는 DAO 구현을 매우 간단하게 만들 것이기 때문에 매핑 정보를 그냥 하드 코딩할 것이다. 다음은 DAO 구현체에서 매핑 작업을 수행하는 메서드 세 가지를 보여준다.
private Attributes getAttributes(Contact contact){
Attributes returnValue = new BasicAttributes();
returnValue.put("mail", contact.getMail());
returnValue.put("uid", contact.getUserId());
returnValue.put("objectClass", "inetOrgPerson");
returnValue.put(
"description", contact.getDescription());
returnValue.put("sn", contact.getLastName());
returnValue.put("cn", contact.getUserId());
returnValue.put("givenName", contact.getFirstName());
return returnValue;
}
private Contact getContact(Attributes attributes) {
Contact contact = new Contact();
contact.setDescription(
getAttributeValue(attributes, "description"));
contact.setLastName(
getAttributeValue(attributes, "sn"));
Table 11.1 JavaBean to LDAP attribute mapping
Bean property LDAP attribute
userId uid
mail mail
description description
lastName sn
firstName givenName
contact.setFirstName(
getAttributeValue(attributes, "givenName"));
contact.setMail(getAttributeValue(attributes, "mail"));
contact.setUserId(
getAttributeValue(attributes, "uid"));
return contact;
}
private String getAttributeValue(
Attributes attributes, String attrID
) {
Attribute attribute = attributes.get(attrID);
try {
return (null==attribute?"":(String)attribute.get());
} catch (NamingException e) {
throw new DaoException(e);
}
}
Attributes 인터페이스는 Sun 의 JDK에 포함돼 있는 JNDI 패키지의 일부이며 또한 동일한 패키지에 포함돼 있는 BasicAttributes 클래스가 이의 구현체이다. Contact 클래스는 LDAP 디렉터리에 매핑하고자 하는 빈즈이다. 마지막으로 getAttributeValue() 메서드는 도우미 메서드로써 null 값 등을 처리해 주어 매핑 과정을 단순화하고, JNDI 종속적인 예외들을 DaoException으로 변경해 주는 일을 한다.
다른 DAO 구현체들과 마찬가지로 데이터베이스로의 접근을 어디에서 어떻게 할지에 대한 몇 가지 결정을 내려야 한다. JNDI 컨텍스트를 제공해 주는 J2EE 컨테이너를 사용하고 있다면 JNDI 컨텍스트를 사용하는 것이 끌릴 것이다. 만약 요구사항에 적합하다면 이를 사용하지 않을 이유가 없다. 하지만 JNDI를 사용하면 몇 가지 트레이드오프가 있다. 이는 코드는 단순해지지만 테스트는 어렵게 만든다. 요구사항이 뭐냐에 따라서, 이 정도 희생은 감수할 만하다고 할 수도 있다.
이번 예제에서는 가능한 한 테스트하기 쉽게 만들려고 한다. 따라서 생성자 기반의 의존성 삽입 방식을 통해서 DAO 클래스의 설정을 실행시간에 할 수 있게 만들 것이다. iBATIS의 DAO는 이런 식으로 사용할 수 없기 때문에, 디폴트 값으로 지정했으면 하는 값 두 개를 사용하는 디폴트 생성자도 함께 만들 것이다. 11.3 절에서는 Spring 프레임워크로 DAO 계층을 구성해 볼 것이다. 이때는 설정 파일을 통해서 실행시간 설정을 할 수 있다. 하지만 지금 당장은 디폴트 생성자 메서드를 사용하자.
두 번째 생성자는 두 개의 파라미터를 받아서 디폴트 생성자에게 하드코딩한 두 가지 설정을 수행한다. 하나는 Contact 빈즈에서 사용할 LDAP DN 속성을 결정한다. 이 속성은 데이터 베이스 테이블의 기본키와 같은 역할을 한다. 하지만 테이블에서 행에 대한 기본키의 역할처럼 특정 단일 세그먼트에서만 값이 유일한 게 아니라 전체 디렉터리에서 동일한 값이 나오면 안 된다. 아래 메서드는 DAO 구현체에서 Contact 빈즈에 중복되지 않는 DN을 생성해준다.
private String getDn (String userId){
Return MessageFormat.format(this.dnTemplate, userId);
}
두 번째로 해야 할 설정은 초기 디렉터리 컨텍스트를 가져오는 것이다. DAO의 디폴트 생성자는 하드코딩한 프로퍼티를 사용하여 LDAP 디렉터리에 접속한다. 다시 한 번 말하지만, 두 번째 생성자를 사용하면 다른 목적을 수행하는데 필요한 사용자 정의 프로퍼티를 지정할 수 있다.
private DirContext getInitialContext() {
DirContext ctx = null;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
log.error("Exception getting initial context", e);
throw new DaoException(e);
}
return ctx;
}
이제 빈즈를 매핑하고 LDAP 디렉터리에 접속하는데 필요한 모든 인프라스트럭처 코드를 구축했으니, DAO 구현체를 구성할 때가 되었다. 처음으로 볼 메서드는 이 중에서 가장 간단한 것으로 userId를 통해서 연락처를 찾는 것이다. 이 메서드를 구현하는 코드가 아래에 있다.
public Contact getById(String id) {
DirContext ctx = getInitialContext();
Attributes attributes;
try {
attributes = ctx.getAttributes(getDn(id));
} catch (NamingException e) {
throw new DaoException(e);
}
return getContact(attributes);
}

여기서는 디렉터리 컨텍스트를 가져와서 이를 통해 userId 값으로 받은 파라미터의 DN을 기반으로 하여 연락처의 속성들을 가져온다. 일단 속성들을 가져오면 이를 Contact 빈즈로 변환해서 반환한다. LDAP에 종속적인 NamingException이 발생하면 DaoException으로 변경해서 다시 던진다. DAO 메서드의 시그너처에는 데이터 소스를 가리키는 특징이 들어가면 안 되기 때문이다.
삽입(insert) 작업은 LDAP 식으로 말하면 바인딩이라고 부른다. 우리 예제에서는 이것이 LDAP의 바인딩인지 절대로 알 수 없다. DAO에서 이 과정을 감싸서 처리하고 바인딩 대신 insert라고 부를 것이기 때문이다.
public Contact insert(Contact contact) {
try {
DirContext ctx = getInitialContext();
ctx.bind(getDn(
contact.getUserId()),
null,
getAttributes(contact));
} catch (Exception e) {
log.error("Error adding contact", e);
throw new DaoException(e);
}
return contact;
}

유사하게, 수정(update)과 삭제(delete) 메서드도 LDAP에 종속적인 클래스를 사용해서 일을 처리하고서 예외는 위와 같은 기법을 사용해서 던진다. JNDI 용어로 수정은 리바인딩(rebinding)이고 삭제는 언바인딩(unbinding)이라고 부른다. 어쨌든 우리는 DAO 패턴을 사용하기 때문에 이런 용어들은 애플리케이션에서는 볼 수 없다. 우리가 DAO 구현체에서 만든 것이 LDAP에 종속적인지 여부는 결코 외부에서는 알 수가 없다.
앞으로 개발하면서 다루게 될 낯선 데이터 소스에 LDAP만 있는 것은 아니다. 그 모든 것들을 이 한 장에서 다룰 수 없기 때문에 가까운 미래에 다루게 될 확률이 높은 다른 것, 바로 웹 서비스에 대해서 살펴보자.

11.2.2 예제: 웹 서비스로 DAO 사용하기
DAO 패턴을 사용하여 웹 서비스를 구축하는 것은 정말 좋은 생각이다. 추상 계층 같은 것을 통해 웹 서비스를 사용하는 이유는 웹 서비스를 사용하는 컴포넌트를 테스트하는 것이 간편해지기 때문이다. 예를 들어 사용하고자 하는 것이 신용 카드를 처리하는 서비스이거나 혹은 다른 원거리 서비스라서 접속 시간이나 실행 시간, 결과를 처리하는 시간 등이 오래 걸린다고 할 때, 만약 서비스 처리를 기다려야 한다면(그것도 여러 번) 테스트하는 것이 아주 심각하게 느려질 수 있다. 게다가 서비스가 요청 단위로 요금을 내야 하거나 혹은 (아마존, 구글, eBay처럼) 공개적인 것이어서 반환되는 데이터가 일정치 않고 계속 변한다면, 통합이나 사용자 검수 테스트(User Acceptance Test) 이외의 목적으로 사용하는 것은 하기 힘들게된다. 비용이 너무 많이 들고, 반환되는 데이터가 너무 다양하기 때문이다. 이런 문제와 더불어 시간이 지나면 결과가 바뀌는 테스트를 위해서 데이터를 준비할 필요가 정말 있을까? 대신 의미 있는 정적(혹은 쉽게 예측 가능한) 데이터를 필요로 하는 단위 테스트를 작성하는 것이 낫다.
그럼 여러분이 시스템을 만들고 있고 시스템 애플리케이션 안에서 사용자들이 구글 검색을 할 수 있길 바란다고 하자. 웹 서비스를 호출하는 구글 API는 매우 간단해서 쉽게 사용할 수 있다. 하지만 지금은 다른 검색 엔진들도 유사한 API를 제공하기 때문에 좀 더 (모든 검색엔진에서) 일반적으로 사용할 수 있는 검색 API를 만들어 구글 구현체를 감싸서 애플리케이션에서 그 감싸고 있는 API를 호출하도록 만들 것이다.
먼저 검색 인터페이스를 만들고 검색 결과를 반환 받는 구조를 정할 필요가 있다. 자, 간단한 빈즈와 인터페이스를 만드는 것으로 시작해보자.
public class SearchResult {
private String url;
private String summary;
private String title;
// getters and setters omitted...
}
public interface WebSearchDao {
List getSearchResults(String text);
}

여기서 우리의 빈즈에는 세 개의 프로퍼티가 있고, 인터페이스에는 List 타입을 반환하는 한 개의 메서드가 있다. 다음은 구글 API를 사용한 검색 API의 구현체다.
public class GoogleDaoImpl implements WebSearchDao {
private String googleKey;
public GoogleDaoImpl(){
this("insert-your-key-value-here");
}
public GoogleDaoImpl(String key){
this.googleKey = key;
}
public List getSearchResults(String text){
List returnValue = new
ArrayList();
GoogleSearch s = new GoogleSearch();
s.setKey(googleKey);
s.setQueryString(text);
try {
GoogleSearchResult gsr = s.doSearch();
for (int i = 0; i < gsr.getResultElements().length;
i++){
GoogleSearchResultElement sre =
gsr.getResultElements()[i];
SearchResult sr = new SearchResult();
sr.setSummary(sre.getSummary());
sr.setTitle(sre.getTitle());
sr.setUrl(sre.getURL());
returnValue.add(sr);
}
return returnValue;
} catch (GoogleSearchFault googleSearchFault) {
throw new DaoException(googleSearchFault);
}
}
}

구글 API는 작업을 하려면 키가 필요하다는 점만 제외하고는 DAO 인터페이스와 매우 유사하다. 우리의 예제에서는 구현체에 하드 코딩한 키를 사용하기 때문에 이것은 문제가 되지 않는다. 실제 운영할 애플리케이션에서는 여럿이 공유하는 키보다는 특정 사용자 전용키를 제공하는 것이 아마도 더 나을 것이다.
여기서 iBATIS DAO 계층의 또 다른 한계를 볼 수 있다. DAO 클래스의 인스턴스를 다중으로 제공할 수 없고 디폴트 생성자만을 사용하기 때문에, 이 DAO 를 우리가 원하는 방식대로 실행시키려면 몇 가지 추가적인 작업을 해줘야만 한다.
다음 절에서는 좀 더 고급 설정 기법을 제공해 주는 Spring 프레임워크를 사용하여 더 많은 기능을 가진 DAO 계층을 만드는 방법을 알아볼 것이다.

11.3. Spring DAO 사용하기
iBATIS와는 상관없이, Spring 프레임워크를 사용하여 애플리케이션의 데이터 접근 계층을 다루는 방법은 매우 많이 있다. 이번 절에서는 Spring의 iBATIS 지원 기능을 사용하여 데이터 접근 계층을 구축하는 방법을 공부할 것이다.

11.3.1 코드 작성하기
Spring 프레임워크는 데이터 접근 객체에 템플릿 패턴을 사용하여 iBATIS를 지원한다. 템플릿 패턴을 사용한다는 것은 Spring이 제공해 주는 클래스(SqlMapClientTemplate)를 개발자가 작성하는 DAO가 상속받아서 구현을 시작한다는 의미이다. Spring에서 이러한 기법을 사용해서 만든 AccountDao 구현은 다음과 같은 형태가 될 것이다.
public class AccountDaoImplSpring
extends SqlMapClientTemplate
implements AccountDao
{
public Integer insert(Account account) {
return (Integer) insert("Account.insert", account);
}
public int update(Account account) {
return update("Account.update", account);
}
public int delete(Account account) {
return delete(account.getAccountId());
}
public int delete(Integer accountId) {
return delete("Account.delete", accountId);
}
public List getAccountListByExample(
Account account) {
return queryForList("Account.getAccountListByExample",
account);
}

getMapListByExample(Account account) {
return queryForList("Account.getMapListByExample",
account);
}
public List
getIdDescriptionListByExample(Account account) {
return
queryForList("Account.getIdDescriptionListByExample",
account);
}
public Account getById(Integer accountId) {
return (Account) queryForObject("Account.getById",
accountId);
}
public Account getById(Account account) {
return (Account) queryForList("Account.getById",
account);
}
}

예리한 독자는 앞 장에서 본 코드와 거의 같다는 것을 알아차렸을 것이다. 하지만 여기서 알아둬야 할 것은 상속받는 클래스가 이전 코드와 다르다는 것이다. 이 클래스에서 다른 모든 점은 앞 장과 동일하다. 자 그렇다면 이제, 언제 어떤 클래스를 상속받아야 하는지 의문이 생길 것이다. 다음 절에서 알아보자.

11.3.2 왜 iBATIS 대신에 Spring을 사용하는가?
이것은 당연히 물어볼 만한 질문이다. 이 책은 iBATIS에 대한 책인데 DAO 계층에 다른 것을 사용하는 방법에 대해 언급하는 이유는 무엇일까? Spring과 iBATIS는 그들만의 장점과 단점을 가지고 있고, 둘 중에 무엇을 사용할지 결정하려면 서로의 장단점에 대한 이해는 물론 개발하는 애플리케이션이 요구하는 것이 무엇인가도 이해해야 한다.
iBATIS DAO 계층의 장점은 빠르고 쉬운 솔루션이라는 점이다. 이미 iBATIS SQL Maps를 다운로드 하였다면, iBATIS DAO 프레임워크도 역시 사용할 수 있게 된 것이다. 필요한 것이 트랜잭션과 커넥션 관리뿐이라면 Spring보다 훨씬 더 간단하게 사용할 수 있는 프레임워크인 셈이다. 이러한 경우에는 iBATIS DAO 계층이 애플리케이션에 적합할 것이다.
iBATIS DAO의 간결함은 또한 가장 큰 단점이 되기도 한다. 일단 DAO 패턴을 사용하기 시작하면, 결합도가 낮아짐으로써(decoupling) 테스트하기 쉽다는 장점도 누릴 수 있게 되는것이다. 개발자는 애플리케이션의 다른 영역에서도 동일한 접근 방법을 사용하고자 할 것이다.
예를 들면 스트럿츠 애플리케이션에서 비즈니스 로직 클래스와 DAO 클래스 사이에서 사용하는 것과 동일한 접근 방법을 Action 클래스와 비즈니스 로직 클래스 사이에서도 사용할 것이다. Action 에서 필요한 코드의 구현을 알 필요 없이 오직 필요한 인터페이스만 알면된다. 또한 구현체는 설정을 통해 끼워 넣게 된다. 이렇게 하면 Action 클래스를 간결하게 유지하면서 모든 계층이 테스트하기 쉬워진다.
계층 간의 분리를 관리하는 것과 더불어, Spring 은 iBATIS DAO 계층과 마찬가지로 커넥션과 트랜잭션도 관리할 수 있다. Spring의 큰 장점은 이것이 DAO 계층에만 적용되는 것이 아니라 애플리케이션의 모든 부분에 적용된다는 점이다.

11.4. 개발자 스스로 DAO 계층을 생성하기
때로는 iBATIS DAO 지원도 Spring DAO 지원도 여러분이 필요한 것을 정확히 채워주지 못할 수 있다. 이럴 때는 “스스로 만든 DAO 계층을 굴릴” 필요가 있다.
바닥부터 DAO 계층을 만들어가는 것은 굉장히 어려운 작업처럼 보일 것이다. 하지만, 실제로 구현하기 꽤 수월한 패턴임을 알고 나면 놀라게 될 것이다. 효율적인 DAO 계층에는 본질적으로 세 개의 티어가 존재한다.
1. 구현체에서 인터페이스를 분리하기
2. 외부에서 설정된 팩토리를 사용하여 구현체의 결합도 낮추기
3. 트랜잭션과 커넥션 관리기능 제공하기
우리의 목표에 따라서 처음 두 티어를 수행하려면 무엇이 필요한지 알아볼 것이다. 하지만 트랜잭션과 커넥션 관리는 7장을 참고하고 거기서부터 시작하라.

11.4.1 구현체에서 인터페이스를 분리하기
DAO 를 인터페이스와 구현체로 분리해야 하는 이유는 두 가지가 있다. 첫째로 다른 형태의 데이터 접근을 지원해야 할 필요가 생길 경우 구현체를 바꿔 치기 할 수 있기 때문이다. 둘 째로 분리를 하면 테스트가 훨씬 쉽고 빨라진다. 실제 데이터베이스에 접근하는 객체 대신 모의 DAO 객체를 끼워 넣는 것이 가능해지기 때문이다.
IDE를 사용한다면 이 분리 과정을 매우 쉽게 처리할 수 있다. 대부분의 개발 환경에는 리펙터링 툴이 있어서 클래스에서 인터페이스를 분리해낼 수 있다. 하지만 이는 DAO를 구축하는 것에서는 아주 작은 일부분에 불과하다.
개발자가 DAO 패턴을 처음으로 접했다면, 인터페이스를 만들 때 구현 클래스를 JDBC나 iBATIS 혹은 어떤 다른 데이터베이스를 다루는 툴에 종속적인 부분에 노출시켜 버리기가 쉽다. 이것이 인터페이스를 관리할 때 더욱 어려운 부분이다. 이러한 노출은 DAO가 아닌 데이터 접근 구현체에 애플리케이션을 묶어버리기 때문에 문제가 된다. 비록 크게 어려운 일은 아니지만, 사소하게 다룰 문제도 아닌 것이다.
컬렉션(빈즈의 List 같은 것들)은 결과셋을 사용하는 코드를 변경하면 쉽게 사용할 수 있다. 하지만 다른 변경들과 마찬가지로 테스트를 해야 하며, 애플리케이션에서 어디에 이 코드가 위치해 있느냐에 따라서 테스트 과정이 매우 어려울 수도 있다. 예를 들면 대용량 데이터 셋에 가벼운 리포팅 기능을 제공하는 ‘Fast Lane Reader’ 패턴을 사용하는 웹 애플리케이션에서는 JDBD 코드가 뷰 계층과 직접 소통하는 경우가 있다. 이렇게 하면 테스트하기가 너무도 어려워진다. 사람의 개입을 필요로 하는 것은 무엇이든 더 많은 시간을 잡아먹기 때문이다. 게다가 원본뿐만 아니라 테스트 코드를 제대로 수행하도록 재작성하는 것도 어려울 것이다. 한 가지 해결책은 콜백을 사용해서 코드를 작성하여 데이터 접근 속도를 개선하는 것이다.(따라서 이 예제에서는 뷰가 요청한 데이터를 처리하는 RowHandler 같은 것이 있으면 좋을 것이다.)
SQL Maps API를 직접 사용하는 애플리케이션을 좀 더 캡슐화된 API를 사용하도록 수정하는 것은 어느 정도 쉽게 해결할 수 있다. 예를 들어 SqlMapClient 객체의 queryForList() 메서드를 호출하는 클래스를 개발하고 있다면, 이 호출을 스스로 작성한 DAO 클래스를 호출하도록 리팩터링하고 그 클래스의 메서드에서는 List 객체를 반환하게 하면 데이터 사용자 측에서는 오직 스스로 만든 DAO하고만 소통하게 된다.

11.4.2 결합도 낮추기(decoupling)와 팩토리 생성하기
자 이제 인터페이스와 구현의 분리를 끝냈다. 인터페이스와 구현체 둘 다 DAO를 사용하는 클래스에 노출시키면 안 된다. 구현체에 대한 의존성을 제거하는 대신 인터페이스에 대한 의존성을 DAO를 사용하는 클래스에 추가한 것이기 때문이다.
내가 말하고자 하는 것은 DAO에 인터페이스를 추가하고 그 인터페이스를 구현하였다면 그 구현체를 어떻게 사용할 수 있는가 하는 것이다. 팩토리를 사용하지 않는다면 아마도 다음과 같은 방법을 사용하기 십상이다.
AccountDao accountDao = new AccountDaoImpl();
뭐가 문제인지 알겠는가? DAO를 구현에서 분리했음에도 여전히 곳곳에서 구현체를 직접 참조하고 있다. 이것은 여기서 생성한 DAO 객체를 애플리케이션 모든 곳에 전달하지 않는다면 가치가 없는 일이다. 아래와 같은 방식으로 사용하는 것이 더 나은 패턴이다.
AccountDao accountDao =
(AccountDao)DaoFactory.get(AccountDao.class);

이 예제에서는 무엇이 구현체일까? 개발자는 이에 대해 알지도 못하고 신경 쓸 필요도 없다. DaoFactory가 우리 대신 다 처리해 줄 것이기 때문이다. 우리가 신경 써야 할 것은 단지 이 DaoFactory가 AccountDao 인터페이스를 구현한 객체를 반환한다는 점뿐이다. 이 구현체가 일만 제대로 처리한다면 LDAP, JDBC 혹은 어떤 속임수를 사용해서 일을 처리하든 신경 쓰지 않는다.
추상 팩토리를 생성하는 것은 쉽고 재미있다! 그래, 어쩌면 재미는 없을지도 모르지만 여전히 쉬운 것은 사실이다. 이번 절에서는 간단한 팩토리를 만들고 우리가 직접 만든 DAO 클래스를 가져오는데 왜 팩토리를 사용해야 하는지 이야기해보자.
자 대체 DaoFactory는 어떻게 생긴 것일까? 놀랍게도 다음에서 볼 수 있듯이 코드가 몇 십줄밖에 안 된다.
public class DaoFactory {
private static DaoFactory instance = new DaoFactory();
private final String defaultConfigLocation =
"DaoFactory.properties";
private Properties daoMap;
private Properties instanceMap;
private String configLocation = System.getProperty(
"dao.factory.config",
defaultConfigLocation
);
private DaoFactory(){
daoMap = new Properties();
instanceMap = new Properties();
try {
daoMap.load(getInputStream(configLocation));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private InputStream getInputStream(String configLocation)
{
return Thread
.currentThread()
.getContextClassLoader()
.getResourceAsStream(configLocation);
}
public static DaoFactory getInstance() {
return instance;
}
public Object getDao(Class daoInterface){
if (instanceMap.containsKey(daoInterface)) {
return instanceMap.get(daoInterface);
}
return createDao(daoInterface);
}
private synchronized Object createDao(
Class daoInterface
) {
Class implementationClass;
try {
implementationClass = Class.forName((String)
daoMap.get(daoInterface));
Object implementation =
implementationClass.newInstance();
instanceMap.put(implementationClass, implementation);
} catch (Exception e) {
throw new RuntimeException(e);
}
return instanceMap.get(daoInterface);
}
}

확실히 이것이 역사상 가장 멋진 팩토리는 아니다. 하지만 상당히 작으면서도 효율적이다. 이 팩토리의 public 인터페이스는 getInstance()와 getDao() 오직 이 두 개의 메서드만으로 이루어져 있다. Private 생성자는 설정 파일을 읽어 들인다. 이번 경우에 설정 파일은 인터페이스와 구현체의 이름을 나타내는 이름/값 쌍을 저장하는 properties 파일이다. 이 클래스는 스스로를 포함하는 (self-contained) 싱글턴이다. 따라서 getInstance() 메서드는 그저 이 클래스의 유일한 인스턴스를 반환한다. getDao() 메서드는 인터페이스의 구현체를 반환한다. DAO는 나중에 DAO에 대한 실제 요청이 발생할 때 createDao() 메서드에서 생성된다.

댓글 없음:

댓글 쓰기