프로젝트 후기

2009/10/05 11:49

난항의 프로젝트

앞선 포스트에서도 몇차례 언급했었지만 지난 프로젝트는 이런저런 요인들로 난항의 연속이었다. SI에 몸담고 개발자로서의 첫발을 디딘 후로 꽤나 많은 시간들을 보내면서 그동안 적지않은 프로젝트를 경험했지만, 스트레스의 강도로 따지자면 손가락 안에 꼽을 정도로 힘든 프로젝트가 진행되었다.

그래도 PM을 비롯하여 말단 개발자까지 합심하여, 프로젝트 기간내내 대부분의 주말도 반납하고 하루하루 밤늦도록 열성을 다하여 전력질주한 끝에 다소 불안정한 상태였지만 그럭저럭 서비스를 오픈할 수 있었다. 덕분에 지난 7월 오픈을 전후하여 나를 포함해 대부분의 투입 인력이 철수하였다. 하지만 아직까지도 몇몇 SM 인력들이 주어진 숙제를 안은채 서비스를 안정화하고 품질을 높이기 위해 불철주야 노력하고 있다.

난항을 겪는 프로젝트가 흔히 그렇듯이 상대적으로 할 일은 많은데 절대적인 시간이 부족했다. 어느정도의 리스크를 이미 전제조건으로 떠안고 시작한 프로젝트. 끊임없는 강행군으로 심신은 지쳐가고 다가오는 데드라인에 중압감은 쌓일대로 쌓여 프로젝트룸의 공기가 어느덧 숨이 턱턱 막힐 정도로 무거워진 무렵의 어는날엔, 말그대로 드라마에서 나올법한 "드라마틱한 사건"이 벌어지기도 했다. 돌이켜보건데, 다 사람 사는 일이라 벌이진 해프닝으로 치부할 수도 있으련만 프로젝트가 중후반에 이르면서 그만큼 다들 극도의 스트레스를 받고 있었다는 반증이 아닐 수 없다.

어려웠던 프로젝트는 이유가 있기 마련이다. 하지만 현재 시점에서 업무며 설계며, 커뮤니케이션니 협업이니 기타등등의 원초적인 부분에 대해 언급하고 싶지는 않다. 또 이들의 비중이 막대하지만 프로젝트의 성패를 좌우하는 전부가 되지는 못한다. 실제로 보다 복잡한 요인과 설명하기 어려운 요소들이 얼기설기 얽혀있기 마련이다.

단편적 회고

다만 기술적인 관점에 있어서 하나의 단편적인 부분을 언급해보고자 한다. 그러기 전에 먼저 프로젝트의 개발 환경에 대해 짧게 소개하자면, 시스템은 크게 Spring2.5을 기반으로 하여 Struts2iBATIS의 오픈소스 프레임워크의 조합과 기타 라이브러리들을 사용하여 구축되었다. 그리고 AJAX(Prototype 기반)와 DWR(Direct Web Remoting)을 사용하여 UI에서 발생하는 사용자 액션(이벤트)들을 처리하였다.

대강의 환경과 툴, 주요한 라이브러리를 정리해보았다.

Environments

  • JAVA: JSDK 1.5, J2EE 1.4+(Servlet 2.4, JavaServer Pages 2.0 JSTL 1.0 이상 지원 환경)
  • WAS: JEUS 6.0 (개발: Tomcat 6.0.18)
  • WEB: WebtoB 4.1
  • DB: Oracle 10g

IDE & Tools

  • IDE: Eclipse Ganymede 3.4.1
  • Reporting Tool: Report Designer
  • Version Control: Subversion 1.5.5
  • Code Generator: 단위업무에 필요한 JSPs(코드 템플릿), Java(Action/DAO/Model), 설정(Struts/iBATIS) 등의 파일 목록 자동 생성.

Open Source - Framework & Library

  • Java Libraries
    • Spring 2.5.6 (Base)
    • Spring Security 2.0.4 (Security)
    • Struts 2 (MVC)
    • iBATIS 2.3.4.726 (SQL Mapper)
    • Direct Web Remoting 2.0.5 (DWR)
    • SiteMesh 2.4 (Layout)
    • Quartz 1.6.4 (Scheduler)
    • JExcelApi 2.6.9 (MS Excel)
    • Log4J 1.2.15 + Jakarta Commons Logging(JCL) 1.1.1 (Logging)
    • Apache Commons - DBCP, BeanUtils, Collections, etc…
    • FCKeditor 2.4.1 (Rich Text Editor)
  • AJAX & Javascript Libs
    • Prototype JavaScript framework, version 1.6.0.2
    • The Yahoo! User Interface Library (YUI)

라이브러리와 여타 종속성이 있는 라이브러리들을 포함하다 보니, lib 디렉토리가 적잖게 비대해진 면이 있다.

이슈

라이브러리의 조합은 무척 일반적인데 비해, 구현 일정에 있어서 개개인의 노력도와 투자한 시간에 반하여 전반적으로 개발 퍼포먼스가 나지않는 것이 큰 이슈가 되었다. 표준과 개발 가이드가 미비하고 공통 관련 구현체 및 개발 구조의 완성도가 미흡한 상태에서 급하게 개발에 착수한 것이 역시 독이 되었다. 여기서 비롯된 구조적인 결함과 취약점들이 원인이 되어 개발자들에게 혼란을 초래하고 기술적인 진입장벽을 형성하였고 결국 개발 생산성과 효율이 극도로 저하되었다.

주목할 만한 것은 Spring 기반 위에서 Struts의 Action, 비즈니스를 수행하는 Service, DAO 및 iBATIS SqlMap, 그리고 Model을 아우르는 구현부와 설정들에 있어서는 비교적 손쉽고 안정적으로 개발이 진행된 반면, 상대적으로 UI 단을 개발함에 있어서 단연 부하가 심했다는 사실이다. 계층화된 아키텍처로 Presentation(UI) Layer, Service(Business Logic) Layer, Persistence(Data Access) Layer 등 크게 세 계층으로 구분할 때 개인적으로 판단해보건데, P/T Layer을 개발하는데 60% 이상의 공수가 걸리지 않았을까 싶을 정도다. 웹 개발의 특성상 UI에 잔손이 많이 가는 것은 어쩔 수 없는 부분이지만 이클립스에 업무 핵심인 비즈니스 로직과 관련 쿼리를 작성하기 위해 자바 소스나 XML 파일을 열어놓기 보다는 UI 코드를 작성하기 위해 JSP나 JS 파일을 띄워놓는 시간이 단연 많았다.

대부분의 UI 이벤트를 처리하기 위해서 Ajax 및 DWR이 사용되어 javascript에 대한 의존도가 높아진 반면, 개발자 대부분이 상대적으로 Ajax 및 객체지향적인 스크립트에 대한 경험이 취약한 상태였다. 게다가 개발 템플릿으로 만들어진 개별 페이지의 구조가 다소 일반적이지 않은 모양새였는데, 페이지 구조에 일관성을 부여하고 페이지에서 발생할 수 있는 대부분의 기능(script)를 공통화하여 개발 편의를 증대하고자 한 것이 결과적으로 득보다는 실을 가져다 준 셈이었다.

페이지의 구조에 대해서 좀더 자세히 얘기를 하자면 먼저 구현한 페이지의 형태에 대한 이해가 필요하다.

사용자 삽입 이미지

