http://blog.naver.com/jdkim528/140010948813

Struts Hibernate통합 튜토리얼

Struts 와 Hibernate 통합

General

저자 Sebastian Hennebrüder
역자
문서 URL http://www.laliluna.de/tutorials.html
문서 설명 Tutorials for Struts, EJB, xdoclet 그리고 eclipse에 대한 튜토리얼.
작성일자 December, 22th 2004
소스코드 http://www.laliluna.de/assets/tutorials/struts-hibernate-integration-tutorial.zip
소스 사용 소스 코드는 임의의 라이브러리들을 포함하고 있지 않지만 소스들을 포함한다. 웹 프로젝트를 생성시키고, 수작업으로 라이브러리들을 추가하거나 MyEclipse와 우리가 프로젝트?제공했던 소스 압축해제의 도움으로 추가하라.
PDF 버전 http://www.laliluna.de/assets/tutorials/struts-hibernate-integration-tutorial-en.pdf
개발 도구 Eclipse 3.x
MyEclipse plugin 3.8 (웹 어플리케이션들과 EJB(J2EE) 어플리케이션들을 개발하기 위한 Eclipse에 대한 강력한 확장. 나는 MyEclipse에서 이용 가능한 테스트 버전이 있다고 알고 있다.)
데이터베이스 PostgreSQL 8.0 Beta
어플리케이션 서버 Jboss 3.2.5
Tomcat을 사용할 수도 있다

Hibernate 라이브러리 프로젝트 생성하기

우리는 Hibernate 프로젝트의 생성과 테스팅을 시작할 것이다. 두 번째 단계는 비지니스 로직을 추가하고 마지막으로 Struts 부분을 통합시킬 것이다.

Persistence 프로젝트 생성하기

새로운 웹 프로젝트를 생성시켜라.
시작하자.
 "New ..."대화상자를 열기 위해 "Alt+Shift+N"을 눌러라.
웹 프로젝트를 생성시키고 아래에서 보이는 것처럼 프로젝트 이름을 선택하라.
사용자 삽입 이미지
패키지 뷰에서 프로젝트 상을 오른쪽 마우스 클릭하여 Add the Hibernate capabilities를 선택하라.
사용자 삽입 이미지
  두 개의 체크박스들 Add Hibernate 2.1 libraries to projects?와 Append Hibernate 2.1 libraries to project classpath? 를  체크하고 새로이 hibernate 매핑 파일을 생성시키는 것을 선택하라. hibernate 파일은 당신의 hibernate 설정들과 매핑들의 구성을 보관한다.
사용자 삽입 이미지
다음 단계는 데이터베이스를 위한 커넥션 프로파일을 선택하는 것이다.
새로운 프로파일을 생성시키기 위해 "New Profile" 버튼을 선택하라.
Postgres 드라이버가 누락되어 있을 , 새로운 드라이버를 생성시키기 위해 "New Driver"를 클릭하라. 당신은 당신의 데이터베이스 드라이버를 포함하는 jar를 필요로 할 것이다.
우리는 우리의 프로파일 드라이버를 library-web이라 명명한다. 유저네임과 패스워드를 지정하라.
사용자 삽입 이미지
"Copy JDBC Driver and add to classpath(only available when using a Connection Profile)"이 체크되도록 하라. 우리는 PostgreSQL을 사용할 예정이다. MySQL 또는 다른 데이터베이스에 대해 동일한 것을 만드는 것이 어렵지 않다. 당신은 당신의 디스크 상의 어딘가에 데이터베이스 드라이버의 Jar 파일을 갖고 있어야 한다.
사용자 삽입 이미지

다음 단계에서 당신?당신의 SessionFactory에 대한 좋은 이름을 입력해야 한다.
사용자 삽입 이미지
SessionFactory란 무엇인가?
Hibernate는 Hibernate Session 클래스의 오직 한 개의 인스턴스 만이 단위 쓰레드에 사용되는 것을 기대하고 있다. 보통 당신은  ThreadLocal을 구현하는 클래스를 생성시켜야 할 것이다.  MyEclipse는 당신을 위해 이것을 행한다. 당신은 그것을 위한 이름을 지정하기만 하면 된다. 만일 당신이 MyEclipse를 사용하고 있지 않다면 소스들을 살펴보라.
Hibernate 라이브러리들을 줄이기
디폴트로 MyEclipse는 라이브러리들의 무거운 로드를 포함한다. 그것들 중 몇몇은 특정 캐시 구현 용으로만 로컬 개발에 필요로 될 것이다. 당신이 Hibernate의 기본을 배운 후에 당신의 개발을 최적화 하고자 원할 때 웹 사이트http://www.hibernate.org/에서 Hibernate를 다운로드하라. lib 디렉토리에서 당신은 옵션 라이브러리들이 무엇인지를 설명하는 README.txt 를 찾게 될 것이다.
이제 우리는 개발을 시작할 준비가 되어 있다.

데이터베이스 생성하기

데이터베이스와 다음 테이블들을 생성시켜라. foreign key를 잊지 말라!
Postgre SQL 스크립트
CREATE TABLE customer
(
id int4 NOT NULL DEFAULT nextval('public.user_id_seq'::text),
name text,
lastname text,
age int4,
CONSTRAINT customer_pk PRIMARY KEY (id)
)
CREATE TABLE book
(
id serial NOT NULL,
title text,
author text,
user_fk int4,
CONSTRAINT book_pk PRIMARY KEY (id)
)
ALTER TABLE book
ADD CONSTRAINT book_customer FOREIGN KEY (customer_fk) REFERENCES customer (id) ON UPDATE RESTRICT ON DELETE RESTRICT;
MySQL 스크립트
CREATE TABLE customer
(
id int( 11 ) NOT NULL AUTO_INCREMENT ,
name varchar( 255 ) ,
lastname varchar( 255 ) ,
age int( 11 ),
CONSTRAINT customer_pk PRIMARY KEY (id)
) TYPE=INNODB;
CREATE TABLE book( id int( 11 ) NOT NULL AUTO_INCREMENT ,
title varchar( 255 ) ,
author varchar( 255 ) ,
customer_fk int( 11 ),
available TINYINT NOT NULL,
CONSTRAINT book_pk PRIMARY KEY ( id ),
INDEX (customer_fk) ) TYPE=INNODB;
ALTER TABLE book ADD CONSTRAINT book_customer FOREIGN KEY ( customer_fk ) REFERENCES customer( id ) ON UPDATE RESTRICT ON DELETE RESTRICT ;

Hibernate 매핑파일들과 클래스들을 생성하기

View DB Browser (MyEclipse)를 열어라.당신이 그것을 열수 없을 경우에 "Windows>Show View>Other"를 선택하고  MyEclipse Enterprise Workbench에서 DB Browser를 선택하라.
사용자 삽입 이미지



지정하기 전체 커넥션 프로파일을 열어라.
사용자 삽입 이미지

우리가 방금 생성시켰던 두 개의 테이블들을 선택하라. 오른쪽 마우스 버튼을 클릭하고 "Create Hibernate Mapping"을 선택하라.
사용자 삽입 이미지

당신의 LibraryPersistence 프로젝트를 타켓으로 선택하라. 당신이 Postgresql을 사용하고 있을 때, "sequence"를 ID 생성자로 선택하라. 당신이 MySQL을 사용하고 있다면 "native"를 선택하라.
사용자 삽입 이미지

OK를 클릭하라. 좋다! 당신은 방금 당신의 영속 레이어를 생성시켰다.
이제 우리는 무엇이 발생했는지를 알기 위해 우리의 패키지 탐색기를 보다 근접하여 살펴볼 것이다.
먼저 hibernate.cfg.xml을 열어라.
두 개의 매핑 파일들이 위치해 있는 곳을 지정하는 두 개의 새로운 엔트리들이 존재한다. hibernate.cfg.xml과는 별도의 매핑 파일들을 유지하는 것이 좋은 생각이다.  (MyEclipse가 당신을 위해 실제로 행하는 것.)
<!-- mapping files -->
<mapping resource="de/laliluna/library/Book.hbm.xml"/>
<mapping resource="de/laliluna/library/User.hbm.xml"/>

Book.hbm.xml 매핑 파일을 살펴보자. 이 파일에서 클래스와 그것의 속성들로부터 테이블 필드들로의 매핑이 지정되어 있다. 우리의 foreign key가 인지되었다.
<class name="Book" table="book">
<id name="id" column="id" type="java.lang.Integer">
<generator class="sequence"/>
</id>

<property name="title" column="title" type="java.lang.String" />
<property name="author" column="author" type="java.lang.String" />
<property name="available" column="available" type="java.lang.Byte" />

<many-to-one name="user" column="user_fk" class="User" />
</class>
MyEclipse는 클래스 당 두 개의 파일들을 생성시켰다. 첫 번째 파일은 abstract 클래스(AbstractBook)이다. 그것은 당신이 가져오기 과정을 반복할 때마다 덮어쓰여질 것이다. 두 번째 클래스(Book)에서 당신은 당신이 만들고자 원하는 임의의 변경들을 채택할 수 있다. 그것은 오직 한번만 생성된다.
우리는 몇몇 변경들을 행할 예정이다.
Hibernate는 customer로부터 book으로의 관계를 생성시키지 않는다. 우리는 수작업으로 이것을 추가시킬 것이다.
Customer.class 파일에 다음을 추가하라.
private List books;
/**
* @return Returns the books.
*/
public List getBooks() {
return books;
}
/**
* @param books The books to set.
*/
public void setBooks(List books) {
this.books = books;
}
Customer.hbm.xml 파일에서 우리는 books 변수로부터의 매핑을 추가해야 한다. 파일에 "bag" 엔트리를 추가하라.
<hibernate-mapping package="de.laliluna.library">
<class name="Customer" table="customer">
<id name="id" column="id" type="java.lang.Integer">
<generator class="sequence"/>
</id>

<bag name="books" inverse="false">
<key column="customer_fk" />
<one-to-many class="Book"/>
</bag>

<property name="name" column="name" type="java.lang.String" />
<property name="lastname" column="lastname" type="java.lang.String" />
<property name="age" column="age" type="java.lang.Integer" />
</class>

</hibernate-mapping>
우리는 그것을 inverse="false"로 지정한다.
이것은 우리가 데이터베이스 속에 반영되도록 one to many 관계의 한 쪽(books)  상의 속성들에 대해 변경하고자 원한다는 점을 지정한다.
예를 들어:
customer.getbooks().add(aBook);
은 customer 테이블에 대해 foreign key를 작성할 것이다.
위의 파일을 수작업으로 변경하는 것은 매우 좋지 않지만 여기서 다른 방법이 없다.
단점은 당신이 매핑 파일들을 재생성 시킬 때마다 이것이 덮어 씌어질 것이라는 점이다. 우리의 경우에 그렇게 중요하지 않지만 대형 프로젝트에서 이것은 시작 시점을 제외하면 MyEclilpse로부터의 자동생성을 사용하는 것을 불가능하게 만들 것이다. hibernate import 기능은 MyEclipse에 꽤 새로운 것이어서, 당신은 다음 버전들에서 더 큰 개선들이 있을 것임을 믿을 수 있다.

세션 팩토리에 대한 개선

MyEclipse에 의해 생성된 세션 팩토리는 매우 좋은 것이 아니다. 왜냐하면 그것은 당신이 session.close() 메소드를 사용할 때 오류를 실행하도록 하기 때문이다. 세션 팩토리는 당신이 팩토리의 static 메소드 closeSession() 를 사용한다는 점을 예상하고, 그것은 실제로 세션이 닫혀져 있을 경우에 세션을 null로 설정한다.
그러나 문제는 없고, 다음은 메소드에 대한 변경들이다. 팩토리에 대한 현재의 변경들.
public static Session currentSession() throws HibernateException {
Session session = (Session) threadLocal.get();
/*
* [laliluna] 20.12.2004
* 우리는 이 클래스에서 closeSession()이 아닌 표준 메소드 session.close()를 사용하고자 원한다.
* 이를 위해 우리는 다음 코드 라인을 필요로 한다.
*/
if (session != null && !session.isOpen()) session = null;
if (session == null) {
if (sessionFactory == null) {
try {
cfg.configure(CONFIG_FILE_LOCATION);
sessionFactory = cfg.buildSessionFactory();
} catch (Exception e) {
System.err
 .println("%%%% Error Creating HibernateSessionFactory %%%%");
e.printStackTrace();
}
}
session = sessionFactory.openSession();
threadLocal.set(session);
}
return session;
}

Hibernate 부분을 테스트하기

새로운 클래스를 생성하자.
사용자 삽입 이미지

