2008년 2월 1일 금요일

RIFE 시작하기(8) - Adding database support

http://rifers.org 에서 발췌한 내용을 번역 하였습니다.
번역: 이원찬(wonchan.lee@gmail.com)
-------------------------------------------------------

Adding database support

지금까지 Friends application은 친구목록을 저장할 곳이 없어 상당히 제한적이었다. 마지막으로, 데이터베이스 지원 부분을 이 장에서 다룰 것이다. 우리는 당신이 이미 SQL database와 자바의 데이터베이스 프레임웍인 JDBC에 어느정도 익숙하다고 가정할 것이다.

웹어플리케이션을 개발할때 앞의 친구예제 처럼 동적인 데이터를 핸들링할 일일 종종 발생한다. 그것은 뉴스, 기사, 이미지등 웹사이트 내용의 무엇이든 될 수 있다. 데이터베이스를 저장소로 사용하게 되면 어플리케이션을 쉽게 설계하고 확장할 수 있다. 산발적으로 흩어진 데이터파일들을 더 쉽게 관리할 수 있게 된다.

우리는 RIFE와 함께 데이터베이스를 설계로 끌어들여 더 직관적이고 쉽게 할것이다.

datasource참여자

먼저, 저장소(repository)에 데이터소스(datasource) 참여자(participant)를 필요로 한다. 그리고, 약간의 설정정보를 참여자에게 제공해야 한다. rep/participants.xml 을 수정한 후의 모습이다.
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE rep SYSTEM "/dtd/rep.dtd">

<rep>
<participant param="rep/config.xml">ParticipantConfig</participant>

<!-- Add a data sources participant: -->
<participant param="rep/datasources.xml">ParticipantDatasources</participant>

<participant param="sites/friends.xml">ParticipantSite</participant>
</rep>
------------------------------------------------------------------

그리고, rep/datasources.xml에 데이터소스 정의 파일을 아래와 같이 작성했다. 두개의 데이터소스를 설정하였다.

------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE datasources SYSTEM "/dtd/datasources.dtd">

<datasources>

<datasource name="postgresql">
<driver>org.postgresql.Driver</driver>
<url>jdbc:postgresql://localhost:5432/rife</url>
<user>rife</user>
<password>rife</password>
<poolsize>5</poolsize>
</datasource>

<datasource name="mysql">
<driver>com.mysql.jdbc.Driver</driver>
<url>jdbc:mysql://localhost:3306/rife</url>
<user>rife</user>
<password>rife</password>
<poolsize>5</poolsize>
</datasource>

</datasources>
------------------------------------------------------------------

PostgreSQL과 MySQL 두개의 데이터소스를 설정한 이유는 어플리케이션의 코드를 변경하지 않고도 둘중 하나가 사용 불가능할 경우 쉽게 변경할 수 있게 하기 위함이다. 좋은예로, 개발용 기계와 운영용 기계가 환경에 따라 다른 데이터베이스를 사용해야 할때이다. 주의깊은 독자는 아마 이전장의 설정선택자(configuration selector)의 호스트명 선택자를 떠올릴것이다.

다른 혜택은 이 코드가 다른 데이터베이스 에서도 동일하게 작동하므로 다른 사람 또은 다른 프로젝트에도 유용하게 사용될 수 있다는 것이다. 코드 재사용은 시간을 절약하는 좋은 습관이다.

RIFE는 PostgreSQL과 MySQL, Oracle 데이터베이스에 대한 지원을 내장하고 있다. 그밖의 데이터베이스를 사용하기 위한 드라이버도 사용될수 있으나 이 문서의 범위는 벗어난다.

데이터소스(datasource) 인수(parameter)
: 데이터소스 정의를 위해서는 몇개의 인수를 필요로 한다.

- 사용자명과 비밀번호 : 데이터베이스와 연결하기 위해 user과 password태그에 사용자명과 비밀번호를 설정한다.

- 데이터베이스 드라이버 : driver태그에 어떤 데이터베이스 드라이버를 사용할지 설정한다. org.postgresql.Driver, com.mysql.jdbc.Driver, oracle,driver.OracleDriver 같은 것들중 하나가 사용될 수 있다.

- 주소(URL) : url태그는 데이터베이스의 위치를 설정한다. 맨앞의 jdbc는 주프로토콜을 의미하고 그 다음의 postgresql 또는 mysql은 서브프로토콜이다. 그 다음에 따라오는 localhost는 호스트명이며 포트번호가 뒤따라올 수 있다. 마지막의 rife는 데이터베이스명이다.