충분히 응용될 수 있지만 일반적이지는 않은 페이지 구조이다. Main이 되는 JSP에는 검색, 목록, 상세, 등록, 수정, 삭제 등과 관련한 각각의 UI 요소 및 Form들을 포함하기 위한 영역으로 정의된 Place Holder(DIV)들이 위치한다. 각 영역에는 목록, 상세, 등록, 수정 등의 JSP Fragment(*.jspf)들이 템플릿화 되어 삽입(include)되어진다. 이 형태를 기본 페이지 레이아웃으로 하여 각각의 단위업무 기능에 따라 첨삭이 가해졌다.

쉽게 얘기해서 목록 페이지 따로, 등록(수정) 페이지 따로가 아니라 메인 페이지가 검색, 목록, 상세, 등록, 수정, 삭제 등 각 기능별 템플릿들을 모두 미리 포함(로드)한 상태에서 사용자 액션(이벤트)에 따라 Place Holder 들에 대한 visible 속성 및 필요한 제어 로직을 갖는 구조이다. 사용자 액션에 의해 Request가 발생하면 비동기 방식으로 서비스를 호출(Ajax call)하게 되고, 서비스(Struts Action 및 비즈니스 수행부, DAO 등을 포괄)를 수행 후 result에 따라 XML 형태의 뷰(view) 페이지를 Ajax의 callback method에서 전달받아 jspf 템플릿에 데이터를 주입(치환)하는 방식이다. 완전히 일치하지는 않지만 기본적인 흐름은 다음 그림과 유사하다.

사용자 삽입 이미지

어쨌든 이러한 페이지 구조는 정형화되고 일반적인 패턴의 화면은 아주 쉽고 빠르게 구현할 수 있었지만, 화면의 복잡도가 증가하고 예외적인(다소 일반적이지 못한) UI 유형을 포함하게 될 때에는 처리가 복잡해지고 더불어 몇가지 제약사항을 야기시켰다. 결과적으로 개발자들은 주로 UI의 요소를 컨트롤하고 이벤트를 처리하기 위한 script block과 씨름하는 것에 대부분의 시간을 할애하게 되었다. 또한 Ajax 결과로 Struts에서 View로 넘어온 XML 데이터를 UI 템플릿의 대응하는 위치에 치환하는 작업도 개발자들이 어려워한 부분이었다. 특히 단건의 데이터가 아닌 복합적인 Collection의 형태로 넘어온 목록 데이터를 반복하여 보여주는 처리를 난감해 하였다. 이것은 XML 데이터를 템플릿상의 동일 ID항목으로 치환해주는 공통 script가 갖는 한계점도 있었고, Service, Action, Dao 등의 Base class들과 기타 관련 class 들의 구조, 그리고 Struts Value Stack에 대한 개발자들의 이해가 부족했던 것이 주요한 원인으로 작용하였다.

정리

최근 몇년간의 프로젝트에서는 직접 제반 개발 환경을 세팅하고, 프레임워크를 구현하고, 개발 구조를 정의하고, 개발을 가이드하는 등의 역할을 수행해왔다(프레임워크는 주로 오픈소스를 조합하여 구성). 그런데 이 프로젝트의 경우, 구현 일정이 워낙 촉박한 데다 업무의 특성을 고려하다보니 해당 업무를 전문으로 하는 업체에서 노하우가 녹아있는 완성도 있는 컴포넌트와 솔루션을 들고 들어와서 함께 구축하는 방향으로 급선회하게되었다.

그런데 이것이 애초의 기대한 바와는 다른 부분이 적지 않았다. 개발 사상적인 시각에서도 협력사의 개발자와 약간의 차이가 있었다. 이러한 부분을 극복하기 위해 개발과 병행하여 기본이 되는 코드와 구조를 꾸준히 리뷰하면서 개선할 점을 논의하고 반영해 나갔다. 그렇지만 근본 틀을 바꿀 수는 없는 상황이라 여전히 한계가 있었다. 문제가 된 페이지의 구조와 Ajax 위주의 개발 패턴 역시, 이미 많은 것들이 이들을 기반으로 맞물려 돌아가는 상황이라 들어낼 수는 없는지라 대안으로 복잡한 화면 처리를 위한 유형을 하나 더 패턴화시키는 정도로 처리하였다.

정리하자면, 필요에 따라 응용할 수 있는 페이지의 구조를 오히려 전체 페이지에 대한 표준으로 일반화시킨 것과 지나치게 Ajax 기반으로 처리하는 개발 패턴이 구현상의 가장 큰 오류가 아니었나 싶다. 프로젝트에서 철수하고 더더욱 확신이 드는 생각이 두가지 있다. 하나는 "일반적인 것이 좋은 개발 구조"라는 것이다. "일반적"이라는 것에 대한 정의가 모호해질 수 있겠지만, 어떤 개발자들이 붙더라도 쉽게 적응하고 응용할 수 있도록 진입장벽을 낮추고 잘 구조화 시켜야한다. 덧붙여 일반적이라는 것이 품질 또한 일반적일 것을 보장할 것이라 생각하지는 않는다. 확신의 또다른 한가지는 "과유불급". 지나친 것은 좋지 않다는 것이다. 이를테면 Ajax가 UI의 가능성과 유연성을 높이는데 도움이 되는 것은 사실이지만 이것이 전체 이벤트를 제어하는 메인 기술이 되어서는 안된다는 생각이 든다. 필요한 부분에 적절히 사용되어야 한다.

대강 정리를 해보았지만 한편으로는 프레임워크와 스트럭처를 조금더 보완하고, 개발 전에 아키텍처의 컨셉과 개발 절차 및 응용 단계에 대해서 개발자들과 충분히 공유할 수 있었다면, 몇가지 단점들을 충분히 상쇄(trade-off)할 수 있는 장점도 있지 않을까 싶기도 하다(이를 설명하자면 전체 개발 구조에 대해서 좀더 이해해야 할 내용들이 많아진다). 협력사의 인력들도 중고급 이상으로 구성된 상당한 고수들이었고, 고심해서 작업한 흔적이 전체적으로 역력했다. 적용한 프레임워크와 개발 구조에 대해서는 쉽게 평가하기 전에 좀더 고민해볼 부분 많을 것으로 생각한다. 다만 한가지 확실한 것은 시도는 좋았지만, 실험정신을 발휘하기엔 시기가 적절하지 않았다는 것이다.

P.S.

지난 프로젝트의 성패를 떠나서 여러가지 관점에서 시사하는 바가 적지않다. 경험이 쌓여가는 것과는 무관하게 여전히 배워야할 것도, 공부해야할 것들도 많다. 반성해야할 것도 많고 인간적으로도 좀더 성숙해야 되지 않을까 싶기도 하다. 개인적으로 그동안 개발 방법론적인 부분에 있어서도 부족함이 많았는데, Agile 개발 프로세스나 XP(Extreme Programming), TDD 등 연구하고 적용해볼 분야가 많음을 새삼 느껴본다.

뜨거운 청춘들과 가정이 있는 사람들이 개인사와 가족을 뒤로 하고 했던 고생들이 그저 고생으로 끝나고 남는것이 없지 않기를 바란다. 문제점이 없진 않았지만, 시행착오를 통해 나를 비롯해 다들 조금더 성숙한 개발자가 될 수 있는 계기가 될 수 있기를 진심으로 바란다.