다음 예제를 살펴보자.
public class LibraryTest {
private Session session;
private Logger log = Logger.getLogger(this.getClass());
public static void main(String[] args) {
/*
* hibernate는 log4j를 필요로 한다. log4j.properties 파일을 지정하거나
*
* PropertyConfigurator.configure(
* "D:_projekteworkspaceLibraryPersistencesrclog4j.properties");
*
* 또는 다른 방법으로 표준 구성을 생성시키기 위해 다음을 따르라.
* BasicConfigurator.configure();
*/
BasicConfigurator.configure();
try {
LibraryTest libraryTest = new LibraryTest();
libraryTest.setSession(HibernateSessionFactory.currentSession());
libraryTest.createBook();
libraryTest.createCustomer();
libraryTest.createRelation();
libraryTest.deleteCustomer();
libraryTest.listBooks();
// [laliluna] 20.12.2004 종료 시에 항상 세션을 닫아라
libraryTest.getSession().close();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* book을 생성시키고 그것을 db에 저장한다 .
*
*/
private void createBook() {
System.out.println("############# create book");
try {
Transaction tx = session.beginTransaction();
Book book = new Book();
book.setAuthor("Karl");
book.setTitle("Karls biography");
session.save(book);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* 사용자를 생성시키고 그것을 db에 저장한다
*
*/
private void createCustomer() {
System.out.println("############# create user");
try {
Transaction tx = session.beginTransaction();
Customer customer = new Customer();
customer.setLastname("Fitz");
customer.setName("John");
customer.setAge(new Integer(25));
session.save(customer);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* book과 user를 생성시키고 둘 사이에 관계를 생성시킨다.
*
*/
private void createRelation() {
System.out.println("############# create relation");
try {
Transaction tx = session.beginTransaction();
Customer customer = new Customer();
customer.setLastname("Schmidt");
customer.setName("Jim");
customer.setAge(new Integer(25));
/* 중요 당신이 고객들을 도서에 할당 하기 전에 당신은 먼저 고객을 저장시켜야 한다.
* Hibernate는 당신이 엔트리를 지정할 때에만 ID를 생성시키고 읽어 들인다.
* ID는 그것이 foreign key일 때 필요하다 .
*/
session.save(customer);
Book book = new Book();
book.setAuthor("Gerhard Petter");
book.setTitle("Gerhards biography");
session.save(book);
Book book2 = new Book();
book2.setAuthor("Karl May");
book2.setTitle("Wildes Kurdistan");
session.save(book2);
session.flush();
book.setCustomer(customer);
book2.setCustomer(customer);
tx.commit();
// [laliluna] 20.12.2004 고객은 자동적으로 업데이트되지 않아서, 우리는 고객을 갱신해야 한다
session.refresh(customer);
tx = session.beginTransaction();
if (customer.getBooks() != null) {
System.out.println("list books");
for (Iterator iter = customer.getBooks().iterator(); iter.hasNext();) {
Book element = (Book) iter.next();
System.out.println("customer:" + element.getCustomer());
System.out.println("customer is now:" + element.getCustomer());
}
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
}
}
private void deleteCustomer() {
System.out.println("############# delete customer");
try {
Transaction tx = session.beginTransaction();
Customer customer = new Customer();
customer.setLastname("Wumski");
customer.setName("Gerhard");
customer.setAge(new Integer(25));
/* 중요 당신이 고객을 도서에 할당하기 전에 먼저 고객을 저장해야 한다.
* Hibernate는 당신이 엔트리를 저장할 때에만 ID를 생성시키고 읽어 들인다.
* ID는 그것이 foreign key일 때 필요하다
*/
session.save(customer);
Book book = new Book();
book.setAuthor("Tim Tom");
book.setTitle("My new biography");
session.save(book);
book.setCustomer(customer);
tx.commit();
// [laliluna] 20.12.2004 그리고 이제 우리는 book 테이블에서 foreign key를 null로 설정하게 될 고객을 삭제할 예정이다
tx = session.beginTransaction();
// [laliluna] 20.12.2004 고객은 자동적으로 업데이트 되지 않아서 우리는 고객을 갱신해야 한다.
session.refresh(customer);
session.delete(customer);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* db 내의 모든 도서들을 리스트 한다
*
*/
private void listBooks() {
System.out.println("####### list customers");
Query query;
Transaction tx;
try {
tx = session.beginTransaction();
query = session.createQuery("select c from Customer as c");
for (Iterator iter = query.iterate(); iter.hasNext();) {
Customer element = (Customer) iter.next();
List list = element.getBooks();
System.out.println(element.getName());
if (list == null)
System.out.println("list = null");
else {
for (Iterator iterator = list.iterator();iterator.hasNext();) {
Book book = (Book) iterator.next();
System.out.println(book.getAuthor());
}
}
System.out.println(element);
}
tx.commit();
} catch (HibernateException e1) {
e1.printStackTrace();
}
System.out.println("####### list books");
try {
tx = session.beginTransaction();
query = session.createQuery("select b from Book as b");
for (Iterator iter = query.iterate(); iter.hasNext();) {
System.out.println((Book) iter.next());
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
}
}
/**
* @return Returns the session.
*/
public Session getSession() {
return session;
}
/**
* @param session The session to set.
*/
public void setSession(Session session) {
this.session = session;
}
}
클래스를 오른쪽 마우스 클릭하고 Run -> Java Application을 선택하라.
사용자 삽입 이미지

그리고 적어도 우리가 PostgreSQL을 사용할 때, 우리는 많은 오류 메시지를 얻었다.
java.sql.SQLException: ERROR: relation "hibernate_sequence" does not exist
PostgreSQL 문제
이것은 import 스크립트 내의 간단한 버그 때문이다. sequence는 hibernate_sequence로 명명된다고 가정된다. 당신이 시리얼 컬럼을 사용할 때 자동적으로 생성된 시퀀스들은 table_column_seq로 명명된다. 예를 들어 book_id_seq.
가장 쉬운 작업은 MyEclipse가 스크립트를 개선하기를 기다리는 것이다.가장 빠른 작업은 hibernate_sequence로 명명되는 시퀀스를 생성시키는 것이다. 단점은 모든 테이블들이 동일한 시퀀스를 공유한다는 점이다. 당신은 무거운 로딩 하에 있는 하나의 테이블을 가지게 될 것이다.
CREATE SEQUENCE hibernate_sequence
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
가장 좋은 방법은  당신의 매핑 파일들을 다시 생성시키지 않는 것이 확실할 때에만 가능하고 book에 대해
<generator class="sequence"/>
에서 매핑을 다음으로 변경하는 것이다. customer에 대한 변경들도 유사하다.
<generator class="sequence">book_id_seq
<param name="sequence">book_id_seq</param>
</generator>
그것은 우리의 어플리케이션에 대한 영속 계층이다.

비지니스 로직 생성하기

비지니스 로직 클래스 생성하기

우리는 하나의 클래스 속에 모든 비지니스 로직을 위치지울 것이다. 나중에 우리의 Struts 부분은 이 클래스 만을 사용할 것이다. 영속 계층에 대한 어떤 직접적인 액세스에 대해서는 걱정하지 말라. 당신은 당신의 영속 계층을 또 다른 것으로 대체하는 것에 대해 생각할 수도 있다.
사용자 삽입 이미지

이 클래스는 우리가 비지니스 로직으로서 필요한 모든 메소드들을 보관할 것이다
  • 도서들을 생성시키고 업데이트하고 삭제하기
  • 고객들을 생성시키고 업데이트하고 삭제하기
  • 도서들을 대여하고 반환하기
  • db로부터 모든 고객들과 도서들을 리스트로 읽어 들이기
Hibernate 예외상황들
예외상황들이 발생할 때 트랜잭션을 롤백 시키고 세션을 즉각 닫는 것이 권장된다. 그것은 우리가 다음으로 행하는 것이다
try
catch {}
finally{}
우리가 사용했던 Hibernate 설계
hibernate 질의는 List의 특정 Hibernate 구현에 대해 List 인터페이스를 반환한다. 이 구현은 세션에 직접 연결된다. 당신이 이 Hibernate 리스트들을 사용할 때 당신의 세션을 닫을 수 없다. 당신은 데이터베이스로부터 세션을 연결해제하고 그것에 다시 연결해야 하고, 캐싱 해결책들 중 하나를 사용하거나 Value Object들에 동작하는 가장 쉽지만 최상의 방법을 취해야 한다.
우리는 가장 쉬운 방법을 취했다:
결론은 우리가 hibernate 리스트의 모든 요소들을 통상의 java.util.List로 복사해야 한다는 점이다.
public class LibraryManager {
/**
* 데이터베이스로부터 모든 도서들을 얻는다
* @return Array of BookValue
*/
public Book[] getAllBooks() {
/* 우리가 나중에 반환할 예정인 도서들을 보관할 것이다 */
List books = new ArrayList();
/* Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* 현재 쓰레드의 세션을 얻는다 */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Query query = session
.createQuery("select b from Book as b order by b.author, b.title");
for (Iterator iter = query.iterate(); iter.hasNext();) {
books.add((Book) iter.next());
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
return (Book[]) books.toArray(new Book[0]);
}
/**
* 프라이머리 키로 도서를 얻는다
* @param primaryKey
* @return a Book or null
*/
public Book getBookByPrimaryKey(Integer primaryKey) {
/* 우리의 반환 값을 보관한다 */
Book book = null;
/* Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
book = (Book) session.get(Book.class, primaryKey);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
return book;
}
/**
* 데이터베이스 내에 지정된 유저에게 도서를 대여 중으로 설정한다
* @param primaryKey
* @param userPrimaryKey
*/
public void borrowBook(Integer primaryKey, Integer
customerPrimaryKey) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Book book = (Book) session.get(Book.class, primaryKey);
Customer customer = (Customer) session.get(Customer.class,
customerPrimaryKey);
if (book != null && customer != null)
book.setCustomer(customer);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
/**
* 고객은 도서를 반환하고, 도서와 고객 사이의 db 내 관계와 도서가 삭제된다
* @param primaryKey
*/
public void returnBook(Integer primaryKey) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Book book = (Book) session.get(Book.class, primaryKey);
if (book != null) // session.get returns null when no entry is
found
book.setCustomer(null);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
/**
* 도서를 업데이트/생성시킨다
* @param bookValue
*/
public void saveBook(Book bookValue) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Book book;
if (bookValue.getId() != null &&
bookValue.getId().intValue() != 0) { // [laliluna] 04.12.2004 load book
from DB
book = (Book) session.get(Book.class, bookValue.getId());
if (book != null) {
book.setAuthor(bookValue.getAuthor());
book.setTitle(bookValue.getTitle());
book.setAvailable(bookValue.getAvailable());
session.update(book);
}
}
else // [laliluna] 04.12.2004 create new book
{
book = new Book();
book.setAuthor(bookValue.getAuthor());
book.setTitle(bookValue.getTitle());
book.setAvailable(bookValue.getAvailable());
session.save(book);
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
/**
* 도서를 삭제한다
* @param primaryKey
*/
public void removeBookByPrimaryKey(Integer primaryKey) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Book book = (Book) session.get(Book.class, primaryKey);
if (book != null) session.delete(book);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
/**
* db로부터 모든 고객들을 반환한다
* @return
*/
public Customer[] getAllCustomers() {
/* 나중에 반환할 예정인 도서들을 보관한다 */
List customers = new ArrayList();
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Query query = session
.createQuery("select c from Customer as c order by c.name");
for (Iterator iter = query.iterate(); iter.hasNext();) {
customers.add((Customer) iter.next());
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
return (Customer[]) customers.toArray(new Customer[0]);
}
/**
* db로부터 고객을 얻는다
* @param primaryKey
* @return the customer class or null, when no customer is found
*/
public Customer getCustomerByPrimaryKey(Integer primaryKey) {
/* 우리의 반환 값을 보관한다 */
Customer customer = null;
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
customer = (Customer) session.get(Customer.class, primaryKey);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
return customer;
}
/**
* 고객을 db에 저장한다
* @param customer
*/
public void saveCustomer(Customer customer) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
if (customer.getId() == null || customer.getId().intValue() == 0)
// [laliluna] 06.12.2004 create customer
session.save(customer);
else {
Customer toBeUpdated = (Customer) session.get(Customer.class,
customer
.getId());
toBeUpdated.setAge(customer.getAge());
toBeUpdated.setLastname(customer.getLastname());
toBeUpdated.setName(customer.getName());
session.update(toBeUpdated);
}
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
/**
* 데이터베이스로부터 고객을 삭제한다
* @param primaryKey
*/
public void removeCustomerByPrimaryKey(Integer primaryKey) {
/* a Hibernate session */
Session session = null;
/* 우리는 항상 트랜잭션을 필요로 한다 */
Transaction tx = null;
try {
/* get session of the current thread */
session = HibernateSessionFactory.currentSession();
tx = session.beginTransaction();
Customer customer = (Customer) session.get(Customer.class,
primaryKey);
if (customer != null) session.delete(customer);
tx.commit();
} catch (HibernateException e) {
e.printStackTrace();
// [laliluna] 17.12.2004 오류가 발생한 후에 트랜잭션을 롤백시키는 것이 권장된다
if (tx != null) try {
tx.rollback();
} catch (HibernateException e1) {
e1.printStackTrace();
}
} finally {
try {
if (session != null) session.close();
} catch (HibernateException e1) {
e1.printStackTrace();
}
}
}
}
우리는 우리의 비지니스 로직을 생성시켰다.
그리고 이제 마지막 부분: 대화상자

대화상자 생성하기

 File > New > Project로 새로운 struts 프로젝트를 생성시키거나 단축히 "Alt+Shift+N"을 사용하라.
Wizard in J2EE Web Project를 선택하라.
사용자 삽입 이미지

프로젝트의 이름을 설정하라
사용자 삽입 이미지

이제 당신의 프로젝트는 여전히 정규 웹 프로젝트이어서, 우리는 struts 가용성을 추가할 필요가 있다. 프로젝트를 오른쪽 마우스 클릭하고 "MyEclipse>Add Struts Capabilities ..."를 선택하라.
사용자 삽입 이미지

새로운 클래스들 위한 기저 패키지와 디폴트 어플리케이션 리소스를 변경하라.
사용자 삽입 이미지

java 빌드 경로 구성하기

Web Project의 프로젝트 프로퍼티를 열고  Hibernate project LibraryPersistence를 선택하라.
사용자 삽입 이미지


디폴트 환영 페이지 생성하기

이제 우리는 디폴트 페이지를 생성시키고자 원한다. 프로젝트에서 WebRoot 폴더를 마우스 오른쪽 버튼 클릭하고 New>JSP를 선택하라.
사용자 삽입 이미지

이름을 index.jsp로 설정하고 템플릿 사용으로  Standard JSP using Struts 1.1을 선택하라. MyEcplise는 JSP 파일을 생성시킬 템플릿을 사용할 것이다.
사용자 삽입 이미지

당신은 프로젝트의 WebRoot 폴더 속에 index.jsp 파일을 찾게 될 것이다. 파일의 상단에서 당신은 struts 태그 라이브러리들의 선언을 찾게 될 것이다. 이들 include들은 struts의 태그에 액세스 하는데 사용될 것이다. 이 경우에 우리는 logic 태그 라이브러리를 필요로 한다.
사용자 삽입 이미지

포함된 logic 태그를 다음 라인 아래에 삽입하라.
<logic:forward name="welcome" />

이 라인은 welcome 이름으로 포워드를 찾도록 struts에게 지시한다. 만일 어플리케이션이 이 포워드를 찾지 못할 경우, 그것은 오류를 진술할 것이다. 다음 절에서 나는 action forward를 간략하게 설명한다.
 /WebRoot/jsp 폴더에 두번째 index.jsp를 생성하라.
파일의 body를 다음과 같이 변경하라:
<body>
Welcome!
<br>
<html:link action="bookList">Show the book list</html:link>
<br>
<html:link action="customerList">Show the customer list</html:link>
</body

글로벌 Action Forward들과  Action 매핑들

액션 포워드란 무엇인가?액션 포워드는 jsp 또는 액션 매핑을 포워드 하는데 사용될 수 있다. 두 가지 다른 액션 포워드들이 존재한다. 글로벌 액션 포워드와 로컬 액션 포워드. 당신은 각각의 jsp 또는 액션 클래스에 대해 글로벌 액션 포워드로 액세스할 수 있다. 로컬 액션 포워드는 할당된 액션 클래스에 의해서만 액세스 될 수 있다.
액션 매핑이란 무엇인가?
액션 매핑은 struts의 심장이다. 그것은 어플리케이션과 유저 사이의 모든 액션들을 관리했다. 당신은 액션 매핑을 생성시킴으로써 어느 액션이 실행될 것인지를 정의할 수 있다.
다이어그램은 당신에게 어플리케이션 서버가 index.jsp의 요청 또는 존재하지 않는 액션 매핑을 어떻게 관리하는지를 보여준다.
첫 번째 단계에서 우리는 새로운 액션 매핑을 생성시킨다. struts-config.xml을 열어, 당신은 WebRoot/WEB-INF 폴더 속에서 그것을 찾게 될 것이다.  액션-매핑 상의 아웃라인 뷰 내를 마우스 오른쪽 버튼 클릭하라.
사용자 삽입 이미지


사용자 삽입 이미지

MyEclipse는 struts 파일들을 생성시키는 몇몇 좋은 특징들을 제공한다. struts-config.xml과 아웃라인 뷰를 열어라.
마법사로 새로운 액션을 생성시키기 위해 action-mappings 엔트리를 마우스 오른쪽 버튼 클릭하라.
사용자 삽입 이미지


Use case를 default로 선택하고 Action Type을 Forward로 선택하라. Forward path는 환영 페이지 /jsp/index.jsp이다.

사용자 삽입 이미지

존재하지 않은 액션 매핑의 모든 요청들을 캐치하기 위해, 우리는 unknow="true" 파라미터를 action foward에 수작업으로 추가해야 한다.
<action-mappings>
<actionforward="/jsp/index.jsp"path="/default"unknown="true"/>
</action-mappings>
위에 지정된 jsp를 생성시키고 코드를 다음과 같이 변경하라:
<%@ page language="java"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html:html locale="true">
<head>
<html:base />
<title>index.jsp</title>
</head>
<body>
Welcome!
<br>
<html:link action="bookList">Show the book list</html:link>
<br>
<html:link action="customerList">Show the customer list</html:link>
</body>
</html:html>
두 번째 단계에서 당신은 글로벌 액션 포워드를 생성시킨다. MyEclipse의 아웃라인 윈도우로 가서 Global Forward를 선택하라.
사용자 삽입 이미지

Forward Scope로서 Global Forward를 선택하라.이름은 당신이 디폴트 페이지에 설정했던 것과 동일한 이름을 사용하라. Global Forward는 당신의 액션 매핑을 참조한다.
사용자 삽입 이미지

당신은 편집기 윈도우에서 다음을 보게 될 것이다.
<global-forwards>
<forward name="welcome"path="/default.do"redirect="true"/>
</global-forwards>
<action-mappings>
<actionforward="/jsp/index.jsp"path="/default"/>
</action-mappings>

도서 목록

다음은 모든 이용 가능한 도서들의 목록을 사용하는 경우이다.
New Form, Action and JSP를 선택하라.
사용자 삽입 이미지


사용자 삽입 이미지

Use Case은 bookList이고, Superclass는 org.apache.struts.ActionForm이다. 이 메소드를 생성시키기 위해 public void reset를 선택하라.
jsp 탭으로 가서 생성될 jsp의 이름을 설정하라.
사용자 삽입 이미지


도서 목록의 액션 매핑과 액션 클래스

액션 클래스에 대해 다음 변경을 행하라.
Superclass는 org.apache.struts.Action
Optional Details 상에서 Form Bean bookListForm을 선택하라..
input source는  /jsp/bookList.jsp이다.
사용자 삽입 이미지

이제 액션 매핑에 포워드 showList를 추가하라.
사용자 삽입 이미지

파일을 생성하자.

액션 폼 클래스의 소스 코드를 편집한다

BookListForm.java 파일을 열고 다음을 추가하라.
public class BookListForm extends ActionForm
{
private Book[] book = new Book[0];
/**
* @return Returns the book.
*/
public Book[] getBooks() {
return book;
}
/**
* @param book The book to set.
*/
public void setBooks(Book[] bookValues) {
this.book = bookValues;
}
/**
* Method reset
* @param mapping
* @param request
*/
public void reset(ActionMapping mapping, HttpServletRequest request) {
book = new Book[0];
}
}
당신은 getter 메소드와 setter 메소드들을 타이프할 필요가 없다. project -> select Source -> Generate Getters/Setters 상을 마우스 오른쪽 버튼 클릭하라. .

액션 클래스의 소스 코드를 편집한다

de.laliluna.tutorial.library.action 패키지 내에서  bookListAction 액션 클래스를 발견하게 될 것이다.
bookListAction 클래스를 열고  execute 메소드를 편집하라. form bean에서 메소드에 의해 반환된 도서들의 배열을 저장하라. mapping.findForward(„showList“)명령은 showList 이름막?로컬 포워드를 검색할 것이다.
public class BookListAction extends Action
{
/**
* Method loads book from DB
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
{
BookListForm bookListForm = (BookListForm) form;
// [laliluna] 27.11.2004 get busines logic
LibraryManager libraryManager = new LibraryManager();
// [laliluna] 29.11.2004 update the form bean, from which the jsp will read the data later.
bookListForm.setBooks(libraryManager.getAllBooks());
return mapping.findForward("showList");
}
}

jsp 파일 내에 도서 리스트를 디스플레이 한다

bookList.jsp 를 열고 다음 소스 코드를 추가하라.
<%@ page language="java"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %>

<html>
<head>
<title>Show book list</title>
</head>
<body>

<table border="1">
<tbody>
<%-- set the header --%>
<tr>
<td>Author</td>
<td>Book name</td>
<td>Available</td>
<td>Borrow by</td>
<td> </td>
<td> </td>
<td> </td>
</tr>
<%-- start with an iterate over the collection books --%>
<logic:iterate name="bookListForm" property="books" id="book">
<tr>
<%-- book informations --%>
<td><bean:write name="book" property="author" /></td>
<td><bean:write name="book" property="title" /></td>
<td><html:checkbox disabled="true"
name="book"
property="available"/>
</td>
<td>
<%-- check if a customer borrowed a book,
when its true display his name
otherwise display nothing --%>
<logic:notEmpty name="book" property="customer">
<bean:write name="book" property="customer.name" />,
<bean:write name="book" property="customer.lastname" />
</logic:notEmpty>
<logic:empty name="book" property="customer">
-
</logic:empty>
</td>
<%-- borrow, edit and delete link for each book --%>
<td>
<%-- check if a user borrowed a book,
when its true display the return link
otherwise display the borrow link --%>
<logic:notEmpty name="book" property="customer">
<html:link action="bookEdit.do?do=returnBook"
paramName="book"
paramProperty="id"
paramId="id">Return book</html:link>
</logic:notEmpty>
<logic:empty name="book" property="customer">
<html:link action="bookEdit.do?do=borrowBook"
paramName="book"
paramProperty="id"
paramId="id">Borrow book</html:link>
</logic:empty>
</td>
<td><html:link action="bookEdit.do?do=editBook"
paramName="book"
paramProperty="id"
paramId="id">Edit</html:link>
</td>
<td><html:link action="bookEdit.do?do=deleteBook"
paramName="book"
paramProperty="id"
paramId="id">Delete</html:link>
</td>
</tr>
</logic:iterate>
<%-- end interate --%>

<%-- if books cannot be found display a text --%>
<logic:notPresent name="book">
<tr>
<td colspan="5">No books found.</td>
</tr>
</logic:notPresent>

</tbody>
</table>

<br>
<%-- add and back to menu button --%>
<html:button property="add"
omclick="location.href='bookEdit.do?do=addBook'">Add a new book
</html:button>
 
<html:button property="back"
omclick="location.href='default.do'">Back to menu
</html:button>
</body>
</html>


<logic:iterate> 태그는 도서들의 배열을 루프 순환한다. 그 태그 내에서 당신은 book 이름을 가진 프로퍼티들에 액세스 한다. <bean:write> 태그는 도서의 프로퍼티 예를 들어 제목을 프린트한다.  <logic:notEmpty> 태그와 <logic:empty> 태그로 우리는 사용자가 도서를 대여했는지 아닌지 여부를 체크한다.
당신은 액션 클래스를 가진 form bean을,  action form 클래스를 가진 액션 매핑을 그리고 어떤 것을 디스플레이 할 jsp를 생성시켰다.

어플리케이션 테스트

jboss를 시작하고 프로젝트들(LibraryPersistenceLibs, LibraryPersistence,LibraryWeb)을 분해된 아카이브로서 배치하라.
사용자 삽입 이미지

Jboss 배치 문제
당신이 JBoss 프로젝트를 배치할 때 매우 자주 라이브러리들을 잠근다. 결과는 당신이 재배치할 때 다음 메시지를 얻는다는 점이다.
Undeployment failure on Jboss. File ....jar Unable to be deleted.
이 문제의 쉬운 해결책은 두 개의 프로젝트, 당신이 재배치하지 말아야 하는 라이브러리들을 포함하는 프로젝트, 당신의 Hibernate 프로젝트를 포함하는 프로젝트를 생성시키는 것이다. 우리는 이것을 설명하는 튜토리얼을 갖고 있다. 당신이 재배치할 때 이 문제를 만나게 되면 이 튜토리얼을 참조하라.

도서를 추가, 편집, 대여, 삭제하기

In the next step we have to add the following use cases.
  • Add books
  • Edit books
  • Borrow / return books
  • Delete books

새로운 form bean

새로운 form bean과 action form 클래스를 생성하라. 유즈 케이스를  bookEdit로 설정하고 Optional details – Methods 상의 모든 메소드들을 제거하라.  MyEcplise는 우리를 위해 jsp 파일을 생성시킨다.
de.laliluna.tutorial.library.form 내에 있는 BookEditForm.java 클래스를 열어라.
book 속성과 customerId 속성을 생성시켜라.

public class BookEditForm extends ActionForm {

private Book book = new Book();

/**
* we will need this field to save the customer id in the dialogs where a customer borrows a book
*/
private Integer customerId;
사용자 삽입 이미지

속성들에 대한 getters 와 setters 를 생성시킬 것이다. 그런 다음 book 속성에 대한 모든 delegate 메소드들을 생성시킨다.
사용자 삽입 이미지

소스 코드는 다음과 같이 보인다.
public class BookEditForm extends ActionForm {
private Book book = new Book();

/**
* we will need this field to save the customer id in the dialogs where a customer borrows a book
*/
private Integer customerId;

/**
* @return Returns the book.
*/
public Book getBook() {
return book;
}
/**
* @param book The book to set.
*/
public void setBook(Book book) {
this.book = book;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object arg0) {
return book.equals(arg0);
}
/**
* @return
*/
public String getAuthor() {
return book.getAuthor();
}
/**
* @return
*/
public Boolean getAvailable() {
return book.getAvailable();
}
/**
* @return
*/
public Customer getCustomer() {
return book.getCustomer();
}
/**
* @return
*/
public Integer getId() {
return book.getId();
}
/**
* @return
*/
public String getTitle() {
return book.getTitle();
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return book.hashCode();
}
/**
* @param author
*/
public void setAuthor(String author) {
book.setAuthor(author);
}
/**
* @param available
*/
public void setAvailable(Boolean available) {
book.setAvailable(available);
}
/**
* @param customer
*/
public void setCustomer(Customer customer) {
book.setCustomer(customer);
}
/**
* @param id
*/
public void setId(Integer id) {
book.setId(id);
}
/**
* @param title
*/
public void setTitle(String title) {
book.setTitle(title);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return book.toString();
}
/**
* @return Returns the customerId.
*/
public Integer getCustomerId() {
return customerId;
}
/**
* @param customerId The customerId to set.
*/
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
}

유즈케이스: 도서 편집하기

다음 단계는 우리가 도서를 편집하는데 필요한 어떤 것을 생성시키는 것이다.

Action Mapping

새로운 액션 매핑을 생성하라. 첫 번째 액션 클래스와는 차이점이 존재한다. 새로운 액션 클래스는 슈퍼 클래스 org.apache.struts.DispatchAction을 상속받을 것이다. 디스패치 액션은 execute 메소드를 호출하지 않지만 파라미터로 지정된 다른 메소드들을 호출한다. 사용자가 Edit Link를 클릭할 때 Edit 메소드가 호출될 것이고, create 링크를 클릭할 때 디스패치 액션은 create 메소드를 호출한다.
사용자 삽입 이미지

Parameter 탭에서 우리는 do  파라미터를 추가한다. 이들 파라미터는 디스패치 액션 클래스에 의해 필요하다.
사용자 삽입 이미지

네 개의 새로운 포워드들을 추가하라. 하나는 edit 페이지에 대한 포워드이고, 두 번째는  add 페이지에 대한 포워드, 세 번째는 borrow 페이지에 대한 포워드, 네 번째는 도서 목록을 도서목록으로 포워드 리다이렉트 .
사용자 삽입 이미지
 
 
 
 
 
 
 
 
 
 
사용자 삽입 이미지

사용자 삽입 이미지


사용자 삽입 이미지

마지막 포워드는 다른 것들과는 다르다. 그것은 기존의 액션 매핑을 참조하고 사용자에게 리다이렉트 된다.
New > JSP로 존재하지 않는 jsp 파일들을 생성시키자.
bookAdd.jsp
bookEdit.jsp
bookBorrow.jsp
jsp 파일들의 소스 코드를 편집하라
bookAdd.jsp파일을 열고 소스 코드에 다음을 추가하라.
<%@ page language="java"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %>

<html>
<head>
<title>Add a book</title>
</head>
<body>
<%-- create a html form --%>
<html:form action="bookEdit">
<%-- print out the form data --%>
<table border="1">
<tbody>
<tr>
<td>Author:</td>
<td><html:text property="author" /></td>
</tr>
<tr>
<td>Title:</td>
<td><html:text property="title" /></td>
</tr>
<tr>
<td>Available:</td>
<td><html:checkbox property="available" /></td>
</tr>
</tbody>
</table>
<%-- set the parameter for the dispatch action --%>
<html:hidden property="do" value="saveBook" />

<br>
<%-- submit and back button --%>
<html:button property="back"
omclick="history.back();">
Back
</html:button>
 
<html:submit>Save</html:submit>
</html:form>
</body>
</html>
<html:form> 태그는  새로운 HTML 폼을 생성시키고 액션 매핑에 대한 action=“bookEdit“ 파라미터를 참조한다.  <html:text> 태그는 도서의 author 프로퍼티를 가진 텍스트 필드를 생성시킨다. <html:hidden>?name do를 가진 hidden 폼 필드이다. 우리는 이 hidden 필드를 필요로 한다. 왜냐하면 그것은 어느 메소드가 호출될 것인지를 디스패치 액션 메소드에게 통보하기 때문이다.
bookEdit.jsp파일을 열어라.  당신은 bookAdd.jsp 파일의 소스 코드를 사용할 수 있고 다음 라인들을 변경할 수 있다.
<title>Edit a book</title>
Add the following line above <html:hidden property="do" value="saveBook" />
<%-- hidden fields for id and userId --%>
<html:hidden property="id" />
bookBorrow.jsp파일을 열고 다음을 추가하라.
<%@ page language="java"%>
<%@ page isELIgnored="false"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic" %>

<html>
<head>
<title>Show customers</title>
</head>
<body>
<html:form action="bookEdit">
<table border="1">
<tbody>
<%-- set the header --%>
<tr>
<td>Last name</td>
<td>Name</td>
<td>Borrow</td>
</tr>

<%-- start with an iterate over the collection users --%>
<logic:present name="customers">
<logic:iterate name="customers" id="customer">
<tr>
<%-- book informations --%>
<td><bean:write name="customer" property="lastname" /></td>
<td><bean:write name="customer" property="name" /></td>
<td><html:radio property="customerId" value="${customer.id}" /></td>
</tr>
</logic:iterate>
</logic:present>
<%-- end interate --%>

<%-- if customers cannot be found display a text --%>
<logic:notPresent name="customers">
<tr>
<td colspan="5">No customers found.</td>
</tr>
</logic:notPresent>
</tbody>
</table>

<%-- set the book id to lent --%>
<html:hidden property="id" />

<%-- set the parameter for the dispatch action --%>
<html:hidden property="do" value="saveBorrow" />

<%-- submit and back button --%>
<html:button property="back"
omclick="history.back();">
Back
</html:button>
 
<html:submit>Save</html:submit>
</html:form>
</body>
</html>

dispatch 액션 클래스의 메소드들

bookEditAction.java 파일을 열고 다음 메소드들을 추가하라.
public class BookEditAction extends DispatchAction {
/**
* loads the book specified by the id from the database and forwards to the edit form
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward editBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("editBook");
BookEditForm bookEditForm = (BookEditForm) form;

/* lalinuna.de 04.11.2004
* get id of the book from request
*/
Integer id = Integer.valueOf(request.getParameter("id"));
// [laliluna] 28.11.2004 get business logic
LibraryManager libraryManager = new LibraryManager();
bookEditForm.setBook(libraryManager.getBookByPrimaryKey(id));
return mapping.findForward("showEdit");
}

/**
* loads a book from the db and forwards to the borrow book form
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward borrowBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("borrowBook");

BookEditForm bookEditForm = (BookEditForm) form;

/* lalinuna.de 04.11.2004
* get id of the book from request
*/
Integer id = Integer.valueOf(request.getParameter("id"));

/* lalinuna.de 16.11.2004
* load the session facade for book and user
* get the book information and get all users
*/
LibraryManager libraryManager = new LibraryManager();

// [laliluna] 28.11.2004 save book in the form
bookEditForm.setBook(libraryManager.getBookByPrimaryKey(id));
// [laliluna] 28.11.2004 save customers in the reqest
request.setAttribute("customers", libraryManager.getAllCustomers());

return mapping.findForward("showBorrow");
}

/**
* return a book from a customer
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward returnBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("returnBook");

BookEditForm bookEditForm = (BookEditForm) form;

/* lalinuna.de 04.11.2004
* get id of the book from request
*/
Integer id = Integer.valueOf(request.getParameter("id"));

// [laliluna] 28.11.2004 get business logic
LibraryManager libraryManager = new LibraryManager();

libraryManager.returnBook(id);

return mapping.findForward("showList");
}

/**
* deletes a book from the database
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward deleteBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("deleteBook");

BookEditForm bookEditForm = (BookEditForm) form;

/* lalinuna.de 04.11.2004
* get id of the book from request
*/
Integer id = Integer.valueOf(request.getParameter("id"));

// [laliluna] 28.11.2004 get business logic
LibraryManager libraryManager = new LibraryManager();

libraryManager.removeBookByPrimaryKey(id);

return mapping.findForward("showList");
}
/**
* forwards to the add book form
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward addBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
System.out.println("addBook");

BookEditForm bookEditForm = (BookEditForm) form;

return mapping.findForward("showAdd");

}

/**
* saves the borrow assigned in the form in the database
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward saveBorrow(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
BookEditForm bookEditForm = (BookEditForm) form;

// [laliluna] 28.11.2004 get business logc
LibraryManager libraryManager = new LibraryManager();
libraryManager.borrowBook(bookEditForm.getId(), bookEditForm.getCustomerId());

return mapping.findForward("showList");
}

/**
* updates or creates the book in the database
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward saveBook(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) {
BookEditForm bookEditForm = (BookEditForm) form;

// [laliluna] 28.11.2004 get business logic
LibraryManager libraryManager = new LibraryManager();
libraryManager.saveBook(bookEditForm.getBook());
return mapping.findForward("showList");
}
}

유즈 케이스 고객 목록

우리는 도서목록과 동일한 방법으로 이 목록을 생성시킨다. 액션, 폼, 포워드들을 동시에 생성시키기 위해 마법사를 선택하라. 우리의 유즈 케이스는 고객 목록이다. 아래에 보이는 것처럼 대화상자를 편집하라.
사용자 삽입 이미지


Methods 탭에서 변경들을 잊지 말라. JSP 탭에서 다음 JSP를 생성시켜라.
사용자 삽입 이미지

다음 단계는 당신의 JSP가 보이기 전체 호출될 액션을 설정하는 것이다. 아래에 표시된 대로 변경하라.
사용자 삽입 이미지

마지막 단계는 고객 목록을 보여주는 JSP로의 포워드 시키게 될 액션으로부터 포워드 폼을 생성시키는 것이다.
사용자 삽입 이미지

이제 우리는 우리가 우리의 유즈케이스에 필요한 모든 파일들을 생성시켰다. 다음 단계는 그것들에 컨텐트를 채우는 것이다.

action form class의 소스 코드를 편집한다

CustomerListForm.java 파일을 열고 소스 코드에 다음을 추가하라.
public class CustomerListForm extends ActionForm {
private Customer[] customers = new Customer[0];
/**
* @return Returns the customers.
*/
public Customer[] getCustomers() {
return customers;
}
/**
* @param customers The customers to set.
*/
public void setCustomers(Customer[] customers) {
this.customers = customers;
}
}
빈이 액션으로부터 JSP로 데이터를 전달하는데에만 사용되므로, 우리는 여기서 reset 메소드를 필요로 하지 않는다.
액션 클래스를 편집하라.
public class CustomerListAction extends Action
{
/**
* loads customers from the db and saves them in the request
* @param mapping
* @param form
* @param request
* @param response
* @return ActionForward
*/
public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
{
CustomerListForm customerListForm = (CustomerListForm) form;
// [laliluna] 29.11.2004 get business logic
LibraryManager libraryManager = new LibraryManager();

customerListForm.setCustomers(libraryManager.getAllCustomers());
return mapping.findForward("showCustomerList");
}
}

고객 목록 디스플레이 하기

customerList.jsp 파일을 열고 파일의 컨텐트를 다음으로 변경하라.
<%@ page language="java"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-logic" prefix="logic"%>
<html>
<head>
<title>JSP for customerListForm form</title>
</head>
<body>
<table border="1">
<tbody>
<%-- set the header --%>
<logic:present name="customerListForm" property="customers">

<tr>
<td>Name</td>
<td>Last name</td>
<td>Age</td>
<td></td>
<td></td>
</tr>
<%-- start with an iterate over the collection books --%>
<logic:iterate name="customerListForm" property="customers" id="customer">
<tr>
<%-- book informations --%>
<td><bean:write name="customer" property="name" /></td>
<td><bean:write name="customer" property="lastname" /></td>
<td><bean:write name="customer" property="age" /></td>
<%-- edit and delete link for each customer --%>
<td><html:link action="customerEdit.do?do=editCustomer"
paramName="customer"
paramProperty="id"
paramId="id">Edit</html:link>
</td>
<td><html:link action="customerEdit.do?do=deleteCustomer"
paramName="customer"
paramProperty="id"
paramId="id">Delete</html:link>
</td>
</tr>
</logic:iterate>
<%-- end interate --%>
</logic:present>
<%-- if customers cannot be found display a text --%>
<logic:notPresent name="customerListForm" property="customers">
<tr>
<td colspan="5">No customers found.</td>
</tr>
</logic:notPresent>

</tbody>
</table>

<br>
<%-- add and back to menu button --%>
<html:button property="add"
omclick="location.href='customerEdit.do?do=addCustomer'">Add a new customer
</html:button>
 
<html:button property="back"
omclick="location.href='default.do'">Back to menu
</html:button>
</body>
</html>
우리는 유즈 케이스를 마쳤다. 자 테스트하자.

유즈 케이스 고객 추가, 수정, 삭제

다음 단계에서, 우리는 다음 프로세스들을 추가하고자 원한다.
  • 고객 추가
  • 고객 수정
  • 고객 삭제
"New Form, Action and JSP"을 선택하라.

사용자 삽입 이미지


create a JSP file을 선택하라.
사용자 삽입 이미지

action 페이지로 진행하라. Super Class로서 DispatchAction 을 선택하라.
사용자 삽입 이미지

파라미터 생성을 선택하라:
사용자 삽입 이미지

아래와 같이 세 개의 포워드들을 생성시켜라.

사용자 삽입 이미지

Customer form bean


Customer 타입의 새로운 속성을 추가하라
private Customer customer;
book form bean에 대해 행했던 것처럼, getter- 메소드와 setter- 메소드를 생성시키고 클래스의 모든 delegate 메소드들을  생성시켜라.
사용자 삽입 이미지

클래스의 소스 코드는 다음과 같다
public class CustomerEditForm extends ActionForm {

private Customer customer;

/**
* @return Returns the customer.
*/
public Customer getCustomer() {
return customer;
}
/**
* @param customer The customer to set.
*/
public void setCustomer(Customer customer) {
this.customer = customer;
}
/**
* Method reset
* @param mapping
* @param request
*/
public void reset(ActionMapping mapping, HttpServletRequest request) {

customer=new Customer();

}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public boolean equals(Object arg0) {
return customer.equals(arg0);
}
/**
* @return
*/
public Integer getAge() {
return customer.getAge();
}
/**
* @return
*/
public Integer getId() {
return customer.getId();
}
/**
* @return
*/
public String getLastname() {
return customer.getLastname();
}
/**
* @return
*/
public String getName() {
return customer.getName();
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
public int hashCode() {
return customer.hashCode();
}
/**
* @param age
*/
public void setAge(Integer age) {
customer.setAge(age);
}
/**
* @param id
*/
public void setId(Integer id) {
customer.setId(id);
}
/**
* @param lastname
*/
public void setLastname(String lastname) {
customer.setLastname(lastname);
}
/**
* @param name
*/
public void setName(String name) {
customer.setName(name);
}
/* (non-Javadoc)
* @see java.lang.Object#toString()
*/
public String toString() {
return customer.toString();
}
}

액션 클래스의 소스 코드를 편집한다

de.laliluna.library.struts.action 패키지에서 CustomerEditAction.class 파일을 열고 다음 메소드들을 추가하라.
첫 번째는 고객을 수정하기 바로 전 단계이다. 그것은 데이터베이스로부터 고객 데이터를 로드시키고 그것을 form bean에 저장한다.
/**
* loads customer from the db and forwards to the edit form
* @param mapping
* @param form
* @param request
* @param response
* @return
*/
public ActionForward prepareEdit(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
CustomerEditForm customerEditForm = (CustomerEditForm) form;

Integer id = Integer.valueOf(request.getParameter("id"));
LibraryManager libraryManager = new LibraryManager();

customerEditForm.setCustomer(libraryManager.getCustomerByPrimaryKey(id));

return mapping.findForward("editCustomer");
}
다음 메소드는 고객 생성 JSP가 열리기 바로 전 단계이다. 실제로 그것은 JSP로 오직 포워드 만 행한다.
/**
* prepares the add form (actually only forwards to it)
* @param mapping
* @param form
* @param request
* @param response
* @return
*/
public ActionForward prepareAdd(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
return mapping.findForward("addCustomer");

}
고객에 대한 업데이트와 생성은 다음 메소드로 행해진다.
/**
* saves the customers and forwards to the list
* @param mapping
* @param form
* @param request
* @param response
* @return
*/
public ActionForward saveCustomer(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
CustomerEditForm customerEditForm = (CustomerEditForm) form;
LibraryManager libraryManager = new LibraryManager();
libraryManager.saveCustomer(customerEditForm.getCustomer());

return mapping.findForward("customerList");
}
마지막으로 delete를 클릭할 때 다음 메소드가 호출된다.
/**
* deletes the customers and forwards to the list
* @param mapping
* @param form
* @param request
* @param response
* @return
*/
public ActionForward deleteCustomer(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
CustomerEditForm customerEditForm = (CustomerEditForm) form;
LibraryManager libraryManager = new LibraryManager();
libraryManager.removeCustomerByPrimaryKey(customerEditForm.getCustomer().getId());

return mapping.findForward("customerList");
}
비지니스 로직이 분리되어 유지될 때, 액션 내의 코드는 항상 매우 짧고 읽기가 쉽다.

jsp 파일의 소스 코드를 편집한다

WebRoot/jsp/ 폴더에서 editcustomer.jsp로 명명된 새로운 파일을 생성시켜라.
editcustomer.jsp 파일을 열고 파일의 내용을 변경하라.
<%@ page language="java"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-bean" prefix="bean"%>
<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html"%>

<html>
<head>
<title>JSP for customerEditForm form</title>
</head>
<body>
<html:form action="/customerEdit">
<html:hidden property="id"/>
<html:hidden property="do" value="saveCustomer"/>
Name: <html:text property="name"/><br/>
Last name <html:text property="lastname"/><br/>
Age <html:text property="age"/><br/>
<html:submit/><html:cancel/>
</html:form>
</body>
</html>

어플리케이션 테스트

jboss 를 시작하고 프로젝트를 패키지 아카이브로 배치하라.
사용자 삽입 이미지
 
 
 
 
 
 
 
 
 
 
 
 

 
 
 
 
 
끝!

* ps. 역자는 이 번역글이 매끄롭지 못해 개정하고 싶지만 시간의 여력이 없어
그대로 업로드 하기로 결정하였습니다(원본 상의 소스에 있어 오류들이 그대로 반영되어 있는데,
앞뒤의 문맥을 통해 파악하면 무난히 테스트할 수 있을 것입니다).
차후 역자의 샘플링 프로젝트를 통해보충할 기회를 마련할 수 있기를 스스로 희망하는 바입니다.
소스 상의 오류에 관한 문의는 댓글달기를 통해 역자에게 요청 바랍니다.
신고
Posted by The.민군

Hibernate 첫 번째 예제 - 튜토리얼

                                 원문 :http://www.laliluna.de/first-hibernate-example-tutorial.html

                                 역자 :김종대(jdkim528@korea.com)

이 튜토리얼은 Hibernate를 사용하는 간단한 예제를 보여준다. 우리는 Hibernate가 어떻게 동작하는지를 보여주는 간단한 자바 어플리케이션을 생성시킬 것이다.

(광고 생략...)

개괄

저자:Sebastian Hennebrueder

날짜: December, 19th 2005

사용된 소프트웨어와 프레임워크

Eclipse 3.x

MyEclipse 4.x가 권장되지만 선택사항임

Hibernate 3.x (3.1을 사용함)

소스 코드:http://www.laliluna.de/download/first-hibernate-example-tutorial.zip

소스들은 라이브러리들을 포함하지 않는다. hibernate.org에서 라이브러리들과 당신의 데이터베이스 드라이버를 내려 받고 그것들을 아래에 설명되어 있는 프로젝트에 추가한다. 예제는 당신의 데이터베이스 설정들과 동작하도록 구성되어야 한다! 튜토리얼을 읽기 바란다.

튜토리얼의 PDF 버전: http://www.laliluna.de/download/first-hibernate-example-tutorial-en.pdf

Hibernate2용 이전 PDF 버전: http://www.laliluna.de/download/first-hibernate-2-example-tutorial-en.pdf

짧은 개요

Hibernate는 객체 관계형 매핑을 위한 솔루션이고 영속 관리 솔루션 또는 영속 계층이다. 이것은 아마 Hibernate를 배우는 아무나 이해 가능한 것은 아니다.

당신이 생각할 수 있는 것은 아마 당신이 당신의 어플리케이션에 몇몇 기능들(비지니스 로직)을 갖도록 하고 데이터베이스 내에 데이터를 저장하고 싶어하는 것이다. 당신이 자바를 사용할 때 모든 비지니스 로직은 통상적으로 다른 클래스 타입들인 객체들과 동작한다. 당신의 데이터베이스 테이블들은 전혀 객체들이 아니다.

Hibernate는 데이터베이스 테이블들을 어떤 클래스로 매핑시키는 솔루션을 제공한다. 그것은 데이터베이스 데이터를 어떤 클래스로 복사한다. 반대 방향으로 그것은 객체들을 데이터베이스에 저장하는 것을 지원한다. 이 과정에서 객체는 하나 이상의 테이블들로 전환된다.

저장소에 데이터를 저장시키는 것은 영속이라 명명된다. 그리고 테이블들을 객체들에 복사하는 것 등등은 객체 관계형 매핑이라 명명된다.

Java 프로젝트를 생성한다

Eclipse를 사용하여 새로운 프로젝트를 생성시키기 위해Ctrl+n (Strg+n)키를 누른다. Java 프로젝트를 선택한다. 우리는 그것을 FirstHibernateExample라 명명할 것이다.

MyEclipse를 사용하여 Hibernate용 프로젝트를 준비한다

MyEclipse를 사용하고 있다면, 패키지 탐색기 내에서 당신의 프로젝트 상을 마우스 오른쪽 버튼 클릭하고Add Hibernate capabilities.를 선택한다.

사용자 삽입 이미지


마법사를 계속하고src디렉토리 내에 새로운hibernate.cfg.xml을 생성시킨다.

마지막 단계에서 당신은 Hibernate SessionFactory를 생성시킬 수 있다. 나는 내 자신의 것을 생성 시키는 것을 선호한다. 당신은 아래에서 그것을 찾을 수 있다.

Hibernate용 프로젝트를 준비한다

당신이 MyEclipse를 사용하고 있지 않을 때http://www.hibernate.org/웹 사이트로부터 Hibernate를 내려 받아라.

파일을 추출한다. Hibernate는 많은 라이브러리들의 목록으로 구성되어 있다. 당신은 그것들 모두를 필요로 하지 않는다. lib 디렉토리 내에 필수적인 것을 설명하고 있는 README 파일이 존재한다. 당신의 프로젝트 properties를 열고, “Java Build Path”를 선택하고, “Add External Jars”을 클릭하고 아래에 보이는 라이브러리들을 당신의 프로젝트 경로에 추가한다.

사용자 삽입 이미지

SessionFactory를 생성시킨다

세션 팩토리는 Hibernate에서 중요하다. 그것은 단위 쓰레드 당 오직 한 개의 세션 인스턴스만이 사용됨을 보증하는 설계 패턴을 구현하고 있다. 당신은 단지 이 팩토리로부터 당신의 Hibernate 세션을 얻을 것이다.

de.laliluna.hibernate 패키지 내에 HibernateSessionFactory로 명명된 클래스를 생성시키고 아래의 소스 코드를 추가한다.

/** *  * @author Sebastian Hennebrueder * created Feb 22, 2006 * copyright 2006 by http://www.laliluna.de */package de.laliluna.hibernate;import javax.naming.InitialContext;import org.apache.log4j.Logger;import org.hibernate.HibernateException;import org.hibernate.Session;import org.hibernate.SessionFactory;import org.hibernate.cfg.Configuration;import org.hibernate.cfg.Environment;/** * @author hennebrueder 이 클래스는 오직 한 개의 SessionFactory 만이 초기화 되고 *         컨피그레이션이 싱글톤으로 쓰레드 안전하게 행해진다는 점을 보증한다. *         실제로 그것은 단지 Hibernate SessionFactory를 포장한다. *         JNDI 이름이 구성될 때 세션은 JNDI에 바인드 되고,  *         그 밖의 경우 그것은 오직 로컬 상으로 저장된다. *         당신은 임의의 종류의 JTA 또는 Thread transactionFactory들을 사용하는 것이 자유롭다.  */public class InitSessionFactory { /**  * Default constructor.  */ private InitSessionFactory() { } /**  * hibernate.cfg.xml 파일의 위치. 주의: 위치는 Hibernate가 사용하는 classpath 상에 있어야 한다  * #resourceAsStream 스타일은 그것의 구성 컨피그레이션 파일을 검색한다.  * 그것은 Java 패키지 내에 있는 config 파일에 위치된다 -   * 디폴트 위치는 디폴트 Java 패키지이다.<br>  * <br>  * 예제: <br>  * <code>CONFIG_FILE_LOCATION = "/hibernate.conf.xml".   * CONFIG_FILE_LOCATION = "/com/foo/bar/myhiberstuff.conf.xml".</code>  */ private static String CONFIG_FILE_LOCATION = "/hibernate.cfg.xml"; /** hibernate 컨피그레이션의 싱글톤 인스턴스 */ private static final Configuration cfg = new Configuration(); /** hibernate SessionFactory의 싱글톤 인스턴스 */ private static org.hibernate.SessionFactory sessionFactory; /**  * 아직 초기화 되지 않았다면 컨피그레이션을 초기화 시키고 현재 인스턴스를 반환한다  * 현재 인스턴스를 반환한다  *   * @return  */ public static SessionFactory getInstance() {  if (sessionFactory == null)   initSessionFactory();  return sessionFactory; } /**  * ThreadLocal Session 인스턴스를 반환한다. 필요하다면 Lazy는   * <code>SessionFactory</code>를 초기화 시킨다.  *   * @return Session  * @throws HibernateException  */ public Session openSession() {  return sessionFactory.getCurrentSession(); } /**  * 이 메소드의 행위는 당신이 구성했던 세션 컨텍스트에 의존한다.  * 이 팩토리는  다음 프로퍼티 is intended to be used with a hibernate.cfg.xml  * <property name="current_session_context_class">thread</property>를   * 포함하는 hibernate.cfg.xml과 함께 사용되게 기안되어 있다. 이것은   * 현재의 열려진 세션을 반환할 것이거나 존재하지 않을 경우 새로운 세션을 생성시킬 것이다  *   * @return  */ public Session getCurrentSession() {  return sessionFactory.getCurrentSession(); } /**  * 심지어 하나 이상의 쓰레드가 sessionFactory를 빌드하려고 시도하는 경우조차도   * 안전한 방법으로 sessionFactory를 초기화 시킨다  */ private static synchronized void initSessionFactory() {  /*   * [laliluna] 다시 null을 체크한다 왜냐하면 sessionFactory가 마지막 체크와 현재의 체크 사이에   * 초기화 되었을 수도 있기 때문이다   *    */  Logger log = Logger.getLogger(InitSessionFactory.class);  if (sessionFactory == null) {    try {    cfg.configure(CONFIG_FILE_LOCATION);    String sessionFactoryJndiName = cfg    .getProperty(Environment.SESSION_FACTORY_NAME);    if (sessionFactoryJndiName != null) {     cfg.buildSessionFactory();     log.debug("get a jndi session factory");     sessionFactory = (SessionFactory) (new InitialContext())       .lookup(sessionFactoryJndiName);    } else{     log.debug("classic factory");     sessionFactory = cfg.buildSessionFactory();    }   } catch (Exception e) {    System.err      .println("%%%% Error Creating HibernateSessionFactory %%%%");    e.printStackTrace();    throw new HibernateException(      "Could not initialize the Hibernate configuration");   }  } }  public static void close(){  if (sessionFactory != null)   sessionFactory.close();  sessionFactory = null;  }}


Log4J 구성하기

당신이 위에서 보았듯이 우리는 log4j 라이브러리를 추가시켰다. 이 라이브러리는 소스 디렉토리 내에 컨피그레이션 파일처럼 행하거나 다음 오류로서 당신을 맞이한다.

log4j:WARN No appenders could be found for logger (TestClient).log4j:WARN Please initialize the log4j system properly.

루트 디렉토리에 log4j.properties로 명명된 파일을 생성시키고 다음을 삽입시킨다:

### direct log messages to stdout ###log4j.appender.stdout=org.apache.log4j.ConsoleAppenderlog4j.appender.stdout.Target=System.outlog4j.appender.stdout.layout=org.apache.log4j.PatternLayoutlog4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n### set log levels - for more verbose logging change 'info' to 'debug' ###log4j.rootLogger=debug, stdoutlog4j.logger.org.hibernate=info#log4j.logger.org.hibernate=debug### log HQL query parser activity#log4j.logger.org.hibernate.hql.ast.AST=debug### log just the SQLlog4j.logger.org.hibernate.SQL=debug### log JDBC bind parameters ###log4j.logger.org.hibernate.type=info### log schema export/update ###log4j.logger.org.hibernate.tool.hbm2ddl=info### log HQL parse trees#log4j.logger.org.hibernate.hql=debug### log cache activity ###log4j.logger.org.hibernate.cache=info### log transaction activity#log4j.logger.org.hibernate.transaction=debug### log JDBC resource acquisition#log4j.logger.org.hibernate.jdbc=debug### enable the following line if you want to track down connection ###### leakages when using DriverManagerConnectionProvider ####log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace

데이터베이스 드라이버를 추가한다

Hibernate는 데이터베이스에 접근할 데이터베이스 드라이버를 필요로 한다. 프로젝트 properties를 열고, “Java Build Path”를 클릭하고, “Add External Jars”를 선택하고 당신의 데이터베이스 드라이버를 추가한다. 당신이 PostgreSQL를 사용할 때 당신은 http://jdbc.postgresql.org에서 당신의 데이터베이스 드라이버를 찾을 수 있고 MySQL을 사용하고 있다면 이곳http://www.mysql.de/products/connector/j에서 찾을 수 있다.

데이터베이스와 테이블드을 생성시킨다.

Create a database with MySql 또는 PostgreSQL 또는 당신이 좋아하는 DBMS에 데이터베이스를 생성시킨다. 그것을 “firsthibernate”로 명명한다.

PostgreSql을 사용한다면 테이블을 생성시키기 위해 다음 스크립트를 사용하라:

CREATE TABLE "public"."honey" (  id SERIAL,   name text,   taste text,   PRIMARY KEY(id));


MySql을 사용하고 있다면 다음 스크립트를 사용하라:

CREATE TABLE `honey` (  `id` int(11) NOT NULL auto_increment,  `name` varchar(250) default NULL,  `taste` varchar(250) default NULL,  PRIMARY KEY  (`id`)) ENGINE=MyISAM DEFAULT CHARSET=latin1

클래스를 생성시킨다

Create a new class named “Honey” in the package “de.laliluna.example”. Add three fields id, name and taste and generate (Context menu -> Source -> Generate Getter and Setter) or type the getters and setters for the fields. Then create an empty constructor.

package de.laliluna.example;/** * @author laliluna * */public class Honey { private Integer id; private String name; private String taste;  public Honey(){  }  /**  * @return Returns the id.  */ public Integer getId() {  return id; } /**  * @param id The id to set.  */ public void setId(Integer id) {  this.id = id; } /**  * @return Returns the name.  */ public String getName() {  return name; } /**  * @param name The name to set.  */ public void setName(String name) {  this.name = name; } /**  * @return Returns the taste.  */ public String getTaste() {  return taste; } /**  * @param taste The taste to set.  */ public void setTaste(String taste) {  this.taste = taste; }}



매핑 파일들을 생성시킨다

이미 생성되어 있지 않다면 루트 디렉토리에 “hibernate.cfg.xml”로 명명된 새로운 파일을 생성시킨다.

hibernate 파일 내에 다음을 추가한다. 당신의 데이터베이스 구성에 적합하게 username과 password를 변경하는 것을 잊지 말라.

PostgreSQL 버전:

<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE hibernate-configuration PUBLIC          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"><hibernate-configuration><session-factory> <property name="connection.url">jdbc:postgresql://localhost/firsthibernate</property> <property name="connection.username">postgres</property> <property name="connection.driver_class">org.postgresql.Driver</property> <property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property> <property name="connection.password">p</property> <property name="transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property>    <!--  thread is the short name for      org.hibernate.context.ThreadLocalSessionContext      and let Hibernate bind the session automatically to the thread    -->    <property name="current_session_context_class">thread</property>    <!-- this will show us all sql statements -->    <property name="hibernate.show_sql">true</property> <!-- mapping files --> <mapping resource="de/laliluna/example/Honey.hbm.xml" /></session-factory></hibernate-configuration>


MySQL 버전:


<?xml version='1.0' encoding='UTF-8'?><!DOCTYPE hibernate-configuration PUBLIC          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"><hibernate-configuration><session-factory> <property name="connection.url">jdbc:mysql://localhost/firsthibernate</property> <property name="connection.username">root</property> <property name="connection.driver_class">com.mysql.jdbc.Driver</property> <property name="dialect">org.hibernate.dialect.MySQLDialect</property> <property name="connection.password">r</property> <property name="transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property>    <!--  thread is the short name for      org.hibernate.context.ThreadLocalSessionContext      and let Hibernate bind the session automatically to the thread    -->    <property name="current_session_context_class">thread</property>    <!-- this will show us all sql statements -->    <property name="hibernate.show_sql">true</property>  <!-- mapping files --> <mapping resource="de/laliluna/example/Honey.hbm.xml" /></session-factory></hibernate-configuration>


이 파일은 데이터베이스에 대한 구성 우리의 경우에는 PostgreSQL 데이터베이스의 구성 그리고 모든 매핑 파일들을 포함한다. 우리의 경우 그것은 단지 파일 Honey.hbm.xml이다.

<property name="dialect">org.hibernate.dialect.PostgreSQLDialect</property>

태그는 dialect를 구성한다. 당신의 데이터베이스에 맞게 이것을 변경하라. 당신의 데이터베이스를 위한 dialect를 찾기 위해 Hibernate 레퍼런스의 “SQL Dialects” 장에서 찾아보라.

de.laliluna.example 패키지 내에 Honey.hbm.xml을 생성시키고 그것을 다음으로 변경하라:

PostgreSQL 버전:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" ><hibernate-mapping> <class name="de.laliluna.example.Honey" table="honey"> <id name="id" column="id" type="java.lang.Integer">  <generator class="sequence">    <param name="sequence">honey_id_seq</param>   </generator> </id>  <property name="name" column="name" type="java.lang.String" /> <property name="taste" column="taste" type="java.lang.String" /> </class></hibernate-mapping>


MySQL 버전:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" ><hibernate-mapping> <class name="de.laliluna.example.Honey" table="honey"> <id name="id" column="id" type="java.lang.Integer"> <generator class="increment"/> </id> <property name="name" column="name" type="java.lang.String" /> <property name="taste" column="taste" type="java.lang.String" /> </class></hibernate-mapping>


이 파일에서 우리의 클래스 Honey로부터 데이터베이스 테이블 honey로의 매핑이 구성되어 있다.

테스트 클라이언트를 생성한다

“de.laliluna.example” 패키지 내에 Java 클래스 “TestClient”를 생성시킨다.

다음 소스 코드를 추가한다. 그것은 데이터베이스 내에 엔트리들을 생성시키는 메소드, 그것들을 업데이트하고 리스트하는 메소드들을 포함하고 있다.

/** * Test application for example  * @author Sebastian Hennebrueder * created Jan 16, 2006 * copyright 2006 by http://www.laliluna.de */package de.laliluna.example;import java.util.Iterator;import java.util.List;import org.apache.log4j.Logger;import org.hibernate.HibernateException;import org.hibernate.Session;import org.hibernate.Transaction;import de.laliluna.hibernate.InitSessionFactory;public class TestExample { private static Logger log =Logger.getLogger(TestExample.class); /**  * @param args  */ public static void main(String[] args) {  Honey forestHoney = new Honey();  forestHoney.setName("forest honey");  forestHoney.setTaste("very sweet");  Honey countryHoney = new Honey();  countryHoney.setName("country honey");  countryHoney.setTaste("tasty");  createHoney(forestHoney);  createHoney(countryHoney);  // our instances have a primary key now:  log.debug(forestHoney);  log.debug(countryHoney);  listHoney();  deleteHoney(forestHoney);  listHoney(); } private static void listHoney() {  Transaction tx = null;  Session session = InitSessionFactory.getInstance().getCurrentSession();  try {   tx = session.beginTransaction();   List honeys = session.createQuery("select h from Honey as h")     .list();   for (Iterator iter = honeys.iterator(); iter.hasNext();) {    Honey element = (Honey) iter.next();    log.debug(element);   }   tx.commit();  } catch (HibernateException e) {   e.printStackTrace();   if (tx != null && tx.isActive())    tx.rollback();  } } private static void deleteHoney(Honey honey) {  Transaction tx = null;  Session session = InitSessionFactory.getInstance().getCurrentSession();  try {   tx = session.beginTransaction();   session.delete(honey);   tx.commit();  } catch (HibernateException e) {   e.printStackTrace();   if (tx != null && tx.isActive())    tx.rollback();  } } private static void createHoney(Honey honey) {  Transaction tx = null;  Session session = InitSessionFactory.getInstance().getCurrentSession();  try {   tx = session.beginTransaction();   session.save(honey);   tx.commit();  } catch (HibernateException e) {   e.printStackTrace();   if (tx != null && tx.isActive())    tx.rollback();  } }}


축하합니다. 당신은 Hibernate 세계에서 당신의 첫 번째 단계들을 마쳤습니다.

우리는 당신이 Hibernate 세계에 빠르게 진입하기를 원했다. Hibernate를 사용하는 많은 복잡한 토픽들과 더 나은 구현이 존재한다. 예를 들어, 각각의 메소드 내에서 세션을 열고 닫는 것은 좋은 연습이 아니다. 하나의 세션은 재사용될 수 있으며 그 동안에 많은 시간을 절약한다.

당신이 최상의 실전에 대해 더 많은 것을 배우고자 원할 경우 우리의 세미나 또는 다른 튜토리얼을 살펴보길 바란다.

Copyright and disclaimer

This tutorial is copyright of Sebastian Hennebrueder, laliluna.de. You may download a tutorial for your own personal use but not redistribute it. You must not remove or modify this copyright notice.(이 튜토리얼은 Sebastian Hennebrueder, laliluna.de에 저작권이 있다. 당신은 당신 자신의 개인 용도로 튜토리얼을 내려받을 수 있지만 그것을 재배포할 수 없다. 당신은 이 저작권 경고를 제거하지 말아야 하거나 변경시키지 말아야 한다.이하 생략...)

The tutorial is provided as is. I do not give any warranty or guaranty any fitness for a particular purpose. In no event shall I be liable to any party for direct, indirect, special, incidental, or consequential damages, including lost profits, arising out of the use of this tutorial, even if I has been advised of the possibility of such damage.

신고
Posted by The.민군

웹 응용프로그램 보안 취약점 발견하기

제공:한빛 네트워크
저자:Shreeraj Shah, 한동훈 역
원문:Detecting Web Application Security Vulnerabilities

코드 리뷰를 통한 웹 응용프로그램 취약점 발견

언어나 플랫폼에 관계없이 응용프로그램 소스 코드가 취약점의 주요 원인이다. CSI의 취약점 분포에 대한 조사에 따르면 프로그래밍 오류에 의한 취약점이 64%를 차지하며, 설정 문제가 36%에 이른다. IBM 연구소에 따르면 1,500 라인의 코드에 최소 한 개 이상의 보안 문제가 발생한다고 한다. 소스 코드 리뷰를 끊임없이 수행하는 한편, 취약점을 찾아내기 위해 웹 응용프로그램을 평가하고 감사하는 것이 필요하다.

문제 도메인

웹 응용프로그램 개발에 인기 있는 언어로는 액티브 서버 페이지(ASP), PHP, 자바 서버 페이지(JSP) 등이 있다. 모든 프로그래머는 객체를 구현하고, 작성하는 자신만의 방법을 갖고 있다. 이들 언어들은 프로그래머가 쉽게 개발할 수 있도록 API와 지시문(directive)을 제공한다. 불행히도, 프로그래밍 언어는 보안에 대해 어떠한 보장도 하지 못한다. 프로그래머가 작성한 코드가 다양한 공격 방법(attack vectors)에 대해 안전하다는 것을 보장하는 것은 프로그래거의 책임이다.

또한, 실제 시스템에 코드를 배포하기 전에 개발된 코드를 외부나 내부에서 보안 관점에 따라 평가하는 것은 필수다. 또한, 커스터마이징되는 응용프로그램, 프로그래머가 코딩하는 다양한 방법들이 존재하기 때문에 소스 코드에 존재하는 취약점을 결정하기 위해 한 가지 도구만 사용하는 것도 불가능하다. 소스 코드 리뷰에는 다양한 도구의 조합, 취약점을 찾아내기 위한 지능적인 분석(Intellectual Analysis)이 필요하다. 소스 코드는 매우 방대하며, 어떤 경우에는 수백, 수천만 라인에 이르기 때문에 제한된 시간안에 소스 코드 라인을 일일이 분석하는 것은 불가능하다. 때문에, 취약점 분석 도구가 필요하다. 도구는 정보(information)를 결정하는 데에만 도움을 줄 수 있다. 예를 들어, 이는 정보와 함께 연결해야 하는, 보안 잣대(security mindset)를 갖춘 지능(intellect)이다. 정보와 지능을 함께 사용하는 접근 방법은 소스 코드 리뷰에서 일반적으로 추천되는 방법이다.

가정(Assumption)

리뷰를 자동화하는 것을 설명하기 위해 ASP.NET으로 작성한 웹 응용프로그램 예제를 사용할 것이다. 소스 코드 분석 도구로는 파이썬 스크립트 예제를 만들었다. 여기서 설명하는 접근 방법은 어떤 언어로 작성된 웹 응용프로그램이든 적용할 수 있다. 또한, 다른 프로그래밍 언어를 사용해서 자신만의 도구를 작성하는 것도 가능하다.

방법론과 접근방법

내 경우에는 코드 리뷰 연습에 접근하는 나의 방법을 특정 목표를 가진 논리적인 단계로 나누었다.
  • 의존성 결정
  • 진입점(Entry Point) 식별
  • 위협 매핑(Threat mapping) 및 취약점 발견
  • 완화 작업(Mitigation) 및 대체 수단
의존성 결정

코드 리뷰 연습을 시작하기 전에 전체 아키텍처와 코드들의 의존성에 대해 이해할 필요가 있다. 이들을 이해하면 보다 나은 검토를 할 수 있으며, 핵심에 집중할 수 있다. 이 단계의 핵심 목표는 의존성을 명확히 밝혀내고, 이 정보를 다음 단계로 연결하는 것이다. 그림1은 검토에 사용할 사례 연구, 웹샵(web shop)의 전체 아키텍처를 표현한 것이다.

사용자 삽입 이미지
그림1. 웹 응용프로그램 아키텍처(webshop.example.com)

응용프로그램은 몇가지 의존성을 갖고 있다.
  • 데이터베이스. 웹 응용프로그램은 벡엔드 데이터베이스로 MS-SQL 서버를 운영하고 있다. 코드 리뷰를 할 때 이 인터페이스도 조사해야 한다.
  • 플랫폼 및 웹 서버. 응용프로그램은 닷넷 플랫폼 및 IIS 웹 서버에서 실행된다. 이는 두가지 관점에서 유용하다. 1) 배포를 안전하게 하는 것, 2) 소스 코드 종류와 언어를 결정하는 것
  • 웹 리소스 및 언어. 이 예제에서 ASPX와 ASMX는 웹 리소스이다. 이들은 C#으로 작성된 전형적인 웹 응용프로그램과 웹 서비스 페이지를 의미한다. 이들 리소스는 코드 리뷰 동안 패턴을 결정하는 데 유용하다.
  • 인증(Authentication). 응용프로그램 인증은 LDAP 서버를 사용해서 사용자를 인증한다. 인증 코드는 핵심 컴포넌트이며, 분석이 필요하다.
  • 방화벽. 응용프로그램 레이어 방화벽이 있으며, 컨텐트 필터링이 활성화되어 있다.
  • 서드파티 컴포넌트. 응용프로그램에서 사용하는 모든 서드 파티 컴포넌트는 분석이 필요하다.
  • 인터넷으로부터의 정보 접근. 응용프로그램들이 인터넷으로부터 소비하는 RSS 피드나 이메일 같은 정보들도 고려해야 한다.
이러한 정보들을 알고 있으면, 코드를 이해하는 것이 보다 수월해진다. 다시 말해서, 전체 응용프로그램은 C#으로 작성되었으며, IIS를 운영하는 웹 서버에서 제공되고 있다. 바로 이것을 대상으로 할 것이다. 다음 단계는 응용프로그램의 진입점을 식별하는 것이다.

진입점 식별

이 단계의 목표는 웹 응용프로그램의 진입점(entry point)를 식별하는 것이다. 웹 응용프로그램은 다양한 위치(sources)로부터 접근될 수 있다(그림2). 따라서, 각 위치를 평가하는 것, 각 위치와 관련된 위험을 평가하는 것이 중요하다.

사용자 삽입 이미지
그림2. 웹 응용프로그램 진입점

이들 진입점은 응용프로그램에 정보를 제공한다. 이러한 값들은 응용프로그램에서 데이터베이스, LDAP 서버, 처리 엔진, 다른 컴포넌트들에 접근하다. 이러한 값들이 보호되지 않으면 응용프로그램에서 잠재적인 취약점이 열릴 수 있다. 관련된 진입점은 다음과 같다.
  • HTTP 변수. 브라우저 또는 최종 클라이언트(end-client)에서 응용프로그램에 정보를 전송한다. 요청 정보들의 집함은 폼, 쿼리 스트링 데이터, 쿠키, 서버 변수(HTTP_REFERER 등)와 같은 다양한 진입점을 의미한다. ASPX 응용프로그램은 이들 데이터를 Request 객체로 사용한다. 코드 리뷰 연습 동안 이 객체의 사용을 살펴봐야 한다.
  • SOAP 메시지. 응용프로그램은 SOAP 메시지 기반 웹 서비스에 접근할 수 있다. SOAP 메시지는 웹 응용프로그램에서 잠재적인 진입점이 될 수 있다.
  • RSS 및 Atom 피드. 많은 신규 응용프로그램들은 서드 파티의 XML 기반 피드를 소비하고, 최종 사용자에게 다양한 포맷으로 출력을 표현한다. RSS와 Atom 피드는 XSS 또는 클라이언트측 스크립트 실행과 같은 새로운 취약점을 만들 수 있는 가능성을 갖고 있다.
  • 서버로부터의 XML 파일. 응용프로그램은 인터넷을 통해 파트너들로부터 XML 파일을 소비할 수도 있다.
  • 메일 시스템. 응용프로그램은 메일링 시스템으로부터 메일을 소비할 수도 있다.
위 목록은 사례 연구에서 사용하는 응용프로그램의 중요 진입점들이다. 다양한 파일들로부터 패턴을 추적하고 분석하고, 정규식을 사용해서 제출된 데이터에서 핵심 패턴을 찾아내는 것이 가능하다.

파이썬으로 코드 검사하기

scancode.py는 소스 코드 검색 유틸리티이다. 이 유틸리티는 리뷰 과정을 자동화해주는 간단한 파이썬 스크립트다. 파이썬 스캐너는 특정 목표를 수행하는 함수 3개로 구성되어 있다.
  • scanfile 함수는 특정 취약점과 관련된 정규식 패텬을 검색하기 위해 전체 파일을 검색한다.
    ".*.[Rr]equest.*[^\n]\n" # 객체 호출을 검색".*.select .*?[^\n]\n|.*.SqlCommand.*?[^\n]\n" # SQL 실행 지점을 검색".*.FileStream .*?[^\n]\n|.*.StreamReader.*?[^\n]\n" #파일 시스템 접근을 검색".*.HttpCookie.*?[^\n]\n|.*.session.*?[^\n]\n" # 쿠키 및 세션 정보를 검색"" # 응용프로그램의 의존성을 검색".*.[Rr]esponse.*[^\n]\n" # Response 객체 호출을 검색".*.write.*[^\n]\n" # 브라우저로 전달되는 정보를 검색".*catch.*[^\n]\n" # 예외 처리를 검색
  • scan4request 함수는 ASP.NET Request 객체를 사용하는 응용프로그램 진입점을 찾기 위해 파일을 검색한다. ".*.[Rr]equest.*[^\n]\n" 패턴을 실행한다.
  • scan4trace 함수는 파일에서 변수의 이동을 분석한다. scan4trace 함수에 변수 이름을 전달하고, 이 변수가 사용되는 라인들의 목록을 반환한다. 이 함수는 응용프로그램 수준의 취약점을 발견하는 데 핵심 역할을 수행한다.
이 프로그램을 사용하는 것은 쉽다. 이 프로그램은 앞에서 설명한 함수들의 동작을 결정하는 몇 가지 스위치를 사용한다.
D:\PYTHON\scancode>scancode.pyCannot parse the option string correctlyUsage:scancode -flag -sG : Global matchflag -sR : Entry pointsflag -t  : Variable tracing                  Variable is only needed for -t option
예제:
scancode.py -sG details.aspxscancode.py -sR details.aspxscancode.py -t details.aspx pro_idD:\PYTHON\scancode>
이 스캐너는 먼저 파이썬의 regex 모듈을 임포트한다.
import re
이 모듈을 임포트하면, 대상 파일에 대해 정규 표현식을 실행하는 것이 가능해진다.
p = re.compile(".*.[Rr]equest.*[^\n]\n")
이 라인은 정규 표현식을 정의한다. 여기서는 Reqest 객체 검색에 대한 정규식을 정의하고 있다. 이 정규식을 사용하고, match() 메서드는 파일에서 정규식 패턴과 일치하는 모든 인스턴스를 수집한다.
m = p.match(line)
진입점 검색

대상 코드에서 가능한 진입점을 찾기 위해 details.aspx 파일을 scancode.py로 검색해보자. -sR 스위치는 진입점을 찾기 위해 사용한다. detailas.aspx 페이지에 이를 실행하면 다음과 같은 결과를 생성한다.
D:\PYTHON\scancode>scancode.py -sR details.aspxRequest Object Entry:22 :    NameValueCollection nvc=Request.QueryString;
이 부분은 응용프로그램의 진입점이며, QueryString 정보는 NameValue 컬렉션 집합에 저장되는 위치이다.

다음 코드는 위 정보를 잡아두는 함수를 구현한 것이다.
def scan4request(file):        infile = open(file,"r")        s = infile.readlines()        linenum = 0        print 'Request Object Entry:'        for line in s:                          linenum += 1        p = re.compile(".*.[Rr]equest.*[^\n]\n")        m = p.match(line)        if m:                                                   print linenum,":",m.group()
이 코드에서는 파일을 열어두고, 지정된 정규식 패턴을 사용해서 Request 객체를 잡아낸다. 이와 같은 접근 방법을 사용해서 다른 진입점들도 찾아낼 수 있다. 예를 들면, 다음 코드와 같은 방법을 사용해서 쿠키나 세션과 관련된 진입점을 식별해낼 수 있다.
# Look for cookie and session management        p = re.compile(".*.HttpCookie.*?[^\n]\n|.*.session.*?[^\n]\n")        m = p.match(line)        if m:                print 'Session Object Entry:'                print linenum,":",m.group()
응용프로그램에서 진입점들의 위치를 찾아낸 후에는 취약점을 찾아보기 위해 이들을 추적하고 검색할 수 있다.

위협 매핑 및 취약점 발견

진입점을 발견하는 것은 위협 매핑(threat mapping)과 취약점 발견을 위해 범위를 좁힐 수 있게 해준다. 진입점은 추적에 있어 필수요소다. 응용프로그램에서 해당 변수가 실행 흐름을 따라 어디로 이동하는지, 미치는 영향이 어디인지 밝혀내는 것이 중요하다.

이전 검색은 응용프로그램에서 Request 객체 항목을 찾아냈다.
22 :    NameValueCollection nvc=Request.QueryString;
스크립트를 -t 옵션과 함께 실행하면 변수를 추적하는 데 도움이 된다.(전체를 추적하려면 더 이상 추적가능한 것이 없을 때 까지 추적해야 한다)
D:\PYTHON\scancode>scancode.py -t details.aspx nvcTracing variable:nvc   NameValueCollection nvc=Request.QueryString;   String[] arr1=nvc.AllKeys;        String[] sta2=nvc.GetValues(arr1[0]);
이 코드는 값을 nvc에서 sta2로 할당하는 것을 알 수 있으며, sta2도 추적해야 한다.
D:\PYTHON\scancode>scancode.py -t details.aspx sta2Tracing variable:sta2        String[] sta2=nvc.GetValues(arr1[0]);        pro_id=sta2[0];
여기서 pro_id 변수에 값이 할당되는 것을 발견했으므로 이에 대한 추적을 반복한다.
D:\PYTHON\scancode>scancode.py -t details.aspx pro_idTracing variable:pro_id   String pro_id="";        pro_id=sta2[0];   String qry="select * from items where product_id=" + pro_id;   response.write(pro_id);
마침내 변수를 끝까지 추적해냈다. 이 예제에서는 페이지 하나에 대해서 추적을 여러번 반복했지만, 응용프로그램간에 다양한 페이지를 이동하는 것을 추적하는 것도 가능하다. 그림3은 완전한 출력 결과를 나타낸 것이다.

사용자 삽입 이미지
그림3. 추적을 통한 취약점 발견

소스 코드와 그림에서 표시한 것처럼 소스에서 입력 검증이 없다. 즉, SQL 삽입 취약점이 존재한다.
String qry="select * from items where product_id=" + pro_id;
이 응용프로그램은 pro_id를 받아서 SELECT 문에 그대로 전달한다. SELECT 문을 조작하고, SQL 데이터에 삽입하는 것이 가능하다.

마찬가지로, 다른 라인은 크로스사이트 스크립팅(XSS) 취약점에 노출되어 있다.
response.write(pro_id);
검증되지 않은 pro_id 값을 브라우저에 전달하는 것은 공격자가 희생자(victim)의 브라우저에서 실행될 수 있는 자바스크립트를 삽입하는 것을 가능하게 한다.

스크립트의 -sG 옵션은 전체(Global) 검색 루틴을 실행한다. 이 루틴은 파일 객체, 쿠키, 예외 등을 검색한다. 각각은 잠재적인 취약점을 가지며, 이러한 검색은 취약점을 식별하고, 예상되는 위협에 매핑할 수 있다.
D:\shreeraj_docs\perlCR>scancode.py -sG details.aspxDependencies:13 : Request Object Entry:22 :    NameValueCollection nvc=Request.QueryString;SQL Object Entry:49 :    String qry="select * from items where product_id=" + pro_id;SQL Object Entry:50 :    SqlCommand mycmd=new SqlCommand(qry,conn);Response Object Entry:116 :    response.write(pro_id);XSS Check:116 :    response.write(pro_id);Exception handling:122 :    catch(Exception ex)
이러한 코드 리뷰 접근 방법은 최소한의 노력으로 진입점, 취약점, 변수 추적을 할 수 있게 해준다.

완화작업 및 대체수단

취약점을 식별한 후에는 다음 단계로 위협(threat)을 완하(mitigate)시키는 것이다. 시스템의 배치에 따라 위협을 완하하는 다양한 방법이 있다. 예를 들어, 웹 응용프로그램 방화벽에 ', "과 같은 특정 문자 집합을 무시하는 규칙을 추가하는 것으로 SQL 삽입공격을 완화시키는 것도 가능하다. 이러한 문제를 완하시키는 가장 좋은 방법은 안전한 코딩 지침을 적용하는 것이다. 코드 레벨에서 변수를 사용하기 전에 적절한 입력값 검증을 하는 방법을 사용하는 것이다. SQL 레벨에서는 SQL SELECT 문 삽입공격을 피하기 위해 prepared statement나 저장 프로시저를 사용하는 것이 중요하다. XSS 취약점의 위협을 완화하기 위해서는 최종 클라이언트에 제공되는 컨텐트의 <과 >을 필터링하는 것이 필요하다. 이러한 조치들은 전체 웹 응용프로그램에 대한 위협을 경감시킨다.

결론

코드 리뷰는 취약점을 발견하고, 실제 소스를 이해하는 데 있어 매우 강력한 도구이다. 이를 "화이트박스(whitebox)" 접근방법이라 한다. 의존성 결정, 진입점 식별, 위협 매핑은 취약점을 발견하는데 도움이 된다. 이러한 모든 단계는 아키텍처 및 코드 리뷰가 필요하다. 코드는 본질적으로 복잡하며, 어떠한 도구도 필요한 것들을 모두 만족시킬 수 없다. 전문가로서 코드 리뷰를 수행할 때 필요한 도구들을 재빨리 작성할 필요가 있으며, 코드 베이스가 매우 클 때 이러한 도구들을 실행할 수 있어야 한다. 코드 라인을 일일이 살펴보는 것은 가능한 일이 아니다.

기사의 앞에서 논의한 것처럼, 접근 방법 중의 한 가지는 진입점에서 시작하는 것이다. 규모가 큰 소스 코드에서 다양한 패턴들을 뽑아내고 이들을 연결하기 위해 어떤 언어로 쓴 복잡한 스크립트 또는 프로그램을 작성할 수 있다. 변수 또는 함수를 추적하는 것은 전체를 탐색을 밝혀내고, 취약점을 발견하는데 큰 도움이 되게 하는 것이 핵심이다.

Exhibit 1 -scancode.py

신고
Posted by The.민군

MySQL 관리자를 위한 오픈 툴

제공:한빛 네트워크
저자:Baron Schwartz, 허정수 역
원문:Open Tools for MySQL Administrators

MySQL은 MySQL 서버를 모니터링하고 장애해결을 위한 몇몇 툴을 제공하고 있다. 하지만, 그 툴들은 MySQL 개발자나 관리자에게 항상 적합한 것은 아니며, 일부 상황(원격 혹은 웹을 통한 모니터링 등)에서 제대로 작동하지 않는다. 다행히 MySQL 커뮤니티에서는 다양한 공짜 툴들을 제공함으로서 이러한 갭을 매꿀 수 있다. 그렇지만, 이러한 공짜 툴은 웹 검색을 통해서 찾기 어렵다. 사실은 웹 검색은 바로 사용할 수 있는 프로젝트보다는 이미 종료되었거나 혹은 특수 목적의 프로젝트까지 찾아내기 때문에 짜증나는 일이 될 수 있다.

하지만 걱정 하지 않아도 된다. 필자가 이런 일을 이미 끝냈기 때문에 당신은 할 필요가 없다. 필자는 유용하다고 생각되는 툴을 안내할 것이다. 이 기사의 끝에서는 도움이 되지 않는 툴도 적어 두었으므로 참고하기 바란다.

본 기사는 MySQL 서버의 상태를 모니터링하는 툴에 국한된 기사이므로, 쿼리를 작성하거나 테이블을 작성하는 등에 관련된 툴에 관해서는 논의하지 않을 것이다. 또한 자유로운 오픈 소스 프로그램에만 집중할 것이다.

쿼리와 트랜잭션 모니터링을 위한 툴

쿼리를 모니터링하는 전통적인 툴은Jeremy Zawodny의 mytop이다.  mytop은 펄로 작성되어 있으며 터미널 상에서 실행되고, 연결에 관련된 모든 정보를 표 형식으로 보여준다(마치 프로세스 정보를 보여주는 유닉스의 top 명령과 같다.) 출력되는 정보로는 컨넥션 ID, 연결 상태, 현재 실행 중인 쿼리가 있다. mytop을 보면서 당신은 쿼리를 EXPLAIN 할 수도 있으며 쿼리를 종료시킬 수도 있고 이외 몇 가지 기능을 더 수행할 수 있다. mytop 결과의 상단에서는 MySQL 서버의 정보 즉, 버전, uptime, 초당 쿼리 수 등의 정보를 볼 수 있다. 이외에도 몇 가지 정보가 있지만 별로 쓰여지지는 않았다.

젠투나 페도라 코어같은 GNU/LINUX 배포판에서 mytop을 볼 수도 있고, 또한 Jeremy의 웹 사이트로부터 설치할 수도 있다. mytop은 매우 크기가 작으며 의존성도 적다. 반면에 mytop이 한동안 유지보수 되지 못한 프로그램이라 MySQL 5.x에서는 정상적으로 작동하지 않는다는 단점이 있다.

mytop과 비슷한 툴로서mtop이라는 툴이 있다. mytop과 마찬가지로 터미널 상에서 표 형식으로 정보를 표현하고 mytop의 기능 대비 약간의 기능이 빠졌지만 다른 기능이 또한 포함되어 있으므로 두 툴은 거의 비슷하다. mtop 또한 펄로 작성되었으며 몇몇 OS에서 패키지 형태로 제공되기도 하면서 소스포지에서 다운받아 설치할 수 도 있다. 하지만 mtop 또한 활발하게 유지보수 되지 않고 있으며 최신 MySQL에서는 정상적으로 작동하지 않는다.

일부 개발자들은 다른 스크립트에서 쉽게 사용할 수 있는 MySQL 프로세스 리스트를 출력하는 스크립트를 만들기도 하였다.  예로SHOW FULL PROCESSLIST스크립트가 있으며,MySQL Forge에서 다운로드가 가능하다.

필자가 개발한innotop은 MySQL과 InnoDB를 모니터링할 수 있는 프로그램이다. MySQL 의 인기가 올라가면서 InnoDB는 MySQL에서 트랜잭션을 지원하는 스토리지 엔진으로 많이 사용되고 있다.  InnoDB는 MySQL의 스토리지 엔진과 차별된 기능을 많이 가지고 있다. 따라서, InnoDB를 모니터링하기 위해서는 다른 접근 방법이 필요하다. innotop 툴은 SHOW INNODB STATUS 명령의 결과로 나온 많은 량의 정보를 보여준다. 원래 SHOW INNODB STATUS의 결과는 잘 정리 되어있지 않고, 텍스트 양이 많기 때문에 실시간 모니터링에 사용하기에는 별로 좋지 않다. 따라서 나는 편의를 위해 형식화된 출력을 할 수 있도록 innotop을 개발하였다. 현재 innotop은 나의 회사에서 메인 모니터링 툴이 되었다.

innotop은 앞에서 언급한 툴들보다 가장 기능이 좋은 툴이며, 기존 툴들을 완벽하게 대치할 수 있다. innotop은 프로세스 목록과 상태 정보를 출력할 수 있으며 쿼리를 분석하고 종료할 수 있는 기능을 제공한다. 다른 툴에서 제공하지 않는 많은 기능을 제공하고 있는데 현재 트랜잭션 목록을 출력할 수 있으며, LOCK에서 대기 중인 트랜잭션, 데드락 정보, Foreign key 에러, I/O와 로그 통계, InnoDB row operation과 세마포어 통계, InnoDB 퍼버 풀 정보, 메모리 사용, INSERT 버퍼, Adaptive hash index에 관한 정보를 출력할 수 있다. mytop 및 그와 비슷한 프로그램들이 보여주는 MySQL의 표준 정보들도 물론 출력할 수 있다. 구성이 쉽고 상호 작용이 가능한 도움말도 제공한다.

innotop은 설치도 쉽다. 실행이 바로 가능한 펄 스크립트이지만, 아직 패키지화 되어 있지 않기 때문에 필자의 웹 사이트에서 다운로드하여 설치해야만 한다.

웹을 기반으로한 툴로는 mytop의 클론 버전인phpMyTopajaxMyTop이 있다. 웹을 기반으로한 툴은 쉘에 직접 접근할 수 없거나 원격에서 MySQL 서버에 접근할 수 없을 경우에 웹 서버를 통하여 MySQL에 접속할 수 있다. ajaxMyTop이 좀 더 최신의 툴이며, 활발하게 개발되고 있다. 또한 Ajax의 도움으로 전통적인 GUI 프로그램과도 유사하다.(Ajax를 사용하였기 때문에, 페이지 리플레시가 없다.)

웹 기반의 유명한 툴로 phpMyAdmin이 있다.phpMyAdmin은 마치 맥가이버 칼 처럼 테이블을 디자인하고, 쿼리를 실행하고, DB 사용자 관리를 하는 등 많은 기능들을 제공한다. 하지만, 프로세스 목록을 보는 등의 기능은 약간 부족한다.

만약 서드파티 툴(third-party tool)을 사용할 수 없는 환경이거나 혹은 서드파티 툴을 사용하고 싶지 않은 경우 MySQL 자체의 유틸리티인 mysqladmin을 통하여 MySQL을 모니터링할 수도 있다. 예를 들어 쿼리 캐시의 상태 변화를 보고 싶은 경우 다음의 명령을 내리면 된다.

$ mysqladmin extended -r -i 10 | grep Qcache

물론 innotop도 위의 기능을 수행할 수 있다. innotop의 V 모드를 보라. innotop을 실행할 수 없을 때 간편하게 실행시킬 수 있는 방법이다.

MySQL 서버를 모니터링하기 위한 툴

때로는 MySQL 서버에서 수행 중인 쿼리를 모니터링 하기 보다는 시스템의 성능적인 측면을 분석해야하는 경우도 있다. 이때는 일반적인 커맨드 라인 유틸리티를 이용하여 MySQL 서버가 사용하는 시스템 리소스를 모니터링 할 수도 있고,Giuseppe Maxis'MySQL 리소스 소비량 측정'스크립트를 사용할 수도 있다. 이 툴은 MySQL 서버의 프로세스 ID를 이용하여 프로세스에 관련된 것들을 재귀적으로 진찰하여 그 결과를 출력한다. 이에 대한 더 많은 정보는오라일리 데이터베이스 블로그에 있는 Giuseppe의 기사를 읽어보기 바란다.

MySQL 포지 사이트는 일별 Mysql 관리와 프로그래밍 업무를 위한 팁, 트릭 및 소스 코드를 얻을 수 있는 훌륭한 곳이다. 예를 들어, 리플리케이션 속도를 측정하거나 네트워크 인터페이스를 통해 전송되는 쿼리를 캡쳐하는 쿼리 프로파일러 등의 도움을 얻을 수 있다.

또 다른 훌륭한 자원으로는mysqlreport가 있다.  mysqlreport는 디자인이 잘 된 프로그램으로서 MySQL 상태 정보를 지식으로 변환할 수 있다. mysqlreport는 관련된 변수를 출력하고, MySQL 사용자를 위해 의미있는 정렬을 할 수도 있다. 서버의 문제를 미리 알 수 없을 경우, 문제를 해결할 때 이 툴을 사용하는게 좋고 생각한다. 예를 들어 누군가가 나에게 CPU 점유율이 100%인 MySQL 서버의 부하를 줄여달라고 이야기할 때,  내가 처음으로 하는 행동은 mysqlreport를 실행하는 일이다. 고객과 10분 이야기 하는 것보다 mysqlreport의 결과를 한번 보는 것으로 더 많은 정보를 얻을 수 있다. 부하를 줄이기 위해서 내가 어디를 봐야 하는지 즉시 알 수 있다. Key Read 비율이 높은 경우와 Index Scan 비율이 높은 경우 Key Buffer가 너무 작은 것과 인덱스가 너무 큰 것을 찾아 볼 수 있다. 직관적으로 SHOW STATUS 결과만을 보는 것은 시간이 많이 걸릴 수 있다.

mysqlreport 사이트에서는 설치 방법과 사용 방법이 모두 적혀 있다. 더불어 실제의 예를 들어 mysqlreport의 결과를 어떻게 해석해야 하는지에 대한 훌륭한 튜토리얼도 볼 수 있다. 그 중 일부는 MySQL 내부를 심도있게 다루며, MySQL 개발자들에게 추천할 만 하다.

그 다음으로 보편적인 업무는 MySQL 서버 모니터링을 자동화하여 MySQL이 살아있는지 자동으로 알도록 하는 일이다. 이러한 프로그램은 직접 개발할 수도 있으며, 바로 사용할 수 있는 툴을 구할 수도 있다.MySQL 온라인 투표에 따르면,Nagios가 이러한 일을 위한 가장 인기 있는 툴이었다. 또한mon을 위한Watchdog MySQL 모니터 플러그인도 있다. 나의 회사에서는 자체 개발한 툴을 사용 중이지만, 조만간 Nagios를 사용할 예정이다.

유용하지 않다고 판단된 툴들

Quicomm MySQL 모니터 툴은 phpMyAdmin과 마찬가지로 웹 기반의 툴인데, phpMyAdmin에 비해서 적은 기능을 제공한다.

또다른 웹 기반 툴로서 "MySQL 시스템 옵티마이저"라고 공언하는MySysop이 있지만, MySQL 시스템을 최적화하는데 별다른 정보를 주지 않는다. MySysop의 결과는 큰 신뢰를 할 수 없어서 다른 방법으로도 같은 결과가 나오는지 조사를 해야 했다. MySysop을 설치하고 실행할 수 있을 때까지, 필자는 mysqlreport를 오래 동안 사용하고 있을 것이다.

마지막으로 필자는구글 mMaim(MySQL Monitoring And Investigation Module)을 사용하는 방벙에 대해 이해할 수가 없었다. 구글 mMaim은 구글의 오픈 소스 코드 공헌의 일부분으로 구글 내부적으로는 mMaim을 사용하는 것으로 보인다. 그러나 메일링 리스트에서 보듯이 mMaim의 사용법을 알 길이 없다. 또한 메일링 리스트에서는 구글이 단순히 mMaim을 발표하기 위해서 그 코드를 공개했다는 것을 알 수 있다. 이러한 행위를 분석하는 동안 나는 mMaim 코드를 어떻게 사용해야 하는지 전혀 알 수 없었다.

결론

당신의 업무에서 사용할 목적으로 툴을 찾는다면, 필자는 innotop과 mysqlreport 그리고 커맨드 라인 명령들을 추천하고자 한다. 필자는 mytop을 사용하곤 했지만, innotop이 트랜잭션에 관련된 정보를 포함한, 더 많은 정보를 제공하기 때문에 요즘은 innotop을 사용 중이다. MySQL 서버에 무엇이 잘못되어 있는지 분석하고자 할 때, MySQL 서버 상태에 관련된 mysqlreport의 스냅샷 결과를 일치시키는 것은 불가능했다. MySQL 리소스 사용과 성능에 관한 것을 알고 싶을 때는 일반 커맨드 라인 유틸리티를 이용한 (Giuseppe Maxia의 것과 같은) 스크립트를 증대시켰다.

본 기사에 언급되지 않은 다른 툴들도 있지만 본 글에 소개된 툴들은 모두 공짜이고 오픈 소스이며 당신이 찾을 수 있는 다른 툴들이 가질 수 있는 거의 모든 기능을 가지고 있다.

관련 링크

1.innotop- MySQL과 InnoDB 모니터링에 관련된 강력한 툴
2.mysqlreport- 쉽게 바로 사용할 수 있는 MySQL 상태 레포트를 생성하는 툴
3.MySQL Forge- MySQL 사용자들끼리 코드를 공유하는 커뮤니티
4.mytop- MySQL 쿼리와 프로세스를 모니터링하는 전통적인 툴
신고
Posted by The.민군

RSS와 AJAX: 간단한 뉴스 리더

제공:한빛 네트워크
저자:Paul Sobocinski, 이대엽 역
원문:RSS and AJAX: A Simple News Reader

Ajax RSS 파서

Ajax(Asynchronous JavaScript And XML)와 RSS(Really Simple Syndication)는 웹을 매료시키는 두 가지 기술이다. 일반적으로 RSS는 사람 혹은 조직에 뉴스를 제공하기 위하여 사용된다. 이것은 웹 사이트로부터 "RSS 피드(feed)"를 공급받음으로써 이루어진다. RSS 피드는 단순히 특정한 방식으로 구조화되어 있는 XML 파일에 대한 링크일 뿐이다. RSS 스펙에는 XML 파일로 기대되는 구조로 나타나 있다. 예를 들어, 스펙에는 제목, 저자, 그리고 설명 태그를 필요로 하는데, 따라서 모든 RSS XML 파일은 최소한 이 3가지 태그를 가지게 된다.

여기에서 사용하게 될 RSS 스펙의 버전은 2.0인데, 가장 최신이며 3가지 RSS 스펙(0.98, 1.0, 2.0) 중 가장 널리 사용되고 있는 것이다. 다행히도, RSS 2.0은 RSS 1.0에 비해 훨씬 덜 복잡하므로 여러분은 아래에 링크되어 있는 RSS 2.0 스펙에 금방 익숙해질 것이다:blogs.law.harvard.edu/tech/rss. 만약 여러분이 RSS에 대한 포괄적인 소개를 원한다면 3가지 RSS 스펙을 모두 다루고 있는 아래 링크를 방문해 보라:www.xml.com/pub/a/2002/12/18/dive-into-xml.html

왜 RSS를 파싱하기 위해 Ajax를 사용할까? Ajax를 사용함으로써 웹 브라우저로 전달되는 RSS XML 파일을 처리하는 작업을 생략할 수 있는데, 따라서 서버의 부하를 줄일 수 있다. 또한 Ajax는 사용자로 하여금 좀 더 끊김 없는 웹 경험을 가질 수 있게끔 해주는데, 이는 서버로부터 전달받는 전체 RSS XML 파일을 페이지를 갱신하지 않고도 가져 올 수 있기 때문이다. 마지막으로 Ajax는 XML 파일을 처리할 수 있도록 설계되어 있으므로 RSS를 간단하고 우아한 방법으로 파싱할 수 있다.

이 기사 때문에 여러분이 Ajax에 익숙해질 필요는 없다. 하지만 기본적인 자바스크립트에 대한 이해는 강력히 권장한다.

아래에 파서가 어떻게 작동할 것인지에 대해 나와있다. 먼저 RSS 피드 파일명이 HTML form에서 선택된다. 사용자가 전송 버튼을 클릭하면 getRSS() 함수가 호출된다. 이 함수는 지정된 RSS XML 파일을 서버로부터 읽어오는 역할을 한다. 한번 서버로부터 읽어오는 것이 성공하면, processRSS() 함수가 전달받은 XML 파일을 자바스크립트 객체로 변환한다. 마지막으로 showRSS(RSS) 함수가 호출되는데, 이 함수는 RSS 자바스크립트 객체에 저장되어 있는 몇 가지 정보를 HTML 페이지를 갱신함으로써 보여준다. 아래의 도식은 이러한 일련의 단계들을 요약한 것이다.

사용자 삽입 이미지
그림 1. 전체적인 설계

HTML 파일

먼저 HTML 파일을 살펴볼 것이다. 위쪽의 폼 엘리먼트에 작성된 코드는 어느 RSS 피드를 읽어올 것인지를 결정하고, 아래쪽의 루트 div 엘리먼트에 작성된 코드는 RSS 자바스크립트 객체에 저장되어 있는 정보를 보여주기 위해 사용된다.
<html><head>    <!--B-->    <script language="javascript" src="rssajax6.js"></script>    <!--C-->    <style type="text/css">        #chan_items { margin: 20px; }        #chan_items #item { margin-bottom: 10px; }        #chan_items #item #item_title { font-weight: bold; }    </style></head><body>    <!--A-->    <form name="rssform" onsubmit="getRSS(); return false;">        <select name="rssurl">            <option value="test-rss.xml">test RSS feed</option>            <option value="google-rss.xml">google RSS feed</option>        </select>        <input type="submit" value="fetch rss feed" />    </form>    <div id="chan">        <div id="chan_title"></div>        <div id="chan_link"></div>        <div id="chan_description"></div>        <a id="chan_image_link" href=""></a>        <div id="chan_items"></div>        <div id="chan_pubDate"></div>        <div id="chan_copyright"></div>    </div></body></html>
이제 나머지 대부분의 HTML 코드는 무시하기로 하고 <!-A-->로 표시되어 있는 폼 엘리먼트에 집중하기로 하자. RSS XML 파일의 이름은 select 엘리먼트의 option 태그의 value 속성으로 지정되어 있다. 사용자가 이러한 파일 가운데 하나를 선택하고 나면 폼이 전송된다. 전체 과정을 시작시키는 자바스크립트가 onsubmit 태그에 있는 것을 볼 수 있다. 자바스크립트 함수가 호출된 다음 폼 전체가 서버로 "틀에 박힌" 방식대로 전달되는 것을 막기 위해 false를 리턴하도록 return false 문장을 추가하였다. 만약 return false 문장을 생략했다면 전체 페이지가 갱신되면서 Ajax를 이용하여 읽어왔던 모든 데이터를 잃어버리게 될 것이다. 마지막으로 남은 하나는 <!-B-->로 표시된 부분에서 분리되어 있는 파일을 참조하도록 자바스크립트 코드가 헤더 부분에 포함되어 있다는 것이다. 여러분이 의아해 할 경우를 대비해서 설명하는 것이지만 <!-C-->로 표시된 부분에서 <style> 태그의 내용은 브라우저에게 showRSS(RSS) 함수에 의해 RSS 데이터가 HTML 페이지에 쓰여졌을 때 어떻게 보여주어야 하는지를 나타내는 것이다.

서버로부터 XML 읽어오기 : getRSS() 함수

아래에 getRSS() 함수가 나타나 있다:
function getRSS(){    /*A*/    if (window.ActiveXObject) //IE        xhr = new ActiveXObject("Microsoft.XMLHTTP");    else if (window.XMLHttpRequest) //other        xhr = new XMLHttpRequest();    else        alert("your browser does not support AJAX");    /*B*/    xhr.open("GET",document.rssform.rssurl.value,true);    /*C*/    xhr.setRequestHeader("Cache-Control", "no-cache");    xhr.setRequestHeader("Pragma", "no-cache");    /*D*/    xhr.onreadystatechange = function() {        if (xhr.readyState == 4)        {            if (xhr.status == 200)            {                /*F*/                if (xhr.responseText != null)                    processRSS(xhr.responseXML);                else                {                    alert("Failed to receive RSS file from the server - file not found.");                    return false;                }            }            else                alert("Error code " + xhr.status + " received: " + xhr.statusText);        }    }    /*E*/    xhr.send(null);}
이 함수를 단계별로 훑어보면서 그것이 무엇을 하는지 생각해 보자. 코드내에 포함되어 있는 레이블은 아래의 대응되는 설명을 가리킨다.

A: 서버와 통신하기 위하여 먼저 XMLHttpRequest(XHR) 객체를 정의해야 한다. 이 객체는 브라우저를 갱신하지 않고도 서버에 접속하도록 해주는 것이며 이는 모든 Ajax 애플리케이션의 핵심이다. 기대하는 바과 같이 인터넷 익스플로러는 XHR 객체를 파이어폭스나 사파리와 같은 다른 브라우저와는 다르게 정의한다. 여기에서는 어느 브라우저에서 자바스크립트가 실행되고 있는지 결정하기 위해 객체 감지(object detection) 기법을 사용하였으므로 따라서 XHR 객체를 적절하게 정의하게 된다.

B: 이 코드는 xhr.open() 함수를 호출함으로써 XHR 객체를 초기화 하는 것이다. 이 함수는 3개의 인자를 가지는데, 첫번째는 서버로부터 파일을 읽어오는 데 사용할 메소드, 두번째는 읽어올 파일의 이름, 그리고 세번째는 응답을 비동기적으로 받고 싶다면 true로 설정한다. 여기에서는 서버로 아무런 데이터도 보내지 않을 것이므로 GET 요청으로도 충분하다. RSS XML 파일의 이름은 HTML 폼으로부터 직접 가져오게 된다. 마지막으로 비동기 응답으로 지정함으로써 파일 수신에 대해 "대기"할 필요가 없어진다. 사실 수신이 완료되었을 때 호출되는 함수를 정의함으로써 언제 사용가능한지를 알 수도 있다(이것은 차후에 좀 더 설명할 것이다).

C: 서버로부터 갱신된(캐싱되지 않은) 요청을 하는 것이 중요한데, 여기에서 그렇게 되도록 보장하기 위해 요청 헤더를 설정하였다(Pragma는 하위 호환성을 위해 포함시켰다).

D: 서버와의 연결을 비동기적으로 수행하게끔 지정하였기 때문에 XHR 객체는 서버의 응답이 사용가능하게 될 때 어떤 함수가 호출될지 알아야 할 필요가 있다. 이것은 XHR 객체의 onreadystatechange 속성에 사용하고자 하는 것인데, XHR 객체의 readyState 속성이 변경되었을 때 실행시키고자 하는 함수와 동일한 것으로 설정한다. 목적을 달성하기 위해서 readyState의 값이 4인지만을 신경쓰면 되는데, 왜냐하면 이것이 응답이 사용가능함을 의미하기 때문이다. xhr.status의 값이 200이면 성공적인 응답을 받았음을 의미하며 다른 모든 상태 코드는 적절한 응답을 받지 않았음을 의미한다.

E: 이미 위에서 서버로부터 응답을 받기 위하여 XHR 객체를 초기화 하였다. 이제 여기에서 서버의 요청/응답 과정을 수행하기 위해 xhr.send() 함수를 호출한다. 전송할 데이터가 아무것도 없으므로 인자로서 null을 전달한다.

F: 여기까지 도달했다면 서버로부터의 응답을 성공적으로 수신했으며 XML 파일을 처리할 준비가 된 것이다. 마지막으로 단지 비어있는 응답을 받지는 않았는지 확인만 하면 된다. responseText 속성을 검사함으로써 확인 절차를 수행할 수 있으며(responseText는 텍스트 형태로 수신된 파일을 저장하고 있다) 이제 processRSS() 함수를 호출할 준비가 되었다.

XML 파싱하기: processRSS() 함수와 RSS2Channel 객체

아래에 processRSS() 함수가 나타나 있다.
function processRSS(rssxml){    RSS = new RSS2Channel(rssxml);    showRSS(RSS);}
이 함수는 단순히 RSS2Channel 객체의 생성자를 호출하고 rssxml을 전달한다. 이 인자는 특별한 의미를 지니고 있는데, 그것은 모든 RSS 정보를 포함한다는 것이다. 게다가 자바스크립트는 이것을 XML 객체로서 인식할 수 있으며, 따라서 자바스크립트에 내장된 DOM(문서 객체 모델; Document Object Model)의 함수와 속성을 그것에 사용할 수 있다. 이는 서버의 응답을 받기 위하여 XHR 객체의 속성인 responseXML을 사용하였으므로 이러한 작업이 가능하다. 만약 responseText를 사용하였다면 XML을 파싱하는 것은 훨씬 더 어려워질지도 모른다..

이제 RSS2Channel 객체를 검토할 것이다. 각각의 RSS XML 파일은 항상 정확히 하나의 채널(channel) 엘리먼트를 포함하고 있는데, 이 엘리먼트는 모든 RSS 데이터를 포함하고 있다. 여러분이 기대하고 있는 바와 같이, 이 데이터는 몇 개의 하위 혹은 자식 엘리먼트로 구성되어 있다. 따라서 채널은 RSS XML 파일의 루트 엘리먼트이며 이것은 RSS2Channel 객체에 의해 표현된다. 이 객체가 아래에 나타나 있다.
function RSS2Channel(rssxml){    /*A*/    /*required string properties*/    this.title;    this.link;    this.description;    /*optional string properties*/    this.language;    this.copyright;    this.managingEditor;    this.webMaster;    this.pubDate;    this.lastBuildDate;    this.generator;    this.docs;    this.ttl;    this.rating;    /*optional object properties*/    this.category;    this.image;    /*array of RSS2Item objects*/    this.items = new Array();    /*B*/    var chanElement = rssxml.getElementsByTagName("channel")[0];    var itemElements = rssxml.getElementsByTagName("item");    /*C*/    for (var i=0; i<itemElements.length; i++)    {        Item = new RSS2Item(itemElements[i]);        this.items.push(Item);    }    /*D*/    var properties = new Array("title", "link", "description",           "language", "copyright", "managingEditor", "webMaster",           "pubDate", "lastBuildDate", "generator", "docs", "ttl", "rating");    var tmpElement = null;    for (var i=0; i<properties.length; i++)    {        tmpElement = chanElement.getElementsByTagName(properties[i])[0];        if (tmpElement!= null)            eval("this."+properties[i]+"=tmpElement.childNodes[0].nodeValue");    }    /*E*/    this.category = new RSS2Category(chanElement.getElementsByTagName("category")[0]);    this.image = new RSS2Image(chanElement.getElementsByTagName("image")[0]);}
시작하기에 앞서 코드들을 좀 더 작은 조각으로 쪼갠 다음 각각의 것들을 하나씩 설명할 것이다.

A: 안내하자면 이것은 값을 할당하고자 하는 모든 속성의 목록을 나열한 것이다. 이러한 각각의 속성들은 RSS XML 엘리먼트에 대응된다. 예를 들어, this.language 속성을 <language>en-us</language> XML 태그안에 있는 문자열, 이 경우에는 en-us과 동일하게 설정할 것이다. 몇 가지 다른 속성들은 RSS2Channel과 같은 사용자 정의 객체가 될 것이다. 이것은 곧 좀더 자세하게 설명할 것이다.

B: 여기에서는 두 개의 변수를 생성하였는데 하나는 channel 엘리먼트의 내용을 저장하기 위한 것이고, 다른 하나는 item 엘리먼트의 배열을 저장하기 위한 것이다. 이를 위하여 getElementsByTagName() 함수를 사용하였는데, 이 함수는 XML 파일내의 지정된 태그명과 일치하는 모든 엘리먼트의 배열을 리턴한다. 앞서 언급했던 것과 같이 RSS XML 파일은 오직 한 개의 channel 태그를 포함하고 있기 때문에 한 개의 엘리먼트만을 가진 배열을 리턴할 것으로 예상할 수 있다. 따라서 객체를 전달받기 위해 함수 호출부의 끝에 [0]을 추가하여 그것을 chanElement에 할당하였다. 반면, itemElements는 배열이어야 할 필요가 있는데 왜냐하면 RSS XML 파일은 다수의 <item> 태그를 가질 것이기 때문이다.

C: 이 루프는 itemElements 배열을 순회하며 각각의 item 엘리먼트에 대해 파싱을 수행한다. RSS XML 파일내의 <item> 태그는 여러 개의 자식 태그를 포함하고 있으므로 의미있는 방법으로 이러한 데이터들을 저장할 RSS2Item 객체를 만들 필요가 있다. 코드에서는 현재 item 엘리먼트를 생성자에 전달하여 생성된 객체를 Item에 할당한다. 이 루프의 수행이 완료되면 RSS2Channel 객체의 items 속성들은 사용자 정의 RSS2Item 객체들의 배열을 포함할 것이다. RSS2Channel에 대한 작업이 완료되면 RSS2Item 객체에 대해 계속 논의할 것이다.

eval() 함수의 사용

계속 이어가기 전에 여러분이 eval() 함수에 익숙하지 않을 경우에 대비해서 eval() 함수에 대해 간략히 설명하고자 한다. 이 함수는 하나의 인자를 가지는데 이 인자는 여러분이 실행시키고자 하는 프로그램의 자바스크립트 코드를 포함하는 문자열이다. 예를 들어 eval('return true')는 return true와 동일하다. 앞으로 계속 보겠지만 이 함수는 수많은 속성을 가진 객체를 처리할 때 유용하다.

D: 이제 속성 값으로 단순 문자열을 가지는 객체의 모든 속성들을 설정해볼 것이다. 이러한 모든 속성들이 chanElement 객체에서 동일한 방식으로 가져오기 때문에 설정하고자 하는 모든 속성의 이름을 가진 배열을 하나 정의한 다음 for 루프를 이용하여 배열을 순회한다. 여기에서는 시험하고 있는 XML 태그의 실질적인 문자열 값을 가져오기 위해 childNodes와 nodeValue라는 두 개의 속성에 접근할 것이다. 첫 번째 속성인 childNodes는 객체 배열의 폼 안에 있는 모든 자식 XML 엘리먼트를 노출시켜 주며, 두 번째 속성은 XML 엘리먼트의 실질적인 문자열 값을 얻는다. 여기에서 가져오고 있는 속성의 경우 그것들이 아무런 자식 XML 태그를 가지고 있지 않기 때문에 childNodes에 의해서 오직 하나의 엘리먼트만이 리턴된다. 그리고 나서 nodeValue는 childNodes[0]안에 있는 엘리먼트의 값을 가져오게 된다.

E: 마지막으로 this.category와 this.image 속성을 지정한다. D에서 논의했던 속성과는 달리 여기에서는 자식 태그를 갖고 있으므로 이러한 XML 엘리먼트(RSS2Category와 RSS2Image 각각)를 처리하기 위해 사용자 정의 객체를 생성해야 한다. 이제 RSS2Category 함수를 살펴보기로 하자:
function RSS2Category(catElement){    if (catElement == null) {        this.domain = null;        this.value = null;    } else {        this.domain = catElement.getAttribute("domain");        this.value = catElement.childNodes[0].nodeValue;    }}
이 객체는 두 개의 속성(domain과 value)을 가진 단순한 객체이다. value 속성은 XML 태그의 실질적인 내용을 포함하는데 반해, domain 속성은 XML domain 태그 속성의 내용이 설정된다. 예를 들어, 일반적인 category XML 엘리먼트의 모습은 다음과 같다: <category domain="Syndic8">1785</category>. 이 경우 this.domain의 값은 Syndic8이 되며 this.value의 값은 1785가 된다. XML 태그로부터 domain 속성을 구하기 위해 getAttribute() 함수를 이용하여 파라미터로서(이 경우, domain) 읽어오기를 원하는 태그의 속성을 전달한다.

RSS XML 파일내의 image 태그는 오직 속성만을 포함하고 있으므로 RSS2Image 생성자는 getAttribute() 함수를 확장시켜 사용한다.
function RSS2Image(imgElement){    if (imgElement == null) {    this.url = null;    this.link = null;    this.width = null;    this.height = null;    this.description = null;    } else {        imgAttribs = new Array("url","title","link","width","height","description");        for (var i=0; i<imgAttribs.length; i++)            if (imgElement.getAttribute(imgAttribs[i]) != null)                eval("this."+imgAttribs[i]+"=imgElement.getAttribute("+imgAttribs[i]+")");    }}
이제 RSS2Channel 객체에 남아있는 마지막 속성인 items에 대해 알아볼 것인데, items는 RSS2Item 객체들의 배열을 포함하고 있다. 이 객체의 코드가 아래에 나타나 있다:
function RSS2Item(itemxml){    /*A*/    /*required properties (strings)*/    this.title;    this.link;    this.description;    /*optional properties (strings)*/    this.author;    this.comments;    this.pubDate;    /*optional properties (objects)*/    this.category;    this.enclosure;    this.guid;    this.source;    /*B*/    var properties = new Array("title", "link", "description", "author", "comments", "pubDate");    var tmpElement = null;    for (var i=0; i<properties.length; i++)    {        tmpElement = itemxml.getElementsByTagName(properties[i])[0];        if (tmpElement != null)            eval("this."+properties[i]+"=tmpElement.childNodes[0].nodeValue");    }    /*C*/    this.category = new RSS2Category(itemxml.getElementsByTagName("category")[0]);    this.enclosure = new RSS2Enclosure(itemxml.getElementsByTagName("enclosure")[0]);    this.guid = new RSS2Guid(itemxml.getElementsByTagName("guid")[0]);    this.source = new RSS2Source(itemxml.getElementsByTagName("source")[0]);}
RSS2Item 객체는 RSS2Channel과 여러모로 비슷하다. 먼저 얻어오게 될 속성을 나열하는 것으로부터 시작한다(A). 그런 다음 문자열 속성들을 순환하여 관련된 XML 태그의 내용을 각각 할당한다(B). 마지막으로 적절한 사용자 정의 객체의 생성자를 호출하여 객체의 속성을 설정하는데, 각 경우에 대응되는 데이터를 포함하고 있는 XML 엘리먼트를 전달한다.

RSS2Item 객체에서 찾아볼 수 있는 사용자 정의 객체들이 아래에 나열되어 있다. 그것들은 RSS2Category와 RSS2Image 객체와 비슷하며 앞에서 논의되었던 함수나 속성들을 아무것도 사용하지 않는다.
function RSS2Enclosure(encElement){    if (encElement == null) {        this.url = null;        this.length = null;        this.type = null;    } else {        this.url = encElement.getAttribute("url");        this.length = encElement.getAttribute("length");        this.type = encElement.getAttribute("type");    }}function RSS2Guid(guidElement){    if (guidElement == null) {        this.isPermaLink = null;        this.value = null;    } else {        this.isPermaLink = guidElement.getAttribute("isPermaLink");        this.value = guidElement.childNodes[0].nodeValue;    }}function RSS2Source(souElement){    if (souElement == null) {        this.url = null;        this.value = null;    } else {        this.url = souElement.getAttribute("url");        this.value = souElement.childNodes[0].nodeValue;    }}
이제 RSS 객체를 완전하게 정의하였으므로 마지막 과정으로 옮겨갈 수 있는데, 마지막 과정은 객체의 실제 내용을 보여주는 것이다.

RSS 데이터 보여주기: showRSS(RSS) 함수

showRSS(RSS) 함수의 자바스크립트 코드를 살펴보기 전에 앞에서 언급되었던 HTML 페이지의 루트 div 엘리먼트를 살펴보기로 하자.
<div class="rss" id="chan">        <div class="rss" id="chan_title"></div>        <div class="rss" id="chan_link"></div>        <div class="rss" id="chan_description"></div>        <a class="rss" id="chan_image_link" href=""></a>        <div class="rss" id="chan_items"></div>        <div class="rss" id="chan_pubDate"></div>        <div class="rss" id="chan_copyright"></div>    </div>
코드에서 볼 수 있듯이, 루트 div 엘리먼트는 여러 개의 자식 div 태그들을 가지고 있다. 이 태그들은 아래에 나타나 있는 showRSS(RSS) 함수에 의해 RSS 객체안의 데이터들을 보여줄 것이다.
function showRSS(RSS){    /*A*/    var imageTag = "<img id='chan_image'";    var startItemTag = "<div id='item'>";    var startTitle = "<div id='item_title'>";    var startLink = "<div id='item_link'>";    var startDescription = "<div id='item_description'>";    var endTag = "</div>";    /*B*/    var properties = new Array("title","link","description","pubDate","copyright");    for (var i=0; i<properties.length; i++)    {        eval("document.getElementById('chan_"+properties[i]+"').innerHTML = ''"); /*B1*/        curProp = eval("RSS."+properties[i]);        if (curProp != null)            eval("document.getElementById('chan_"+properties[i]+"').innerHTML = curProp"); /*B2*/    }    /*C*/    /*show the image*/    document.getElementById("chan_image_link").innerHTML = "";    if (RSS.image.src != null)    {        document.getElementById("chan_image_link").href = RSS.image.link; /*C1*/        document.getElementById("chan_image_link").innerHTML = imageTag            +" alt='"+RSS.image.description            +"' width='"+RSS.image.width            +"' height='"+RSS.image.height            +"' src='"+RSS.image.url            +"' "+"/>"; /*C2*/    }    /*D*/    document.getElementById("chan_items").innerHTML = "";    for (var i=0; i<RSS.items.length; i++)    {        item_html = startItemTag;        item_html += (RSS.items[i].title == null) ? "" :                  startTitle + RSS.items[i].title + endTag;        item_html += (RSS.items[i].link == null) ? "" :                  startLink + RSS.items[i].link + endTag;        item_html += (RSS.items[i].description == null) ? "" :                  startDescription + RSS.items[i].description + endTag;        item_html += endTag;        document.getElementById("chan_items").innerHTML += item_html; /*D1*/    }    return true;}
A: RSS 피드안의 channel 항목의 개수를 알 길이 없으므로 그러한 RSS 항목을 위해 동적으로 HTML을 생성해야 한다. 이것들은 RSS2Item 데이터를 포함하게 될 HTML 태그의 기본값들이다. 또한 호환성을 위해 img HTML태그를 동적으로 생성할 것이다.

B: 여기에서는 RSS2Category 객체안의 문자열 속성들을 순회하는데, 이것은 생성자 내에서 했던 것과 유사하다. 기존의 RSS 피드로부터 남아있을지도 모를 데이터를 제거하기 위하여 B1 라인에 있는 innerHTML 속성을 초기화한다. getElementById() 함수를 호출하여 HTML로부터 필요한 구체적인 div 엘리먼트들을 읽어올 수 있는데, 그러한 속성을 제공하는 것이 정의되어 있으므로 B2 라인에 있는 div 엘리먼트에 새로운 값을 설정한다.

C: 다시 한번 RSS 피드로부터 이미지를 포함하게 될 HTML 엘리먼트를 가져오기 위하여 getElementById() 함수를 사용한다. 이미지를 링크시켜야 하므로 div 엘리먼트 대신 앵커 엘리먼트(a)를 사용한다. 앵커 엘리먼트의 href 속성은 어떤 이미지가 링크되어야 하는지를 지정하므로 그 값을 RSS.image.link (C1)의 값으로 할당한다. 엘리먼트의 내용은 B 파트(C2)에서 했던 것과 같이 innerHTML 속성을 이용하여 채워진다.

D: 여기에서는 RSS 객체내의 항목들을 보여주는 부분이다. 제목, 링크, 설명을 포함하는 각각의 RSS 항목에 대한 div 태그가 정의되어 있다. 이해를 돕기 위하여 다른 속성들은 생략하였다. 각각의 div 태그는 innerHTML 속성 (D1)을 이용하여 chan_items의 부모 태그의 내용에 추가된다.

요약

Ajax RSS 파서는 윈도 XP상에서 인터넷 익스플로러 6.0과 파이어폭스 1.5.0.6로 테스트하였다. RSS2Channel 객체는 RSS 2.0 스펙의 모든 요소들을 지원하지는 않는다. 생략된 것들에는 cloud, textInput, skipHours, 그리고 skipDays가 있다. 대부분 이러한 RSS 엘리먼트들은 서버측에서만 유용할 뿐이므로 클라이언트측 파서에 포함되는 것은 이치에 맞지 않을지도 모른다.

코드를 한 줄씩 모두 알아본 뒤에 여러분은 동일한 기능들이 반절 정도의 코드만 가지고도 달성할 수 있을 것이라고 생각할지도 모른다. 특히, XML 엘리먼트로부터 직접 RSS 속성을 읽어내는 방식으로 showRSS(RSS) 함수를 작성함으로써 RSS 객체를 완전히 생략할 수 있었다. 확실이 이것은 가능하다. 하지만, showRSS() 함수는 RSS2Channel 객체가 이용될 수 있는 하나의 예제에 불과함을 의미하는 것 뿐이다. 의미있는 RSS 데이터를 포함하는 RSS 객체를 선언함으로써 훨씬 더 확장가능한 응용 프로그램을 작성할 수 있다. 예를 들어, 코드는 다수의 피드를 읽어낼 수 있도록 쉽게 확장될 수 있다. 이러한 피드들로부터 만들어진 RSS 객체는 조작될 수 있거나 혹은 다른 피드와 비교될 수 있다(여러분은 일정한 간격을 두고 새로운 피드를 읽어올 수 있으며, 그리고 그것을 이전의 것과 비교해볼 수 있다). RSS 객체를 분리한다는 것의 중요한 점은 이러한 증가하는 복잡성을 가진 응용 프로그램의 개발을 더 용이하게 해준다는 것이다.

지금까지 논의했던 모든 파일들은 아래에서 다운로드 가능하다.
신고
Posted by The.민군

PHP 데이터객체로 비즈니스 로직 단순화하기

제공:한빛 네트워크
저자:Darryl Patterson, 박종필 역
원문:Simplify Business Logic with PHP DataObjects

PHP5와 단순한 데이터 객체

요즘 웹 개발자들에게 있어서 데이터베이스로 작업하는 것은 흔한 일이다. 단순한 폼 처리부터 큰 규모의 웹 어플리케이션에 이르기 까지 거의 언제나 데이터베이스를 필요로 한다. 몇 개의 프로젝트 진행 후, 사실상 모든 프로젝트에 있어서 4개의 간단한 데이터베이스 작업이 여러 번 반복된다는 사실을 깨닫기 까지는 오래 걸리지 않는다. 4개의 작업은 다음과 같다:
  1. 레코드 선택(SELECT).
  2. 존재하는 레코드 갱신(UPDATE).
  3. 새로운 레코드 삽인(INSERT).
  4. 레코드 삭제(DELETE).
약간 수정된 쿼리들이 여러 곳에 필요하기 때문에, 코드 도처에 재입력하거나 복사하고 붙인 쿼리를 찾을 수 있다. 많은 사람들이 Pear::DB 또는 DBX 같은 데이터 추상 계층(data abstraction layer)을 사용한다. 데이터 추상 계층을 사용하는 것은 좋은 방법이다. 그러나 그 것의 주된 목표는 RDBMS를 투명하게 하고 데이터베이스 공급자를 아주 쉽게 교체하기 위한 것이다. 데이터베이스의 데이터를 입출력 하는 함수의 표준화된 방법을 제공하지 못하기 때문에, 데이터 추상 계층은 데이터베이스의 테이블 구조를 추상화하는 방법을 제공하지는 못한다. 이 문제를 해결해 줄 수 있는 것이 독립적인 데이터 입출력 계층(data access layer)이다.

데이터 입출력 계층은 여러 가지 방법으로 구현할 수 있다. 그러나 여기 흥미로운 데이터객체(DataObjects)라는 한 가지 방법이 있다. 데이터객체의 개념은Data Access ObjectTransfer Objects라고 불리는 충분히 입증된 디자인패턴들에 기초 한다. 이 방법들은 아주 복잡하다. 그러나 약간의 상상력만 발휘한다면, 이 패턴들의 기본적인 개념을 사용할 수 있고 PHP5의 고유의 데이터 계층을 만들기 위한 목적으로 아주 쉽게 작업할 수 있다. 자 그러면 함께 살펴 보기로 하자.

데이터객체란 무엇인가?

방금 전 언급한 대로 데이터객체의 개념은 디자인패턴으로부터 왔다. 만약 디자인패턴에 대한 관심을 조금이라고 가지고 있다면, 그 것들이 객체지향(object orientation)에 의존한다는 사실을 알 것이다. 이 말은 곧, PHP5의 새로운 객체 모델을 광범위하게 사용한다는 것이다. 또한 이 예제를 위해 MySQL을 사용할 것이다(비록 Pear:DB 같은 것이 더 사용하기는 쉬울지도 모른다).

기본적으로, 데이터객체는 데이터베이스의 테이블을 직접 표현하기 위한 코드 클래스 이다. -- 모든 테이블을 클래스로 만들 것이다. 클래스는 테이블의 필드와 정확히 일치하는 멤버 변수(member variables)들을 가질 것이다. 게다가, 위에서 언급한 적어도 4개의 기본적인 작업을 하기 위한 메소드(method) 또는 함수를 갖는다. 사용자 정보가 들어 있는 간단한 테이블이 있다고 가정한다:
TABLE: UseruserId  INTfirstName VARCHAR(30)lastName  VARCHAR(40)email   VARCHAR(100)
이제 테이블의 필드 이름들과 일치하는 멤버 변수를 갖는 클래스를 만들 것이다. 나는 비슷한 클래스 이름들과의 혼란을 피하기 위해 데이터객체 클래스 이름에 'DO_' 라는접두어를 사용한다; 이 방법은 PHP에서 네임스페이스를 구현하기 위해 일반적인 방법이다. 아래 코드를 보자:
class DO_User {  public $userId;  public $firstName;  public $lastName;  public $email;}
위 코드는 사용자 정보 테이블로부터 하나의 행을 표현하는 단순하고 작은 랩퍼(wrapper)이다. DO_User 인스턴스(instance)는 오직 한 번에 하나의 행만 가져 올 수 있다. 이 객체를 이용해서 데이터베이스로부터 어떻게 데이터를 가져 올까? 특정 사용자 정보를 데이터베이스로부터 가져오기 위해 get()라는 새로운 메소드를 추가하자. 필요한 사용자 정보를 가져오기 위해 userId(기본키: the primary key)라는 매개변수(parameter)를 제공할 것이다.
File contents of: class-DO_User.php<?phpclass DO_User {  public $userId;  public $firstName;  public $lastName;  public $email;  // This function will perform a select on the table looking for // a specific userId.  public function get($userId)  {    $sql = 'SELECT * FROM User WHERE userId='    . mysql_escape_string($userId);    $rs  = mysql_query($sql);    $row = mysql_fetch_array($rs);    $this->userId  = $row['userId'];    $this->firstName = $row['firstName'];    $this->lastName  = $row['lastName'];    $this->email   = $row['email']  }}?>
이 단순한 데이터 객체를 가지고, 이제 PHP 코드를 사용해서 데이터베이스와 작업할 수 있다. 데이터베이스를 사용하기 위해 SQL을 사용할 필요가 없다. 웹 브라우저에 사용자의 정보를 가져오고 보여주기 위한 데이터객체 스크립트가 아래에 있다:
<?phpinclude_once('class-DO_User.php');$user = new DO_User();// We'll use a literal integer here, // but this could come from anywhere,// such as $_POST or $_GET$user->get(5);?><html>  <head>    <title>User Info</title>  </head>  <body>    <p>Here is the user info:</p>    <table border="1">      <tr>        <td>User ID</td>        <td><?=$user->userId?></td>      </tr>      <tr>        <td>First Name</td>        <td><?=$user->firstName?></td>      </tr>      <tr>        <td>Last Name</td>        <td><?=$user->lastName?></td>      </tr>      <tr>        <td>Email</td>        <td><?=$user->email?></td>      </tr>    </table>  </body></html>
데이터객체를 사용함으로써 아주 단순하고 매우 깨끗한 코드를 만들 수 있다. 사용자 테이블에 단순한 쿼리를 실행하기 위한 get() 메소드는 검색을 위해 특정의 기본키(userId) 가 필요하다. 기본키를 가지고 테이블에 질의 할 때 오직 하나의 레코드만 반환 받을 수 있다는 점을 기억하자. 단순 데이터객체는 아주 잘 수행된다. 다중 행을 검색하는 방법은 잠시 후에 다룰 것이다.

데이터객체가 레코드를 검색할 때 레코드의 데이터가 데이터객체의 멤버 변수들로 복사되는 점에 주목하자. 이 것이 테이블의 열 이름(column names)들과 데이터객체 멤버 변수들의 이름을 정확히 일치시켜야 하는 이유이다.

데이터 입출력 대 데이터베이스 추상화

여기서 잠시 데이터 입출력 계층 대 데이터베이스 추상화 계층에 대해 살펴 보자. 이 두 가지는 서로 다른 것으로 설명 된다. 전체의 유연성을 위해 각각 사용하거나 두 가지를 동시에 사용할 수 있다.

데이터베이스 추상 계층은 백그라운드에서 실행되는 RDBMS에 감춰져 있다. 만약 재치 있고 주의 깊다면 SQL문을 작성할 수 있고, 하나의 데이터베이스 서버에서 다른 서버로의 변경을 함수 또는 SQL문의 수정 없이 쉽게 처리할 수 있다.Pear:DB는 이런 것을 아주 훌륭히 처리한다.

반면에 데이터 입출력 계층은 테이블 구조 아래에 숨겨져 있다. 테이블에 대해 데이터 입출력 계층으로 사용함으로써 어플리케이션의 비즈니스 계층에서 어떤 SQL도 사용하지 않고 테이블의 데이터를 다룰 수 있다. 데이터객체는 데이터 입출력 계층을 도입하기 위한 좋은 선택이다. 테이블과 직접 관계를 맺고 있어, 전체 어플리케이션에서 수정 없이 재사용 할 수 있다.

데이터객체를 사용하기 전에는, 데이터 계층은 기능에 기초해 캡슐화되고 그룹화된(encapsulated and grouped) 쿼리들로 된 클래스들의 구성으로 만들었었다; 예를 들어, 모든 쿼리들은 등록되고 관리되는 사용자 로그인과 관련 된 경우가 그렇다.. 이런 종류의 계층이 가지고 있는 문제점은 서로 다르지만 유사한 부분이 많은 쿼리가 존재한다는 점이다. 누군 어플리케이션을 사용했는지 로그 남기기 위해 사용자 레코드가 필요하다. 또한 "사용자 정보 수정 화면."을 출력하기 위해 똑 같은 쿼리를 두 개의 다른 클래스에 복사와 붙이기를 해야 한다. 이는 코드의 구조에 기인한다. 만약 사용자의 레코드가 필요한 경우 테이블 구조를 표현한 데이터객체를 사용한다면 어떤 코드에서도 고려할 필요가 없다.

데이터베이스 추상 계층과 데이터 입출력 추상 계층을 함께 사용할 수 있다. PHP 고유의 데이터베이스 함수 대신 Pear:DB를 이용한 데이터객체를 간단히 사용할 수 있다. RDBMS를 변경할 경우가 생길 경우를 대비해 대부분 Pear:DB를 사전에 사용한다. 5년 안에 RDBMS를 변경할 경우가 생길 때는 안전을 위해 Pear:DB를 사용할 것이다. 그 외의 경우에는 PHP 고유의 DB 함수들을 사용하는 것이 코딩과 실행 속도 면에서 훨씬 빠르다. Pear:DB는 다른 추상 계층의 추가로 인해 근소하기는 하지만 실행 속도가 지연되는 결과가 발생할 수 있다.

행 삽입하기

레코드를 검색하는 경우에 데이터객체를 사용하는 것은 좋은 방법이다. 그러나 다른 경우에도 사용할 수 있어야 한다. 새로운 레코드를 테이블에 추가하는 경우를 위해 데이터를 INSERT하는 새로운 메소드를 데이터객체에 추가하자. [파일 소스]
public function insert(){  $sql = "INSERT INTO        User      SET        firstName='" . mysql_escape_string($this->firstName) . "',        lastName='"  . mysql_escape_string($this->lastName)  . "',        email='"   . mysql_escape_string($this->email)   . "'";    mysql_query($sql);  $this->userId = mysql_insert_id();}
위 코드가 틀리 다고 생각하는가? 삽입하는 데이터 부분이 어디에서 오는지 이상해 할 지도 모르겠다. 데이터를 호출하기 전에는 데이터 객체에 존재한다. 때문에 insert()에 메소드에 데이터를 매개변수로 전달할 필요가 없다. 코드에서 마지막에 insert_id는 반환되고 데이터객체에 저장된다. 그래서 다른 테이블에 관련된 데이터를 삽입하기 위해서 객체가 현재 사용 가능한 상태이다. 아래 이 새로운 메소드의 사용 방법을 보라.
<?phpinclude_once('class-DO_User.php');$user      = new DO_User();$user->firstName = 'Jane';$user->lastName  = 'Doe';$user->email   = 'jane.doe@example.com';$user->insert();?><html>  <head>    <title>INSERT Example</title>  </head>  <body>    <p>The user was added to the User table.     The userId is: <?=$user->userId?></p>  </body></html>
위 예에서는 새로운 레코드를 User 테이블에 삽입하고 있다. 데이터객체를 생성하고, 필드에 변수를 할당한 후에 insert() 메소드를 호출한다. 아주 쉽다. 또한 레코드를 복사하기 위해 get()과 insert() 메소드를 함께 사용할 수 있다. 그 것은 전부가 아니며; 레코드를 복사하기 위해 약간의 수정을 해야 한다. 이메일 주소를 수정할 경우, 어떻게 하는지 아래를 보자:
<?phpinclude_once('class-DO_User.php');$user = new DO_User();// Again just using a literal int for now.$user->get(5);// Change the email address in the DataObject// NOTE: This doesn't affect the DB at all, // just the value in the DataObject$user->email = 'jdoe@example.com';// Next, we call insert() to create a NEW record in the User table$user->insert();?><html>  <head>    <title>Copy Row INSERT Example</title>  </head>  <body>    <p>The user was copied to the User table.     The userId is: <?=$user->userId?></p>  </body></html>
단지 이 것으로만 된다니, 흥분된다! 데이터객체를 사용하여 데이터베이스 관련 작업을 아주 쉽게 처리하기를 바란다.

행 갱신하기

몇 개의 값을 레코드에서 검색하고, 새로운 값을 삽입하는 것은 중요한 일이다. 보통 레코드를 변경하기 위하여 갱신 작업을 하게 된다. 이제 데이터객체에 update() 메소드를 추가해 보자: [파일 소스]
public function update(){  $sql = "UPDATE        User      SET        firstName='" . mysql_escape_string($this->firstName) . "',        lastName='"  . mysql_escape_string($this->lastName)  . "',        email='"   . mysql_escape_string($this->email)   . "'      WHERE        userId="   . mysql_escape_string($this->userId);    mysql_query($sql);}
위 메소드는 insert() 만큼 유용하다. 이 시점 전에, 다시 한 번 데이터객체에서 필요한 데이터를 구하는 것에서 작업을 시작해야 한다. 이 메소드는 단순히 존재하는 데이터를 수정하기 위해 사용된다. 사용법은 아래를 참고하자:
<?phpinclude_once('class-DO_User.php');$user = new DO_User();// Again just using a literal int to get a row.$user->get(5);// Change the email address$user->email = 'janedoe@example.com';// Perform the update$user->update();?><html>  <head>    <title>UPDATE Example</title>  </head>  <body>    <p>The user updated. The userId is: <?=$user->userId?></p>  </body></html>
처음 데이터객체를 보면, 값을 찾을 수가 없다. 데이터객체 코딩을 보면 몇 개의 반환 값이 있는 것처럼 보일 것이다. 그 값을 실제 보여주기 위하여 실제 사용하기 전까지는 의미가 없다. 예제의 마지막 보다 더 쉽게 데이터베이스에 이메일 주소를 수정할 있을까? 아주 간단한 코드처럼 보일 것이다. 그러나, 위 코드에서 보여주는 두 가지 방법은 매우 가독성이 높고 깨끗하다.

행 삭제하기

세 번째 다룰 내용은 데이터베이스의 기본적인 작업 중 4번째 이다. 데이터객체의 이점은 이 점에서 명백하다. delete() 메소드를 추가하여 삭제에 관해 검토해 보자: [파일 소스]
public function delete(){  $sql = "DELETE FROM        User      WHERE        userId=" . mysql_escape_string($this->userId);    mysql_query($sql);}
delete() 메소드의 사용 방법은 update()와 동일하다. 삭제($user->get(5))할 행을 찾고, delete($user->delete()) 메소드를 호출한다. 다시 한 번 말하지만, 삭제할 행의 기본키가 데이터객체에 존재하여야 한다.

결과 집합

이제 데이터객체 DO_User을 사용하여, 행을 찾고 삽입, 삭제 또는 갱신을 할 수 있다. 데이터객체는 테이블에서 단일 행을 나타내는 단순한 랩퍼라고 언급한 것을 기억하는가? 여러 행을 반환하는 쿼리를 실행하기 위해서 무엇을 해야 하는가? 지금까지 소개한 데이터객체로는 이 작업을 할 수 없다. 결과 집합을 얻기 위해서는 또 다른 랩퍼가 필요하다.

이 문제를 해결하기 위한 가장 단순한 접근 방법은 검색한 모든 행을 포함하는 데이터객체 배열을 만드는 것이다. 처음에는 이 방법이 논리적인 것 같아 보인다. 그러나 쿼리의 실행으로 반환되는 행이 10,000, 100,000, 또는 그 이상일 경우에는 어떻게 할 것인가? 그렇다면 매우 큰 객체 배열을 사용해야 할 것이다. 이로 인해 어플리케이션의 메모리 사용량이 늘어나고, 웹 사이트가 다운될 시기에 도달하게 된다.

고유(또는 Pear::DB)의 결과 집합을 위해 적당한 랩퍼가 필요하다. 이 랩퍼는 각 행의 배열을 반환하지 않을 것이다. 대신, DO_User 데이터객체의 인스턴스를 반환한다. 근사하지 않은가!

이를 위해 몇 개의 새로운 클래스를 만들어야 한다. 읽기만 가능(직접 결과 집합에 행을 수정하거나 삭제할 수 없다)하기 때문에 항상 ReadOnlyResultSet이라고 호칭한다. 그리고 J2EE 디자인패턴에서 유래한 이름이기도 하다(일관성에 박수를 보내자). 이 클래스를 만들기 전에, 현재 데이터객체에서 사용하는 데이터의 행의 묶음을 찾기 위해서 DO_User에 하나 이상의 메소드를 추가해야 한다. 행을 찾을 때, 새로운 함수는 새로운 ReadOnlyResultSet 클래스의 새로운 인스턴스를 반환하게 된다. 이 것은 소리(ReadOnlyResultSet를 읽는…) 보다 쉽다. Find() 메소드를 추가할 것이다. [파일 소스]
public function find(){  $sql = "SELECT * FROM User";    // This array will hold the where clause  $where = array();    // Using PHP 5's handy new reflection API  $class = new ReflectionClass('DO_User');  // Get all of DO_User's variable (or property) names  $properties = $class->getProperties();    // Loop through the properties  for ($i = 0; $i < count($properties); $i++) {    $name = $properties[$i]->getName();    if ($this->$name != '') {      // Add this to the where clause      $where[] = "`" . $name . "`='"       . mysql_escape_string($this->$name) . "'";    }  }    // If we have a where clause, build it  if (count($where) > 0){    $sql .= " WHERE " . implode(' AND ', $where);  }      $rs = mysql_query($sql);  include_once('class-ReadOnlyResultSet.php');  return new ReadOnlyResultSet($rs);}
이 것 중의 한 가지는 걸작이다. PHP5에는 클래스 및/또는 객체에 관한 특정한 혹은 모든 정보를 검색할 수 있는 내장된Reflection API가 존재한다. 여기서 데이터객체로부터 기인된 완전한 속성(property) 이름(멤버 변수 이름)을 반영해 사용한다. 때때로 이 새로운 API를 살펴 보라. 매우 유용하다.

find() 메소드를 호출하기 전에, 검색하고자 하는 속성들을 데이터에 입력해야 한다. 만약 첫 번째 이름으로 어떤 사람을 검색하고자 할 때 속성을 설정하고 find() 메소드를 호출해야 한다. find() 메소드는 쿼리와 ReadOnlyResultSet의 반환 값들로 구성된다. 이 메소드 사용법을 알아 보기 전에, ReadOnlyResultSet 클래스를 코딩 해 보자.

ReadOnlyResultSet 코딩

ReadOnlyResultSet 클래스는 데이터객체 인스턴스가 반환하는 결과 집합을 위한 랩퍼이다.나는 일반적인 ReadOnlyResultSet를 모든 프로젝트를 위해 사용한다. 그 것은 수정할 필요 없이 아주 잘 작동한다. 이 새로운 랩퍼는 getNext(), rowCount(), 그리고 reset()과 같은 세 개의 메소드로 구성된다. 클래스를 위한 코드를 살펴보자: [파일 소스]
File contents of: class-ReadOnlyResultSet.php<?php class ReadOnlyResultSet { // This member variable will hold the native result set  private $rs; // Assign the native result set to an instance variable  function __construct($rs)  {    $this->rs = $rs;  } // Receives an instance of the DataObject we're working on  function getNext($dataobject)  {    $row    = mysql_fetch_array($this->rs);    // Use reflection to fetch the DO's field names    $class    = new ReflectionObject($dataobject);    $properties = $class->getProperties();    // Loop through the properties to set them from the current row    for ($i = 0; $i < count($properties); $i++) {      $prop_name        = $properties[$i]->getName();      $dataobject->$prop_name = $row[$prop_name];    }        return $dataobject;  }  // Move the pointer back to the beginning of the result set  function reset()  {    mysql_data_seek($this->rs, 0);  }  // Return the number of rows in the result set  function rowCount()  {    return mysql_num_rows($this->rs);  }}?>
주석에서처럼 참조 변수 전달을 위해 &를 사용하곤 한다. PHP5에서는 Java와 같이 참조에 의해 객체를 전달한다. 그 점이 위 예에서 &를 사용하지 않은 이유이다.

결과 집합을 위한 랩퍼이기 때문에, 이 클래스를 위한 생성자는 결과 집합을 전달하는 것이 필요하다. 이 예제처럼, 그 것은 MySQL 고유의 결과 집합이다, 미리 정해졌다는 이유 때문이다. reset() 메소드는 고유 결과 집합의 포인터가 처음에 0으로 되돌리게 움직인다. rowCount() 메소드는 고유 결과 집합의 행의 수를 반환한다. getNext() 메소드는 좀더 호기심을 돋운다. 좀더 자세히 분석해 보자.

첫 번째, 이 클래스는 모든 데이터객체와 작업할 필요가 있기 때문에, 이 메소드가 데이터객체와 통신할 방법이 있어야 한다. 그 이유 때문에 데이터의 다음 행을 채우기 위하여 데이터객체를 전달해야 한다. 보통 이 것은 빈 데이터객체일 것이다. 메소드에서 첫 번째 줄은 mysql_fetch_array()를 이용해 고유 결과 집합으로부터 다음 행을 가져 온다. 다음, Reflection API를 사용해 전달된 모든 객체의 속성을 반복시킬 필요가 있다. ReflectionClass 대신 ReflectionObject 클래스를 사용한다는 점에 주의하자. 이 것은 클래스가 아닌 객체에서 역공학을 시도하기 때문이다.

반복문을 실행 시킬 때, 고유 행의 각 필드 값에서 데이터객체의 각 속성 값으로 할당한다. 모든 작업이 끝나면, 할당된 데이터객체를 반환 받는다. 이 클래스는 모든 데이터객체와 작동 하고 이 클래스를 수정할 필요가 없다.

이제 ReadOnlyResultSet이 실제 어떻게 작동하는 지 보자. 여기 모든 사용자 중에서 어떻게 첫 번째 이름이 jane인 사람 찾는 예가 있다:
<?phpinclude_once('class-DO_User.php');$user      = new DO_User();// We'll use a literal string here, // but this could come from anywhere,// such as $_POST or $_GET$user->firstName = 'jane';// Call find(), which returns an instance of ReadOnlyResultSet$rs        = $user->find();?><html>  <head>    <title>Found Users</title>  </head>  <body>    <p>Here the found users:</p>    <table border="1">      <tr>        <td>User ID</td>        <td>First Name</td>        <td>Last Name</td>        <td>Email</td>      </tr>?>// Loop through the result setfor ($i=0; $i < $rs-rowCount(); $i++) {  // Pass on a new instance of DO_User, receiving it back filled in  $userRow = $rs->getNext(new DO_User());?> // Display the current row in an HTML table      <tr>        <td><?=$userRow->userId?></td>        <td><?=$userRow->firstName?></td>        <td><?=$userRow->lastName?></td>        <td><?=$userRow->email?></td>      </tr><?php}<?php    </table>  </body></html>
$rs->getNext()를 호출할 때 새로운 DO_User 인스턴스를 전달해야 하는 점에 주목하라. 이 점은 ReadOnlyResultSet가 어떤 종류의 데이터객체와 작업해야 하는지를 안다는 것이다. ReadOnlyResultSet는 다음 행 데이터의 새로운 데이터객체에 존재하고, 현재 존재하는 데이터 객체를 반환한다. 기억하라, PHP5는 항상 참조에 의해 객체를 전달한다. 그래서 getNext()에게 전달된 새로운 데이터 객체는 getNext()로부터 반환된 객체는 완전히 동일하다. 다시 한 번, 유용하고, 가독성 높고 깨끗한 코드를 작성해 보았다.

앞으로의 과제

소개한 다섯 개의 기본적인 메소드를 능가하는 보다 많은 함수들을 추가할 수 있다. 나는 종종 사용자 로그인 인증 메소드를 추가한다. 이 메소드는 사용자 이름과 비밀번호를 매개변수로 받아 true 혹은 false를 반환한다. 만약 true가 반환되면, 검색된 레코드를 데이터객체에 채운다. 만약 매우 특수하거나 보다 복잡한 검색이 필요하다면, 이 작업을 완수할 새로운 메소드를 추가한다. 본문에서 보여준 기본적인 메소드들은 데이터객체의 시발점일 뿐이다.

이 다섯 개의 메소드를 일반화하여 슈퍼 클래스로 만드는 것도 가능하다. Reflection API로 이 것을 쉽게 할 수 있다.

또한 이 번에 소개한 단순한 데이터객체 구현은 한 가지 제한이 있다는 점에 유의하자. 테이블 조인(join)을 구현하기가 쉽지 않다는 점이다. 조인이 필요할 때는 ReadOnlyResultSet 를 반환하는 데이터객체 뷰를 생성하면 된다. 비록 다른 방법으로 조인 문제를 해결 하여도, 데이터 삽입, 삭제, 수정 작업은 불가능하다.

마지막으로, Pear::DB_DataObject 패키지를 사용할 수 있다는 점에 주목할 필요가 있다. 이 것은 견고한 패키지 이다, 그러나 데이터객체 개념에 익숙하지 않다면 다소 이해하기 어려울 수 있다. 이 기사는 Pear::DB_DataObject에 대한 좋은 소개서이다. 다음 번에 이 패키지에 대한 글을 쓰게 되길 희망한다.

이 기사는 첫 번째 목표는 데이터객체를 살펴 보는 것이다. PHP5를 사용하여 단순하게 구현된 데이터객체를 보여준다. 약간의 수정을 하여, PHP4 뿐만 아니라, 원하는 어떤 데이터베이스 추상 계층(가령, Pear::DB 또는 DBX)에도 에서도 이 구현을 적용할 수 있다. 많은 사람들이 데이터베이스 테이블의 구조 제공하는 쿼리와 필요한 PHP 파일들을 생성에 의해 데이터객체를 자동으로 초기화 하는 스크립트를 작성하여 게시하고 있다. 이 것은 시간을 많이 단축해 준다.

다음 번 기사에서는 Pear::DB_DataObject와 PHP 5의 새로운 Reflection API를 다뤄 보기를 기대한다.

관련 자료Darryl Patterson은 토론토 Centennial College의 수석 강사이며, PHP, SQL, Java/J2EE, HTML, JavaScript 그리고 CGI/Perl 등의 많은 프로그래밍 강의화 개발한 경험이 있다
신고
Posted by The.민군

웹사이트 데이터 분석 기초(3) - 웹데이터 분석 업체 선택

제공:한빛 네트워크
출처:웹사이트 분석의 기술: 온라인 비즈니스 성공을 위한 100가지 제안Chapter 1.

웹데이터 분석 업체의 선택은 가장 중요한 결정 중 하나다. 업체들간에는 우열이 있으며, 모든 회사의 요구를 만족시켜줄 수 있는 업체는 존재하지 않는다.

웹데이터 분석 분야는 여러 업체들로 넘쳐나고 있다. 그 중에는 정말 훌륭한 업체도 있고 그렇지 못한 업체도 있다. 이들의 공통점이라면 독자의 돈을 가져가려고 혈안이 되어 있다는 사실 정도다. 업체 선택 절차는 때때로 가장 고통스런 절차이기도 하다. 주요 차이점을 이해하고, 일류급 업체의 예를 간단하게 살펴보면 이러한 고통을 줄일 수 있다.

웹데이터 분석 업체는 크게 전달 방식과 데이터 수집 방법을 기준으로 분류할 수 있다. 여기서 전달 방식이란 웹데이터 분석 업체에서 제공하는 서비스를 이용하는 방법을 말하는 것으로, 자신의 서버에 프로그램을 설치하도록“소프트웨어”의 형태로 제공하는 것과, 관리를 해주는 “호스팅 서비스”로 나눌 수 있다. 데이터 수집 방법에 따라 분류해보면, 웹서버 로그파일[Hack #22]과 자바스크립트 페이지 태그[Hack #28] 로 나눌 수 있다. 두 가지 모두 대다수의 업체에서 제공하고 있으므로, 지금부터는 전달 방법에 따른 분류로 계속 이야기하겠다.

소프트웨어

소프트웨어는 가장 기본적인 형태로, 널리 이용되고 있다. 소프트웨어 방식을 선택하는 이유는 처음부터 끝까지 과정을 직접 주관할 수 있다는 점과 그 유연성 때문이다. 또한 초기투자비용(소프트웨어 구입비)은 다소 높지만, 구입 첫해 이후 유지비는 초기비용의 17~22퍼센트 정도밖에 되지 않는다(호스팅 서비스 모델의 비용에 대해 읽을 때쯤이면 왜 그러한지 이해할 수 있을 것이다). 소프트웨어 방식을 사용하기로 결정했다면, 회사 내부에 프로그램을 실행할 수 있는 환경을 마련해야 하고, 소프트웨어 및 하드웨어도 직접 유지보수할 여력을 갖추어야 한다. 소프트웨어는 일반적으로 서버의 로그파일을 데이터 소스로 사용한다.

사용자 삽입 이미지
[그림 1-4] 웹서버 로그파일의 예


호스팅 서비스

호스팅 서비스 모델(아웃소싱 모델 혹은 ASP(Application Service Provider) 모델이라고도 한다)은 소프트웨어를 직접 설치, 실행, 관리하길 원하지 않는 회사가 많다는 점을 이용한 것이다. 호스팅 서비스 모델을 적용할 경우, 사내 IT 그룹은 보통 초기 도입 시에만 관여하며, 비즈니스 및 마케팅부서 쪽에서 리포트와 데이터 수집 메커니즘을 지속적으로 수정할 수 있도록 해준다. 소프트웨어를 구입하는 방법보다 도입 첫해의 비용은 훨씬 낮지만, 그 후의 비용은“사용한 만큼 지불한다”는 원칙 때문에 증가할 수밖에 없다. 즉, 방문자가 방문한 횟수만큼 비용(백만 페이지 뷰당 비용)을 지불하기 때문에, 매해 또는 매달 방문자가 늘어난다면 그 비용은 조금씩 늘어난다. 호스팅 서비스는 클라이언트 자바스크립트 페이지 태그를 데이터 소스로 사용하는 것이 보통이다.

소프트웨어와 서비스를 제공하는 유명 업체

다음 [표 1-1]에 잘 알려진 웹데이터 분석 전문업체를 데이터 소스와 전달 방식에 따라 분류해 보았다.

 

전달 방식데이터 소스
업체명소프트웨어호스팅 서비스로그파일페이지 태그
ClickTracksOOOO
WebTrendsOOOO
WebSideStory

 

O

 

O
Omniture

 

O

 

O
Coremetrics

 

O

 

O
UrchinOOOO
Sane SolutionsO

 

OO
Visual SciencesOOOO
Fireclick

 

O

 

O
IBM SurfAidO

 

OO
[표 1-1] 유명 웹데이터 분석 전문업체

위와 같이, 한 업체에서 여러 가지 데이터 수집 방법과 전달 방법을 지원하는 경우가 많다. 특히 ClickTracks, Urchin, Visual Sciences, WebTrends는 모든 방법을 다 지원한다. 웹데이터 분석 전문업체 선택에 도움이 될 만한 업체별 설명을 덧붙인다.

  • ClickTracks
    ClickTracks는 브라우저 오버레이[Hack #62] 모델을 대중화시킨 것으로 알려져 있다. 이 방법으로, PPC(pay-per-click, 클릭횟수당 일정금액을 지불하는 방법) 트래픽을 웹페이지에 직접 보여준다. 비용이 가장 저렴한 도구 중의 하나로, 도입 초기 레벨에 적절하며, 1사용자 소프트웨어 라이선스를 495달러부터 시작하는 상품이다.
  • WebTrends
    WebTrends는 이 시장에서 장수기업이며, 호스팅 서비스 방식 및 소프트웨어 방식을 모두 제공한다. 버전 7부터는 페이지뷰에 기반한 가격 책정 방식(대규모부터 소규모 비즈니스 시장까지 모두 적용)을 제공함으로써 경쟁력이 예전보다 더욱 강해졌다. (소규모 비즈니스 에디션의 소프트웨어 버 전은 495달러부터, 호스팅 서비스 버전은 월 35달러부터의 비용으로 제공된다.)
  • WebSideStory
    초창기 호스팅 서비스 제공업체 중의 하나이며, 웹데이터를 분석한 데이터를 마이크로소프트 엑셀파일로 저장할 수 있도록 자동화한 최초의 회사이다. 최근에 호스팅 검색 및 컨텐트 관리 시스템 업체를 인수해, 소규모 비즈니스에 예전보다 많은 서비스를 제공하고 있다.
  • Omniture
    시각적으로 매력적인 사이트캐털리스트 인터페이스로 널리 알려져 있다. 거의 모든 온라인 비즈니스에 적합하다.
  • Coremetrics
    코어매트릭스사는 온라인 소매거래, 금융서비스, 여행 사이트를 서비스하는 것으로 유명하다. 유연한 데이터 웨어하우스를 기반으로 하기 때문에, 복잡한 방문자 프로파일을 만들 수 있고, BizRate, Commision Juction, Foresee Results와 같은 회사에서 외부 데이터를 통합[Hack #32]해본 경험을 갖고 있다.
  • Urchin
    Urchin은 웹데이터 분석 프로그램 전문업체로, 다수의 호스팅 서비스 제공업체에서 이 회사의 프로그램을 직접 제공하고 있다. 오랜 역사를 갖고 있으며, 최근에는 호스팅 서비스도 제공하고 있다. 가격대 성능비가 우수하다.
  • Sane Solutions
    Sane Solutions사의 넷트랙커 프로그램은 현재 가장 널리 사용되는 웹데이터 분석 도구의 하나다. 넷트랙커는 인터페이스가 세련됐을 뿐만 아니라, 웹사이트에서 수집한 데이터를 BI(Business Intelligence; 비즈니스 지능)에 유용한 형태로 바꾸어주는 ETL(Extract, Transform, Load; 추출, 변환, 로드) 도구 시장에서 업계를 선도하고 있다.
  • Visual Sciences
    비주얼사이언스사는 비교적 신생업체에 속하며, 웹데이터 분석 프로그램이라기보다는 데이터 분석엔진을 제공한다. 다양한 종류의 데이터를 분석할 수 있으며, 이 회사의 비주얼 워크스테이션 프로그램은 이 분야에서 가장 강력하고 유연한 도구이다.
  • Fireclick
    파이어클릭은 호스팅 서비스 제공업체로 경쟁 사이트와 벤치마킹을 할 수 있는 도구[Hack #93], 소매사업자를 위한 브라우저 오버레이[Hack #62], 엑셀파일로 데이터 분석 결과 저장[Hack #91] 과 같 은 부가 기능을 지닌 도구를 처음으로 제공한 업체이다. 파이어클릭은 가격정책에 신중한 온라인 쇼핑몰 운영자에게 적합한 도구다.
  • IBM SurfAid
    IBM 서프에이드는 다양한 웹 관련 소스(복합물품 판매 데이터베이스 포함)에서 얻은 데이터를 통합할 수 있는 데서 유래된 이름이다. 서프에이드는 비즈니스 솔루션으로 IBM, 오라클, 마이크로소프트 같은 회사의 솔루션을 이용하는 이에게 가장 적합한 도구다.
결론

웹데이터 분석 소프트웨어를 처음 접하는 경우라면, 주변에서 이러한 프로그램을 사용하고 있는지가 궁금할 것이다. 어떻게 보면 업체별 차이점이 작기도 하고, 단순히 취향의 문제일 수도 있다. 따라서 주변에 이용 경험을 갖고 있는 사람이 있다면 조언을 얻어 보는 것이 좋다(처음 차를 살 때 아버지의 조언을 구하는 것처럼 말이다). 필자는 야후! 그룹스에 있는 웹데이터 분석 및 분석 포럼(http://groups.yahoo.com/group/webanalytics/)에 꼭 들러보길 권한다. 2004년에 이미 1000개 이상의 관련 그룹이 생성되어 있었으며, 각 그룹에서는 기꺼이 조언 및 도움을 제공해줄 것이다.

얻으려는 것을 명확히 따져본 다음, 소프트웨어의 옵션을 비교해보고, 데모를 요청해보자. 가능하다면 직접 다운로드해서 설치를 해보는 것도 좋은 방법이다(ClickTracks, WebTrends, Urchin, Sane Solutions와 같은 업체는 모두 무료 시험버전을 다운로드할 수 있도록 제공한다). 호스팅 서비스를 이용할 계획이라면 시연해 달라고 요청해보자.

어떠한 일을 하든지 간에, 무조건 처음 알아본(혹은 처음 연락을 해온) 업체를 선택할 필요는 없다. 다시 한 번 강조하지만, 마치 처음 차를 구입하는 것처럼 좋든 싫든 간에 한 번 선택한 웹데이터 분석 프로그램은 절대 잊지 못할 것이라는 점을 명심하자.

신고
Posted by The.민군

웹사이트 데이터 분석 기초(1) - 전문용어 익히기

제공:한빛 네트워크
출처:웹사이트 분석의 기술: 온라인 비즈니스 성공을 위한 100가지 제안Chapter 1.

웹데이터 분석의 세계에서는 용어가 매우 중요하다. 인터넷에서의 활동을 관찰하는 것에 많은 경험을 가진 사람은 드물다. 따라서 용어를 잘 사용해서 설명해주는 것이 중요하다. 기술적인 부분에 관심을 갖고 있는 사람이라면, 이 핵을 통해서 사람들의 활동을 비트와 바이트로 바꾸어 저장하는 방법을 이해할 수 있을 것이다. 그리고 마케팅적인 측면에서 접근하는 경우라 하더라도 기초적인 정보를 어디서, 어떻게 얻는지에 대해 이해할 수 있을 것이다.

[그림 1-1]은 기본 용어의 이해를 돕기 위한 것이다. 이 피라미드 모형에서와 같이, 사용할 수 있는 데이터의 크기가 작아짐에 따라 정보의 가치는 높아진다. 데이터량이 가장 큰 바닥부분은 “히트”이며, 최상단은 “순방문자”로, 측정할 수 있는 것들 중의 “성배”[가장 유용한 데이터라는 의미]라고 할 수 있다.

사용자 삽입 이미지
[그림 1-1] 웹데이터 분석에서 쓰이는 데이터의 피라미드 모델


자신이 이 분야의 용어들에 대해서 잘 알고 있는 경우라 할지라도, 각 용어간 경계는 혼동스러울 것이다. 그래서 지금부터 그 의미를 명확히 구분해서 혼동을 줄이고자 한다.

히트(Hits)웹데이터 분석에서 가장 많이 사용되는 용어다. 때문에 과용되기도 하고 잘못 이해되기도 한다. 사람들은 “사이트 히트수”, “ 페이지 히트수”, “ 서치엔진 히트수” 같은 용어를 너무 자주 사용한다.

WebTrends사에서는 히트를 다음과 같이 정의하고 있다.

“히트란 사용자가 웹페이지를 보거나 파일을 다운로드하는 것과 같은 웹사이트 상에서의 활 동을 말한다.”
정의를 보면 의미가 명확한 것 같다. 하지만 “페이지뷰”라는 용어와 다른 점을 묻는다면 선뜻 대답하기 어려울 것이다. 그럼 “파일을 다운로드하는 것”이란 부분을 잘 살펴보자. 여기서 파일이란 실행 파일, PDF 파일, 사운드 파일, JPEG, PNG, GIF와 같은 이미지 파일 등을 포함한다.

문제는 여기서 하나의 “페이지”란 기술적으로 수백 개의 “히트”일 수도 있다는 점이다. 왜냐하면 페이지를 구성하는 요소인 텍스트, 이미지 파일 하나하나가 전부 히트로 간주되기 때문이다.

이렇게 페이지 하나를 읽어올 때, 그 페이지에 포함된 이미지 파일수에 따라 수백 개의 히트가 기록되기도 하기 때문에, 실제 업무의 참고자료로 쓰기는 어렵다. 하지만 슬퍼하기엔 아직 이르다.

이제 우리는 “히트”라는 단어가 흔히 오용되곤 한다는 사실을 인지하고 다음으로 넘어가자. 그리고 이제부터는 “히트” 대신 “페이지뷰”, “ 검색엔진의 참조횟수”와 같은 용어를 사용하자. 웹데이터 분석 분야에 있어 “히트”는 한물간 용어다.

페이지뷰(Page View)

페이지뷰는 웹데이터 분석의 기본단위로, 한 사람이 하나의 웹페이지를 본 것을 말한다. 또한 방문자의 클릭스트림(Clickstream: 특정 사이트를 방문한 뒤, 그 후 클릭한 것들을 뜻함)을 알 수 있으므로, 방문자의 관심을 보여주는 척도이기도 하다.

인터넷 광고 측정표준을 관장하는 인터넷 광고국(IAB: Interactive Advertising Bureau)에서는 “인터랙티브 방문자 측정”과 “광고캠페인 결과 보고 및 감사 지침”이라는 문서에서 페이지뷰에 대해 다음과 같이 언급하고 있다.

“페이지뷰란 웹브라우저가 요청한 것을 웹서버가 응답한 것이라고 정의할 수 있다. 그런데 이때 검색엔진 로봇이 요청한 것이나 웹서버 에러코드 출력과 같은 것은 제외시킴으로써, 실제 사람이 본 페이지에 가장 근접한 값을 산출한다.”
본서에서는 페이지뷰를 다음과 같이 정의한다.

“페이지뷰는 웹사이트 방문자가 요청한 문서(즉, 내용이 담긴 하나의 웹페이지)를 성공적으로 읽어온 횟수를 말한다. 이때, 전송 방법이나 컨텐트를 요청받은 빈도와는 상관이 없다.”
물론 페이지뷰를 콕 찍어서 하나로 정의하기는 어렵다. 그렇지만 전반적인 개념을 이해하는 것은 중요하다. 실제로 페이지뷰는 특정 웹사이트나 특정 웹페이지의 인기를 가늠할 수 있는 손쉬운 방법이다.

방문(Visits)

방문은 세션 또는 사용자 세션이라고도 하며, 웹사이트를 돌아다닐 때의 페이지를 모은 것으로 정의된다(이를 “클릭스트림”이라고도 한다). IAB에서는 다음과 같이 정의하기도 한다.

“하나의 페이지뷰에 해당하는 텍스트/그래픽을 다운로드하고, 30분 동안 사이트 내에서 활동이 있는 경우, 하나의 세션으로 정의할 수 있다.”
그다지 복잡하지 않게 보일지 모르지만, 사람들이 웹사이트를 돌아다닐 때를 떠올려보면 위 정의가 애매모호하다고 느낄 것이다. 다음의 두 예를 보자.

  • 현이는 브라우저에 URL을 입력한 다음, 링크를 클릭해서 원하는 사이트에 들어갔다. 그리고 일정 시간 동안 일을 처리했다. 그 후 다른 사이트로 이동했다.
  • 믿음이는 브라우저에 URL을 입력한 다음, 링크를 임의로 클릭해서 이곳저곳을 서핑했다. 그러는 도중에 커피를 마시러 나갔다 오기도 했고, 식사도 하러 다녀왔다. 그러면서 중간 중간에 전화 통화도 했다. 웹사이트 내용은 자세히 보지 않았지만, 여러 웹사이트를 여기저기 돌아다녔다.
둘 다 인터넷을 서핑하는 전형적인 예라고 할 수 있다. 현이의 방문이 끝나는 때는 알기 쉬우나, 믿음이의 경우는 어렵다. 웹서핑을 하는 사람의 진정한 의도는 알기가 거의 불가능하므로, 약간의 가정이 필요하다. 가장 기본적인 가정은 30분 동안 아무런 클릭이 없는 경우에 이미 그 사이트를 “떠난 것”으로 간주하는 것이다.

“왜 30분이죠?”라고 묻고 싶을 것이다. 좋은 질문이다. 그다지 만족할만한 대답은 아닐지 모르지만, 그게 가장 널리 쓰이고 있는 나름대로의 기준이기 때문이다.

방문의 가장 적절한 정의는 다음과 같다.

“방문이란, 특정 방문자의 웹 상에서의 활동(여러 웹페이지를 클릭해서 이동하면서 보는 것; 클릭스트림이라고도 한다)을 하나로 셈한 것이다. 단, 여기서 30분 동안 클릭 또는 다른 활동이 없으면 방문자가 웹사이트를 떠난 것으로 간주하고, 이것이 하나의 방문이 된다.”
이와 같이 방문자 한 명이 클릭하는 횟수에는 제한이 없다. 단, 클릭은 29분 59초 이내에 이루어졌을 때만 유효하다. 이렇게 하면, 동일한 방문자가 한 사이트를 하루에 여러 번 방문하더라도 한 사람으로 인식할 수 있다(한 번 방문에 30분의 제한이 있기 때문). 그리고 방문자수와 방문횟수의 비율은 주요성능척도(KPI: Key Performance Indicators)[Hack #94] 이기도 하다. 방문은 유료 검색엔진 광고와 순수 검색 결과,[Hack #42와 #43] 배너광고캠페인[Hack #40] 을 구분하는 데 아주 중요한 요소다.

순방문자(Unique Visitors)

웹사이트 데이터 분석의 세계에서는 각 개인을 “순방문자”라고 한다. [그림 1-1]에서 보듯이 순방문자는 피라미드 모델에서 최상위층을 차지하고 있으며, 세 가지 형태(완전익명, 부분익명, 기명[Hack #5] 로 존재한다. 또 한 가지 기억해야 할 점은 여기서 순방문자란 진짜 사람을 이야기하는 것이지, 웹로봇[Hack #23] 을 지칭하는 것은 아니라는 점이다.

IAB에서는 순방문자에 대해 다음과 같이 엄격하게 정의해 놓았다.

“순방문자란 일정 시간 동안 사이트를 방문한 실제 사람수를 뜻한다. 이때, 일정 시간 이내의 클릭 및 기타 활동은 모두 한 명의 순방문자로 간주한다.”
짧게 정의 내렸지만 중요한 내용을 포함하고 있다. 특히 “일정 시간 내”라는 개념, 그리고 “순방문자”와 “방문”의 관계가 중요하다. 저자는 순방문자를 다음과 같이 정의하는 것이 가장 적절하다고 생각한다.

“한 사람이 웹브라우저를 이용해서 웹사이트를 방문할 때를 하나의 순방문자로 친다. 이때, 사용자가 읽은 페이지수, 클릭수, 머문 시간은 상관이 없다. 방문한 시간대가 달라도 한 명의 방문자로 식별할 수 있어야 하고, 가급적 여러 브라우저에서도 데이터를 공유해 진정한 한 개인을 식별할 수 있어야 한다.”
설명이 다소 복잡한데, 순방문자 역시 독자나 저자와 같은 한 명의 “사람”이라는 점을 인지했다면 소기의 목적을 달성한 것이다. 그리고 한 명의 순방문자로 인정하는 데는 시간제한이 있다는 사실을 꼭 기억하자.

참조자(Referrers)

특정 웹사이트로 사람들을 끌어 모으는 것을 “참조 트래픽”이라고 한다. 참조자라는 말은 여기서 유래했다. 웹사이트, 검색엔진, 배너광고, 블로그, 이메일 등과 같은 것에서 참조될 수 있다. 즉, 온라인의 기본자원으로부터 웹사이트를 방문하게 되고, 페이지뷰도 발생하게 된다. 이를 HTTP 요청을 통해 확인해볼 수 있다.

216.219.177.29 -- [15/May/2000:23:03:36 -0800] "GET /index.htm HTTP/1.0" 200 956 " http://www.webanalyticsdemystified.com" "Mozilla/2.0 (compatible; MSIE4.0; SK; Windows 98)" 212.219.31.219 -- [15/May/2000:23:03:42 -0900] "GET /mail/email_marketing.htm HTTP/1.0" 200 956 "http://www.altavista.digital.com/cgi-bin/querybin/ query?pg=aq&text=yes&d0=1%2fnov%2f99&q=email+marketing %2a&stq=30" "Mozilla/4.05 [en] (Win 95; I)" 121.12.31.45 -- [15/May/2000:23:03:56 -0300] "GET /index.htm HTTP/1.0" 200 956 "http://www.oreilly.com/lists/links.php?link_list_id=134" "Mozilla/4.0 (compatible; MSIE4.01; Windows 98)"
한 줄씩 분석해보자.

  • 23:03:36에 http://www.webanalyticsdemystified.com에 접근한 순방문자가 index.htm이라는 파일을 요청했다.
  • 23:03:42에 알타비스타에서“email marketing”이라는 키워드로 검색을 한 순방문자가 /mail/email_marketing.htm이라는 파일을 요청했다.
  • 23:03:56에 오라일리 웹사이트(http://www.oreilly.com/list/links.php?link_ list_id=134)의 링크를 따라온 순방문자가 index.htm이라는 파일을 요청했다.
“참조”를 실용적으로 해석해보면,

“참조에는 링크를 건 페이지의 전체 URL이 포함되어야 한다. 또한 전체 URL을 표시해주지 못하는 경우에는 트래픽의 근원을 알 수 있도록 최대한 설명을 해주어야 한다.”
인터넷을 이용한 마케팅에 있어 이메일은 아주 중요한 요소이지만, 실제로 참조 URL을 제공하는 이메일 프로그램은 그리 많지 않다. 그래서 위 정의에서 두 번째 문장을 추가했다. 참조 URL을 분석할 때에는 반드시 일부가 아닌 전체(즉, http://www.oreilly.com/books/hacks/websitemeaurementhacks.html 및 그 뒤에 오는 질의문자열(? 다음에 오는 문자열도 포함해서))를 살펴봐야 한다. 이렇게 해야만 최초로 링크를 건 곳을 추정해볼 수 있다. 그럴 수 없는 경우라면, 요청 URL에 참조 링크에 관한 정보를 집어넣는 방법을 고려해보자.

[그림 1-2]에서 보듯이Web Analytics Demystified 웹사이트를 방문했을 때, 방문자가 2004년 12월 캠페인에서 왔고(campaign=Dec2004), “ 지금 구매”메시지를 클릭했으며(message=buy_now), creative는 image였고(creative=image), 링크 식별자는 54412(id=54412)였음을 알 수 있다. 훌륭한 웹데이터 분석 프로그램[Hack #3] 이라면, 광고캠페인 및 이메일 추적 기능[Hack #41] 을 사용해서 이러한 것들을 알아낼 수 있을 것이다.

사용자 삽입 이미지
[그림 1-2] 참조 URL


결론

결국, 각 용어를 이해하는 것은 웹데이터 분석을 이해하는 데 뿌리가 된다. 따라서 각 용어 및 관련 하위 용어를 제대로 이해하도록 한다.“ 순방문자”를 뜻하고자 할 때 “방문자”라고 표현하면 오해를 살 수 있다. 용어를 정확하게 사용할 수 있어야만 비로소 다음과 같이 말할 수 있을 것이다.

  • “지난 한 주 동안 평균 페이지뷰가 폭발적으로 증가한 사실을 면밀히 관찰하고 있습니다.”
  • “가장 큰 온라인 파트너사로부터의 방문자 대 순방문자 비율이 현저히 떨어졌습니다. 따라서 우리는 그들이 종단 메시지를 변경한 것은 아닌지 연락을 취하고 있습니다.”
  • “최근의 광고 덕분에 페이지뷰가 20배나 늘었습니다. 지금 저희 평균 광고 CPM은 30달러가 넘었고, 이는 수익의 측면에서 볼 때 아주 의미 있는 숫자입니다.”
  • “히트요? 히트는 야구에서나 쓰는 단어인데, 왜 지금 얘기하시는 거죠?” 자, 이제 위 문장들이 무슨 뜻인지 잘 이해가 될 것이다. 용어를 올바르게 사용해야만 웹데이터 분석과 그에 따른 의사소통을 제대로 할 수 있다는 것을 명심하자.
신고
Posted by The.민군

웹사이트 데이터 분석 기초(2) - 웹데이터 분석을 위한 준비단계

제공:한빛 네트워크
출처:웹사이트 분석의 기술: 온라인 비즈니스 성공을 위한 100가지 제안Chapter 1.

웹데이터 분석은 은제 탄환[서양전설에 따르면, 늑대인간을 죽일 수 있는 유일한 것은 은으로 만든 탄환이다. 그런 연유에서 묘책, 특효약과 같은 뜻을 지니게 되었다]이 아니다. 전설의 세계에서도 은제 탄환은 실제 존재하지 않는 것으로 귀결되었다.

웹데이터 분석을 통해 성공하고 싶다면 다른 방법론도 병행하는 것이 좋다. 즉, 고객관계관리(CRM: Customer Relationship Management), 영업자동화(SFA: Sales Force Automation), 전사적 자원관리(ERP: Enterprise Resource Planning) 등도 같이 적용하는 것이 좋다.

웹데이터 분석에 있어서도 약어가 있다. 예를 들어 “WMO”는 “웹데이터 분석 및 최적화(Web Measurement and Optimization)”의 약어이며, 데이터를 분석해서 웹사이트를 개선한다는 뜻을 담고 있다.“SMI”는 “사이트 측정 방법 통합(Site Metrics Integration)”의 약어로, 타 사이트와 동일한 방법과 기준으로 측정해야 한다는 뜻을 담고 있다. 적절한 약어 사용과 이해는 중요하다는 것을 항상 기억하고 있도록 하자.

다음 사항들을 잘 이행한다면, 웹사이트를 혁신적으로 개선할 수 있을 것이다.

시작하기 전에 목표를 분명히 하라

회사들이 흔히 범하는 실수는 구매이유를 충분히 묻지 않은 채 소프트웨어나 서비스를 구매하곤 하는 것이다. 웹데이터 분석 프로그램도 그렇다. 투자하기 전에 웹데이터 분석에 대한 투자로 얻을 수 있는 것이 무엇인지를 책임자들이 모여 미리 짚고 넘어가는 것이 좋겠다. 이때, 웹데이터 분석전략을 세운다. 즉, 웹사이트의 목적을 명확히 한다.[Hack #38] 다음은 명확한 투자이유의 예다.

  • “우리는 소매업자이며, 마진이 아주 낮습니다. 마케팅 비용을 많이 들이지 않고 온라인 구매 횟수와 단가를 높이고 싶습니다.”
  • “고객지원 비용이 너무 높습니다. 고객지원 홈페이지를 최적화해서 기본적인 질문을 인터넷에서 찾도록 유도하고, 회사업무에 부하가 되는 전화문의는 되도록 줄이고 싶습니다.”
  • “우리 은행은 온라인 은행 프로그램을 제공하고 있는데, 고객들이 불평이 좀 있습니다. 많은 사용자들이 편하게 사용할 수 있도록 사용성을 개선하고 싶습니다.”
  • “회사를 설립한지 얼마 되지 않았으며, 경쟁이 심한 시장으로 진입하려는 중입니다. 우리의 제안을 투자자에게 이해시키고, 규모가 큰 경쟁회사와 차별화를 꾀해야 합니다.”
전반적인 목표가 명확해지고 나면, 웹데이터 분석 업체에 할 이야기[Hack #3] 도 더욱 쉬워진다. 그 뒤에는 구체적인 구현 방법을 결정하고(2장), KPI를 위한 전략을 개발[Hack #91] 하고, 회사 상급자에게 하고자 하는 일에 대해 명확히 설명한다.

경영진에게 충분한 설명을 통해 이해시키고, 전폭적인 지원을 받도록 하자

웹데이터 분석으로 효과를 보려면 웹사이트를 변경할 권한도 가지고 있어야 한다. 때로는 전체 온라인 전략을 바꿔야 할 필요(이때, 경영진의 전폭적인 지지가 바로 뒤따라 준다면 쉽다)를 느낄 때가 있을 것이다. 자신이 웹사이트 소유자인 경우에는 아주 간단하지만, 대다수의 경우 누군가에게 보고를 해야 하는 입장이고, 보고 받은 사람 역시 또다시 누군가에게 보고를 해야 하는 체계 속에 있다. 따라서 이러한 경우에는 난관을 미리 예상하고 스스로 대비하자. 필요한 것을 경영진에 요구만 할 것이 아니라 그로 인해 얻을 수 있는 것도 같이 이해시키면 내부 분란을 피하는 데 큰 도움이 된다.

경영진을 끌어들이려면, 7장의 내용과 다음의 핵들을 그들이 직접 읽도록 하는 것도 나쁘지 않다.

  • 웹데이터 분석의 실제[Hack #2]
  • 마케팅 용어의 이해[Hack #37]
  • 전환율 정의[Hack #39]
  • 사업목적[Hack #38]
인원 구성이 중요하다

사람이 중요하다는 점을 항상 명심하자. 회사에서는 일반적으로 소프트웨어에는 막대한 돈을 쏟아 부으면서도 전문가의 고견을 실행에 옮기는 데는 인색하다. 웹데이터 분석의 경우도 마찬가지다. 웹데이터 분석에 한 명의 전담인원(데이터를 분석하고, 프로그램을 유지보수하는 인원)[Hack #4] 을 두면 훨씬 더 나은 결과를 가져온다는 것이 여러 연구 결과를 통해 증명되었다.

가장 이상적인 경우는 두 명의 전담인원을 두는 것이다. 이때, 한 명은 구현과 웹데이터 분석 업체측과 협조의 역할을 맡는다. 다른 한 명은 데이터를 가공하고 활용하는 역할을 맡는다. 그리고 무엇보다도 이러한 내용을 기업 내부에 보고하는 체계를 갖추는 것이 중요함을 잊지 말자.

사람수가 아무리 많아도, 전담인원이 아니라면 별 소용이 없다. 인적/금전적 자원을 할당할 여력이 있고, 여기에 의지가 동반되며, 지속적인 개선을 할 수 있을 때 비로소 결실을 얻을 수 있다는 것을 명심하자.

데이터 분석과 개선: 지속적인 개선 과정

목표 설정, 경영진의 승인, 팀 구성을 모두 마치고 나면, 본격적인 일은 지금부터 시작이다. 전체 비즈니스에 웹데이터 분석 방법을 통합하려면 지속적인 개선 과정을 거치는 것이 가장 신뢰할 수 있는 방법이다. [그림 1-3]은 데이터 수집 및 보고 프로그램을 사용해서 “측정, 보고, 분석, 최적화”의 순환 과정을 보여준다.

사용자 삽입 이미지

[그림 1-3] 지속적인 개선 과정


이와 같이 과정은 아주 간단하다. 그런데 회사는 너무 즉흥적으로 웹데이터 분석에 임하는 경향이 있다. 즉, 외부 채널(이메일, 전화, CEO 측근의 조언 등)을 통해 문제점을 듣고 해결에 뛰어드는 경우가 많다. 이러한 것이 꼭 나쁘다는 것은 아니지만 항상 이런 식이어서는 곤란하다는 것이다. 성공적인 회사들을 살펴보면“지속적인 개선 과정”을 통해, 고객이 전화로 불만을 터뜨리기 전에 미리 대비해 왔음을 알 수 있다.

이러한 주의 점들을 잊지 않는다면, 데이터 분석은 회사에 큰 도움이 될 것이다. 웹데이터 분석업계 전문가이자 홈쇼핑네트워크사의 프로그래밍 및 마케팅 담당 부사장이었던 짐 노보씨는 다음과 같이 말했다. 결국 웹데이터 분석을 제대로 활용하는 회사는 업무결정에 있어 이미 고객 데이터를 활용하던 회사인 경우가 많다. 즉, 직접 마케팅, 자동차 제조사, 출판사와 같은 부류의 회사들은 결정을 내리기 위한 데이터 마이닝에 이미 익숙해 있었다. 이에 반해, 그 외 회사들은 즉흥적인 의사결정을 내리는 경우가 많았다. 노보씨에 따르면, 독자가 전자와 유사한 회사에 속해 있다면 이 책에 있는 대부분의 아이디어를 별 문제 없이 받아들일 것이라고 한다. 하지만 후자의 경우라면 일단 계속해서 책을 더 읽어보자.
신고
Posted by The.민군

웹사이트 데이터 분석 기초 - 들어가며

제공:한빛 네트워크
출처:웹사이트 분석의 기술: 온라인 비즈니스 성공을 위한 100가지 제안Chapter 1.

제목을 보고, 기초 같은 게 따로 있냐고 반문하는 사람들이 많을 것이다. 하지만 기초를 튼튼히 해야, 앞으로 우리가 살펴볼 부분들이 머리에 쏙쏙 들어올 것이다.

웹사이트 데이터 분석에서 사용하는 용어는 혼란스럽고 애매모호한 것이 많다. 또한 잘못된 가정에 기초한 경우도 있다. 그리고 데이터 분석을 데이터 수집광 같은 사람들이나 관심 있어 하는 영역으로 치부하기도 한다. 따라서 실제 비즈니스를 하는 사람들이 단순한 형태의 웹데이터 분석(유료 사용성 연구나 온라인 설문조사)까지도 피해왔다는 사실도 놀랄만한 일은 아니다.

하지만 더 이상 그래선 안 된다!

지난 몇 년간 웹데이터 분석 프로그램 제작 회사는 상당 수준의 발전된 연구 결과를 내놓았다. 이를 기반으로 해서 이해하기 쉽고 사용하기 쉬운 프로그램들이 등장했다. 지금은 주요 업체들이 공통 용어 제정 및 데이터 수집에 관련된 역사 연구 등을 시작하는 단계다. 웹데이터 분석에 관심을 갖는 사람은 점점 늘어나고 있으며, 실제로 데이터 분석을 위한 자원 할당을 더욱 늘려가고 있다.

그런데 아이러니한 점은 웹데이터 분석을 시작 및 활용하게 된 계기가 경기침체와 회사 내부의 책임소재를 따지면서인 경우가 많았다는 점이다.

왜 웹사이트 데이터를 분석하는가?

마케팅 및 광고비용이 얼마나 유용하게 쓰이는지를 관찰하기 위해서 웹사이트 데이터를 분석하는 경우가 많다. 중급 이상 규모 회사의 인터넷마케팅 담당자라면, 단순히 웹사이트 관리뿐만 아니라 이메일, 배너/키워드광고, 유기적 검색, 내부 검색, 컨텐트, 온라인 브랜드 관리 등에 대해서도 전반적으로 같이 하는 경우가 많다.

웹사이트 데이터 분석의 간단한 역사

웹사이트 데이터 분석의 역사를 요약해보면 다음과 같다.
“태초에 WebTrends가 있었으니, 보기에 좋았더라. 하지만 결국 WebTrends는 선하지 못하게 되었고, 시장이 급속히 성장하여 50여 개의 업체가 난무하게 되었더라(이는 실제 세계가 필요로 하는 45여 개의 업체보다 많은 수니라).”[성경의 창세기 구절에 빗대어 표현한 것임]
그럴듯한 이야기다. 하지만 실제는 이와 다르다. 미국 오리건 주 포틀랜드에 위치한 WebTrends사는 “적절한 장소, 적절한 타이밍”을 공략하여 단기간에 성공을 이루었다. 전성기일 때에는 전세계에 55,000개 이상의 고객사를 거느리기도 했을 정도로 큰 성공을 거두었지만, 결국에는 시장 변화에 신속하게 대응하지 못하여 주저앉고 말았다. 하지만 다행히도 그 후 원래의 모습을 회복하여 현재는 시장의 선도자로 인정받고 있다.

웹데이터 분석이라는 것 자체가 이미 상당히 훌륭한 아이디어다. 그래서 개나 소나 여기에 뛰어 들었고, 결국 겨울에 오리건 주에 넘쳐나는 버섯들처럼 관련 프로그램들이 쏟아져 나오기 시작했다. 지금은 이러한 업체들이 전 세계적으로 최소한 100여 곳이 넘는 것으로 집계되고 있다. “클릭트랙스”, “ 클릭랩”, “ 클릭스트림”, “ 클릭카덴스”와 같은 회사가 있다.[영어회사명은 “ClickTracks”, “ Clicklab”, “ Clickstream”, “ Clickcadence”로, 이름만 보아도 그 역할이 직관적 으로 와 닿는다]그런데 투자전문분석가들은 중견업체들(이미 성공을 거두고 이제는 규모도 제법 큰 업체들, 즉 WebTrends, Ominiture, WebSideStory, Coremetrics, Sane Solutions 등)부터 웹데이터 분석 시장이 조만간 침체를 겪을 것이라고 예견하고 있다.

www.webanlyticsdemystified.com/history.asp에 가면 이 업계의 역사를 그림과 함께 상세히 살펴볼 수 있다. 또한 업체의 탄생 및 실제 적용사례를 요약한 PDF 파일도 구할 수 있다.

웹데이터 분석이란?

웹사이트 데이터 분석에는 쓸모 있는 것들이 참 많다. 지금부터 이 책을 통해서 하나씩 설명해 나가겠다. 하지만 다음과 같은 것들은 웹사이트 데이터 분석의 범주에 포함될 수 없다는 것을 짚고 넘어가려 한다.

  • 사용성 테스트
  • 성능 모니터링
  • 마케팅을 대신해주는 것
  • 사람을 대신해주는 것
  • 은제 탄환[서양 전설에 따르면, 늑대인간을 죽일 수 있는 유일한 것은 은으로 만든 탄환뿐이라고 한다. 그런 연유에서 묘책, 특효약과 같은 뜻을 지니게 되었다]
사용성 테스트와 성능 모니터링은 서로 긴밀하게 연관된 분야로, 웹데이터 분석을 할 때 데이터를 제공해주기도 하고 반대로 이득을 얻을 수도 있는 분야다. 하지만 엄밀히 말해 이들 자체를 웹데이터 분석이라고 하기는 어렵다. 세 번째와 네 번째의 경우는 웹데이터 분석을 통해 얻을 수 있는 이점이다. 즉, 이 책에 제시된 핵들을 이용해서 좀 더 지능적이고 주도면밀한 마케팅을 할 수 있다는 것뿐이지, 이들을 웹데이터 분석이라고 할 수는 없다. 그리고 마지막의 은제탄환은 영화 속에서나 존재하는 것이지, 실제 세계에 이러한 것은 없다.

웹데이터 분석 vs 웹 분석

현재 널리 사용되는 “웹 분석”이라는 용어는 “웹데이터 분석”과 미묘한 차이가 있다.

  • 웹데이터 분석(Web measurement) 데이터를 수집하고 분석하여, 쓸모 있는 형태나 사람들이 읽을 수 있는 형태로 바꾸는 행위(예: 분 석 보고서)
  • 웹 분석(Web analytics) 단체나 회사가 특정 행동을 취할 수 있도록 웹사이트 데이터 분석 보고서를 해석하는 행위
“나만의 핵 만들기”에 대하여

이 책의 본문에는 전반에 걸쳐 “나만의 웹데이터 분석 프로그램 만들기” 핵을 담았다. 이러한 핵에서는 웹데이터 수집 및 분석을 위한 프로그램 작성법을 소개한다. 심화된 핵은 각 핵을 더욱 발전시킨 것으로, 각 장의 주제에 부합하는 기능들을 담고 있다. 본문에서 이러한 핵들을 추가한 이유는 더 나은 새 웹데이터 분석 솔루션이 필요해서라기보다는, 직접 만들어봄으로써 이해하기도 쉽고 접근하기도 쉬워지기 때문이다. 이러한 핵들을 잘 활용하면, 완성도 높은 고차원적인 프로그램도 잘 이해할 수 있을 것이다.

최근 들어 RSS와 웹로그(블로그)의 사용이 폭발적으로 증가하고 있다. 따라서“나만의 RSS 추적 프로그램 만들기”에 관한 핵 2개를 추가했다. 여기서 사용된 분석기(아주 간단한 자바스크립트 페이지 태그를 기초로 한 것으로, 펄 언어를 사용해서 작성했으며, “ 나만의 핵 만들기”를 조금 참고하여 만들었다)는 좋은 아이디어를 적용해서 확장하는 좋은 예이다. 현재로선 RSS 피드를 분석할 수 있는 클라이언트 프로그램이 없으므로, 독자가 이러한 분야에 관심이 있다면 이 핵들은 상당히 흥미로울 것이다.

사전지식

나만의 웹데이터 분석 프로그램 및 RSS 추적 프로그램을 만들기 위해서는 다음과 같은 것들을 사전에 알고 있거나 새로 배우려는 노력을 해야 한다.

  • 펄 프로그래밍 언어를 잘 다루면 유리하다. 물론, 본서에서는 잘 모르는 사람을 위한 설명도 곁들여 놓았다.
  • 파일 퍼미션을 설정할 줄 알고, 파일시스템의 구동 원리에 대해 이해하고 있으면 좋다.
  • 웹서버에 접근권한을 갖고 있고, 설정을 바꾸는 것 정도는 할 수 있는 것이 좋다.
  • 문서 헤더를 바꾸는 방법 등과 같은 기초적인 P3P 정책을 이해하고 있으면 도움이 된다.
  • 무엇보다도 끈기와 배우려는 열정이 중요하다.
프로그램을 구동하는 데 필요한 파일 및 소스코드는 http://www.webanalyticsdemystified. com/byo에서 얻을 수 있으며, 무료로 사용할 수 있는 오픈소스다.
신고
Posted by The.민군

ASP.NET - 데이터베이스에서 이미지를 가져오기

저자:한동훈

저번 기사에서는 데이터베이스에 이미지를 저장하는 방법을 살펴봤습니다. 이번 시간에는 데이터베이스에 저장된 이미지를 가져오는 방법을 살펴보겠습니다. 핵심 처리는 윈도우 응용프로그램이나 웹 응용프로그램에 관계없이 동일합니다.

먼저, SQL Server에서 이미지를 가져올 때 사용할 저장프로시저를 작성합니다. 저장프로시저의 이름은 "GetImage"입니다.
CREATE PROCEDURE GetImage  @idx intASBEGIN  SET NOCOUNT ON;  SELECT [FileName], [Image]  FROM pictures  WHERE idx = @idxEND
이미지의 인덱스(Idx) 값을 인자로 받아서 해당 인덱스에 저장된 이미지와 이미지의 원래 파일명을 가져옵니다.

다음은 ASP.NET 응용프로그램을 작성합니다. 파일 이름은 View.aspx로 합니다. 프로그램에서 사용할 네임스페이스를 추가합니다.
using System.Data.SqlClient;using System.IO;using System.Drawing;using System.Drawing.Imaging;using System.Text;
다음은 페이지를 로딩할 때 실행되는 Page_Load() 함수를 수정합니다. 가져올 이미지의 Idx 값은 View.aspx?idx=1 과 같이 쿼리스트링을 사용합니다. Idx 값이 없는 경우에는 프로그램 실행을 중단합니다. 프로그램 실행을 중단하기 위해 Response.End()를 사용했습니다.
protected void Page_Load(object sender, EventArgs e)  {    if (Request.QueryString["idx"] == null)    {      Response.End();    }    else    {      GetImage(Request.QueryString["idx"]);    }  }
쿼리스트링으로 전달된 값이 있으면 GetImage() 함수를 사용해서 나머지 처리를 수행합니다. GetImage() 함수의 구현은 다음과 같습니다.
private void GetImage(string idx)  {    SqlConnection sc = null;    SqlCommand scmd = null;    SqlDataReader sdr = null;    sc = new SqlConnection("Data Source=(local);         Initial Catalog=pictures; Integrated Security=True;");    scmd = new SqlCommand("GetImage", sc);    scmd.CommandType = CommandType.StoredProcedure;    SqlParameter spIdx = new SqlParameter("@idx", SqlDbType.Int);    spIdx.Value = idx;    scmd.Parameters.Add(spIdx);    sc.Open();    sdr = scmd.ExecuteReader();
SqlConnection 클래스를 사용해서 연결을 생성하고, SqlCommand 클래스에서는 GetImage 저장프로시저를 설정합니다. SqlParameter 클래스는 매개변수 idx를 설정합니다. 연결을 연 다음에는 ExecuteReader() 함수를 사용해서 쿼리를 실행하고 결과값을 SqlDataReader 클래스로 받습니다.
while (sdr.Read())    {      byte[] image = (byte[])sdr["Image"];      MemoryStream ms = new MemoryStream(image, 0, image.Length);      Bitmap bitmap = new Bitmap(ms);      Response.ContentEncoding = System.Text.Encoding.UTF8;      Response.ContentType = "image/jpeg";      Response.AddHeader("Content-Disposition", "attachment; filename="         + Server.UrlEncode(sdr["FileName"].ToString()));            bitmap.Save(Response.OutputStream, ImageFormat.Jpeg);    }
sdr.Read() 함수를 호출해서 데이터를 읽어옵니다. 보통은 Read() 함수를 호출해서 결과값이 NULL 인지 아닌지를 확인하는 if 문을 사용해야 하지만, 레코드의 값이 하나 뿐이기 때문에 while() 문을 사용했습니다. if 문을 사용할 때 보다 코드를 간결하게 할 수 있는 장점이 있기 때문에 레코드가 하나인 경우에만 사용합니다(여러 레코드를 처리하는 경우에는 코드 수정이 거의 없다는 장점도 있습니다).

데이터베이스에서 읽어온 이미지 정보는 이진 파일입니다. 따라서, 바이트의 배열로 변환합니다. Bitmap 클래스를 사용하면 데이터를 비트맵 이미지로 만들 수 있습니다. 생성자 중에 하나가 스트림을 받아서 이미지로 변환하는 기능이므로 이를 이용합니다.

파일로 전달되는 이미지가 아니기 때문에 브라우저에 이미지에 대한 정보를 알려줘야 합니다. 따라서, HTTP 헤더를 이용해서 이미지라는 것을 알려줍니다. 또한, 이미지의 파일이름을 알려주기 위해 헤더를 추가합니다. 마지막은 Save() 함수를 호출해서 이미지를 Response.OutputStream으로 출력하며, 이미지 포맷은 Jpeg으로 지정합니다.

다음은 데이터베이스에서 가져온 이미지를 화면에 출력하는 전체 소스코드입니다. 페이지는 View.aspx이며, UI에는 어떤 코드도 작성하지 않습니다.
using System;using System.Data;using System.Configuration;using System.Collections;using System.Web;using System.Web.Security;using System.Web.UI;using System.Web.UI.WebControls;using System.Web.UI.WebControls.WebParts;using System.Web.UI.HtmlControls;using System.Data.SqlClient;using System.IO;using System.Drawing;using System.Drawing.Imaging;using System.Text;public partial class View : System.Web.UI.Page{  protected void Page_Load(object sender, EventArgs e)  {    if (Request.QueryString["idx"] == null)    {      Response.End();    }    else    {      GetImage(Request.QueryString["idx"]);    }  }  private void GetImage(string idx)  {    SqlConnection sc = null;    SqlCommand scmd = null;    SqlDataReader sdr = null;    sc = new SqlConnection("Data Source=(local);       Initial Catalog=pictures; Integrated Security=True;");    scmd = new SqlCommand("GetImage", sc);    scmd.CommandType = CommandType.StoredProcedure;    SqlParameter spIdx = new SqlParameter("@idx", SqlDbType.Int);    spIdx.Value = idx;    scmd.Parameters.Add(spIdx);    sc.Open();    sdr = scmd.ExecuteReader();    while (sdr.Read())    {      byte[] image = (byte[])sdr["Image"];      MemoryStream ms = new MemoryStream(image, 0, image.Length);      Bitmap bitmap = new Bitmap(ms);      System.Drawing.Image im = System.Drawing.Image.FromStream(ms);      Response.ContentEncoding = System.Text.Encoding.UTF8;      Response.ContentType = "image/jpeg";      Response.AddHeader("Content-Disposition", "attachment; filename="         + Server.UrlEncode(sdr["FileName"].ToString()));            bitmap.Save(Response.OutputStream, ImageFormat.Jpeg);    }    sdr.Close();    sc.Close();    sc.Dispose();    scmd.Dispose();    sdr.Dispose();  }}
신고
Posted by The.민군

MySQL에서의 랭킹 데이터 최적화 - (3)

제공 :한빛 네트워크
저자 :Baron Schwartz
역자 :노재현
원문 :How to Optimize Rank Data in MySQL

[이전 기사 보기]
MySQL에서의 랭킹 데이터 최적화 - (2)
MySQL에서의 랭킹 데이터 최적화 - (1)

다른 방법에 비해서 이 방법이 얼마나 더 효율적일까?

이제 까지 설명했던 방법은 O(n) 알고리즘이라고 볼 수 있다.(물론 가장 좋은 케이스는 O(1)일 때이겠지만, 최악의 경우는 n의 크기가 커질 수록 안 좋아지게 된다) 대안으로 O(n2) 알고리즘을 이용하는 방법도 있다. 다음을 보자.
-- MySQL에서는 사용할 수 없는 쿼리 이다. 설령 된다고 하더라도 굉장히 느릴것이다.update score_ranked as o   set rank_in_game = (      select count(distinct score) from score_ranked as i      where i.score >= o.score and i.game = o.game);-- 이 쿼리 역시 실행 불가능하다.-- 최악의 쿼리이므로 절대 사용하지 말자.update score_ranked as o   set rank_in_game = (      select count(distinct score) from (select * from score_ranked) as i      where i.score >= o.score and i.game = o.game);update score_ranked as o   inner join (      select l.game, l.score, count(distinct r.score) as rank      from score_ranked as l         left outer join score_ranked as r on l.game = r.game            and l.score <= r.score      group by l.game, l.score   ) as i on o.game = i.game and o.score = i.score   set o.rank_in_game = i.rank;
위의 Query들이 여러분이 SQL Query로 할 수 있는 마지막 방법이라면, 문제를 해결 할 수 있는 다른 방법은 없을까? 한 가지 방법은 커서를 PHP와 같은 프로그래밍 언어로 흉내내는 방법이다. 무슨 의미인기하면 우선 전체 테이블의 데이터를 모두 읽어들인후에 각 row들에 대해서 루프를 돌면서 랭킹을 계산하게 된다. 그리고 계산된 결과를 MySQL 서버에 업데이트를 하게 된다. 하지만 이 방법은 정말 최악의 방법이기 때문에 사용하지 않는 것이 좋다.

컨설턴트로서 보건데 대부분의 사람들은 위의 방법에 실패하게 되면, 아마도 랭킹 테이블에서 랭킹 컬럼을 삭제하고 COUNT 명령과 오프셋 값을 가진 limit 명령을 사용하게 될 것이다. 하지만 이 방법 마저도 대부분은 잘 작동하지가 않아서 필자에게 도움을 요청하는 사람들이 많이 있었다.

이렇게 한 번 생각을 해보자: 필자가 제안했던 1번 시나리오에서 읽기 명령은 O(n)의 알고리즘에 해당되고, 쓰기 명령은 O(1)에 해당되게 된다. 여기서 COUNT 명령과 오프셋 값을 가진 LIMIT 명령을 제거하고 나면 읽기 명령은 O(1) 알고리즘에 해당되고, 쓰기명령은 O(n)에 해당되게 된다. 필자는 여기서 보통 쓰기 명령보다는 읽기 명령이 많을 것이라고 생각한다.

랭킹 컬럼은 정말 성능 향상에 도움이 되는 것일까?

항상 직감에만 의존해서 Query의 성능을 판단하는 것은 별로 좋은 방법이 아니다. 실제로 어떻게 Query가 실행이 되는지 쿼리 실행계획이나 Query를 실행해서 실행시간을 확인해 보는 것이 좋다. 또 프로파일링과 벤치마킹을 통해서 최적화된 쿼리가 얼마나 효과가 있었는지를 확인할 수 있다. 쿼리를 만든 후에 필자는 먼저 Query를 다른 컴퓨터(다른 프로세스를 거의 실행시키지 않고 있는) 먼저 프로파일링을 해보게 된다. 이렇게 해서 Query가 얼마나 부하를 가지는지를 확인해 볼 수가 있다. 이런 목적으로 필자가 MySQL의 Query Profiler를 만들게 되었다.

여기서 랭킹 게시판에서 사용하고 있는 세 개의 Query를 프로파일링을 해보았다. 1010번 유저는 전체 랭킹에서도 상위에 있는 유저 이기 때문에 반대로 낮은 랭킹을 가지고 있는 755번 유저도 같은 방법으로 프로파일링을 해보았다. 프로파일링 결과를 한 번 보도록 하자.

DesignDesign #1Design #2Design #3
Gamer101075510107551010755
Time0.030.20.050.20.20.01
Queries443322
Optimizer Cost21793217935177516322
Table locks555533
Table scans112210
Index range scans221101
Rows sorted320100004018208
Rows read309435998820403200512748239
Fixed reads32010000209208
Next-row reads2000220002422087289
Bookmark lookup10010100103222873414
Full scans initiated1100 0
Next in index610199752030920000100008
Temp table inserts1000010000401887278


잘 보니 몇몇 Query는 두명의 유저에 대해서 같은 인덱스를 이용하고 있는 것 같지 않다. 하지만 최적화에 들어가기 전에 랭킹 컬럼부터 시작해서 조금씩 속도를 증가시켜 보도록 하겠다. 위의 프로파일링 데이터를 보고 있자니 실행계획과 실행횟수에만 의존해서 Query의 성능을 측정하는게 꼭 맞는것만은 아니라는 것을 알 수 있다. 쿼리의 실행시간 자체는 크게 다르지 않지만 다른 수치들이 Query실행에 따라서 발생되는 부하가 얼마나 다른지를 보여주고 있다.

프로파일링 결과 자체는 전체 랭킹에서 상위 랭크가 포함된 페이지를 볼 때 더 좋은 성능을 보인다는 것을 보여주고 있다. 여러분의 애플리케이션이 이와 같은 패턴을 잘 이용해서 최적화를 한다면 성능에 크게 문제가 없겠지만 항상 이런 경우만 있을 것이라고는 장담할 수가 없다. 예를 들어, 낮은 랭킹을 가진 유저가 자기 자신의 랭킹을 자주 확인하려고 하는 경우에 성능에 크게 문제가 될 수가 있다. 그럼 이제 낮은 랭킹을 가진 페이지를 Query할때의 경우에 대해서 최적화를 해보도록 하자.

위의 결과를 보면 랭킹 컬럼을 이용할 때 프로파일링 결과 자체는 문제가 없어보이기 때문에 이제 벤치마킹을 해보도록 하겠다.

벤치마크 결과

필자가 Query 들의 성능을 더 자세히 알아보기 위해서 벤치마킹을 한 번 해보았다. 벤차마킹은 이전에 보았던 Query 들을 랜덤하게 선택해서 실행했다. 여기서 사용된 벤치마킹 코드는 이 글이 있는 사이트에서 다운로드 받을 수 있다.

벤치마킹에 사용한 데이터는 처음에 필자가 제시했던 100,000개의 데이터를 그대로 사용하였고, 대부분의 부하는 하나의 디스크와 하나의 CPU를 가지고 있는 필자의 컴퓨터의 CPU에 집중되었다.

여기서는 innotop을 이용해서 어떤 Query가 느린 성능을 보이는지를 확인해 보았다. 이전에도 말했지만 확실하게 COUNT 명령과 오프셋을 가진 LIMIT 명령어가 가장 느린 성능을 보이고 있었다.

동시에 많은 유저들이 접속하는 환경을 시뮬레이션 하기 위해서 몇 개의 쓰레드를 더 추가하고 랜덤하게 선택된 점수를 가지고 지속적으로 업데이트를 해보았다. 두 번째, 세 번째 시나리오의 경우에는 단순히 점수만 변경하지 않았고 랭킹 컬럼도 같이 업데이트를 하고 있다. 여기서는 부하가 많이 걸리는 연산이라고 판단이 되더라도 배치 업데이트를 이용하지 않고 매번 업데이트를 실행하도록 하고 있다.

이 테스트에서는 총 5번을 실행해서 평균을 구했다. 각각의 실행은 1부터 20까지 쓰레드의 수를 변경하면서 테스트를 진행하였고 각각의 쓰레드는 총 100페이지에 달하는 랭킹 게시판의 페이지를 요청하게 하였다. 그럼 벤치마킹한 결과를 보도록 하자.

Read ThreadsDesign #1Design #2Design #3
Reads Only1 Writer5 WritersReads Only1 Writer5 WritersReads Only1 Writer5 Writers
124.1416.9910.2095.6652.2340.003071.691614.221690.79
213.9412.8511.2549.8339.5124.901567.32999.32898.81
39.739.188.0034.4826.4523.311052.53647.87639.45
48.217.526.0724.4524.3715.93783.22468.53549.41
56.595.775.0520.6116.1713.14622.64410.76410.57
65.775.074.2216.1416.6810.32510.60416.45375.16
74.784.333.7614.4515.067.82431.09373.96230.78
84.393.763.3312.0512.176.41382.65344.19248.74
94.003.302.9611.0411.185.15344.93278.55188.06
103.493.022.699.569.514.82307.32231.17164.88
113.142.702.468.699.514.00273.30227.49186.22
122.902.502.258.397.303.62247.84215.50134.52
132.372.332.107.406.583.50226.10200.11186.03
142.162.161.966.625.963.08216.78182.34178.42
151.912.011.857.355.162.96195.52185.21156.92
161.741.881.716.704.752.90180.02165.87151.03
171.661.751.615.914.402.53181.98130.18135.61
181.511.661.535.083.972.44157.87147.98117.41
191.421.571.454.943.722.33149.26147.36106.36
201.321.501.385.093.522.18140.16134.02114.53


시나리오 3번의 성능이 얼마나 뛰어난지 보자. 이 성능 측정결과가 바로 나의 최적화에 대한 육감을 입증하고 있다. 최적화에 가장 중요한 핵심은 바로 많은 row를 검색하는 부분을 제거하는 것이다.

그것말고도 랭킹 컬럼에 대한 업데이트가 성능에 끼치는 영향을 파악하는 것도 중요하다. 왜냐하면 위의 결과 자체가 좋지 않았을 경우 랭킹 컬럼을 추가하는 방법 자체가 좋지 않은 방법이었기 때문이다. 위에서 보듯이 업데이트 Query로 인해서 성능의 하락이 조금 있었는데 바로 이 것으로 업데이트 성능을 향상시키는게 얼마나 중요한 것인지를 알 수 있다.

rank_in_game 컬럼에 대한 부하 측정값을 보면 균일하지 않은것을 볼 수 있다. 이전에도 설명했듯이 일부 업데이트는 아주 가볍게 실행할 수 있지만, 다른 일부 업데이트는 DB에 부하가 상당히 많이 가는 명령어 일 수가 있다. 위의 벤치마크 스크립트에 있는 몇몇 Query중에 부하를 발생시키는 쿼리들 때문에 이런 균일하지 않은 결과가 나타나게 된것이다.

시나리오 2번과 3번을 결과를 보면 시나리오 1번의 업데이트와 동일한 문제로 퍼포먼스가 떨어져 보이는 것 처럼 보인다. 하지만 실제 상황에서는 위와 같은 결과가 나오지 않을 것이다. 진짜 문제는 랭킹 정보를 포함한 업데이트가 필자의 벤치마킹에서는 최적화가 되어 있지 않기 때문에 느린 성능을 보이고 상대적으로 랭킹 정보를 포함하지 않은 업데이트에서는 빠르게 업데이트를 할 수 있기 때문이다. 실제 상황에서는 시나리오 1번이 가장 느린 성능을 보일 것이고, 시나리오 2번과 3번이 큰 성능향상을 보일 것이다. 그러나 지금으로서는 벤치마크 프로그램을 수정하고 다시 실행하지 않는한 이 말을 증명하기 싶지가 않다.

필자가 백만개의 row를 가지고 다시 벤치마크 프로그램을 시작했을때는 결과를 모두 다 볼 수가 없엇다. 1번 시나리오의 실행속도가 현저하게 느렸기 때문에 실행이 다 끝나기까지 기다릴 수가 없었고 중간에 얻은 결과로만 봤을때 1번 시나리오는 테이블이 커질 수록 점점 더 안 좋은 결과를 보였다. 동시에 5개의 리더(Reader)를 이용해서 랭킹게시판의 페이지를 요청했을때 1번 시나리오는 초당 0.1페이지를 가져온 반면에 2번 시나리오는 2.5페이지, 3번 시나리오는 420페이지를 가져왔다.

이 부분에 대해서 필자에게 궁금한 점이 있으면 언제든지 연락하기 바란다.

더 나은 방법을 찾아서

위에서 나온 벤치마킹 결과를 보면 랭킹 테이블에 랭킹 컬럼을 넣는 것만으로도 많은 성능향상이 있음을 알 수 있다. 이 결과는 곧 조금 더 연구해 보면 더 좋은 결과를 얻을 수도 있다는 것을 의미하기도 한다. 여기서는 필자가 비록 테스트해보고 벤치마크의 결과를 보여주지는 못하지만 생각해본 아이디어를 한 번 공유해 보고자 한다.

랭킹 테이블에 pos_in_game이라는 위치값을 넣어보면 어떨까? 이 값을 넣으면 랭킹 게시판을 20개 단위로 나누어서 페이지를 요청할 때 좀 더 효율적으로 처리할 수가 있다. 물론 단점도 있다. 그건 테이블의 pos 값을 유지하기가 쉽지 않다는 것이다.

더 좋은 방법은 점수 테이블을 작게 만들고 랭킹 테이블을 따로 만들어서 관리하는 것이다. 이 방법을 사용하게 되면 전체적으로 테이블의 크기를 크게 줄일 수가 있는데, 그건 InnoDB가 primary key에 대한 컬럼 정보를 포함한 추가적인 인덱스를 점수 테이블에 생성하기 때문이다. 추가적인 인덱스로 인한 크기 증가가 보통은 허용할 만한 수준이기는 하지만 이 경우에는 인덱스의 크기가 필자가 원하는 것보다 더 크게 된다. rank_in_game 컬럼의 추가만으로도 데이터와 인덱스의 크기가 78~102메가 바이트까지 증가하게 된다.

랭킹 테이블을 점수 테이블의 primary key를 가지고 있어야 한다. 이렇게 되면 위의 예시 때의 인덱스의 크기보다 더 작은 크기를 유지할 수 있다.

또 count 컬럼을 만들어서 동일한 점수를 가지는 row가 얼마나 있는지를 점수 테이블에 기록할 수도 있다. 이렇게 되면 점수 테이블과 랭킹 테이블에서 COUNT() 명령을 사용하지 않아도 된다. 대신에 SUM()명령을 이용해서 같은 효과를 볼 수 있게 될 것이다. 테이블에 중복된 데이터가 많을 수록 더 많은 효과를 볼 수도 있고 추가적으로 SUM에 대한 결과도 테이블에 추가 컬럼을 만들어서 저장해 두게 되면, 원하는 점수에 대한 랭킹도 알 수 있고 점수 테이블에 얼마나 많은 동일한 row가 존재하는지도 쉽게 알 수가 있게 될 것이다.

쿼리 캐시 기능을 이용해 보는건 어떨까?

스마트 캐싱이 보통 많은 문제점을 해결하는데 도움이 되기는 하지만 MySQL의 쿼리 캐시는 위와 같은 경우에는 도움이 되지 않는다. 하나의 row를 업데이트 하게 되면 캐시되었던 쿼리들이 모두 사용할 수 없게 되고, 이로 인해서 더 많은 성능 저하를 보이기도 한다. 보통은 이런 경우에 쿼리 캐시 기능을 비활성화하는 것이 성능향상에 더 도움이 된다.

결론

이 글에서 MySQL을 이용해서 랭킹 데이터를 페이지 단위로 보여주고, 관리하는데 필요한 최적화 방법에 대해서 다루었다. 이와 같은 문제는 보통 해결하기가 만만치가 않은 문제이다. 그리고 비정규화를 이용할 경우 관리상에 부하가 많이 발생한다는 것도 보였었는데, 만약에 읽고 쓰는 양이 정말 많은 경우라면 비정규화를 사용해 보는 것도 도움이 될 것이다. 필자가 보여준 벤치마크 결과로 볼때 아주 간단한 디자인 만으로도 상당한 성능향상을 이룰 수 있다는 것을 알 수 있다. 데이터가 더 많은 경우에 진짜 제대로 된 성능향상을 볼 수 있을 것이고, 필자가 제안한 다른 방법들을 이용하면 더 뛰어난 성능 향상을 이루어 낼 수도 있다.

이 글에서 다룬 내용이 모든 데이터를 최적화 하는데 적용될 수는 없지만, 이 글로 인해서 여러분의 최적화 과정에 많은 도움이 되었으면 한다.
신고
Posted by The.민군

MySQL에서의 랭킹 데이터 최적화 - (1)

제공 :한빛 네트워크
저자 :Baron Schwartz
역자 :노재현
원문 :How to Optimize Rank Data in MySQL

컴퓨터 게임을 하는 사용자의 랭킹을 관리하는 사이트가 있다고 하자. 이 사이트의 랭킹 게시판에는 최상위의 점수를 가진 유저부터 시작해서 차례대로 유저들의 정보가 게시가 되고 있다. 웹 사이트는 PHP로 개발되었고, MySQL 5 데이터베이스를 이용하고 있다. 또 정보가 자주 변경되는 사이트 이기 때문에 MySQL의 InnoDB를 사용하고 있다.

위와 같은 가정은 주로 인기도라던가 점수와 같은 기준을 가지고 랭킹 데이터를 페이징해서 출력해야 하는 애플리케이션에서 많이 사용하게 되는 전형적인 시나리오라고 볼 수 있다.

보통 게임 사이트의 유저수는 수 백만에 달하고, 이 많은 유저들을 한 화면에 표시할 수 없기 때문에 페이징을 사용하게 된다. 이 말인즉 랭킹 데이터를 잘 정렬한 후에 일정 수의 데이터만 표시하기 위해서 제한 수치를 설정해야 함을 의미한다. 말이야 정말 쉽다. 하지만 실제로 위의 데이터를 추출해 내는 과정은 생각만큼 만만치 않은 과정이다. 아마도 간단하게 생각해 낼 수 있는 SQL 쿼리문으로는 데이터베이스가 견뎌내지 못할 것이고, 유저 데이터가 더욱 많이 늘어날 수록 상황은 더욱 더 악화될 것이다.

이 글에서는 세 가지 해결책을 제시하고자 한다. 일반적인 방법으로 디자인하게 될 경우 아주 안 좋은 성능을 낼 수 있지만, 분명히 해결책은 존재한다.

여기서 소개하는 디자인은 아마 중간급 규모의 사이트에서 유용하게 사용할 수 있을 것이다. 중간급이라고 해서 절대 작은 규모라고 생각하지 말길 바란다. 중간급이지만 최적화되지 않은 한 대의 MySQL 가지고는 택도 없을 수준의 규모라고 생각하면 된다. 그렇다고 여러대의 클러스터를 구성해야 할 만큼 거대하지도 않으니 겁먹진 말기 바란다. 단지 여러분의 사이트에서 어떻게 효율적인 랭킹 데이터 애플리케이션을 만들어 낼 수 있는지를 제시해보고자 함이니 말이다.

사이트 요약

더 나가기 전에 우리가 구성하고자 하는 사이트에 대해서 더 자세히 알아보도록 하자. 우선 10,000명의 유저의 점수를 관리할 것이고, 유저는 다섯 국가에 있는 10가지의 게임을 하게 된다. 여기서는 모든 유저들이 모든 게임을 한다고 가정하겠다. 그러므로 총 100,000개의 점수 기록을 관리하게 되게 된다. 이 글에서는 일부러 유저의 수와 게임의 수를 작게 나타내었다. 너무 크면 여러분의 컴퓨터에서 테스트해보기가 힘들 수도 있기 때문이다.

이 사이트에서 가장 중요한 페이지는 바로 유저들의 랭킹 게시판이다. 이 게시판은 유저의 랭킹을 나라, 게임, 전체와 같은 기준으로 내림차순으로 표시하게 된다. 예를 들어, 1010이라는 유저가 2번 나라에 살고 있고, 1번 게임을 한다고 할 수 있다. 그리고 그 유저는 자신의 나라에서 1번 게임을 기준으로 51위에 랭크되어 있고, 세계에서는 290위에, 세계에 있는 전체게임을 기준으로 할때에는 1867위에 랭크되어 있다.

각각의 랭킹 게시판에서는 한 번에 20명의 유저들을 볼 수 있게 되어 있다. 예를 들어, 1010이라는 유저의 프로필에서 1010 유저의 랭킹 게시판으로 이동하는 링크가 있다고 할때, 이 링크를 클릭하게 되면 1010 유저가 1번 게임에서 51위에 랭크되어 있기 때문에, 랭킹 게시판의 3번째 페이지가 나타나게 될 것이다.

데이터베이스 디자인

아래는 이 글에서 사용하게될 데이터베이스 스크립트이다. 이 스크립트를 실행하게 되면 우리가 필요한 테이블과 이 테이블에 rand() 함수를 이용해서 무작위로 데이터를 생성하게 된다. rand() 함수를 호출할 때 seed 값으로 0을 주었기 때문에 필자가 테스트할 때 사용한 데이터와 동일한 데이터를 가지고 테스트 해 볼 수 있을 것이다.
set @num_gamers    := 10000,    @num_countries := 5,    @num_games     := 10;drop table if exists gamer;drop table if exists game;drop table if exists country;drop table if exists score;drop table if exists semaphore;create table semaphore(i int not null primary key);insert into semaphore(i) values (0);create table gamer(   gamer int not null,   country int not null,   name varchar(20) not null,   primary key(gamer)) engine=InnoDB;create table game(   game int not null,   name varchar(20) not null,   primary key(game)) engine=InnoDB;create table score(   gamer int not null,   game int not null,    score int not null,    primary key(gamer, game),   index(game, score),   index(score)) engine=InnoDB;create table country(   country int not null,   name varchar(20) not null,   primary key(country)) engine=InnoDB;-- 샘플 데이터를 생성하기 위해서 integers 테이블을 사용drop table if exists integers;create table integers(i int not null primary key);insert into integers(i) values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9);insert into country(country, name)   select t.i * 10 + u.i, concat('country', t.i * 10 + u.i)   from integers as u      cross join integers as t   where t.i * 10 + u.i < @num_countries;insert into game(game, name)   select t.i * 10 + u.i, concat('game', t.i * 10 + u.i)   from integers as u      cross join integers as t   where t.i * 10 + u.i < @num_games;insert into gamer(gamer, name, country)   select th.i * 1000 + h.i * 100 + t.i * 10 + u.i,      concat('gamer', th.i * 1000 + h.i * 100 + t.i * 10 + u.i),      floor(rand(0) * @num_countries)   from integers as u      cross join integers as t      cross join integers as h      cross join integers as th;insert into score(gamer, game, score)   select gamer.gamer, game.game, floor(rand(0) * @num_gamers * 10)   from gamer      cross join game;
랭킹 게시판의 페이징 처리를 위한 전형적인 쿼리

1010본 유저의 랭킹을 보여주는 페이지를 시작으로 SQL을 만들어보자. 유저 1010이 위치하는 페이지를 찾기 위한 가장 간단한 방법으로부터 시작을 하면, 우선 랭킹 게시판에서 몇 번째 페이지에 존재하는지를 찾는다. 그 다음으로 그 페이지에 해당되는 20명의 유저를 1번 게임의 점수를 기준으로 내림차순으로 찾는다. 바로 이 방법이 첫 번째 방법이다. SQL로 표현하면,
-- 게임 1번의 1010번 유저를 찾는다. 결과 : 97101select score from score where gamer = 1010 and game = 1;-- 1010번 게이머가 몇 번째에 위치하고 있는가? 결과 : 309.-- 동점인 경우는 게이머의 ID를 이용해서 구분한다.select count(*) from scorewhere score.game = 1 and score.score >= 97101;-- 1010번 게이머가 위치하게 될 랭킹 게시판 페이지의 데이터를 찾는다.select gamer.gamer, gamer.name, score.scorefrom gamer   inner join score on gamer.gamer = score.gamerwhere score.game = 1order by score.score desclimit 300, 20;-- 찾은 페이지의 제일 처음에 나오는 결과값의 랭킹을 찾는다. 결과값 : 284.select count(distinct score.score)from scorewhere score.game = 1 and score.score >= 97101;
마지막 Query는 랭킹 게시판에 표시할 랭킹을 알아내기 위해서 사용한다. 그 위의 Query에서는 표시할 row 들을 가져올 수는 있지만, 몇 번째 랭킹인지까지는 가져올 수가 없다. 그래서 위의 Query를 이용해서 페이지의 첫 번째 랭킹정보를 받아온 후에 차례차례로 다음row를 출력하면서 바로 이전의 유저정보의 게임 점수와 점수가 다른 row가 나오게 되면 랭킹을 1씩 증가시켜 주어야 한다.

더 나은 두가지 디자인

이전의 Query는 단지 20개의 row를 가져오기 위해서 너무 많은 검색을 하고 사용하지 않는 row를 버리고 있다. 이렇게 낭비적인 검색말고 더 좋은 방법은 없을까?

우선 사용하게 될 Query는 랭킹과 그 랭킹이 위치하고 있는 Offset 값을 알아야만 한다. 그런데 안타깝게도 우리가 사용하고 있는 InnoDB는 COUNT() 명령에서는 그다지 빠른 성능을 내지를 못한다.

또 다른 문제는 Offset 값을 포함한 LIMIT 명령이 있는 Query를 어떻게 최적화 시킬것인지 이다. “LIMIT 20”와 같이 사용하는 경우에는 아무 문제가 없다. 하지만 LIMIT 2000, 20과 같이 사용하게 될 경우 검색하는 동안 받아온 row들의 99%를 버리고 20개의 row만을 취하게 되기 때문에 낭비가 굉장히 커지게 된다. 그래서 여기서는 LIMIT명령에 있는 offset 값을 버리고 대신에 "ORDER BY" 명령이 인덱스를 타도록 하게 하고 싶다.

이렇게 하기 위해서 필자는 테이블이 비록 비정규화 되더라도 새로운 column을 하나 추가하도록 하려고 한다. 바로 인덱스된 랭킹 column을 추가하는 것이다. 이렇게 해서 추가되는 관리비용은 잠시 뒤로 물리도록 하자. 바로 다음이 두 번째 해결책이다.
-- 위에서 설명된 바와 같이 게임 점수와 랭킹을 찾는다.select score, rank_in_game from score_ranked where gamer = 1010 and game = 1;-- 랭킹게시판에서의 유저의 위치를 찾는다.  결과 값 : 309select count(*) from score_rankedwhere game = 1 and score >= 97101;select gamer.gamer, gamer.name, u.score, u.rank_in_gamefrom gamer   inner join (      (          -- Fetch start (first 9 rows) of leaderboard.         select gamer, score, rank_in_game         from score_ranked         where game = 1 and rank_in_game <= 290 and gamer <= 1010         order by rank_in_game desc, gamer desc         limit 9      )      union all      (         -- Fetch remaining 11 rows.         select gamer, score, rank_in_game         from score_ranked         where game = 1            and ((gamer > 1010 and rank_in_game = 290) or rank_in_game > 290)         order by rank_in_game, gamer         limit 11      )      order by rank_in_game, gamer   ) as u on gamer.gamer = u.gamer;
좀 복잡해 보이기는 하지만, 이전 Query에 비하면 효과가 있다. Query에서 COUNT() 명령을 제거하였고, LIMIT 명령에서 사용하고 있던 Offset 값또한 사용하지 않게 되었다. 이제 위에서 작성된 Query를 수행하게 되면 랭킹을 포함한 게시판에 필요한 row들을 얻을 수가 있다.

그런데 제일 위에 랭킹 페이지를 20으로 정확하게 나누어 떨어지게 하기 위해서 사용하는 COUNT()명령이 하나 더 남아있다. 꼭 20으로 나누어 떨어지도록 출력해야 하는게 아니라면 이 COUNT() 명령 마저도 제거할 수가 있다. 이 방법이 세 번째 시나리오이다.
-- 위에서 설명된 바와 같이 게임 점수와 랭킹을 찾는다.select score, rank_in_game from score_ranked where gamer = 1010 and game = 1;-- 1010번 게이머가 속한 랭킹 게시판의 페이지 데이터를 찾는다.select gamer.gamer, gamer.name, u.score, u.rank_in_gamefrom gamer   inner join score_ranked as u on gamer.gamer = u.gamerwhere game = 1 and rank_in_game >= 290 and u.gamer >= 1010order by rank_in_game, gamerlimit 20;
더 많이 좋아졌다. 이 Query를 이용하면 최소한의 row를 읽어서 우리가 원하는 랭킹 정보를 얻어낼 수가 있게 되었다. 다른 종류의 랭킹 게시판(예를 들면, 나라를 기준으로 혹은 전체 랭킹 정보 등)또한 같은 방법으로 최적화가 가능할 것이다.

혹시 20개 단위로 페이징을 꼭 해야만 한다면, Query를 시작하기 전에 20개 단위로 페이징을 해야할 위치 값을 먼저 Query로 받아온 후에 이 값을 재사용할 수 있을때까지 유지하면서 사용하는 것이 좋을 것이다.
신고
Posted by The.민군

MySQL에서의 랭킹 데이터 최적화 - (2)

제공 :한빛 네트워크
저자 :Baron Schwartz
역자 :노재현
원문 :How to Optimize Rank Data in MySQL

랭킹 컬럼을 추가하는 방법

랭킹 정보를 가지고 있는 Column 사실 일종의 캐시라고 봐야 한다. 이렇게 랭킹 정보를 저장하게 되면 검색을 할때 보다 빠르게 할 수 있게 된다. 하지만 이 방법에도 안 좋은 점이 있다. 바로 업데이트를 빠르고 부하가 가지 않도록 할 수 있는 방법이 없다면 랭킹 테이블을 관리하는데 상당히 힘들어지게 된다. 필자는 여기서 테이블을 하나 더 만들어서 인덱스를 더 크게 만들고 SELECT Query에서 하고 있던 일을 다른 곳으로 옮겨서 부하를 더 줄여보고자 한다. 이렇게 하는 것이 데이터의 변경이 자주 발생하지 않는 상황에서는 아주 좋은 방법이 될 수 있다. 필자가 초반에 말했듯이 우리가 구성하고자 하는 사이트는 업데이트가 자주 발생하게 될 것이기 때문에 이 방법을 사용 가능하게 만들기 위해서 빠르게 업데이트 할 수 있는 방법이 꼭 있어야만 한다.

다행스럽게도 방법은 있다. 두 가지 방법을 이용하게 되는데, 하나는 MySQL의 유저 변수를 이용해서 쓰기 커서를 시뮬레이션함으로써 랭킹을 한 번에 계산해 내는 것이다. 그리고 두 번째 방법은 스마트 통합 업데이트라는 것을 이용한다. 데이터의 업데이트를 하기전에 이 업데이트가 얼마나 큰 부하를 가져오는지를 계산을 해서 빠르게 업데이트 할 수 있는 경우에만 바로 업데이트를 실행하는 것이다.

다음은 필자가 이전의 Query에서 사용한 rank_in_game 이라는 Column을 추가할 때 사용한 방법이다.
drop table if exists score_ranked;create table score_ranked like score;alter table score_ranked add rank_in_game int not null default 0,   add key(game, rank_in_game);
여기서는 기존의 테이블을 수정하기 보다는 새로운 테이블을 하나 만들어 내고 있다. 이렇게 하면 나중에 수정된 테이블과 수정되기전의 테이블을 가지고 벤치마크를 할 수가 있다.

이제 테이블에 데이터를 넣을 차례이다. 여기서 바로 위에서 말한 MySQL의 유저 변수를 사용해서 데이터를 넣으면서 랭킹을 계산하는 방법을 보게 될 것이다.
set @rank := 0, @game := null, @score := null;insert into score_ranked(gamer, game, score, rank_in_game)select gamer, game, score,   greatest(      @rank := if(@game = game and @score = score, @rank, if(@game <> game, 1, @rank + 1)),      least(0, @score := score),      least(0, @game  := game)) as rankfrom score order by game desc, score desc;
좀 이해하기 어려울 것이지만 걱정하지 말자. 이제 곧 설명할 것이다.

유저 변수 값을 가진 쿼리가 작동하는 방식

MySQL의 유저 변수는 변경과 사용이 동시에 가능하기 때문에 이런 기능을 이용하면 무언가를 만들어낼때 정말 유용하게 사용될 수가 있다. 물론 여기서 간단하게 설명을 하겠지만, 필자의 웹사이트에 MySQL 유저 변수에 관한 글을 몇 개 올려놓았으니 참고하는데 도움이 되었으면 좋겠다. 그럼 우선 Query가 어떻게 작동하는지부터 살펴보도록 하자.
select gamer, game, score,   greatest(      -- 해당 row의 랭킹 값을 찾는다.      @rank := if(@game = game and @score = score, @rank, if(@game <> game, 1, @rank + 1)),      -- Save score and game for next row      least(0, @score := score),      least(0, @game  := game)) as rankfrom score order by game desc, score desc;
GREATEST() 함수는 우선 생각하지 말고 안쪽부터 하나하나 보도록 하자. 우선 유저 변수 선언문은 모든 row에 대해서 실행되게 된다. 즉, 모든 결과를 모아놓고 실행하는 것이 아니고 각 row에 대해서 따로따로 생성될 때 실행을 하게 된다.

다음으로 이 Query는 score 테이블에서 모든 row를 랭킹 순으로 읽어들이게 된다. 당연히 이 Query가 정상적인 결과를 생성해내기 위해서는 row들이 순서대로 잘 차례차례 생성되어야 한다.

Query안에 있는 @rank 변수는 1부터 시작해서 매번 row를 검사할 때 마다 이전 @rank 변수의 값과 현재 읽어온 row의 rank 값을 비교해서 다를 경우 @rank 변수의 값을 1만큼 증가시켜 주고 있다. 그리고 각 row 마다 현재 게임과 점수가 같은지를 확인하고 있다. 게임도 같고 점수도 같을 경우 @rank 변수의 값은 증가시키지 않아도 되기 때문에 그대로 놔두게 되고, 게임이 변경된 경우에는 @rank 변수의 값을 1로 초기화 시키고 있다. 그리고 게임은 같지만 점수가 다를 경우에는 @rank 변수의 값을 1만큼 증가시켜 주고 있는 것을 볼 수 있다.

현재 row의 랭킹 값을 알아낸 후에는 이 랭킹값을 다음 row의 값과 비교하기 위해서 저장해 놓아야 하는데, 이 값을 select 리스트에 있는 column 값에 저장할 수는 없다. 우리의 테이블에는 이 값을 저장하기 위한 column이 없기때문이고, 그래서 여기서는 LEAST(0, ...) 함수안에 넣게 되었다. 그리고 이 모든 것을 GREATEST() 함수안에 넣었다. 이렇게 하면 @score와 @rank 값 지정문이 정상적으로 호출이 되고 사용이 끝나면 소거되게 된다.

GREATEST() 함수는 새로 계산된 @rank 변수 값을 리턴하게 된다.

랭킹 컬럼의 값을 관리하는 방법

여기까지 rank column을 초기화하는 부분에 대해서 설명을 했는데, 이제는 UPDATE 명령을 이용해서 score 테이블의 rank column의 값을 업데이트하는 것에 대해서 설명을 하겠다. 여기서도 이전과 마찬가지로 MySQL의 유저 변수 값을 이용할 수가 있다. 보통 UPDATE 명령에 있는 SET 절에 유저 변수를 바로 사용할 수는 없지만 함수를 이용하면 가능하게 된다.
set @rank := 0, @pos := 0, @game := null, @score := null;update score_rankedset rank_in_game =    greatest(      @rank := if(@game = game and @score = score, @rank, if(@game <> game, 1, @rank + 1)),      least(0, @pos   := if(@game = game, @pos + 1, 1)),      least(0, @score := score),      least(0, @game  := game)),   pos_in_game = @posorder by game desc, score desc;
이 Query를 수행하게 되면 테이블의 모든 row에 업데이트 명령을 수행하게 된다. 모든 row를 업데이트 하고 싶은 경우에 이 Query를 사용하면 좋을 것이다.

다른 DB를 사용해본 독자라면 이런 명령을 DB에 있는 커서를 이용해서 해본 독자도 있을 것이라고 생각한다. MySQL 5 같은 경우 커서를 지원하기는 하지만 읽기 전용이라 이런 용도로 사용할 수는 없다. 위의 방법은 바로 쓰기 커서를 시뮬레이션 한 것이라고 보면 된다.

업데이트 시나리오

위의 방법은 좀 비효율적이다. 모든 row를 매번 업데이트 할 필요는 없지 않은가. 그래서 여기서는 각각 최적화된 네가지 시나리오를 제시해 보고자 한다.

시나리오 1. 가장 좋은 경우. 이 경우는 insert 하거나 delete 하려는 데이터가 다른 데이터에 영향을 끼치지 않는 경우이다. 예를 들어, 1번 게임의 1010 번 유저의 점수를 삭제하려고 한다고 하자. 테이블에 1010 번 유저와 동일한 점수를 가진 유저가 있다면 다른 랭킹 정보를 업데이트 하지 않아도 아무 문제가 없게 된다. 물론 새로운 데이터를 넣으려고 하는 경우에도 같은 점수를 가진 유저가 테이블에 이미 존재했었다면 다른 데이터를 변경시키지 않아도 괜찮다.

시나리오 2. 한 개의 row만 업데이트가 필요한 경우. 예를 들어, 1번 게임의 1010번 유저의 점수가 97084로 줄었다고 하자. 이 때 이미 같은 점수를 가진 유저가 존재한다면 랭킹 정보를 업데이트 할 필요가 없다. 단지 이동한 1010번 유저의 랭킹만 업데이트를 해주면 된다. 또 다른 예로, 점수가 오르거나 줄어들었지만 해당 유저의 위 혹은 아래에 있는 유저의 점수를 벗어나지 않은 경우이다. 예를 들어 아래 테이블을 보도록 하자. 1번 게임의 랭킹 291 ~ 294를 보자. 만약에 2805번 유저의 점수가 97070으로 오른다고 할때, 다른 row에는 아무런 영향을 끼치지 못하게 된다. 왜냐하면 2805번 유저의 랭킹은 여전히 294가 되게 될 것이기 때문이다.

gamergamescorerank_in_game
9094197084291
7462197076292
4839197075293
2805197062294


시나리오 3. 일정 범위의 row 들이 업데이트가 되어야 하는 경우. 위의 테이블에서 4839번 유저의 점수가 97080으로 변경되었다고 하자. 그럼 4938번 유저는 랭킹이 292가 되게 되고, 7462번 유저는 한단계 아래의 랭킹인 293이 되게 된다. 고로 2개의 row가 영향을 받게 된다. 만약에 2805번 유저의 점수가 97078이 되었다고 하자. 이번에는 7462번, 4839번 유저의 랭킹이 한 단계씩 아래로 떨어져야 하고 3개의 row가 영향을 받게 된다. 하지만 모든 row를 업데이트하는것에 비하면 변화가 국지적인것을 알 수 있다.

마지막으로 시나리오 4. 업데이트 되는 row보다 점수가 낮은 모든 row들이 영향을 받게 되는 경우. 1010번 유저의 점수가 97102로 업데이트가 되었다고 했을때 여전히 290번째 위치에 있게 되지만, 1010번 유저보다 점수가 낮은 모든 유저들의 랭킹을 1만큼 떨어트려서 업데이트를 해주어야 한다. 최악의 경우 테이블 내의 모든 데이터를 업데이트 해야할 수도 있다.

각 시나리오에 해당하는 경우를 구분하는 방법은 어렵지 않다. 다음의 inserts, deletes, updates Query에 해당하는 간단한 Query가 몇 개 필요할 뿐이다. 첫 번째 Query 문이 수행해야하는 작업이 부하가 큰 작업이라고 판단하게 되면 다른 Query 문을 이용해서 시나리오에 따른 각각의 작업을 수행할 수 있게 된다. 예를 들어, 다음 Query 문은 1번 게임에 97101의 점수를 가진 유저를 넣는다고 할때 이 작업이 부하가 큰 작업인지 아닌지를 구분하게 된다.
select count(*) as cheap,   coalesce(      avg(rank_in_game),      (         select rank_in_game         from score_ranked use index(score)         where game = 1 and score < 97101         order by score desc limit 1      )   ) as rankfrom score_ranked where game = 1 and score = 97101;
Query가 부하가 크지 않은 작업일 경우에 select 문에 있는 cheap에 0이 아닌 값을 리턴하게 되어 있다. cheap의 값이 0이 나오게 되는 경우에는 서브 Query가 해당 row를 넣어야 하는 범위를 알려주게 되는데, 이 값을 보고 부하가 큰 작업일지 아닐지를 판단하는 것은 바로 여러분의 몫이 된다.(통계적인 방법을 이용할 수 있을지도 모른다) 대부분의 삽입 명령어는 신규 유저의 유입으로 발생되는 명령이라는 것을 감안할 때 삽입해야 하는 데이터의 점수가 낮다면 랭킹 테이블의 끝 부분에 들어갈 확률이 높을 것이고, 이런 경우에는 일부의 row 들만 업데이트 함으로써 랭킹 테이블의 데이터를 유지할 수 있다.

유저 변수가 없는 일반 Query로도 위의 랭킹 데이터를 유지할 수가 있다. 예를 들어서, 위의 테이블에서 2805번 유저의 점수를 97085로 업데이트 하게 될 경우 세 명의 유저의 점수도 같이 업데이트를 시켜줘야만 하게 된다. 이 업데이트는 다음과 같은 Query로 할 수가 있다.
update score_ranked   set score = if(gamer = 2805, 97085, score),       rank_in_game = if(gamer = 2805, 291, rank_in_game + 1)where game = 1 and rank_in_game between 291 and 294;
이와 마찬가지로 다른 경우에 대한 Query 또한 쉽게 작성할 수가 있다. 이런 최적화의 경우에 단일 row의 업데이트일 경우에 최적의 효과를 내지만, 몇몇 Query들은 테이블의 많은 영역을 업데이트 해야할 만큼 부하가 클 수도 있다. 어쩌면 테이블 전체의 업데이트를 해야할 수도 있는데 이런 경우에는 실시간으로 업데이트를 하는 것보다는 모아 놓았다 배치 업데이트를 하는 것이 효율적이다. 여기서 유저 변수를 포함한 Query를 사용하게 되면 부하가 큰 Query들을 배치작업으로 모아놓는 작업도 어렵지 않게 할 수 있지만, 유저 변수를 사용하지 않고 하게 되면 몇몇 Query들을 위의 설명과 같이 느린 연산을 통해서 해야만 하게 될 것이다.
신고
Posted by The.민군

12..JSTL #1

JSTL 2007.04.11 03:01
javacicle-jsp.zip
jstl-1_1-mr2-spec.pdf

1. JSTL(JSP Standard Tag Library )                                                          


http://java.sun.com/products/jsp/jstl/index.jsp


자바의 표준을 정하는 JCP 에서는

태그라이브러리가 우후죽순(?) 사용되는것에 대해

몇몇개의 표준태그라이브러리를 제시하고 있습니다.


이것을 JSTL(JSP Standard Tag Library )이라고 하는데,

아래와 같은 다섯개의 태그라이브러리 집합을 의미합니다.

  • Core:http://java.sun.com/jsp/jstl/core                     
  • XML:http://java.sun.com/jsp/jstl/xml
  • Internationalization:http://java.sun.com/jsp/jstl/fmt
  • SQL:http://java.sun.com/jsp/jstl/sql
  • Functions:http://java.sun.com/jsp/jstl/functions

Area
 
Subfunction
Prefix
Core
Variable support
c
Flow control
URL management
Miscellaneous
XML
Core
x
Flow control
Transformation
I18n
Locale
fmt
Message formatting
Number and date formatting
Database
SQL
sql
Functions
Collection length
fn
String manipulation

 

현재 스펙버전은 JSTL 1.1 입니다만,

이것도 실제 구현해 준곳은 ASF(Apache Software Foundation)입니다.

다음 사이트를 방문하셔서 도규먼트를 읽어보시면

앞으로 이 글의 진행방향을 알수 있겠습니다.

http://jakarta.apache.org/taglibs/doc/standard-doc/intro.html


2. JSTL 의 설치                                                                                      


우리가 JSTL 을 사용하기 위해서 jstl.jar 와 standard.jar 라는

자바라이브러리를 일단 얻어 와야 합니다

http://mirror.apache.or.kr/jakarta/taglibs/standard/jakarta-taglibs-standard-current.zip


압축을 풀면,

                   사용자 삽입 이미지

lib 폴더에 있는

jstl.jar(JSTL 의 스펙 라이브러리)

standard.jar(JSTL 의 실제 구현라이브러리)

이 두개의 jar 파일을 우리의 웹애플리케이션의/WEB-INF/lib폴더에 넣습니다.


지난번 기술 했듯이 web.xml 을 2.4 로 바꾸어 주시고요..

그리고 JSP 에 다음을 선언함으로서

JSTL 을 쓰기위한 준비는 모두 끝납니다.


각각 태그들의 TLD 파일은 jar 파일에 포함되어 있습니다.

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x" %>

.


3. JSTL 맛보기                                                                                      


위에서 압축 푼것중에 standard-examples.war 라는 파일을

Tomcat 의 webapps 폴더에 복사해 놓습니다.


Tomcat 을 재시작하면,

Web Application aRchive(WAR) 파일은 Tomcat 이 시작되면서

Tomcat 이 자동으로 압축을 풀고

standard-examples 이라는 웹애플리케이션을 구동할 준비를 합니다.

webapps 폴더 밑에 standard-examples 가 생기는 것을 눈으로 확인해 볼 수 있습니다.]

WAR 파일을 생성하고 배포하는 것에 대해선 앞으로 언급되리라 봅니다.



http://localhost:8080/standard-examples/ 요청해 볼까요?

사용자 삽입 이미지

JSTL 1.1 Specification을 눌러서 JSTL Specification 을 다운 받아 보십시다.

아래는 다운 받은 문서의 일부입니다..


사용자 삽입 이미지  사용자 삽입 이미지

보시면 각 태그마다

Syntax

Body Content

Attributes

항목들로 일목요연하게 정리가 되어 있습니다.

영어 문서를 어렵게 생각할 필요 없습니다.

이건 영어가 아니라 그저 기호 입니다.


자 , 그럼 각종 예제를 눌러 보면서

거기에 해당하는 스펙 문서를 찾아 보시면

앞으로 JSTL 은 우리들 손에서 놀아나게 될것입니다.


3. core                                                                                                


core 라이브러리는

JSP 의 변수,로직,링크 등등을 관리하는 태그들을 다수 포함하고 있는

표준 태그 라이브러리입니다.


먼저 core 의 set 태그와 out 태그에 대해 알아보렵니다.

[WEN-APP]/part12/JSTL-core-01.jsp

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<P>
<BR>변수입력 : <c:set var="a" value="AAAAAAAA" />

<BR>변수출력 : <c:out value="${a}"/>


<BR>파라미터 출력 : <c:out value="${param.p}"/>


<BR>세션입력 : <c:set var="s" value="MySession" scope="session"/>

<BR>세션출력 : <c:out value="${s}"/>


<BR>변수입력 :
<%
 java.util.HashMap map = new java.util.HashMap();
 map.put("key1" , "value1");
 map.put("key2" , "value2");
 map.put("key3" , "value3");

 String[] array = new String[]{"array1" , "array2"};

 pageContext.setAttribute("map" , map);
 pageContext.setAttribute("array" , array);
%>
<BR>변수출력 :
<BR>
<c:forEach var="item" items="${map}">
 <c:out value="Value of ${item.key} is ${item.value}"/><BR>
</c:forEach>
<BR><c:out value="${array[0]}" />


사용자 삽입 이미지


JSTL은 expression language(EL) 개념을 도입하였습니다.

이것은 eXtensible Stylesheet Language(XSL) 과

XSL Transformations (XSLT) 에서 쓰이는 표현언어와 같습니다.

ant 의 Build.xml 도 이 표현언어를 씁니다.

EL 은 ${ } 안에서 쓰여지는 일종의 스크립트 언어입니다.



위의 제가 만들어 본 예제를 유심히 봐주십시오.

EL을 글로 설명하려고 마음먹은 순간 ,

그것이 어려운 것임을 깨닫고,

가슴으로 느껴야 할 수 밖에 없다는 결론에 다다르기까지

별로 오랜 시간이 흐르지 않았습니다. ㅡ.ㅡ



4. 마치며...                                                                                            


오늘의 요약 : JSTL

                   JSTL Specification

                   expression language


첨부파일은 JSTL Specification 문서와 오늘의 예제 입니다.

앞으로 남은 JSTL 의 태그들에 대해 예제를 보면서

expression language 에 대한 설명을 갈음하고자 합니다.

가슴으로 느낄 그날까지...



신고
Posted by The.민군


티스토리 툴바