- 연결자 (Connection Pool) : 데이터베이스로 연결하는데 많은 시간이 소요되므로 RIFE는 열린 데이터베이스 연결에 대한 풀(pool)을 유지한다. 이것은 많은 트래픽이 발생하는 웹사이트에 특히 성능의 향상을 제공한다.poolsize태그는 연결될 풀의 갯수를 설정하며 0은 풀을 사용하지 않음을 의미한다. 일단, 평균적으로 5를 설정토록 하겠다.

데이터소스 선택

우리는 두개의 데이터소스를 가지고 있으므로 실제 사용할 하나를 선택해야 한다. RIFE의 설정시스템은 단지 파라미터값만 추가해서 어떤 데이터소스가 사용될지 가르키기만 하면 된다. 데이터베이스 코드는 이 파라미터를 읽어서 적절한 작업을 수행하게 된다. 설정을 마친 rep/config.xml파일은 아래와 같다.

------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE config SYSTEM "/dtd/config.dtd">

<config>
<param name="DISPLAY_TITLE">Friends of Mikael</param>

<!-- Add a DATASOURCE parameter: -->
<param name="DATASOURCE">postgresql</param>

</config>
------------------------------------------------------------------

새로운 파라미터는 DATASOURCE이며 사용될 데이터소스의 이름을 설정한다.

Friend bean

우리는 각 친구에 대해 이름, 설명, 주소의 데이터구조를 유지할 것이다. 빈(bean)은 이러한 정보를 위한 완벽한 컨테이너이다. 왜냐하면, 많은 RIFE인터페이스는 많은 속성들을 개별로 다루지않고 빈과 작동하기 때문이다. 이것은 훨씬 명백해 보인다.

------------------------------------------------------------------
package tutorial.friends.backend;

public class Friend
{
private String mFirstname = null;
private String mLastname = null;
private String mDescription = null;
private String mUrl = null;

public Friend(String firstname, String lastname, String description, String url)
{
mFirstname = firstname;
mLastname = lastname;
mDescription = description;
mUrl = url;
}

public void setFirstname(String firstname)
{
mFirstname = firstname;
}

public String getFirstname()
{
return mFirstname;
}

// ... and so on for lastName, description and url

}
------------------------------------------------------------------

lastName, description, url에 대한 setter/getter도 firstName과 마챦가지 이다.

데이터 관리

데이터소스에 접근하기 위해서 테이블을 생성하고 추가, 조회등의 질의(queries)를 하는 작은 클래스를 하나 작성해야 한다. RIFE는 이것을 쉽게 한다. 우리는 단지 com.uwyn.rife.database.DbQueryManager만 상속하면 된다. 이것은 우리가 구현해야할 많은 기초적인 작업을 대신 하며 코드를 깔끔하고 이해쉽게 해준다. 실제 코드는 src/tutorial/friends/backend/FriendManager.java 에서 볼 수 있다. 아래에 중요한 부분에 대한 코드일부를 볼것이다.

올바른 데이터소스 사용하기

이 예제에서 두개의 데이터소스중 어떤것을 사용할지 선택할 것이다.

------------------------------------------------------------------
public FriendManager()
{
super(Datasources.getRepInstance().
getDatasource(Config.getRepInstance().getString("DATASOURCE")));
}
------------------------------------------------------------------

이제 쿼리매니저는 설정파일에 입력된 데이터소스와 함께 초기화될 것이다.

친구 출력하기
친구목록을 출력하기 위한 템플릿과 요소(element)는 이미 작성하였다. 필요한것은 단지 데이터베이스 기능만을 추가하는 것이다. SQL질의를 작성하고 이를 데이터베이스상에서 실행하여 검색된 형(row)들을 가공하면 된다. RIFE는 이런 반복작업을 도와줄 좋은 방벘을 가지고 있다. 가장쉬운 방법은 먼저 코드를 보는것이다. FriendManager의 display메소드를 보자.

------------------------------------------------------------------
public void display(DbRowProcessor processor)
{
Select select = new Select(getDatasource());
select
.from("Friend")
.fields(Friend.class)
.orderBy("firstname");

// fetch every row in the resultset and forward the processing
// of the data to the DisplayProcessor
executeFetchAll(select, processor);
}
------------------------------------------------------------------

display메소드는 요소(element)로 부터 호출되어 모든 친구들을 조회하고 템플릿에 값을 삽입한다. 질의는 Friend빈 클래스를 사용해 생성된다. 이것은 우리가 모든 필드를 서술하지 않아도 됨을 의미한다. 이것은 단지 빈의 모든 속성들을 사용한다.