후기랄 것도 없이 종료한 프로젝트에 대한 간단한 감상이나 적어보려고 했는데, 쓸데없이 글이 길어졌다. -_-

"Development Story" 카테고리의 다른 글

2009/10/05 11:49 2009/10/05 11:49
 
Bookmark and Share

Apache iBATIS Abator (iBATOR)

2008/07/22 17:21
사용자 삽입 이미지
지난 프로젝트에서 iBatis를 사용하면 어베이터(Abator 1.0.0-238)라는 코드 생성기를 함께 활용했었다(abator는 얼마전에 공식 명칭이 iBATOR로 변경되었다).

iBatis용 코드 생성기는 abator 말고도 몇가지가 더 있는데, 이왕이면 iBatis에서 권장하는 툴을 사용하는 것이 좋지 않겠나 하는 생각이 있었고 abator를 제외한 툴들은 더이상 업데이트가 안되고 있었는 것도 abator를 선택한 이유였다.

관련 코드를 자동으로 생성해주는 것 까진 좋았는데, 필드 및 메서드 마다 붙어버리는 abator 관련 주석들이 코드를 지저분하게 만들고 가독성을 떨어뜨리는 단점이 있다. 그리고 기본적인 CRUD를 제외한 쿼리들과 Stored Procedure의 경우는 어쩔 수 없이 수동으로 편집을 해야했기 때문에, 프로젝트 말미엔 abator의 활용도가 많이 떨어지지 않았었나 싶다.

누가 abator를 사용함으로써 개발 효율이 높아졌느냐고 물어본다면, 직접 써보고 판단하라고 얘기하고 싶다. 아무튼 득실을 따진다면 사견이지만, 잃을 건 없다라는 것.

아래는 개발자들을 위해서 작성했던 간단한 소개 자료를 첨삭한 내용이다.


Abator

Abator는 iBATIS를 위한 코드 생성기이다. Abator는 데이터베이스의 테이블에 접근하기 위한 iBATIS 연관 코드들인 Object와 환경 설정 파일(configuration file)을 생성하며, 기본적인 CRUD(Create, Retrieve, Update, Delete) 쿼리를 포함한다.

Abator 설치를 위한 특별한 의존성은 없지만 다음 조건을 만족해야 한다.

  • JRE 1.4 이상.
  • DatabaseMetaData 인터페이스를 구현하는 JDBC 드라이버가 필요하다.

Abator가 생성하는 코드(파일) 목록은 다음과 같다.

  • 테이블 구조에 매치되는 자바 POJOs
  • iBATIS 호환 SQL Map XML 파일들
  • DAO 인터페이스와 구현 클래스들


Abator 사용

1. 설정 파일 작성 - abatorConfig.xml