한번에 한친구씩 보여주기

질의를 실행하는 동안, 우리는 모든 행(row)이 처리될 때까지 DbQueryManager의 fetch메소드를 호출하여 결과를 조회할 수 있다. 이때 DbRowProcessor객체가 인수로 전달된다.

행처리기(row processor)는 데이터베이스코드가 요소와 출력코드로 부터 분리할 수 있는 방법을 제공한다. FriendManager는 이것을 사용하는 요소에 관해 전혀 알지 못한다. 요소도 데이터베이스 코드에 관해 전혀 알지 못한다. 이러한 종류의 디자인은 대부분 어플리케이션의 유지보수를 쉽게 하기위해 항상 요구되는 것이다.

행처리기 작성하기

행처리기의 작성은 간단하다. DbRowProcessor를 확장하고 processRow메소드를 구현하기만 하면 된다. 이 처리기는 display요소 바깥쪽에서는 절대 쓰이지 않는다. 이 클래스는 tutorial/fiends/Display.java의 내부클래스(inner class)로 구현될 것이기 때문이다.

------------------------------------------------------------------
private class DisplayProcessor extends DbRowProcessor
{
private Template mTemplate = null;

/**
* The constructor requires a Template instance in which the retrieved
* data will be filled in.
*
* @param template the Template in which the results will be display
*/
DisplayProcessor(Template template)
{
mTemplate = template;
}

public boolean processRow(ResultSet resultSet)
throws SQLException
{
mTemplate.setValue("firstname",
encodeHtml(resultSet.getString("firstname")));
mTemplate.setValue("lastname",
encodeHtml(resultSet.getString("lastname")));
mTemplate.setValue("description",
encodeHtml(resultSet.getString("description")));
mTemplate.setValue("url",
encodeHtml(resultSet.getString("url")));

mTemplate.appendBlock("rows", "row");

return true;
}
}
------------------------------------------------------------------

데이터베이스와 HTML생성은 매우 명확하게 분리되었다. 행처리기(row processor)가 하는 일은 결과값을 템플릿에 채우는것 뿐이다. 이제 남은일은 요소의 processElement메소드를 호출하는 것이다. 여기서 manager를 생성하고 행처리기와 함께 display 메소드를 호출하게 된다.

------------------------------------------------------------------
FriendManager manager = new FriendManager();
DisplayProcessor processor = new DisplayProcessor(template);

manager.display(processor);
------------------------------------------------------------------

친구 추가

친구를 추가할 수 없다면 조회는 아무런 의미가 없다. 요소와 함께 어플리케이션을 확장해 친구를 추가할 수 있도록 하겠다.

새로운 요소 정의

ADD요소는 매우 단순하다. 이것은 다른것들 처럼 출구(exit)나 데이터링크(datalink)를 가지지 않는다. 유일하게 지금까지 다루지 않았던 submission bean의 사용에 관한 것이다. 이제 사이트파일과 요소파일을 보고 submission bean이 어떻게 작동하는지 설명하겠다.

------------------------------------------------------------------
갱신된 사이트 파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE site SYSTEM "/dtd/site.dtd">

<site>
<arrival destid="DISPLAY"/>

<globalexit name="menu" destid="MENU"/>

<element id="DISPLAY" file="display.xml" url="/display"/>

<element id="MENU" file="menu.xml" url="/menu">

<!-- Add a flowlink to the new add element -->
<flowlink srcexit="add" destid="ADD"/>

<flowlink srcexit="display" destid="DISPLAY"/>
</element>

<!-- Add the new element -->
<element id="ADD" file="add.xml" url="/add"/>

</site>
------------------------------------------------------------------

사이트파일의 주석문이 변경된 내용을 가르키고 있다: 새로운 요소에 대한 메뉴와 요소가 추가되었다.

------------------------------------------------------------------
추가된 요소파일
------------------------------------------------------------------
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE element SYSTEM "/dtd/element.dtd">

<element implementation="tutorial.friends.Add">
<submission name="friend_data">
<bean classname="tutorial.friends.backend.Friend"/>
</submission>
</element>
------------------------------------------------------------------

Submission beans