적절한 설정 파일을 생성한다. 설정은 적어도 다음 요소들을 포함하여야 한다(Abator 사이트 참고).

  • A <jdbcConnection> element to specify how to connect to the target database
  • A <javaModelGenerator> element to specify target package and target project for generated Java model objects
  • A <sqlMapGenerator> element to specify target package and target project for generated SQL map files
  • (Optionally) A <daoGenerator> element to specify target package and target project for generated DAO interfaces and classes (you can omit the <daoGenerator> element if you don't wish to generate DAOs)
  • At least one database <table> element

다음은 abatorConfig.xml의 예제이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE abatorConfiguration
  PUBLIC "-//Apache Software Foundation//DTD Abator for iBATIS Configuration 1.0//EN"
  "http://ibatis.apache.org/dtd/abator-config_1_0.dtd">
 
<!-- ===================================================================== -->
<!-- Abator Config File                                                    -->
<!-- XXX Project, Kyungseo.Park@gmail.com, 2007.10                         -->
<!-- ===================================================================== -->
 
<abatorConfiguration>
  <abatorContext id="SybaseTables" generatorSet="Java5">
 
    <!-- ================================================================= -->
    <!-- jdbcConnection                                                    -->
    <!-- ================================================================= -->
 
   
<jdbcConnection driverClass="com.sybase.jdbc3.jdbc.SybDriver"
        connectionURL="jdbc:sybase:Tds:192.168.1.5:3000/sybdb"
        userId="sybdb"
        password="sybdb">

      <classPathEntry location="jconn3.jar" />
    </jdbcConnection>
 
   
<!--
    <jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver"
        connectionURL="jdbc:oracle:thin:@129.110.30.30:1521:oradb"
        userId="oradb"
        password="oradb">

      <classPathEntry location="classes12.jar" />
    </jdbcConnection>
    -->
 
    <!-- ================================================================= -->
    <!-- javaTypeResolver                                                  -->
    <!-- ================================================================= -->
 
    <javaTypeResolver >
      <property name="forceBigDecimals" value="false" />
    </javaTypeResolver>
 
    <!-- ================================================================= -->
    <!-- javaModelGenerator                                                -->
    <!-- ================================================================= -->
 
   
<javaModelGenerator targetPackage="xxx.business.sys.domain"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
      <property name="trimStrings" value="true" />
      <property name="rootClass" value="xxx.framework.base.BaseObject" />
    </javaModelGenerator>
 
    <!-- ================================================================= -->
    <!-- sqlMapGenerator                                                   -->
    <!-- ================================================================= -->
 
   
<sqlMapGenerator targetPackage="xxx.resources.sql.sys"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
    </sqlMapGenerator>
 
    <!-- ================================================================= -->
    <!-- daoGenerator                                                      -->
    <!-- ================================================================= -->
 
   
<daoGenerator type="IBATIS" targetPackage="xxx.business.sys.dao"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
      <property name="rootInterface" value="xxx.framework.base.BaseDao" />
    </daoGenerator>
 
    <!-- ================================================================= -->
    <!-- table                                                             -->
    <!-- ================================================================= -->
 
    <table tableName="TM_XXX_ROLE" domainObjectName="Role" >
      <property name="useActualColumnNames" value="true"/>
    </table>
 
  </abatorContext>
</abatorConfiguration>


2. Abator 실행(Run)

커맨드 라인을 열고 다음과 같이 입력하여 Abator를 실행할 수 있다. 단, 설정 파일에 정의된 targetProject의 대상 디렉토리는 반드시 미리 존재해야 한다.

java -jar abator.jar abatorConfig.xml false
java -jar abator.jar abatorConfig.xml true
java -cp abator.jar org.apache.ibatis.abator.api.AbatorRunner abatorConfig.xml false
java -cp abator.jar org.apache.ibatis.abator.api.AbatorRunner abatorConfig.xml true

여기서 파라미터의 의미는 다음과 같다.

  • 첫번째 파라미터: 환경 파일의 경로
  • 두번째 파라미터: 파일이 이미 존재할 경우 overwrite 여부

abator 실행을 위한 간단한 배치 파일을 만들어 놓으면 편하다.  예를 들어 “abator.bat” 파일을 만들어 다음과 같이 편집한다.

java -jar abator.jar abatorConfig.xml true

3. Abator 실행 후 작업

다음과 같은 마무리 작업이 필요하다.

  • SqlMapConfig.xml 파일을 생성하거나 편집한다.
  • iBATIS DAO Framework를 사용할 경우, dao.xml 파일을 생성하거나 편집한다.


Abator로 생성된 코드 예제

1. Java Model Classes

Domain Class

package xxx.business.sys.domain;
 
import xxx.framework.base.BaseObject;
import java.util.Date;
 
public class Role extends BaseObject {
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database column TM_XXX_ROLE.role_nmbr
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
Integer role_nmbr;
 
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database column TM_XXX_ROLE.role_nm
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
String role_nm;
 
    ...
    // member field 들에 대한 setter 및 getter
    ...
 
}


Domain Example Classes

package xxx.business.sys.domain;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class RoleExample {
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
String orderByClause;
 
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private List<Criteria> oredCriteria = new ArrayList<Criteria>();
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public void setOrderByClause(
String orderByClause) {
        this.orderByClause = orderByClause;
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public
String getOrderByClause() {
        return orderByClause;
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public List<Criteria> getOredCriteria() {
        return oredCriteria;
    }
 
    ...
 
}


2. SQL Map Files

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd" >
<sqlMap namespace="TM_XXX_ROLE" >
  <resultMap id="abatorgenerated_RoleResult" class="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    <result column="role_nmbr" property="role_nmbr" jdbcType="INTEGER" />
    <result column="role_nm" property="role_nm" jdbcType="VARCHAR" />
    <result column="rmrk" property="rmrk" jdbcType="VARCHAR" />
    <result column="sys_mngm_ysno" property="sys_mngm_ysno" jdbcType="CHAR" />
    <result column="rgsr" property="rgsr" jdbcType="CHAR" />
    <result column="rgsr_dttm" property="rgsr_dttm" jdbcType="TIMESTAMP" />
    <result column="amnd" property="amnd" jdbcType="CHAR" />
    <result column="amnd_dttm" property="amnd_dttm" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="abatorgenerated_Example_Where_Clause" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    <iterate property="oredCriteria" conjunction="or" prepend="where" removeFirstPrepend="iterate" >
      (
      <iterate prepend="and" property="oredCriteria[].criteriaWithoutValue" conjunction="and" >
        $oredCriteria[].criteriaWithoutValue[]$
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithSingleValue" conjunction="and" >
        $oredCriteria[].criteriaWithSingleValue[].condition$
          #oredCriteria[].criteriaWithSingleValue[].value#
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithListValue" conjunction="and" >
        $oredCriteria[].criteriaWithListValue[].condition$
        <iterate property="oredCriteria[].criteriaWithListValue[].values" open="(" close=")" conjunction="," >
          #oredCriteria[].criteriaWithListValue[].values[]#
        </iterate>
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithBetweenValue" conjunction="and" >
        $oredCriteria[].criteriaWithBetweenValue[].condition$
        #oredCriteria[].criteriaWithBetweenValue[].values[0]# and
        #oredCriteria[].criteriaWithBetweenValue[].values[1]#
      </iterate>
      )
    </iterate>
  </sql>
  <select id="abatorgenerated_selectByPrimaryKey" resultMap="abatorgenerated_RoleResult" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    select role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd, amnd_dttm
    from TM_XXX_ROLE
    where role_nmbr = #role_nmbr:INTEGER#
  </select>
  <select id="abatorgenerated_selectByExample" resultMap="abatorgenerated_RoleResult" parameterClass="xxx.business.sys.domain.RoleExample" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    select role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd, amnd_dttm
    from TM_XXX_ROLE
    <isParameterPresent >
      <include refid="TM_XXX_ROLE.abatorgenerated_Example_Where_Clause" />
      <isNotNull property="orderByClause" >
        order by $orderByClause$
      </isNotNull>
    </isParameterPresent>
  </select>
  <delete id="abatorgenerated_deleteByPrimaryKey" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    delete from TM_XXX_ROLE
    where role_nmbr = #role_nmbr:INTEGER#
  </delete>
  <delete id="abatorgenerated_deleteByExample" parameterClass="xxx.business.sys.domain.RoleExample" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    delete from TM_XXX_ROLE
    <include refid="TM_XXX_ROLE.abatorgenerated_Example_Where_Clause" />
  </delete>
  <insert id="abatorgenerated_insert" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated Wed Nov 28 17:31:32 KST 2007.
    -->

    insert into TM_XXX_ROLE (role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd,
      amnd_dttm)
    values (#role_nmbr:INTEGER#, #role_nm:VARCHAR#, #rmrk:VARCHAR#, #sys_mngm_ysno:CHAR#,
      #rgsr:CHAR#, #rgsr_dttm:TIMESTAMP#, #amnd:CHAR#, #amnd_dttm:TIMESTAMP#)
  </insert>
  <update id="abatorgenerated_updateByPrimaryKey" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    update TM_XXX_ROLE
    set role_nm = #role_nm:VARCHAR#,
      rmrk = #rmrk:VARCHAR#,
      sys_mngm_ysno = #sys_mngm_ysno:CHAR#,
      rgsr = #rgsr:CHAR#,
      rgsr_dttm = #rgsr_dttm:TIMESTAMP#,
      amnd = #amnd:CHAR#,
      amnd_dttm = #amnd_dttm:TIMESTAMP#
    where role_nmbr = #role_nmbr:INTEGER#
  </update>
  <update id="abatorgenerated_updateByPrimaryKeySelective" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    update TM_XXX_ROLE
    <dynamic prepend="set" >
      <isNotNull prepend="," property="role_nm" >
        role_nm = #role_nm:VARCHAR#
      </isNotNull>
      <isNotNull prepend="," property="rmrk" >
        rmrk = #rmrk:VARCHAR#
      </isNotNull>
      <isNotNull prepend="," property="sys_mngm_ysno" >
        sys_mngm_ysno = #sys_mngm_ysno:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="rgsr" >
        rgsr = #rgsr:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="rgsr_dttm" >
        rgsr_dttm = #rgsr_dttm:TIMESTAMP#
      </isNotNull>
      <isNotNull prepend="," property="amnd" >
        amnd = #amnd:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="amnd_dttm" >
        amnd_dttm = #amnd_dttm:TIMESTAMP#
      </isNotNull>
    </dynamic>
    where role_nmbr = #role_nmbr#
  </update>
</sqlMap>


3. Java DAO Classes(optional)

DAO Interface

package xxx.business.sys.dao;
 
import xxx.business.sys.domain.Role;
import xxx.business.sys.domain.RoleExample;
import xxx.framework.base.BaseDao;
import java.util.List;
 
public interface RoleDAO extends BaseDao {
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    void insert(Role record);
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    int updateByPrimaryKey(Role record);
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Thu Dec 13 16:43:17 KST 2007
     */

    int updateByPrimaryKeySelective(Role record);
 
    ...
 
}


DAO Implement

package xxx.business.sys.dao;
 
import xxx.business.sys.domain.Role;
import xxx.business.sys.domain.RoleExample;
import com.ibatis.dao.client.DaoManager;
import com.ibatis.dao.client.template.SqlMapDaoTemplate;
import java.util.List;
 
public class RoleDAOImpl extends SqlMapDaoTemplate implements RoleDAO {
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    public RoleDAOImpl(DaoManager daoManager) {
        super(daoManager);
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    public void insert(Role record) {
        insert("TM_XXX_ROLE.abatorgenerated_insert", record);
    }
 
    ...
 
}



참고 사이트

"Development Story" 카테고리의 다른 글

2008/07/22 17:21 2008/07/22 17:21
 
Bookmark and Share

문제의 발생 - Memory Leak

사용자 삽입 이미지
지난 프로젝트 말미에 발생한 이슈에 대해서 정리했던 내용을 잠시 언급하고자 한다. 위키에 정리했던것을 다소 첨삭하였다.

시스템의 계층화된 아키턱처의 구조로 볼 때, Persistence Layer (또는 Data Access Layer)의 경우 "iBatis + iBatis DAO"를 사용하여 구현되었는데, 어느 시점에서 간간이 예의 OOM(OutOfMemory) 에러가 발생했다. 런타임이 지속될수록 어플리케이션의 응답속도가 현저하게 떨어지는 증상이 나타났는데, 어딘가에  Memory Leak(메모리 누수)가 있음이 분명했다.

참고로 iBatis의 정확한 버전은 2.1.6 build# 592 이다. 개발할 당시 최신 버전은 2.3.0 이었는데, 좀더 검증되고 레퍼런스가 많은 버전을 선택했었다.

우선 문제가 발생한 시점을 전후로 변경된 코드 내역들에 대해서 검토하고, 각 계층(특히 Service Layer와 Persistence Layer)에 있어서 base code(class)의 기본적인 프로세스(Life Cycle)를 다시 리뷰하면서 문제 코드에 대한 범위를 좁혀갔다.

다행히 손쉽게 심증이 가는 부분을 찾을 수 있었지만 정확한 원인(물증 :)을 파악할 필요가 있었기 때문에, 기본적으로 GC 로그를 모니터링하고 APM(Application Performance Management)Profiler 툴을 활용하여 문제 영역에 접근해보기로 했다. 사실, 문제 해결보다는 제니퍼를 사용해본 경험이 전무하여 툴을 활용해보고 싶은 마음이 더 강했는지도 모르겠다. ^^; 아무튼 JenniferJProfiler 평가판을 다운로드하였다. 제니퍼는 설치 및 사용에 대한 문서가 간략하고 잘 정리되어 있어 쉽게 설치할 수 있었다.

  • APM: Jennifer 3.2 (2주 평가판)
  • Profiler: JProfiler 5.1 (10일 평가판)


Test 및 결과 요약

테스트 프로그램들을 작성하고 언급한 툴들을 활용하여 결과를 모니터링하였다. 프로그램은 쓰레드로 작성되었으며, 시작 버튼을 누르면 종료 버튼을 누르기 전까지 무한히 서비스를 호출하도록 하였다. 테스트는 개발 환경과 동일한 구성을 갖고 있는 1G 메모리(RAM)의 내 극빈한 M1 노트북에서 실행하였다.

자세한 상황과 과정을 모두 설명하기는 힘들지만, 대강 결론부터 얘기하자면 BaseService 에서 해당 작업을 종료할 때 호출하는 logging 메서드(작업에 대한 사용자 행위 추적을 구현) 부분에서부터 문제가 시작되었다.
 
BaseService에 대해 부연하자면, 모든 business logic을 구현하는 서비스들은 BaseServcie 클래스를 상속하도록 되어 있다. Controller와 유사한 역할을 한다고 봐도 무방할 듯 하다. UI(JNLP 클라이언트)에서 사용자의 액션 및 이벤트 등에 의해 발생하는 모든 서버 단 호출(Service Call)의 경우 호출 종료와 동시에 BaseServcie에 의해 logging 메서드가 호출된다. 하나의 이벤트에 의해 호출되는 서비스가 하나 이상이 될 수 있으므로 이 메서드는 호출 빈도수가 매우 높은 편에 속한다.

테스트 프로그램을 시작하면 BaseServcie 를 상속하는 서비스를 호출하게 되고 동시에 계속해서 logging 메서드가 호출되게 된다.

logging 메서드 호출부를 포함한 상태에서 테스트 프로그램을 실행하고 제니퍼로 모니터 하였다. 아래 그림은 문제 상황에서 제니퍼를 사용하여 메모리의 추이를 지켜본 것이다.

사용자 삽입 이미지

heap 사이즈가 지속적으로 증가하고 있다. 우측에서 갑자기 떨어지는 부분은 종료 버튼을 눌러 테스트 프로그램의 쓰레드를 중단하던 순간인데 이것은 무시해도 좋을 것 같다. 아무튼 상황이 심각해서 쓰레드를 종료하지 않으면 계속 프로그램을 실행하면 나중에는 노트북이 먹통(CPU 100%)이 될 정도록 버벅거렸다.

참고로 로컬에서 돌아가는 WAS(JEUS)의 VM 옵션은 다음과 같다.

<command-option>
 -Xms512m -Xmx512m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:PermSize=128m -XX:MaxPermSize=128m
 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:D:\gc.log
 -server
</command-option>

다음 그림은 위 그림과 동일한 시점에서의 GC로그 콘솔을 캡처한 화면이다.

사용자 삽입 이미지

minor GC가 꾸준히 발생하다가 Full GC가 지속적으로 발생하였다. 화면의 경우 Full GC가 발생한 직후 캡처한 것인데, 최초 Full GC의 경우 14초 이상 소요되는 것을 확인할 수 있다.

Full GC의 경우 속도가 느리고 GC가 일어나는 과정에서 thread가 중지되어 어플리케이션이 멈춰 버리기 때문에 성능과 안정성에 큰 영향을 준다. 위와 같이 Full GC가 지속적으로 발생되는 것만으로도 결국 그만큼 프로그램의 응답 속도가 현저하게 느려지는 결과를 초래하였다. 이것이 시간이 지날 수록 어플리케이션이 느려지는 주요한 원인이 되었다.

다음으로 아래 그림은 테스트 프로그램에서 logging 메서드 호출부를 뺀 상태에서 테스트한 것이다.

사용자 삽입 이미지
 
다음은 동일한 조건과 시점에서의 GC 로그이다.

사용자 삽입 이미지

다소 안정되어 보인다.


원인 분석과 소스 수정


어플리케이션 실행 중 heap 상에 존재하는 객체들(실제 메모리의 Collection 및 Object들)을 대상으로 메모리 누수와 연관된 객체를 파악할 수 있다면 보다 확실하게 문제 코드에 접근할 수 있을텐데, 제니퍼 세팅이 잘못 되었는지 유일하게 Collelction에 대한 실시간 모니터링만 되지 않았다. 대신 JProfiler를 사용해서 확인하였는데, 아래 그림이 해당 화면을 캡처한 것이다.

사용자 삽입 이미지
 
그림으로는 잘 보이지않지만 java.lang.String 타입의 instance count가 1,496,688이고 char[] 배열 타입은 1,423,882개로 나타난니다. 이 둘(문자열)을 합친 메모리만 200M를 웃돌고 있다.

여기서 다시 메소드를 조사해보았다.

사용자 삽입 이미지
 
선택된 부분을 확대해서 보면 다음과 같다.

사용자 삽입 이미지
 
트리에서 선택된 노드는 LoggingService2.logging 메서드이며, 이것은 BaseService의 logging에 의해 호출된다. LoggingService2.logging의 두 단계 아래 노드를 보면 "DaoManagerBuilder.buildDaoManager" 메서드를 확인할 수 있다.

LoggingService2에는 DB에 로그 데이터를 기록하기 위해 DaoCofig로부터 DaoManager를 가져오는 부분을 필수로 포함하고 있다.

DaoManager daoManager = DaoConfig.newDaoManager(DaoResource.JobResName);

상단의 코드에 기술된 DaoConfignewDaoManager 등의 코드는 iBatis의 예제 코드인 JPetStore의 소스 중 동일한 이름의 클래스를 참고하였는데, 이 코드의 전체는 하단과 같다. 프로젝트에서 사용한 newDaoManager 메서드의 코드 블럭은 이 예제와 거의 동일하였다.

package com.ibatis.jpetstore.persistence;
 
import com.ibatis.common.jdbc.ScriptRunner;
import com.ibatis.common.resources.Resources;
import com.ibatis.dao.client.DaoManager;
import com.ibatis.dao.client.DaoManagerBuilder;
 
import java.io.Reader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
 
public class DaoConfig {
 
  private static final
String resource = "com/ibatis/jpetstore/persistence/dao.xml";
  private static final DaoManager daoManager;
 
  static {
    try {
      daoManager = newDaoManager(null);
     
Properties props = Resources.getResourceAsProperties("properties/database.properties");
     
String url = props.getProperty("url");
     
String driver = props.getProperty("driver");
     
String username = props.getProperty("username");
     
String password = props.getProperty("password");
      if (url.equals("jdbc:hsqldb:mem:jpetstore")) {
        Class.forName(driver).newInstance();
       
Connection conn = DriverManager.getConnection(url, username, password);
        try {
          ScriptRunner runner = new ScriptRunner(conn, false, false);
          runner.setErrorLogWriter(null);
          runner.setLogWriter(null);
          runner.runScript(Resources.getResourceAsReader("ddl/hsql/jpetstore-hsqldb-schema.sql"));
          runner.runScript(Resources.getResourceAsReader("ddl/hsql/jpetstore-hsqldb-dataload.sql"));
        } finally {
          conn.close();
        }
      }
    } catch (
Exception e) {
      throw new
RuntimeException("Description.  Cause: " + e, e);
    }
 
  }
 
  public static DaoManager getDaoManager() {
    return daoManager;
  }
 
  public static DaoManager newDaoManager(
Properties props) {
    try {
     
Reader reader = Resources.getResourceAsReader(resource);
      return DaoManagerBuilder.buildDaoManager(reader, props);
    } catch (
Exception e) {
      throw new
RuntimeException("Could not initialize DaoConfig.  Cause: " + e, e);
    }
  }
 
}


JPetStore 예제에서는 getDaoManager()를 사용하여 정적으로 초기화된 단 하나의 DaoManager를 사용했지만, 프로젝트에서는 서브 시스템별로 dao.xml을 정의하고 각 시스템별로 달리 DaoManager를 사용하였으므로 모든 서비스 코드에서는 위에 기술한 바와 같이 DaoConfig의 newDaoManager() 메서드를 통하여 daoManager 인스턴스를 초기화하여 사용하였다.

public class SomeService extends BaseService {
 private DaoManager daoManager;
 private SomeDAO someDao;

 public SomeService() {
  daoManager = DaoConfig.newDaoManager(DaoResource.JobResName);
  this.someDao = (SomeDAO) daoManager.getDao(SomeDAO.class);
 }
 ...
}

참고로 BaseService 유형의 클래스들은 Servlet이 Container에 의해 관리되듯이 단 하나의 인스턴스로 존재한다.

여기서 잠시 iBatis DAO와 관련된 내용에 대해 언급하자면, iBatis DAO 프레임워크를 초기화하기 위해서는 dao.xml 파일 경로를 파라미터로 넘겨주어 DaoManagerBuilder.buildDaoManager()를 호출해야한다. DAO 프레임워크는 이 메서드에서 해당 dao.xml 파일을 읽고 상응하는 DaoManager를 생성하게된다. client는 이 DaoManger를 사용하여 insert 등과 같은 제반 작업을 수행하게 된다. 아래 그림을 참고하면 된다.

사용자 삽입 이미지

DAO sequence diagram (그림 참고)

앞서 설명했듯이, 테스트 프로그램은 문제 상황을 빨리 발생시키기 위해서 무한하게 트랜잭션을 발생시키는 형태를 취했지만 logging 메서드 자체가 매우 빈번하게 호출되는 구조임을 전제로 하고 생각해 볼 때,

일반적으로 특정 메서드의 블럭을 벗어나게 되면 메서드 내에서 지역변수로 사용한 객체는 Garbage Collection의 대상이 대상이 된다. 하지만, 설명한 바와 같이 DaoManagerBuilder.buildDaoManager()에서 dao.xml 파일에 대한 I/O가 일어나고 읽어들인 파일을 parsing하고 DaoManager의 인스턴스를 생성하는 작업을 수행하게 되는데, 결과로 미루어 볼때 어느 정도 시간 간격을 둔 상태에서는 별 무리 없이 수행되었지만, logging과 같이 짧은 시간에 많은 수의 서비스를 수행과 동시에 호출되는 경우 GC가 제대로 되지 못하고 결국 감당을 하지 못하게 되는 듯 하다. 추측컨데, JProfiler에서 보여진 수많은 문자열 관련 객체들은 반복되는 I/O와 parsing 작업의 부산물이라 여겨졌다.

이것이 원인이 맞다면, DaoManager를 초기화 하기 위해 매번 DaoConfig의 newDaoManager를 호출함으로써 빈번한 buildDaoManager 작업을 발생시킬 수 있는 개연성을 갖는 것을 미연에 방지하기 위해 DaoConfig 자체를 수정하는 것이 좋을 듯 했다.

따라서 근본적인 원인을 제공한 DaoConfig의 소스를 아래 코드와 같이 수정한 후 다시 프로그램을 실행하였다. DaoConfig는 싱글톤(singleton)으로 변경하고 각 업무단의 DaoManager를 Hashtable을 사용하여 cache 하도록 수정하였다. 캐슁으로 인한 부작용(?!)은 없을까 잠시 생각해보았는데 dao.xml을 리로드하기 위해서는 어차피 WAS를 재시작할 필요가 있었다. SQL Map 파일의 경우 절대 경로를 사용하여 자동으로 다시 로드 되게끔 되어 있다(개발서버의 경우만 해당).

다음이 대강의 코드이다.

public class DaoConfig {
 
  private static DaoConfig instance = null;
  private static Hashtable<String, DaoManager> daoManagers = null;
 
  private DaoConfig() {
    daoManagers = new Hashtable<String, DaoManager>();
  }
 
  public static DaoConfig getInstance() {
    if (instance == null) {
      instance = new DaoConfig();
    }
    return instance;
  }
 
  public DaoManager getDaoManager(
String resource, Properties props) {
    DaoManager daoManager = null;
    if (daoManagers.containsKey(resource)) {
      daoManager = (DaoManager) daoManagers.get(resource);
    }
    else {
      try {
        Reader reader = Resources.getResourceAsReader(resource);
        daoManager = DaoManagerBuilder.buildDaoManager(reader, props);
        daoManagers.put(resource, daoManager);
      } catch (
Exception e) {
        throw new
RuntimeException("DaoConfig를 초기화할 수 없습니다. 원인: " + e, e);
      }
    }
 
    return daoManager;
  }
 
  /* 호환성을 위해 남겨둔 method들 */
  public static DaoManager newDaoManager(
String resource) {
    return newDaoManager(resource, null);
  }
 
  public static DaoManager newDaoManager(
String resource, Properties props) {
    DaoConfig config = DaoConfig.getInstance();
    return config.getDaoManager(resource, props);
  }
 
}

다시 프로그램을 실행하고 결과를 보여주는 제니퍼 화면을 캡처 하였다. 비교를 위해 이전의 화면들을 다시 붙였다. 수정된 코드가 반영된 결과 화면은 마지막 화면이다.

* 최초의 문제가 있던 상태

사용자 삽입 이미지

* logging 메서드를 호출하지 않은 상태

사용자 삽입 이미지

* DaoConfig를 수정한 이후 상태

사용자 삽입 이미지
 
훨씬 안정적인 상태로 변했다.
이 상태에서의 GC 로그는 다음과 같다.

사용자 삽입 이미지
 
결론적으로 DaoManager를 각각의 서비스 코드에서 매번 초기화하기 위해서 DaoConfig의 newDaoManager 메서드를 호출할 때, 빈번한 DaoManagerBuilder.buildDaoManager()가 호출되는 구조는 문제를 일으킬 여지가 있는 것 같다.

대강 정리를 마치고 내용을 한번 읽어보았는데, 정리가 제대로 된 것 같지는 않다. ^^;
아무튼 요는, 항상 기본에 충실하고 미리 생각하는 개발자가 되자! ^^


P.S. 잘못된 내용이 있다면 지적 바랍니다. ^^



참고

관련 API



 

"Development Story" 카테고리의 다른 글

2008/07/07 11:59 2008/07/07 11:59
 
Bookmark and Share

JPetStore 데모 설치 - Spring 샘플

2007/08/08 13:50

Spring의 포함 예제 중 JPetStore를 설치한다. 

JPetStore 웹 어플리케이션을 배포하기 위해서는 Apache Ant를 사용하여 빌드하여야 한다. 이때, JDK는 1.3 버전 이상, Ant는 1.5 버전 이상이어야 한다.

순서는 다음과 같다.


1 Java 설치

jdk-1_5_0_12 버전이 설치되어 있다. 1.3 버전 이상을 다운로드하여 설치하도록 한다. 설치가 완료되면 환경 변수에 JAVA_HOME으로 C:\Program Files\Java\jdk1.5.0_12 를 등록한다. 그리고 필요한 경우 PATHC:\Program Files\Java\jdk1.5.0_12\bin\ 를 추가한다.


2 Ant 설치

apache-ant-1.7.0 버전이 설치되어 있다. 최신 버전을 다운로드하여 설치한다. 설치 후 PATHC:\Dev\apache-ant-1.7.0\bin\ 를 추가한다.


3 Tomcat 설치

apache-tomcat-5.5.23 버전이 설치되어 있다. 최신 버전을 다운로드하여 설치하도록 한다. 설치 후, 시스템 환경 변수에 CATALINA_HOME으로 C:\Program Files\Apache Software Foundation\Tomcat 5.5을 등록한다.

설치가 완료된 후 Tomcat 서버를 시작해야 한다. 시작되어 있지 않다면 시스템 트레이에서 Apache Service Manager를 사용하여 "Start Service" 한다.

http://localhost:8080 및 http://localhost:8080/jsp-examples 으로 접속하여 제대로 동작하는 지 확인한다.


4 MySql 설치

mysql-5.0.41 버전이 설치되어 있다. 최신 버전을 다운로드하여 설치하도록 한다. 설치 후 PATHC:\Program Files\MySQL\MySQL Server 5.0\bin 를 추가한다.

설치가 완료되었다면 MySQL 서비스를 시작한다. 제어판의 서비스에서 MySQL Service를 더블클릭하여 시작하거나 종료할 수 있다. 서비스에 등록되지 않았다면 C:\Program Files\MySQL\MySQL Server 5.0\bin\mysqld-nt -install 하여 등록하면 된다.

커맨드 프롬프트를 열고 mysql -uroot -p를 입력한다. 패스워드를 입력하고 "mysql> "이란 프롬프트가 뜨면 "show databases;"를 입력하여 아래와 같이 나온다면 제대로 설치한 것이다.

+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| test               |
+--------------------+
5 rows in set (0.09 sec)


5 MySql JDBC Driver(MySQL Connector/J) 설치


mysql-connector-java-5.1.0가 설치되어 있다. 다운로드 받은 후, mysql-connector-java-5.1.0-bin.jar 파일을 CATALINA_HOME/common/lib 로 복사하도록 한다.


6 Spirng - Download

Spring Framework 2.0.6 버전을 다운로드한 후 임시 디렉토리에 압축을 해제한다. 아래 내용은 압축을 푼 내용이 "C:\Temp\spring-framework-2.0.6\"과 같은 경로를 갖는 것을 전제로 설명한다.


7 JPetStore Database 세팅

C:\Temp\spring-framework-2.0.6\samples\jpetstore\db\mysql 디렉토리로 이동한다.
텍스트 에디터를 열고 아래와 같이 편집한 후 현재 디렉토리에 "jpetstore-mysql-create-user.sql" 이름으로 저장한다.

-- Create a new user, grant her rights, and set her password.
grant select, insert, update, delete
on jpetstore.*
to jpetstore@localhost identified by 'ibatis9977';

현재 파일 목록은 다음과 같다.

C:\Temp\spring-framework-2.0.6\samples\jpetstore\db\mysql>ls -asCF
total 16
   0 ./      1 jpetstore-mysql-create-user.sql     5 jpetstore-mysql-schema.sql
   0 ../    10 jpetstore-mysql-dataload.sql

아래와 같이 명령을 실행한다. 이때,  jpetstore-mysql-dataload.sql 파일을 편집하여 제일 상단에 "USE JPETSTORE;"라고 편집하도록 한다.

C:\Temp\spring-framework-2.0.6\samples\jpetstore\db\mysql>mysql -uroot -p < jpetstore-mysql-schema.sql
Enter password: ********

C:\Temp\spring-framework-2.0.6\samples\jpetstore\db\mysql>mysql -uroot -p < jpetstore-mysql-dataload.sql
Enter password: ********

C:\Temp\spring-framework-2.0.6\samples\jpetstore\db\mysql>mysql -uroot -p < jpetstore-mysql-create-user.sql
Enter password: ********


8 JPetStore 배포


C:\Temp\spring-framework-2.0.6\samples\jpetstore\dist\jpetstore.warCATALINA_HOME\webapps 하위에 복사한다. 이때, Tomcat이 자동으로 jpetstore를 배포하게 된다.


9 JPetStore - Driver 설정

CATALINA_HOME\webapps\jpetstore\WEB-INF\jdbc.properties 파일을 편집하기 위해 열도록 한다.  다음과 같이 수정한 후 저장한다.

# Properties file with JDBC-related settings.
# Applied by PropertyPlaceholderConfigurer from "dataAccessContext-local.xml".
# Targeted at system administrators, to avoid touching the context XML files.
jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jpetstore
jdbc.username=jpetstore
jdbc.password=ibatis9977


10 테스트


Tomcat을 종료 했다가 재시작 한 후 http://localhost:8080/jpetstore 로 접속해 데모를 실행한다.

사용자 삽입 이미지


끝. ^^

"Development Story" 카테고리의 다른 글

2007/08/08 13:50 2007/08/08 13:50
 
Bookmark and Share

AppFuse-light (구 Equinox) 설치

2007/07/11 14:27

몇일 전에 appfuse-light(appfuse-light-struts-ibatis-1.8-beta)를 설치해보았다. 설치 내용을 정리한다.


1 AppFuse 소개


다음은 AppFuseDefinition의 내용을 발췌한 것이다.

AppFuse is a Framework
This is sort of the 10,000 foot view of what AppFuse is. It is a structure in which web applications of a variety of types and sizes are created.

AppFuse is a Directory Structure
One of many features of AppFuse is the way it logically separates code into its appropriate locations based on function. Persistence code belongs in src/dao/, manager code in src/service/, controller code in src/web/ and of cource testing code in test/**/.

AppFuse is a Build File
The build.xml file makes for a nice way to create, build, test, package, deploy webapps. It also nicely handles changing dependancies and maintaining properties that may be different in development than in production.

AppFuse is a Sample Application
AppFuse is an example of how to use and integrate many Java technologies while following best practices. There are examples of how to do everything from performing CRUD opperations from your view all the way to the persistence layer to adding a filter to your webapp. Because the common things that are needed in basically all web applications are already in AppFuse, you will only need to figure out how to do the parts that make your webapp unique.

Figure 1 illustrates the conceptual design of a typical AppFuse application

사용자 삽입 이미지


2 설치(appfuse-light-struts2-ibatis-1.8-beta)


2.1 환경 설정

설치 전 아래의 환경이 세팅되어 있어야 한다. 내가 appfuse-light를 설치한 환경은 JPetStore 데모 설치 (iBATIS 또는 Spring 샘플)내용 중 1~5와 동일하다.

  • J2SE 1.4.2 이상을 설치하고 설치 디렉토리를 가리키는 JAVA_HOME 환경변수를 셋팅한다.
  • Ant 1.6.2 이상을 설치하고 ANT_HOME 환경변수를 셋팅한다.
  • Tomcat 4.1.x 이상(추천되는 버전은 5.0.28)을 설치하고 톰캣 설치 디렉토리를 가리키는 CATALINA_HOME 환경변수를 셋팅한다.
  • MySQL 3.23.x 이상(추천되는 버전은 4.1.7)을 설치한다.
  • junit3.8.1 이상 버전에서 junit.jar를 $ANT_HOME/lib로 복사한다.

2.2 파일 다운로드

AppFuse-light 1.8-beta 다운로드 페이지에서 전체 샘플이 포함된 appfuse-light-all-1.8-beta.zip를 다운로드 하던지 원하는 파일을 다운로드한다. 여기서는 appfuse-light-struts-ibatis-1.8-beta.zip를 다운로드하여 설치한다. 파일이 다운로드되면 압축을 푼다. 아래는 D 드라이브의 Temp 디렉토리에 압축을 푼 모습이다.

D:\Temp\appfuse-light-struts2-ibatis-1.8-beta>ls -asCF
total 54
   0 ./            12 LICENSE.txt         17 build.xml     0 src/
   0 ../            7 README.txt           0 lib/          0 target/
   0 .settings/     1 build.properties    14 pom.xml       3 tomcat.xml

2.3 MySQL 사용을 위한 몇가지 변경 사항

AppFuse는 PostgreSQL을 기본 데이터베이스로 사용한다. MySQL을 사용하기 위해서는 몇가지 변경 사항을 적용하여야 한다.

jdbc.property 변경
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\src\main\resources\jdbc.properties 파일을 다음과 같이 편집하여 저장한다.

jdbc.driverClassName=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/appfuse_light?createDatabaseIfNotExist=true
jdbc.username=root
jdbc.password=xxxxxxxx

create-mysql 생성
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\src\main\java\org\appfuse\dao\ibatis\create-mysql.sql 파일을 새로 만들고 다음과 같이 편집하여 저장한다.

drop table IF EXISTS app_user;
drop table IF EXISTS app_user_sequence;
create table app_user (id bigint not null primary key, first_name varchar(50), last_name varchar(50), birthday timestamp);
create table app_user_sequence (id int not null) type=MYISAM;
insert into app_user_sequence values(0);

build.xml 변경
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\build.xml 파일을 열어 데이터베이스 테이블 생성 타겟의 내용을 다음과 같이 mysql 을 사용하도록 변경한다. 여기서 mysql-connector-java-5.1.0-bin.jar의 위치는 상황에 맞게 변경하던지 해당 위치에 MySql JDBC Driver를 복사해놓도록 해야한다.

    <!-- create the database -->
    <target name="createdb" description="Create database tables">
        <sql driver="${jdbc.driverClassName}" url="${jdbc.url}"
            userid="${jdbc.username}" password="${jdbc.password}">
            <fileset dir="${basedir}">
                <include name="src/**/create-mysql.sql"/>
            </fileset>
            <classpath refid="compile.classpath"/>
            <classpath>
                <pathelement location="${basedir}/lib/mysql-connector-java-5.1.0-bin.jar" />
            </classpath>
        </sql>
    </target>

applicationContext-ibatis.xml
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\src\main\webapp\WEB-INF\applicationContext-ibatis.xml 파일을 열어 userIncrementer 부분을 다음과 같이 변경하도록 한다.

    <bean id="userIncrementer" class="org.springframework.jdbc.support.incrementer.MySQLMaxValueIncrementer">
        <property name="dataSource" ref="dataSource"/>
        <property name="incrementerName" value="app_user_sequence"/>
        <property name="columnName" value="id" />
    </bean>

2.4 어플리케이션 배포

커맨드 프롬프트에 ant deploy를 입력하여 배포한다.

D:\Temp\appfuse-light-struts2-ibatis-1.8-beta>ant deploy
Buildfile: build.xml
compile:
    [mkdir] Created dir: D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\classes
    [javac] Compiling 10 source files to D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\classes
    [javac] Note: D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\src\main\java\org\appfuse\web\UserAction.java uses unchecked or unsafe operations.
    [javac] Note: Recompile with -Xlint:unchecked for details.
    [mkdir] Created dir: D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\test\classes
    [javac] Compiling 6 source files to D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\test\classes
     [copy] Copying 3 files to D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\classes
copy-jars:
    [mkdir] Created dir: D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\jars
     [copy] Copying 23 files to D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\target\jars
deploy:
     [copy] Copying 105 files to C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\appfuse-light
     [copy] Copying 24 files to C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\appfuse-light\WEB-INF\classes
     [copy] Copying 22 files to C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\appfuse-light\WEB-INF\lib
BUILD SUCCESSFUL
Total time: 18 seconds
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta>

2.5 테이블 생성

커맨드 프롬프트에 ant createdb를 입력하여 배포한다.

D:\Temp\appfuse-light-struts2-ibatis-1.8-beta>ant createdb
Buildfile: build.xml
createdb:
      [sql] Executing resource: D:\Temp\appfuse-light-struts2-ibatis-1.8-beta\src\main\java\org\appfuse\dao\ibatis\create-mysql.sql
      [sql] 5 of 5 SQL statements executed successfully
BUILD SUCCESSFUL
Total time: 3 seconds
D:\Temp\appfuse-light-struts2-ibatis-1.8-beta>

2.6 Tomcat 시작 및 테스트

톰캣을 시작한 후 http://localhost:8080/appfuse-light 로 접속한다.

사용자 삽입 이미지


3 참고 사이트

 


"Development Story" 카테고리의 다른 글

2007/07/11 14:27 2007/07/11 14:27
 
Bookmark and Share