submission빈은 다른 많은 RIFE의 기능들 처럼 공통된 웹어플리케이션 작업중 폼으로부터의 입력을 핸들리하는 특별히 추상화된 개념이다. submission bean은 빈(bean)을 사용하여 간단하게 셋업된다. classname은 빈의 클래스명을 가르킨다. 이경우 Friend의 표현이다. 빈을 설정한 다음에는 getSubmissionBean이 사용자가 입력한 폼을 빈의 속성값으로 가져오기 위해 사용된다.

------------------------------------------------------------------
Friend friend = (Friend)getSubmissionBean("friend_data", Friend.class);
------------------------------------------------------------------

이방법은 적은 라인만을 필요로 하며 친구객체에 어떤 속성이 추가되거나 삭제되어도 상관없다.

추가 템플릿

마지막으로, 입력을 위한 템플릿을 작성하는 것이다. 이것은 숫자맞추기 게임의 템플릿과 비슷하다.

------------------------------------------------------------------
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">

<html>
<head><title>Add a new friend to the database</title></head>
<body>
<h3>Add a friend</h3>

<!--V 'content'/-->

<!--BV 'content'-->
<form name="friend_data" action="[!V 'SUBMISSION:FORM:friend_data'/]"
method="post">

<!--V 'SUBMISSION:PARAMS:friend_data'/-->
Firstname<br/>
<input type="text" name="firstname" length="51" maxlength="50"/><br/>
Lastname<br/>
<input type="text" name="lastname" length="51" maxlength="50"/><br/>
Description<br/>
<textarea name="description" cols="80" rows="6"></textarea><br/>
Url<br/>
<input type="text" name="url" length="80" maxlength="255"/><br/>
<input type="submit" name="Add this friend."/>
</form>
<!--/BV-->

<!--B 'content_added'-->
<p>The friend has been added.</p>
<!--/B-->

<p><a href="[!V 'EXIT:QUERY:menu'/]">back to menu</a></p>
</body>
</html>
------------------------------------------------------------------

SUBMISSION:FORM: 과 SUBMISSION:PARAMS: 가 submission bean처리에 사용된다.

친구를 데이터베이스에 추가

삽입쿼리를 작성하고 실행하는것은 빈에 모든 데이터가 있을때 쉽게 할 수 있다.

------------------------------------------------------------------
public void add(Friend friend)
throws DatabaseException
{
Insert insert = new Insert(getDatasource());
insert
.into("Friend")
.fieldsParameters(Friend.class);

DbPreparedStatement insert_stmt = getConnection().getPreparedStatement(insert);
insert_stmt.setBean(friend);
insert_stmt.executeUpdate();
}
------------------------------------------------------------------

마무리하기

테이블 생성과 삭제

------------------------------------------------------------------
<element id="INSTALL" file="install.xml" url="/install"/>
<element id="REMOVE" file="remove.xml" url="/remove"/>
------------------------------------------------------------------

이 두개의 단순한 요소는 설치와 제거를 위한 확인버튼만 있다. 목록을 보여주는 작업같은건 필요가 없다. 이 요소들에 대한 정의파일과 템플릿은 classes/elements/install.xml, classes/elements/remove.xml, classes/templates/install.html, classes/templates/remove.html 에 있다.

테이블 생성

install요소는 manager의 install메소드를 호출한다. 이 메소드는 데이터베이스테이블을 생성한다.

------------------------------------------------------------------
public void install()
throws DatabaseException
{
CreateTable create = new CreateTable(getDatasource());
create
.table("Friend")
.columns(Friend.class)
.precision("firstname", 50)
.precision("lastname", 50)
.precision("url", 50)
.nullable("firstname", CreateTable.NOTNULL)
.nullable("lastname", CreateTable.NOTNULL)
.nullable("description", CreateTable.NOTNULL)
.nullable("url", CreateTable.NOTNULL);

executeUpdate(create);

// ...
}
------------------------------------------------------------------

다른 질의들 처럼 빈클래스가 컬럼생성에 사용되었다. 이경우 문자열의 길이나 널(NULL)속성과 같은 더 상세한 속성들을 설정하기 위해 컬럼명이 서술되었다. 그럼에도 불구하고, 빈클래스의 사용은 Friend빈을 위한 저장소로 테이블을 사용하기에 매우 명확하고 분명한 명분을 제공한다.

정리

테이블을 삭제하는 것은 CreateTable대신 DropTable을 사용하는 것만 제외하면 생성하는것과 같다.

------------------------------------------------------------------
public void remove()
throws DatabaseException
{
DropTable drop = new DropTable(getDatasource());
drop.table("Friend");
executeUpdate(drop);
}
------------------------------------------------------------------

댓글 없음: