'Framework/Database'에 해당되는 글 20건

  1. 2010.04.04 DBFSlayers 1
  2. 2010.03.09 DB Framework 2.0 Short Explain 3
  3. 2010.02.24 Framework Cursor (cursor in MySql) 1
  4. 2009.03.26 Framework - client cursor
  5. 2009.03.24 Framework - 커서의 선택 .. and
  6. 2009.03.23 프로시저 vs SQL
  7. 2009.03.19 Framework - 커서(Anonymous)
  8. 2009.03.13 Framework - 커서(Keyset, Dynamic)
  9. 2009.03.12 Framework (Servant)
  10. 2009.03.12 Framework (DBController)
Framework/Database2010. 4. 4. 01:01
Database Framework Scripting Layers is

a lightweight database abstraction layer suitable for high-load websites where you need the scalable advantages of connection pooling. DBFSlayer talks to clients via JSON over HTTP, meaning it's simple to monitor and can swiftly interoperate with any web framework you choose.

Features At A Glance
  • Simple HTTP interface
  • JSON-format messages
  • Multiple DB adapter (currently tested oracle 9 higher, mssql 2000 higher, mysql 5 higher)
  • Connection pooling
  • Multithreaded
  • Straight-forward configuration
  • Simple yet powerful access

Download
https://sourceforge.net/projects/dbfslayers/files/
dbfs_layers_fat.jar include following lib
     - jetty 6.1
     - servlet 2.5, jsp 2.5
     - struts 1.2.9
     - json-lib 2.3
     - bleujin framework_core_fat.jar(http://sourceforge.net/projects/clientcursordbf/)
     - Database JDBC client libraries(offical lib of oracle 9i ,mssql 2000 , mysql 5)


Install
   1. download dbfs_layer_web.zip(https://sourceforge.net/projects/dbfslayers/files/)
   2. extract dbfs_layer_web.zip
   5. download dbfs_layers_fat.jar at 2.dir (https://sourceforge.net/projects/dbfslayers/files/)
   6. configure webapps/simple/WEB-INF/default-config.xml
   6. execute java -jar dbfs_layer_fat.jar
   7. connect http://localhost:8080/simple/index.htm


Configuring Your Database
view webapps/simple/WEB-INF/default-config.xml


Example Usage

run : java -jar dbfs_layers_fat.jar
view : http://localhost:8080/simple/index.htm

<script  language="JavaScript">
    var dc = new Database(new Session('http://localhost:8080/simple/db.do', 'john'));

    var ins = dc.createUserCommand("insert into update_sample values(2, '222')") ;
    $('result').innerHTML = ins.execUpdate() + '<br/>\n' ;

    var cmd = dc.createUserCommand('select * from update_sample where a < :a') ;
    cmd.setPage(3, 1) ;   // 1 page per 3 unit
    cmd.setParam('a', '3') ;

    // var result = cmd.execQuery() ;   // excute. return JSON format
   
    $('result').innerHTML += cmd.execQuery()  + '<br/>\n';

    /*  execute procedure example
    var upt = dc.createUserProcedure('sample@selectEmpBy()') ;
    upt.setPage(3, 1) ;
    $('result').innerHTML = upt.execQuery() ;
   
    */
   
    /*  multiple query(same transaction) example
    var upt1 = dc.createUserCommand('select * from copy_tblc where no1 < :no1') ;
    upt1.setPage(3, 1) ;
    upt1.setParam('no1', '05') ;
   
    var upt2 = dc.createUserProcedure('sample@selectEmpBy()') ;
    upt2.setPage(3, 1) ;
   
    var upts = dc.createUserProcedures('multi query') ;
    upts.add(upt1).add(upt2) ;
   
    $('result').innerHTML = upts.execQuery() ;
    */
</script>



UTF8 Issues

The DBFSlayers JSON requires your data to be in UTF-8 format. This means your database should be in UTF-8 and it should return UTF-8 for queries.



Future Work
  • Batch MDL - batch insert, update, delete
  • Composite Query - select + mdl execute at same transaction
  • LOB Datatype support
  • JTA support at multiple DB
  • Security - add support for HTTPS / HTTP Auth as a basic access control mechanism (security is still primarily handled in the database).
  • Round-Robin Dispatching
  • Automatic Failover

  • Documentation - improvements to the documentation as suggested by the community.
  • Testing - better unit tests are in the works
  • Feeds/Get-if-modified-since Support - some cumulative stats mechanisms could support mechanisms to only download new log messages or if there is new log messages.
  • Language Bindings - the DBSlayer just speaks JSON + HTTP, so many languages should be able to talk to it. We welcome well-written client libraries for any language if you want to share yours.







'Framework > Database' 카테고리의 다른 글

DB Framework 2.0 Short Explain  (3) 2010.03.09
Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin
Framework/Database2010. 3. 9. 12:46

framework_src.zip의 com.bleujin.framework.db.sample에 기본 적인 예제가 있으며 특히 com.bleujin.framework.db.sample에 가장 기초적인 예제가 있다.

해당 예제들을 실행하기 위해서는 sample_mysql.sql(mssql.sql, oracle.sql 역시 모두 같은 역할을 한다.) 의 예제 테이블 등의 내용을 실행시킨다 . 모든 테스트를 실행하기 위해서는 com.bleujin.framework.db.sample.SampleAllTest.java 를 실행한다.


주요 클래스

DBManager

     - 가장 먼저 생성해야 할 클래스로 대부분 JDBC URL과 userID, password를 생성자로 받는다.
        DB별로 혹은
            MySQLDBManger, MySQLPoolDBManager
            MSSQLDBManger, MSSQLPoolDBManager
            OracleDBManger, OracleDBManager, OracleCacheDBManger, Oracle9iCacheDBManger
            H2EmbedDBMangaer, HSQLDBManager 등등

DB별, 접속형태별(Pool여부, Cache여부)로 여러개의 구현체를 가지고 있다.

ex)
DBManager dbm = new MySQLPoolDBManager("jdbc:mysql://novision/test", "bleu", "redf") ;



IDBController


      - DBManger를 생성자로 받는 IQueryable Impl의 Factory 역할을 맡는다.
      - 현재는 IDBController의 구현체로 DBController 한개만 있다.
      - initSelf() 메소드로 dbm의 Pool 초기화 등의 작업을 실행하고 destroySelf() 메소드로 DBManager Pool을 해제시킨다. initSelf()는 생성후 꼭 실행시켜 주어야 한다. 보통의 경우 initSelf()는 프로그램 시작시에 한번, destorySelf()는 프로그램 종료시에 한번 실행한다. Static 변수로 참조하여 사용하는 경우가 일반적이지만 꼭 그럴필요는 없다.

ex)
DBController dc = new DBController(dbm) ;
dc.initSelf() ;
....
dc.destroySelf() ;

     - DBManger와 IDBController를 생성시키고 initSelf()를 실행했다면 이제 쿼리를 실행시킬 준비는 모두 완료되었다.
        가장 간단한 쿼리 실행은
        Rows rows = dc.execQuery(String select) ;
        int result = dc.execUpdate(String mdl) ; 이다.
       



IQueryable
    - 인터페이스이고 execQuery()와 execUpdate()의 주 메소드 2개를 가지고 있다. 모든 실행할수 있는 쿼리는 IQueryable를 상속받는다. 가장 많이 알려진 구현체로 UserCommand, UserCommandBatch, UserProcedure, UserProcedureBatch, UserPorcedures 등이 있다.




IUserCommand

     - 굳이 비슷한 걸 찾자면 JDBC의 PreparedStatement의 역할과 비슷하며 사용방법도 비슷하다.

ex)
IUserCommand cmd = dc.createUserCommand("select * ... where a = ? and b = ?") ;
cmd.addParam(1).addParam("abc") ;
// cmd.addParam(0, 1).addParam(1, "abc") ; // 위구문과 의미가 같다. param index는 0부터 시작한다.

// 혹은 아래와 같이 같은 변수를 여러개 사용하거나 편이성을 위해 named parameter를 사용할수 있다.
IUserCommand cmd = dc.createUserCommand("select * ... where a = :a and b = :b and c >= :a") ;
cmd.addParam("a", 1).addParam(b, "abc") ;

// 만약 특별한 형을 지정하고 싶다면. cmd.addParam("a", 1, Types.LONG) ; 와 같이 명시적으로 지정한다.


      - IUserCommand 실행
Rows rows = cmd.execQuery() ;  // select
int result = cmd.execUpdate() ;     // mdl

       - 실행계획 보기
//실행계획의 해당 DB의 기능을 이용하기 때문에 Format은 DB마다 다른데, MySQL은 표 형태로, MSSQL은 XML Graph형태로, Oracle은 문자열로 보여준다.
cmd.viewPlan(OutputSteam output) ;


       - Page 설정
// 쿼리를 실행하기 전에 Page를 설정하여 해당 Page의 결과값만을 가져올수 있다.
cmd.setPage(Page.create(10, 2)) ;     // 10개씩 했을때 2페이지 즉 11-20번째 row를 가져온다.
cmd.execQuery() ;

이전의 cursor 관련글에서도 확인하였듯이 가장 좋은 방법은 원하는 집함만을 억세스 해서 원하는 집합형태로 가져오는 쿼리를 사용하는 것이 Page 설정에 있어서의 가장 좋은 답이다. 그렇게 생각했기 때문에 예전의 버전에는 Page 관련 기능을 넣지 않았다. 다만 그것은 MySQL의 limit나 오라클의 rownum의 단순한 사용방법보다는 훨씬 더 고난이도이기 때문에 DB별로 자유자재로 할 수 있는 사람이 극히 드물고 또한 매우 번거롭다. 또한 그 효과를 볼수 있는 곳이 제한적이기 때문에 번거로움을 무시하고 모든 곳에 그와 같은 쿼리 방식을 사용하는것은 불필요하다는 아니지만 비효율적이다라는 생각이 들었다.


Rows


   - 쿼리를 실행할수 있는 Interface인 IQueryable의 하위 구현체는 IUserCommand, IUserProcedure, IUserCommmandBatch, IUserProcedureBatch, UserProceudres, TimeoutQuery ... 등등 아주 많은데 그중의 모든 Select 문의 결과값으로 ResultSet의 인터페이스를 구현한 Rows를 반환한다. ResultSet Interface를 구현하였기 때문에 기존의 메소드를 거의 모두 지원하며 몇가지 추가 유틸리티 메소드를 제공한다.

  - 기존의 일반 JDBC에서 반환하는 ResultSet의 구현체와는 다르게 Rows는 Value Object 기반의 클라이언트 커서를 사용한다. 따라서 쿼리의 실행후 pstmt.close(), rs.close(), freeConnection()의 과정은 전부 내부에서 이루어지고 호출자에게 노출되지 않는다. 일단 IDBControler가 초기화된후 dc.getRows() 혹은 cmd.execQuery() 실행후 사용자는 Resource의 반환등에 전혀 신경쓸 필요 없으며 내부적으로 상황에 맞는 결과 캐쉬를 통해 실행속도를 보장한다. rows는 인터페이스 규약상 close()가 있지만 close()를 하지않아도 단순 ValueObject이므로 가비지 컬렉터에 의해 자동으로 정리가 된다.

   - Rows는 거의 순수하게 Value 객체이기 때문에 Framework에서 자동으로 반환한 Resource와 상관없이 Clob 억세스와 쿼리 재실행 등이 가능하며 Serialized XML 형태로 언제든 변환이 가능하다.


rows.getNextPage() ; // 다음 페이지의 rows를 가져온다.
rows.getPrePage() ;; // 이전 페이지의 rows를 가져온다.

   - Clob Access
     JDBC에는 Clob과 Blob의 표준 datetype이 있지만 DB마다 지원여부와 사용방법이 조금씩 다르다. 또한 그 사용방법이 대부분 번거롭게 때문에 varchar의 글자수 제한으로 어쩔수 없이 Clob을 사용해야 하는 경우 매우 불편하다. 그래서 Datatype이 Clob이더라도 DB에 따라 자동 형변환을 통해 rows.getString()을 통해 Access한다. (Blob은 getBinaryStream()) 만약에 Clob에 들어가는 데이타가 100M가 넘는다면 이 방법을 사용하는 것을 신중히 생각해야 할지도 모른다. 그러나 텍스트 1M를 넣는 경우도 매우 흔치 않을뿐더러 그경우에도 Blob으로 다루는 방법을 선택하는게 좋다.

    # insert 혹은 update시 parameter는 DB 벤더에 상관없이 setClob(String str), setBlob(InputStream input) 메소드를 사용하면 된다.




IUserCommandBatch

   - 특정 테이블에 10건의 insert를 해야 한다면 10개의 UserCommand를 실행시키는 것보다 10개의 Arrary를 parameter에 set한후 실행시키는 Batch를 사용하는게 좋다. 평균적으로 최소한 Batch는 건당 insert 속도가 최소 1ms 이하를 보장하며 이는 UserCommand의 기준 속도인 50ms보다 약 50배가 더 빠르다.

ex)
IUserCommandBatch cmd = dc.createUserCommandBatch("insert into update_sample values(?, ?)") ;
int max = 1000 ;
for (int i = 0; i < max; i++) {
        cmd.addBatchParam(0, i) ;
        cmd.addBatchParam(1, i + "th ..") ;
 }
int count = cmd.execUpdate() ;
와 같이 설정하거나..

int[] a = new int[max];
String[] b = new String[max] ;
for (int i = 0; i < max; i++) {
    a[i] = i ;      b[i] = i + "th .." ;
}
cmd.addParam(a).addParam(b) ;
int count = cmd.execUpdate() ;
등과 같이 직접 Array를 인자로 설정한다.


    - 하나의 Batch문에 에 가장 효율적인 건수는 JVM에서 관리되는 Array가 차지하는 메모리에도 밀접한 관련이 있긴 하지만 대략 10,000-100,000 사이가 가장 좋다. row의 평균사이즈를 150-250 byte로 했을때 대부분은 만건당 최대 10초이내의 효과를 보인다.




IUserProcedure

   - 가장 많은 오해를 받지만 가장 효율적이며 많이 사용되는 객체이다. IUserProcedure는 오라클이나 MSSQL의 Procedure와는 직접적인 관계는 없다. 프로그램에 직접 ANSI SQL를 사용했을 경우 이후 DB에 변동이 있다면 재앙수준의 변경을 요구한다. 특히 SQL이 String 변수로 관리되기 때문에 직접 모든 프로그램을 열어서 수정해야 한다. 컴파일 오류가 아닌 런타임 오류가 나기 때문에 수정에 대한 확신도 매우 어렵다.

    이러한 문제로 몇년전부터는 IBatis등에서 XML 형태로 Key Value형태로 관리한후 Key 형태로 Access 하는 방식이 좀더 많이 퍼지게 되었다. 비슷하게 UserProcedure는 앞에서 말한 DB 벤더에 디펜던트한 Procedure와는 직접적 상관없이 그냥 IDName을 가진 Message 객체에 불과하다. 그 Message를 어떻게 해석할 것인가는 DB Manager에 달려 있으며

현재 Framework에 포함되어 있는 OracleDBManger는 패키지와 프로시저, MSSQL은 Procededure로, MySQL은 Procedure 혹은 Function으로 H2DBManager는 특정 XML 파일의 Key-Value 형식의 SQL name으로 해석하고 있다. 즉 UserProcedure는 DB Maanger를 어떻게 구현하는가에 따라 해석방법이 정해지며 일단 같은 name을 가진다면 DB 벤더의 프로시저 혹은 패키지 지원여부와 상관없이 동일한 결과값을 보장해야 한다.


ex)
IUserProcedure upt = dc.createUserProcedure("Sample@selectBy(:a)") ;
upt.addParam("a", 1); 
Rows rows = upt.execQuery() ;

만약 DBManager가 MySQLPoolDBManager라면 위 구문을 Sample_selectBy라는 procedure라고 해석하며
해당 MySQL에는 아래와 같은 Procedure가 있다면 해당 프로시저를 실행한후 그 결과 Rows를 반환한다.

CREATE PROCEDURE sample_selectBy(v_a int)
BEGIN
    SELECT * FROM update_sample WHERE a > v_a ;
END//



만약 DBManger가 오라클이라면 아래와 같이 Sample Package의 selectBy function을 실행시킨후 결과셋을 반환한다.

CREATE OR REPLACE PACKAGE BODY Sample
    is
        function selectBy (v_a number) return Types.cursorType
        is
            rtn_cursor Types.cursorType ;
        begin
            open rtn_cursor For
            select * from update_sample where a > v_a ;
           
            return rtn_cursor ;
        end ;

         ................
       
End Sample ;


     - 이와 같이 DB가 Procedure를 지원하는가 혹은 지원하지 않는가와 상관없이 IUserProcedure는 쓰이며 그 해석방법은 DBManager에게 달려있다. IUserCommand보다 IUserProcedure의 사용을 더 권장하는 이유는 DB 벤더의 장점을 최대한 활용할 수 있고 개발과 유지보수에 있어서의 장점때문이다. 물론 기타 성능이나 보안등의 자잘한 이유는 제껴두고서라도 말이다.


IUserProcedureBatch

    - IUserCommandBatch 처럼 UserProcedure의 Batch 버전이다. 다만 Proceudre 구조상 Batch라서 해서 UserProcedure와 비교해 커다란 성능차이는 없다. 다만 하나의 Procedure에 여러개의 SQL문을 담을 수 있다는 걸 생각하면 복잡한 배치처리 작업에나 쓸만하다.



UserProcedures
    - s가 뒤에 하나 붙어 있다. UserProcedure는 자신이 IQueryable의 구현체이면서 IQueryable의 구현체를 담을수 있는 composite 형태로 되어 있다. UserProcedures에 담긴 모든 MDL문은 자동으로 하나의 Transaction으로 처리가 된다. 그리고 물론 각각의 개별적인 실행보다는 조금 더 빠르다.

ex)
 IUserCommand cmd1 = dc.createUserCommand("insert into update_sample values(?, ?)") ;
 cmd1.addParam(1).addParam("abc") ;
 IUserCommand cmd2 = dc.createUserCommand("delete from update_sample where a = ?") ;
 cmd2.addParam(1);
       
 UserProcedures upts = dc.createUserProcedures("Multi MDL") ;
 upts.add(cmd1).add(cmd2) ;

 int result = upts.execUpdate() ;
 assertEquals(2, result) ;
    
    - 만약 UserProcedures에 여러개의 Select Query를 집어넣고 execQuery()를 실행하면 어떻게 될까? 이 경우에 여러개의 결과값을 반환하게 되는데 Rows.getNextRows()를 통해 다음 결과셋을 얻을수 있다. UserProcedures에 담긴 IQueryable 객체는 add된 순서대로 실행된다.

ex)        
  IUserCommand cmd1 = dc.createUserCommand("select * from copy_sample") ;
  cmd1.setPage(Page.create(10, 1)) ;
  IUserCommand cmd2 = dc.createUserCommand("select 3 from dept_sample") ;
       
  UserProcedures upts = dc.createUserProcedures("Multi Query") ;
  upts.add(cmd1).add(cmd2) ;
       
  Rows first = upts.execQuery() ;  // first query result ;
  assertEquals(true, first.getRowCount() == 10) ;
       
  Rows second = first.getNextRows() ; // second query result ;
  assertEquals(3, second.firstRow().getInt(1)) ;




XAUserProcedure

    - 앞의 UserProcedures는 하나의 DB에 대한 Transaction 보장이며 이기종 DB 즉 멀티 DB에 대한 멀티 MDL문의 분산 트랜잭션은 XAUserProcedure를 통해 할수 있다.


    MSSQL의 접속관리자인 mdc와 오라클의 접속관리자인 odc가 아래와 같이 이미 생성되어 있다고 하자.
// prepare
MSSQLDBManager mManager = new MSSQLDBManager("jdbc:microsoft:sqlserver://localhost:1433;DatabaseName=test", "bleu", "redf");
DBController mdc = new DBController(mManager) ;
mdc.initSelf() ;

OracleDBManager oManager = new OracleDBManager("jdbc:oracle:thin:@novision:1521:al", "al", "redf") ;
DBController odc = new DBController(oManager) ;
odc.initSelf() ;
       

// tx1 : MSSQL용 쿼리 집합을 만든다.        
TxTransaction tx1 = new MSSQL2000TxTransaction(mManager, "mquery") ;
tx1.add((Queryable)mdc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
       
// tx2 : oracle용 쿼리 집합을 만든다.
TxTransaction tx2 = new Oracle9iTxTransaction(oManager, "oquery") ;
tx2.add((Queryable)odc.createUserCommand("insert into copy_tblc values('111', 111)")) ;
       

// add tx : 두개의 쿼리 집합을 XAUserProcedure에 넣는다.
TxTransaction[] txs = new TxTransaction[]{tx1, tx2} ;
XAUserProcedure upts = new XAUserProcedure(txs) ;

// 실행한다.
upts.execUpdate() ;


// clear : 더이상 사용되지 않을 DB 접속정보라면 정리한다.
mdc.destroySelf() ;
odc.destroySelf() ;


      - 보통의 XaTransaction 보다 훨씬 더 간편하게 실행할 수 있다. 결국 인터페이스의 간결화로 호출자는 자세한 XaTransaction 과정에 대해 알 필요가 없다.

MSSQL에서 XaTransaction을 사용하기 위해서는
     * Be sure that you have copied your sqljdbc.dll file from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServer JTA\ to your SQL Server's "binn" directory (most likely C:\Program Files\Microsoft SQL Server\MSSQL\Binn).
     * Then open your instjdbc.sql script from C:\Program Files\Microsoft SQL Server 2000 Driver for JDBC\SQLServerJTA\ within Query Analyzer and run the script. This will install the extended stored procedures into SQL Server so that it can reference the sqljdbc.dll file.

     의 과정을 실행해야 하며 MS DTC가 기동중이어야 한다. MySQL은 얼마전까지 XaTransaction을 지원하지 않았고 현재도 InnoDB storage engine에서만 지원하는 걸로 알고 있다.



CombinedQuery

     - CombinedQuery는 UserProcedure와 조금 비슷한데 단지 UserProcedures와는 달리 MDL문과 Query문을 동시에 실행시킬수 있다는 점이 다르다.


ex)
IUserCommand ins = dc.createUserCommand("insert into update_sample values(?,?)") ;
ins.addParam(1).addParam("abc") ;
       
IUserCommand sel = dc.createUserCommand("select * from update_sample") ;
       
CombinedUserProcedures upts = dc.createCombinedUserProcedures("combined") ;
upts.add(ins, "ins", IQueryable.UPDATE_COMMAND).add(sel, "sel", IQueryable.QUERY_COMMAND) ;

// 타입이 다른 ins문과 sel문을 동시에 실행시킨다.       
upts.execUpdate() ;
       
Map result = upts.getResultMap() ; // for access sel'result
       
int rowcount = (Integer)result.get("ins") ;
Rows rows = (Rows)result.get("sel") ;
       
assertEquals(1, rowcount) ;
assertEquals("abc", rows.firstRow().getString("b")) ;

    - 역시 하나의 Transaction으로 처리된다. 주로 마이그레이션 작업이나 session scope temporary table을 가지고 작업할때 쓰인다. 혹은 Session Level Configuration 명령문과 select문을 혼합할때 쓰이기도 한다. 즉 특정경우에 Set Ansi_null Off 시키고자 할때 session 명령문은 Update로 Select문은 Query로 실행시킨다. 앞어 거듭 말했듯 Rows는 클라이언트 커서이기 때문에 한건을 insert 하고 해당 건을 select하고 해당 건을 지우는 3개의 명령문을 combinedProcedure에 넣고 실행시켜도 제대로 된 Rows 결과물을 볼 수 있다.




ExtraServant


    - ExtraServant는 IQueryable의 구현 클래스가 아니며 이름 그대로 IDBController의 하인 역할를 수행한다. 예컨데 모든 쿼리의 실행시간을 System.out으로 확인하고 싶다고 하자.

ex)
dc.addServant(new StdOutServant(StdOutServant.All)) ;

를 실행시켜 주면 해당 dc로부터 생성된 모든 IQueryable 객체는 실행후 StdOutServant를 통해 쿼리 이름과 실행시간을 System.out에 출력한다. 그와 동시에 300ms 이상의 모든 쿼리는 따로 기록해 두고 싶다고 한다면.

ex)
dc.addServant(new TraceOfLateProcServant(300)) ;
를 추가시키면 된다.

dc는 1개 이상의 ExtraServant를 가지며 모든 쿼리는 해당 쿼리 작업을 완료후 Chain된 ExtranServant를 통해 임의의 추가 행동을 지정할 수 있다. ExtraServant는 별도의 Thread로 관리되며 ExtraServant의 동작시간과 행동은 기존의 작업에 영향을 주지 않는다.

별도의 IDBController를 새로 만들어서 Connection만 공유한채 Servant 정보는 달리 유지시킬수 있다.

ex)
DBController newDc = new DBController("newDC", dc.getDBManager()) ;
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 모든 IQueryable를 화면에 프린트하는 Servant를 추가한다.
newDc.addServant(new StdOutServant(StdOutServant.All)) ; // 한개 더 추가한다.
newDc.initSelf() ;
       
newDc.getRows("select 1 from copy_sample") ;
newDc.destroySelf() ;

위의 경우 newDC는 기존 dc.getDBManger의 Owner가 아니기 때문에 destroySelf()를 실행시켜도 해당 DBManger의 Pool은 파괴되지 않는다.

     - ExtraServant는 다양한 상황에 새롭게 상속받아 구현함으로써 다양한 역할을 수행할 수 있다. 이를테면 특정 사용자가 접속했을때 MSN으로 메시지를 보내주는 Servant를 구현할 수도 있고 특정 쿼리들이 실행되었을때 Log를 남기는 Servant를 구현할수도 있다.



   Handler

   - Rows는 여러종류의 Handler를 사용하여 다양한 타입으로 변신이 가능하다.


Rows rows = dc.getRows("select * from copy_sample order by no1", 10, 1) ;

첫번째 row를 Map 형태로 바꿀 수도 있고
ex)
Map results = (Map)rows.toHandle(new MapHandler()) ;
assertEquals(2, results.size()) ;   // column 수 확인

모든 row를 Map의 List 형태로 바꿀 수도 있고.
List<Map> results = (List<Map>)rows.toHandle(new MapListHandler()) ;
assertEquals(10, results.size()) ; // row수 확인

모든 row를 Bean List 형태로 바꿀 수도 있고
List<TestBean> results = (List<TestBean>)rows.toHandle(new BeanListHandler(TestBean.class)) ;
TestBean row = results.get(0);

assertEquals(1, row.getNo1()) ;
assertEquals("01", row.getNo2()) ;

assertEquals(2, results.get(1).getNo1()) ;
assertEquals("02", results.get(1).getNo2()) ;


특정 컬럼의 값만을 얻어올수도 있다.
Object value = rows.toHandle(new ScalarHandler(2)) ; // 첫번째 row의 2번째 컬럼의 값
assertEquals("1", value.toString()) ;




6년전에 해당 Framework를 처음 개발했을때 목적은 4가지이다.

1. 풀링을 구현하되 프레임워크 사용자가 실수로라도 Connectionless를 만들 수 있는 방법을 원천적으로 차단한다.
    -> Connectio은 외부에서 보이지 않으며 Client Cursor 사용으로 결과셋 전달후 Resource는 자동으로 바로 반환한다. 즉 사용자는 JDBC의 Resource에 대해 알 필요도 없으며 알 수도 없다.

2. 프로그램에 SQL문을 심지 않는다.
    -> UserProcedure의 적극적인 사용으로 Query문은 모두 중앙집중 관리 되며 이는 개별적인 DB 벤더의 장점은 최대한 활용함과 동시에 DB 벤더에 종속되지 않는다는 상이한 목표를 동시에 추구한다. IQueryable의 모든 구현체는 단순히 Message역할만을 수행하기 때문에 JDBC의 PreparedStatement와 달리 정리해줘야 할 Resource가 아니다.

3. LOB 핸들링을 쉽게 한다.
   -> 벤더마다 그리고 버전마다 Lob Handling에 있어서의 사용방법이 다른걸 간단한 인터페이스로 통합시고 사용자는 byte 단위의 Access Handling을 하지 않아도 되게 한다.

4. 사용방법은 쉬워야 함과 동시에 여러가시 발생할 수 있는 상황에 쉽게 적응 가능해야 한다. 동시에 불필요한 퍼포먼스 감소를 일으켜서는 안된다.



이후.. 한동안 추가 기능이 없다가 최근에

  - MySQL을 기존의 MSSQL, Oracle과 더블어 기본 테스트 항목및 지원DB로 함.
  - 인터페이스의 소폭 수정
  - viewPlan(Query의 실행계획보기)
  - Paging (쿼리의 Page 설정, nextPage, prePage Access)
  - NamedParameter

등등의 잡다한 기능을 넣었다. 가장 큰 이유는 Paging 때문인데 JDBC에 Client cursor에 Page 관련 메소드가 있는데 제대로 implement한  JDBC가 거의 없어서 그냥 만들었다. -ㅅ-. Page는 DB 마다 처리 방법이 다르지만 해당 DB에 가장 효율적인 방법을 사용하려고 했다.








사용자는 framework_core_fat.jar를 추가하고 해당 DB의 JDBC jar만 추가시켜서 사용하면 된다.

아래와 같은 Apache Common Jar를 사용한다.
<fatjar.jarsource file="lib\jericho-html-2.5.jar" relpath=""/>  // 얘는 parser framework에 쓰임
<fatjar.jarsource file="lib\jmock-core-1.2.0.jar" relpath=""/>
<fatjar.jarsource file="lib\junit.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-pool-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-beanutils-1.8.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-collections-3.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-dbcp-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-digester.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-fileupload-1.1.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-io-1.2.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-lang-2.3.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-logging-1.0.4.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-net-1.3.0.jar" relpath=""/>
<fatjar.jarsource file="lib\apache-common\commons-validator-1.1.4.jar" relpath=""/>



'Framework > Database' 카테고리의 다른 글

DBFSlayers  (1) 2010.04.04
Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin
Framework/Database2010. 2. 24. 09:55


이전글에서 커서의 일반 지식과 오라클과 MSSQL의 커서 방식에 대해 얘기 했으니 3번째로 MySQL의 커서 방식을 이야기해보자. 사실 개인적으로 MySQL은 상대적으로 자주 사용하는 DB가 아니다. MySQL은 "거의" 무료지만 다른 2개의 디비에 비해 기능과 지원이 부족하고 다른 '완전한' 무료에 비해 그리 많이 좋지도 않는 어정쩡한 위치에 있기 때문이다.

거의 1년만이므로..
다시 기본조건을 상기해보자.

JDBC로 stmt.execQuery("select * from millonRow_tblc") ; 를 실행했을때..


Large ResultSet의 Handling에 관한 문제이다.


우리가 먼저 시작해야 할 점은 사용자는 과연 모든 결과가 필요할까? 라는 점이다.

물론 100만개의 Row가 모두 필요한 프로그램일수도 있다. 하지만 그 1%의 확률로 모든 Row가 필요한 프로그램이라고 하더라도 그 중에 다시 99% 이상은 DB에서 연산이 가능하다. DB는 다른 어떤 통계 프로그램보다 많은 연산자와 빠른 속력을 지원해주기 때문에 정말 화면에 모든 Rows를 뿌려야 하는 0.01%의 확률의 프로그램이 아니라면 연산을 DB에서 하는게 좋다.

어쨌거나 지금 그 0.01%의 확률의 프로그램을 만들어야 한다고 가정하자.
즉 당신은 이유야 어쨌거나 "select * from millonRow_tblc" 모든 결과 셋이 필요하다.


DB가 MySQL일때
pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
rs = pstmt.execQuery() ;
를 실행해보자.

아마도 특별히 Java의 Heap 메모리를 늘리지 않았다면 Out of Memory 예외를 보게 될것이다.
단 한행도 rs.next()를 하지 않았음에도 불구하고 말이다.

한행이 약 100byte라고 가정했을때 0.1k * 1000000 = 100M로
Select의 모든 결과셋의 크기가 기본 Java Heap Memory의 64M를 초과하기 때문이다.


MySQL에서 - 비록 공식문서에는 나와 있지 않지만
당신이 특별한 다른 옵션을 사용하지 않았을때 Client Cursor Location에 Static Cursor Type을 사용한다.

즉 JDBC에서 위 예제를 실행했을때
해당 쿼리의 모든 결과셋을 Client(여기서는 Application Server가 Client)에 전송하고 Client에서는 그 결과셋을 모두 받아서 Client 메모리안에 Table 형의 결과셋을 만들어 채우는 과정을 한다. 보통의 다른 Set 알고리즘과 마찬가지로 기본 Set이 거의 채워지면 해당 Set의 기존 Size를 1.5배 늘리고 이 용량이 다시 채워지면 다시 1.5배를 늘리는 동작을 반복한다.
다시 말해서 위의 경우 DB에 던지는 결과셋을 받아서 계속 Set의 용량을 늘리다가 어느순간 한계 Heap Size에 도달하게 되는 것이다.

따라서 정말 millonRow_tblc의 Table의 모든 Row가 필요하다면 Heap Size를 늘려야 한다. 그러나 근본적인 해결책은 아니다. 왜냐하면 Heap의 Size를 10배로 늘렸다고 해도 10명이 동시에 해당 프로그램을 실행하면 역시 Out of Memory Exception을 보게 된다. 백만 Row를 모두 받아서 Clinet에 Set을 구축하기까지는 꽤 오랜(아마도 컴퓨터 성능에 따라 1 - 10분 사이) 시간이 걸리기 때문에 10명이라는 수치는 매우 자주 일어나는 경우가 될것이다. 물론 사용자가 단 1명이더라도 결과를 보기위해서는 1-10분을 기다려야 한다.



공식적인 해결방법은 아니지만 MySQL에서는 일종의 꽁수같은 방법이 있다. 이것은 일종의 버그이므로 앞으로도 계속 지원되는 방법이라고는 보장할수 없다.

pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
psmt.execQuery() ;

위와 같이 ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY를 사용하고
FetchSize에 Integer.MIN_VALUE를 설정하면
MySQL은 Client에 더이상 Static Cursor를 사용하지 않는다. 위와 같이 작성할경우 MySQL은 FireHouse Cursor방식을 사용한다. FIreHouse Cursor는 앞에서 나왔던 MSSQL의 기본 cursor이다.

공식적인 cursor Type에서는 언급하지 않지만 인터넷 환경에서는 묵시적으로 자주 사용된다. FireHouse 커서 동작방식에 대해 말하기 전에 MYSQL의 JDBC는 Server Cursor Location을 지원하지 않는다. 물론 근본적으로 JDBC의 API에 Cursor Location을 설정할수 있는 메소드가 없기 때문이기도 하지만 그럼에도 MSSQL에서는 CursorType에 따라 묵시적으로 바뀌는 것과 달리 MySQL에서는 아예 Server Cursor Locatiion을 공식적으로 지원하지 않는다.

따라서 MSSQL의 FireHouse 커서와는 조금 다른 동작을 한다.

pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
rs = psmt.execQuery() ;
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}

와 같이 실행하였다면 반갑게도 바로 결과가 나올것이다. Connection Pool을 사용했다면 0.1초 이내에 말이다.

유레카~ 라고 소리지르기 전에..알아야 할 것이 있다.
FireHouse 커서방식은 Static Cursor Type과 달리 모든 결과셋을 Client에 구축하지 않고 Row Level 단위로 Access할수 있는 장점이 있긴 하지만 단점이 2가지가 있다.
첫번째는 rs는 더이상 previons()나 relative/absolute move를 지원하지 않는다. 이는 FireHouse의 동작방식을 생각해보면 당연한 것이다. FireHouse방식에서는 rs.next()를 호출하는 순간에 더 이상 이전 previous의 커서의 위치나 정보를 보관하지 않는다. next()를 호출할때마다 이전 record의 정보는 메모리에서 날림으로서 Out Of Memory를 피하는 방식이다. 그리고 당연히 rs.getRowCount() 같은 메소드도 사용할수 없다. 만약 RowCount를 알고 싶다면 while(rs.next()) i++ 와 같이 구할수 밖에 없다.

첫번째는 사실 그다지 큰 문제는 아니다. 보다 중요한 것은 두번째 단점이다.
두번째 단점은 MYSQL의 Client Location의 FireHouse Cursor는 멈추지 않는다는 것이다.
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}
와 같이 단지 10행만을 사용한다고 하더라도 MYSQL DB는 그걸 인지할수 없으며 여전히 남은 99만 9990개의 남은 row를 Client에 전송한다.

그래서
pstmt = conn.prepareStatement("Select * ....", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(Integer.MIN_VALUE) ;
rs = psmt.execQuery() ;
for( int i = 0 ; i < 10 ; i++) {
   out(rs.getString(1)) ;
}
까지 0.1초가 걸리더라도..

rs.close() ;
이 문장을 실행하는데 1분 - 10분 정도가 걸린다. FireHouse는 일종의 Data Stream이기 때문에 모든 결과셋을 받기 전까지는 rs를 close 할수 없다. 그렇다고 rs.close()를 하지 않으면 해당 Connection은 미아가 되어 얼마지나지 않아 Connection Pool Exception을 보게 된다.

가끔 특정 사이트를 보면 화면에 내용은 거의 다 뿌려졌는데 브라우저 하단의 Progress Bar는 여전히 진행중인 사이트를 본다면 반이상은 JSP로 위의 방식으로 코드를 작성했기 때문이다. response.flush()로 일단 화면에 보여지지만 JSP코드 하단의 rs.close()를 하는데 오래오래 걸리고 있기 때문이다.

MVC가 머고간에 일단 JSP에서 JDBC를 억세스 해서 close()를 하기전에 일단 Access한것만이라도 flush로 먼저 보낸다면 화면에는 빨리나온것처럼 보이기에 일단 급한 마음에 그렇게 한것이다.

Lazy Initialize 패턴같은 사용방식처럼 보일수도 있지만 이런다고 문제는 해결되지 않는다. 일단 Client에서 빨리 보여지느냐 늦게 보여지느냐에 상관없이 FireHouse Cursor Mode에서 Server인 DB는 여전히 남은 Row 들을 보내고 있다. 그 말은 귀중한 자원인 DB는 Network Channel에 사용하지 않을 Row를 쓰고 있고 여전히 Application Servers는 사용하지 않을 Row을 받고 있다는 얘기이다.

최근의 대부분의 하드는 100Mb read/sec 이상의 속도를 가진다. 그럼에도 대부분의 Network Channel은 고작해야 10Mb / sec이기 때문에 필요도 없는 Row를 Network로 보내느라 DB는 계속 바쁘다.

처음에 언급한바와 같이 상황에 따라 DB에 따라 심지어는 버전에 따라 이와 같은 문제를 해결하는 방식은 다르기에 이전의 Mssql의 방식을 사용할수는 없다.(다시 말하지만 MySQL의 JDBC에서는 Server Cursor Location이 없다.) 해결방법은 다음글에서.. =ㅅ=


'Framework > Database' 카테고리의 다른 글

DBFSlayers  (1) 2010.04.04
DB Framework 2.0 Short Explain  (3) 2010.03.09
Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Posted by bleujin
Framework/Database2009. 3. 26. 00:11

앞글에서 이야기한바와 같이 서버커서 선택의 제 1조건은 결과셋의 크기와 결과 셋에서 정말 사용할 비율이다.
이중 결과셋의 크기는 정상인 사람이 인터페이스를 고민하였다면 문제되는 바가 아니고 보다 중요한 것은 결과셋에서 실제 사용할 - 화면에 뿌릴 - 비율이다.

그렇다면 이렇게 생각해보자. 만약 애초에 정확히 사용할 데이타만을 결과셋으로 선택할 수 있다면 - 그렇게 쿼리를 작성할 수 있다면 - 굳이 서버 커서를 사용할 필요가 있을까?
일단 ForwardOnly와 Anonymous 종류는 커서의 이동이 안되고
Static과 Anonymous는 네트워크의 패킷 통신이 비효율적이며 서버의 메모리 자원을 많이 소모한다.

그리고 서버 커서 모두 연결 정리 작업을 이후에 해줘야 하는 단점이 있다.



클라이언트 커서 모델은 위와 같이 커서정보가 클라이언트에 있는 모델을 말한다. (당연히 기존의 커서는 커서가 모두 서버에 위치하였다.)

1. 쿼리 실행을 요청한다.
2. 쿼리를 실행하여 모든 결과 셋을 클라이언트에게 전송한후 연결을 끊는다.
3. 자신의 메모리에 로드된 결과셋을 사용한다.

클라이언트 커서의 주요 장점은 2단계에 있다. 결과셋을 가능한 모아서 던지며 모두 전송한후 클라이언트와 연결을 끊을 수 있다. (실제로는 이 과정을 soft close라고 하는데 실제로 바로 끊는게 아니라 언제든 자동으로 끊어 질수 있는 단계라고 생각하면 된다. 바로 끊어버리지 않는 이유는 clob이나 text등의 lob 객체는 결과셋 전송시 같이 이루어지지 않고 다시 요청이 오면 앞의 inner connection을 재연결해서 실제 데이타를 가져와야 하기 때문이다. 모든 DB는 lob 데이타를 다루는데 이러한 address 방식을 사용하기 때문에 모델링시 신중하게 결정해야 한다. 만약 자신의 환경이 이런 lob 데이타를 하나의 결과셋에 많이 포함하는 특수한 구조라면 서버커서도 마찬가지이니 이에 해당하는 클라이언트 커서 모델을 별도로 고려해보는게 좋다.)

일단 전송이 완료되면 클라이언트에 있는 결과셋은 기존의 서버에 있던 결과셋의 일종의 복제이므로 커서의 컨트롤 방식은 같고 커서의 전후진과 absolute 이동이 가능하다는 장점이 있다. 또한 보통의 경우 메모리 자원이 많이 요구되는 DB의 메모리를 보다 절약할 수 있기 때문에 자원의 균형적인 사용에도 도움이 된다. 


이 방식의 유일한 단점은 앞서 말한바와 같이  정확히 사용할 데이타만을 결과셋으로 선택할 수 있는 SQL 쿼리 능력을 갖추는 일이다. 극한 성능을 위해 좀더 정확한 selection을 원한다면 쿼리가 좀 복잡하지만 만약 주로 앞부분의 데이타가 사용되는 페이징 인터페이스라면 단순히

(v_pageNo : 페이지 번호, v_listNum : 페이지당 리스트건수)
select columns
from
     (select columns, rownum rnum from table where condition and rownum <= v_pageNo * v_listNum ) base
where rnum between v_listNum * (v_pageNo - 1) + 1 and v_pageNo * v_listNum and rownum <= v_listNum

로 정도로 사용(만약 MSSQL이라면 TOP N 형식으로는 N을 동적으로 사용할 수 없기 때문에 Set rowcount 를 이용해야 한다. ) 해도 기존의 서버 커서모델보다는 빠르니 그다지 미리 걱정할 필요는 없다. (클라인언트 커서 방식일때 좀 더 빠른 이유는 네트워크의 효율적 전송과 다중 사용자 환경에서 DB 서버의 메모리를 더 작게 그리고 더 적은 시간동안 사용하기 때문이다. )

클라이언트 커서 모델은 어쩌면 낳설게 들릴지 모르지만 이전의 POJO방식의 인터페이스의 기본 바탕이 되는 모델이다. 다만 OR-Mapping 프레임워크나 EJB에서는 row 단위 억세스가 일어나기 때문에 클라이언트 커서의 장점이 거의 나타나지 않는다. 이 블로그의 DB Framework는 클라이언트 커서 모델을 기반으로한 DB Framework이기 때문에 결과셋인 Rows는 클라이언트 커서를 사용한다.  




'Framework > Database' 카테고리의 다른 글

DB Framework 2.0 Short Explain  (3) 2010.03.09
Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Framework - 커서(Anonymous)  (0) 2009.03.19
Posted by bleujin
Framework/Database2009. 3. 24. 00:45

클라이어트 프로그램이 웹이라면 아마 Keyset이나 Dynamic 커서는 99.9%의 확률로 써야 할 경우가 없을 것이다. connectionl less한 웹에서 사실 동시성은 거의 의미가 없다. 사실 별도의 어드민 작업이 없다고 확신할 수 있다면 굳이 commited data만을 읽을 필요조차 없다.

따라서 대부분 사용하는 커서는 Forward Only, Static, Anonymous(가칭) 3가지이다. 프로그래머가 의도하든 의도하지 않았든 말이다.

Forward Only는 쿼리의 결과셋 모두가 클라이언트 프로그램에 필요할경우 사용하며, 셋을 유지하지 않아도 되고 네트워크 패킷도 여러 로우를 묶어서 던지기 때문에 효율적이다.

Stati은 쿼리의 결과셋 모두 혹은 대부분이 클라이언트에 필요할경우 사용하며, 셋을 임시 영역에 유지하는데 메모리 비용과 패킷수량이 많아지는 단점이 있지만 커서의 앞뒤 이동이 가능하다는 점이 장점이다.

Anonymous는 쿼리의 결과셋의 처음 일부만 필요할 경우 사용하며, 패킷 수량이 많아지는 단점이 있지만 부분범위 처리가 가능하다는 장점이 있다.


커서를 선택하는 일반적인 기준은 아래와 같다.
1. result Size : 결과 셋의 전체 사이즈가 크면 Static은 쓰지 않아야 한다.
2. result Size에서 필요한 Data의 비율 : 퀴리 결과에 비해 클라이언트 필요한 부분이 아주 일부분에 불과하다면 Anonymous 커서를 사용해야 한다.
3. 필요한 커서 행동 (스크롤링 여부, 업데이트 가능 여부) : 쿼리결과 대부분이 사용될경우 스크롤링이 필요없다면 ForwardOnly, 스크롤링이 필요하다면 Static을 고려해볼수 있다. 단 단순히 갯수만을 알고자 한다면 굳이 Static을 선택하지 않고 차라리 select count(*) 쿼리를 별도로 던지는게 더 빠를수도 있다.
4. 커서 자체의 속도(예컨데 open 시) : 커서 자체의 속도는 필요한 정보가 가장 적기 때문에Forward Only가 다른 방식보다 빠르다.
5. 다른 유저가 변경했을때의 변경의 visibility level : 앞에서 말했듯이 connection less 환경에서는 거의 의미가 없지만 특이 케이스일경우 고려해 볼 수 있다.


쿼리셋의 전체 혹은 일부만을 원하는지의 여부가 가장 결정적인 선택 기준이 된다. 만약 select의 결과가 단지 한개의 row이거나 small Set이고 전체 셋을 모두 사용할 것이라면 그냥 default 커서(Forward Only)을 사용한다. 그러나 만약 update가 가능해야 한다면  default커서는 사용할 수 없다. 디폴트 커서는 커서의 행의 수를 미리 알 수 없고 absolute 등의 이동도 불가능하다.

여기서는 상세하게 적지 않겠지만 다이나믹 커서는 내부의 임시 영역에 Set 형태를 만들지 않기 때문에 처음 몇개의 row만 사용한다면 스태틱이나 키셋 커서에 비해 빠를 수 있다. 단 조인을 사용하는 경우 다이나믹 커서는 락킹 처리 문제로 더 느려진다. 키셋이나 스태틱 커서만이 absolute fetch가 가능하다. 반면에 이 두 방식의 커서는 Set을 tempDB에서 build 하기 때문에 tempDB 사이즈에 영향을 받는다.
 
위 기준에 맞춰 커서의 종류를 선택하였으면 앞글에서 적었듯이 쿼리를 전체범위처리냐 부분범위처리냐에 맞게 바꿀수 잇는 능력이 필요하다.


.... 이게 커서의 끝이 아니다.

프레임워크를 개발하는 입장에서 본다면 위 방식은 심각한 문제가 있다. 커서를 사용하는 방식은 DB마다 상이하며 또 모든 DB가 이를 지원하지도 않는다. MSSQL의 경우처럼 어떤 커서를 선택하느냐에 따라 JDBC URL을 바꿔야 한다면 모든 프로그래머가 이를 기억하고 있을리가 없다. 게다가 2005ver에서는 또 다르다!. 프레임워크는 어려운걸 쉽게 만들수는 없더라도 복잡한걸 간결하게는 만들어줘야 하는데 설사 그게 성능등의 이유로 어쩔수 없다고 하여도 위의 선택기준은 지나치게 문제가 복잡해진다.

그래서 위의 커서를 이해하였다면 그 내용을 모두 잊어버려야 한다. 농담이 아니다. 위 커서방식들은 당신이 아주 전문가이고 개별적인 환경하에서의 특이한 케이스가 아니라면 쓸수가 없다. 물론 그렇다고 과거의 무지의 상태로 돌아가서 우연히 좋은 방식이 선택이 되기를 혹은 드라이버가 알아서 좋은걸 선택해주지 않을까 하며 기도하는 것도 방법은 아니다.


사실 앞에서 커서의 모든 종류를 얘기한 것이 아니다. 단지 "서버커서"의 모든 종류를 얘기했을 뿐이다. 서버커서는 앞의 PPT에서 보이듯이 커서 - 일종의 포인터 -가 DB 서버에 위치한다.

Client Cursor는 말 그대로 Cursor의 위치가 데이타베이스가 아닌 클라이언트에 위치하는 모델이다. 후에 좀더 자세히 설명하겠지만 초기의 데이타베이스는 서버 커서를 지원하지 않았고 프로그래머는 사용하는 언어로 DBMS 벤더가 제공하는 DB-Library 커서 함수들을 사용하여 각자 구현하여야만 했다.

이후 각 DBMS 업체가 Server Cursor를 제공함으로써 Client Cursor 모델 방식은 한동안 잊혀졌으나 최근의 인터넷 환경과 맞물려 다시 사용되고 있는 모델이다. Client Cursor는 결과셋 전체를 한꺼번에 클라이언트에게 전송하며 이후 서버와 연결과 무관하게 클라이언트는 커서를 동장시킬수 있다. 즉 데이타베이스는 일단 결과셋 모두를 클라이언트게 전송하면 더이상 클라이언트와 연결을 유지할 필요가 없고 이후 커서 동작은 모두 클라이언트에 버퍼링된 결과 셋에서 이루어지게 된다. 이는 인터넷의 Stateless 환경과 매우 잘 맞기 때문에 ADO.NET의 Reader나 DataSet 그리고 JDBC의 RowSet이 클라이언트 커서 모델을 사용한다. 현재는 Server Cursor가 더 많이 사용되고 있지만 앞으로 점점 더 Client 커서를 사용하는 경우가 많으리라 생각된다.

클라이언트 커서를 사용하기 위한 가장 중요한 서버 커서의 문제와 관련이 있다. 앞의 서버 커서는 쿼리의 결과셋의 크기와 실제 클라이언트가 사용하는 비율이 커서 선택의 제 1기준이었지만 클라이언트 커서는 쿼리의 결과셋 모두를 사용해야 의미가 있고(그렇지 않으면 Forward Only 커서의 문제점을 답습하게 된다.) 이는 달리 말해 페이징 처리등의 이유로 결과셋의 일부만 사용된다면 애초에 쿼리를 페이지 기반으로 수정해야 하고 할 수 있어야 한다.



이글과 다음 글들을 처음 생각했던 7년전에 올렸다면 어떻게 됐을까? 하고 가끔 생각한다. 그때에 나는 클라이언트 커서 방식이 앞으로 가장 효율적이기에 가장 널리 사용될것이라고 생각했지만 7년동안 내가 참여한 프로젝트들을 제외하면 사실 위와같은 방식을 사용하는 경우를 보지 못했다. (그렇다고 상황에 맞게 서버 커서를 적절하게 사용하는 경우도 못봤다.)

그렇기에 아마도 이글과 다음의 커서 글들은 사실이 아니다. 무슨 말이냐면 클라이언트 커서에 대한 이야기와 사용의 장점은 상대적이고 주관적이며 그리고 아마도 소수이다. common is not common 상식이 보편이 아닌것처럼 보편 또한 사실이 아니지만 우리는 흔히 보편이 사실이라고 생각한다. 대부분의 책이나 인터넷등에서 아예 커서에 대한 언급 자체가 거의 없다시피 하기 때문에 자체 실험 데이타를 제외하면 충분한 근거를 찾기가 어렵다. 그래서 아마도 이글들은 사실이 아니고 보편적이지 않다.

'Framework > Database' 카테고리의 다른 글

Framework Cursor (cursor in MySql)  (1) 2010.02.24
Framework - client cursor  (0) 2009.03.26
프로시저 vs SQL  (0) 2009.03.23
Framework - 커서(Anonymous)  (0) 2009.03.19
Framework - 커서(Keyset, Dynamic)  (0) 2009.03.13
Posted by bleujin
Framework/Database2009. 3. 23. 00:29

이전 글에도 쓴적이 있지만 없다면 할 수 없겠지만 있다면 가능하면 이용하자는 주의여서 DB의 프로시저도 적극적으로 활용한다. 물론 DB의 다른 기능들도 마찬가지로 주의깊게 살펴보면서 가능하면 사용하려고 노력하는 편이다.

보통의 경우 프로시저라 함은 IF ELSE나 FOR 등의 구문이 섞여서, 절차형 패러다임의  SQL 스파게티로 오해하는 경향이 있는데 여기서는 그냥 간단한 SQL이 프로시저 형태로 저장되는 것으로 한정해서 말하기로 한다.

프로시저가 일반 SQL에 비해서 가지는 장점은 여러가지가 있다.

가장 대표적으로 성능의 경우 저장프로시저는 쿼리 실행계획을 메모리에 저장하여 저장된 실행계획을 사용하기 때문에 구문 분석이나 최적화 과정을 반복하지 않기 때문에 빠른 실행을 할수 있고 설사 캐시에 없더라도 표준화 등의 작업을 하지 않기에 좀더 빠르다. 이 차이는 쿼리의 전체 속도의 부분이기 때문에 상황에 따라 조금 다르긴 하지만 대부분의 OLTP에서의 0.1s 미만의 쿼리의 경우 체감적으로는 50-100% 정도의 차이가 있다. (일반 Statement의 SQL이라도 자주 실행되는 구문이라면 역시 마찬가지로 실행계획의 재사용이 되지만 조금 차이가 있다.)

그밖에도 긴양의 SQL이나 2번 보내는 대신 프로시저명만 보내면 되기 때문에 네트워크 전송속도에도 미묘한 향상이 있다.

두번째의 이유로는 보안이 강화된다는 장점이 있다. 저장 프로시저를 사용하면 특정 종류의 SQL 인젝션 공격(특히 AND나 OR등의 연산자를 사용해 유효한 입력 매개 변수 값에 명령을 추가하는 공격)을 저지할 수 있고 프로그램이 사용하는 로그인 아이디에 테이블등에 직접 권한을 주지않고 프로시저 실행권한만을 주는 간접적인 보안계층을 관리할 수 있으므로서 생기는 프로그램 손상가능성을 줄여주는 장점도 있다.

그밖에도 몇가지 세세한 장점에 대해 얘기할 수 있지만 이는 인터넷 조금만 찾아보면 나오는 얘기이니 대충 생략하기로 하자. 사실 나는 이러한 장점이 중요하다고 생각하지 않는다. 정치학의 기본적인 원리중 하나는 아무리 변화가 필요하고 중요하다는 이성적 확신이 있더라도 개인과 조직의 문화는 변화를 거부하기 때문에 이러한 이유로 프로시저를 사용하는 이유에 대한 동의를 이끌어내기 힘들다는걸 여러번 절감해 왔다.


개인적으로 프로시저의 진정한 장점은 위와 같은 수치적인 측면이 아니라 다른 점에 있다고 생각한다.

그 이유중 하나로 컴파일의 문제를 들 수 있다. 초등학생 시절 베이직 언어를 배웠던 시절을 생각해보면 1000라인 이상의 프로그램을 짜는게 무척 힘들었었다. 물론 실력이 미숙하기도 하고 베이직의 구조적 한계의 문제도 있겠지만 되돌아보면 베이직이 컴파일이 되지 않는 인터프리터인 이유도 컸다고 생각한다. 컴파일 과정이 없이 매번 실행시켜서 그닥 도움이 되지 않는 에러 메시지로 버그의 원인을 찾는 과정은 고통스러웠고 찾았을때 기쁘기도 했지만 지금 생각해보면 시간낭비에 가깝다.

프로그램의 String이 아니라 저장 프로시저 형태로 저장되었을때 이러한 단순한 코딩 실수 방지 뿐 아니라 보다 장기적인 유지보수적인 장점이 있다. DB의 스키마 등은 변경되지 않는 것이 이상적이지만 현실적으로는 프로그램의 생명주기동안 계속 변경이 발생한다. 컬럼 정보같은 변경(추가/삭제/수정) 변경이 발생했을때 프로시저는 의존관계를 훨씬 더 정확하게 확인할 수 있고 동시에 검사와 테스트가 가능하다는 장점이 있다. 그리고 일반적으로 저장 프로시저를 변경하는 방법이 코드의 수정, 테스트, 배포를 다시하는 것보다는 시간과 노력을 절약할 수 있다.


두번째의 잘 알려지지 않는 장점중의 하나는 추상화에 있다. 다른 글에도 언급한적이 있지만 나는 SQL 구문자체가 비즈니스 로직이라고 생각하지 않는다. (아주 복잡한 로직이 결합된 프로시저는 앞에서 제외한다고 말하였다.) 오히려 클라이언트 로직 입장에서 본다면 viewArticle이라는 프로시저명만으로 충분하지 세세한 SQL이나 어떤 테이블을 참조하는 지에 대한 정보는 오히려 번거롭기만 하다.

예컨데 위의 viewArticle의 경우 조회수의 증가와 해당 Article의 정보 2가지의 결합이다. 하지만 클라이언트 로직 입장에서 본다면 viewArticle의 세부 과정이 2단계이든 3단계이든 굳이 알 필요가 없다고 생각하고 그럴경우 update문 하나와 select 문을 하나 던지는 로직보다 그냥 viewArticle이라는 프로시저를 실행하면 된다고 생각하는게 도움이 된다. 즉 좀더 추상화를 잘 활용하게 된다.



위의 장점에도 불구하고 저장 프로시저의 단점이 있다면 숙련된 전문가가 필요하다는 점이다. 개발자가 프로시저를 숙련되게 사용할 수 있다면 더할 나위 없지만 그렇지 않다면 일방적으로 DBA에게 위임하는 것보다 차라리 교육을 통해 개발자가 관련 지식을 습득하는 방법이 낫다고 생각한다. A분야의 전문가와 B분야의 전문가 협력하는 식의 방법이 겉으로 보이기에는 멋져보일지 모르지만 실제 커뮤니케이션에는 대부분의 엔트로피 손실로 오랫동안 팀을 이룬게 아니라면 오히려 부정적이다.

'Framework > Database' 카테고리의 다른 글

Framework - client cursor  (0) 2009.03.26
Framework - 커서의 선택 .. and  (0) 2009.03.24
Framework - 커서(Anonymous)  (0) 2009.03.19
Framework - 커서(Keyset, Dynamic)  (0) 2009.03.13
Framework (Servant)  (0) 2009.03.12
Posted by bleujin
Framework/Database2009. 3. 19. 19:10

  public void testStatic() throws Exception {
    DBManager dbm = new MSSQLPoolDBManager("com.microsoft.jdbc.sqlserver.SQLServerDriver", 
         
"jdbc:microsoft:sqlserver://novision:1433;DatabaseName=test", "bleu", "bleu") ;
    Connection conn = null;
    ResultSet rs = null;
    PreparedStatement pstmt = null;

    try {
      conn = dbm.getConnection()
      pstmt = conn.prepareStatement(getProcSQL());
      rs = pstmt.executeQuery()
      populate(rs)
    catch (SQLException ex) {
      throw ex;
    finally {
      closeWithoutException(rs);
      closeWithoutException(pstmt);
      dbm.freeConnection(conn)
    }
  }

  private String getProcSQL() {
    return "select custNo, fixLength from customer_tblc";
  }

  private void populate(ResultSet rowsthrows SQLException {
    int i = 0;
    while (rows.next() && i++ < 10) {
      System.out.print(rows.getString(1));
    }
  }


위 코드가 customer_tblc에서 10건을 읽어서 화면에 쓰는 프로그램이 아니라는 얘기는 이미 한적이 있다. 위 프로그램은 실제로는 customer_tblc를 모두 읽어서 client에게 Network를 통해 모두 전송해주고 client는 단지 10건만 필요한데도 불구하고 모두 받아서 버려야 하는 바보같은 코드이다.

만약 sensitive 방식을 사용하면 이번에는 customer_tblc를 모두 읽어서 데이타 베이스의 temporary space에 Set 형태로 저장해서 요청시마다 한건씩 건네주는 Static 커서를 사용한다. 나중에 다시 이야기 하겠지만 Static 커서는 특정 상황에 따라 유용성이 있을 수도 있지만 위의 상황에는 적합하지 않다. 

원하는 것은 화면에 찍을 "일부"만 읽어서 "일부"만 전송이 되는 방식이다. 이렇게 하기 위해서는 전혀 생뚱맞을지 모르지만 위 코드의 경우 JDBC 문자열을 바꿔야 한다.

jdbc:microsoft:sqlserver://novision:1433;DatabaseName=test;selectMethod=cursor 와 같이 JDBC URL에 selectMethod=cursor 이란 문자열을 추가해야 한다. 이 방식의 커서에 대한 명칭은 디비마다 제멋대로 이므로 이 글에서는 Anonymous 커서라고 부르자. 원칙적으로 selectMethod=cursor는 트랜잭션에 관련한 프로퍼티이지만 row level 락의 문제로 커서에도 영향을 준다.



Anonymous 커서 동작은 위와 같다.(오라클의 아키텍쳐 이름이지만 MSSQL도 동작방식은 같다. 다른 DB는 아키텍쳐 동작에 관한 문서가 없다시피 해서 단지 추측만 할 수 있다.)

1. 쿼리를 실행한다.
2. 쿼리의 결과에 관계하는 "일부 블럭"이 Buffer Cache 메모리에 올라간다.(MSSQL은 Page)
   FetchSize만큼의 Row만 Set형태로 임시 공간에 PreLoad하고 커서는 첫번째 row를 가르킨다.
3. Client가 request fetch를 한다.
4. Server는 현재 Temporary에 있는 일부 Set의 현재 row를 리턴한다.
5. Client는 confirm message를 발송한다.
3-5 동작을 반복한다. 만약 fetchSize 이상의 row를 요청하면 2번이 한번 더 실행된다. 즉 만약 FetchSize가 10이라면 2, (3, 4, 5) * 10, 2, (3, 4, 5) * 10, 2 ... 와 같은 방식으로 실행된다.
6. 커넥션을 닫기 전에 임시 저장공간의 Set과 SQL AREA의 cursor 정보를 정리한다.


단지 selectMethod=cursor만 추가하여 첫번째의 글과 같은 방식으로 테스트를 해보면
connection : 2360
execute : 78 (2438 - 2360)
populate : 31 (2469 - 2438)
close : 0 (2469 - 2469)
가 나온다.

이전의 forward only cursor의 실행결과는 아래와 같았다.
connection : 2375
execute : 47 (2422-2375)
populate : 31 (2453-2422)
close : 6616 (9069-2453) <- 모든 데이타를 읽고 전송하느라 close 하는데에 오래 걸린다.


연결하는데 만드는데 2.3초가 걸렸지만 풀링을 만드는건 단 한번만 하는 동작이므로 역시 이는 신경쓸 필요 없다. 주의해서 볼것은 실행이 0.078초 그리고 close까지 완료하는데 총 0.1초정도 걸렸다. 데이타량도 약 58만건으로 변동이 없고 갑자기 DB가 눈부시게 빨라져서도 아니고 단지 부분범위 처리 Select를 했기 때문이다. 그리고 일부만 Client에게 전송하였다. 단지 커서만 바꿨을 뿐인데 처음의 default cursor와(forward only) 7초와 0.1초의 약 70배 차이가 발생한다. 이는 58만건일때의 차이이므로 만약 데이타가 더 많다면 그 차이가 더 벌어진다. 





네트워크 패킷 캡쳐 화면을 보면 이 사실은 더욱 명확해진다. 이전의 forwardOnly는 모든 row의 데이타를 받았기 때문에 패킷의 수가 아주 많았지만 Anonymous 방식은 단지 수백개의 패킷만을 받았다. 3개식 순서대로 나타나는 것은 fetch와 send가 동기적임을 뜻한다. request Fetch by Client -> Send Packet By Server -> confirm message by Client 이 3가지 동작이 반복해서 일어난다.

--
잠깐 상관없는 얘기를 하자면 성능과 관련해서 테스트를 할때는 항상 총알을 2발이상 쏴야 한다. 암살자가 저격을 할때 영화나 만화에서처럼 이마를 겨냥해서 딱 한방을 쏘고 폼나게 돌아서는 짓은 아주 바보같은 짓이다. 저격 포인트라는 것 자체가 저격하기 쉽지 않다는 걸 뜻하고 정말 한방에 목표를 죽이는게 쉽지 않기 때문에 총은 2발 이상을 쏘는게 좋다. 즉사가 아니더라도 과도한 피출혈로도 죽일 수 있기 때문에 가능하면 여러발을 쏘는게 좋다. 테스트를 할 때도 마찬가지이다. 위 테스트들은 한번 한게 아니라 기관총을 쏘듯 수천번을 테스트 한 결과이고 평균치는 거의 변함이 없다.
--


마땅한 명칭이 없어서 Anonymous라고 지칭했지만 명칭만 제멋대로 인게 아니라 이 커서를 사용하기 위한 방법도 제 멋대로다. selectMethod=cursor는 마법의 지팡이가 아니기 때문에 JDBC URL에 단순히 이 문장을 써준다고 빨라지는 것은 아니다. 많은 상황에서의 단지 하나의 예일 뿐이다. (예컨데 ResultSet.TYPE_SCROLL_SENSITIVE 방식을 사용하게 되면 Static 커서를 사용하게 되므로 영향을 미치지 않는다. )

MSSQL2000은 연결 문자열을 바꿔야 하지만 이를테면 오라클은 이 글의 Anonymous 방식이 default이다. 어떤 DB는 SQL을 바꿔야 하고 또 어떤 드라이버를 사용하면 안되고 다른 드라이버를 사용해야 할때도 있었다. MSSQL도 2005버전이상이 되면 selectMethod=cursor로 되지 않는다. DB마다 버전마다 드라이버마다 다르기 때문에 매 프로젝트마다 이를 테스트 해보고 방법을 찾아야 했다. 그리고 일부 디비는 이런 처리방식이 애초에 존재하지 않는 경우도 있었다. (일단 DB가 부분범위 처리를 지원하지 않으면 안되고 드라이버에서 부분범위 처리를 구현했어야 한다.)

위 방식의 장점은
1. 원하는 만큼만 select를 해서 원하는 만큼만 전송이 이루어지기 때문에 상대적으로 매우 빠르게 동작한다.
실제 필요한 부분이 쿼리셋의 단지 부분일 뿐이라면 그  결과에 대해 모두 알 필요는 없다는 것이 위 방식의 핵심이고 Anonymous cursor를 사용하는 이유가 된다.



단점은

1. 매 fetch시마다 3번의 Network 통신이 발생한다.
이 문제의 해결은 다음글에 적기로 하자.



2. 항상 쓸 수 있는 방식이 아니다. 쿼리의 종류에 영향을 받는다.

항상 쓸수 있는 방식이 아니라는 얘기는 이렇다. 이를테면 부서별 사원수를 알고 싶다고 하자.

select deptno, count(*) from emp group by deptno 와 같은 쿼리는
흔히 전체범위 쿼리라고 불리는데 전체를 읽지 않고서는 제대로 된 결과를 얻을 수 없다. 그래서 위와 같은 쿼리는 사용자의 선택에 상관없이 Static 방식의 커서가 사용된다. 그러나 조금 생각을 달리 해볼수도 있다.

이를테면 부서가 아주 세분화되서 수천개에 이르고 역시 화면에 보고자 하는 것은 그 일부분이라고 하자. 그럴경우 내가 보고자 하는 부분은 일부 부서이지만 쿼리는 전체 부서를 대상으로 실행이 된다. (전체범위 쿼리의 대표적인 예는 group by, order by, 집계함수 등이 있다.)

그럴경우
select deptno, (select count(*) from emp where deptno = x1.deptno ) from dept x1
와 같이 부분범위 처리가 가능한 쿼리로 바꾸고 Anonymous 커서 방식을 사용할 수도 있다. 요는 응용의 문제이므로 이 쿼리는 전체범위 처리이므로 어쩔수없어 라고 단념할 필요는 없다.

같은 쿼리지만 전체범위용 쿼리와 부분범위용 쿼리로 자유자재로 바꿀수 있는 것은 개발자로서 아주 중요한 능력이다. 이를테면 성적테이블을 대상으로 해서 79점 이하의 점수가 없는 학생의 리스트를 얻고자 한다고 할때

전체 범위 처리용 쿼리는

select 학생번호
from 성적테이블
group by 학생번호
having decode(sign(점수 - 80), -1, -1, 0) >= 0
....와 같이 작성해야 하지만


부분 범위 처리용 쿼리는
select 학생번호
from 학생테이블 x1
where not exists(select 1 from 성적태이블 where rowunum = 1 and 학생번호 = x1.학생번호 and 점수 < 80)
.....와 같이 작성해야 한다.

블랙박스 주장하는 사람에게는 애석하겠지만 이와 같이 커서방식과 쿼리는 서로 영향을 받게 된다. 달리 말해서 개발자는 자신이 실행하는 쿼리의 형태를 알아야 하고 이에 따라 적절한 커서 방식을 선택해야 한다. (위 2개의 쿼리는 모델의 mandatory의 여부에 따라 미묘하게 결과가 달라질 수 있다.)




아마도 여기서
"커서방식에 따라 많은 성능차이가 발생하기 때문에 DB, 버전, 드라이버마다 틀린 위 방법을 각자 찾아서 알아서 사용해라"
라고 끝이라고 생각했을지 모르지만 진정 흥미로운 부분은 다음 글 부터이다.


'Framework > Database' 카테고리의 다른 글

Framework - 커서의 선택 .. and  (0) 2009.03.24
프로시저 vs SQL  (0) 2009.03.23
Framework - 커서(Keyset, Dynamic)  (0) 2009.03.13
Framework (Servant)  (0) 2009.03.12
Framework (DBController)  (0) 2009.03.12
Posted by bleujin
Framework/Database2009. 3. 13. 07:31

키셋이나 다이나믹 커서는 최소한 connectionless 웹환경이라면 거의 쓰이지 않기 때문에 잘 보이지 않지만 동시성이 매우 중요한 일부에서는 고려될수 있는 커서 방식이다.



  1. WAS는 쿼리 실행을 요청한다.  
  2. DB는 쿼리의 "모든" 결과의 "Key 셋(unique index)"을 메모리에 snapshot 형태로 만든다. 모든 데이타의 load를 완료하면 Client의 신호를 기다린다
  3. Client(WAS)는 Fetch(rs.next())를 요청한다.
  4. DB는 cursor 정보를 유지하면서 current Key의 정보로 DB를 lookup해서 row의 정보를 알아낸다
  5. 알아낸 row를 Client에게 보낸다. 
  6. 패킷(1개의 row만 들어있음)을 받았다는 confirm 메시지를 보낸다.
  3,4,5,6의 동작을 9번 더 반복한다

KeySet은 Static과 상당히 비슷하지만( 실제 메모리에 로드하는 것은 전체 결과셋이 아니라 결과셋의 Key만을 메모리에 올린다.(만약 테이블에 unique index가 없다면 이 방식은 실행할 수 없으므로 이 시점에서 자동으로 static cursor 모드로 변경된다.) 메모리에는 Key들의 정보만 있기 때문에 매 Fetch 시마다 데이타베이스 lookup을 해서(해당 키로 DB를 다시 Access read한다.) 해당 Row의 정보를 리턴한다. (물론 select 컬럼에 key가 아닌 컬럼이 있을 경우이다. 만약 select 컬럼이 모두 key의 부분이라면 역시 static mode와 동일하다.)

여기서 맨처음의 예제처럼 하나의 테이블에서라면 문제없겠지만 만약 쿼리가 2개이상의 테이블을 조인하는 경우라면 어떻게 될까? 조인된 테이블은 실제 존재하는 것이 아니므로 Key 정보를 유지할수 없다. 여기서의 구현은 사실 DB와 드라이버 마다 틀리기는 하지만 대부분은 이경우 keyset이 작동하지 않는다.

키셋을 사용한다면 가장 큰 이유는 Key정보만을 메모리에 유지하고 나머지 Data는 매번 새로 read를 하기 때문에 다른 세션에서 수정한 값을 실시간으로 읽을 수 있는 장점이 있다. 사실 JDBC의 sensitive 방식은 바로 이 keyset을 뜻한다. 다른 Session의 update에 sensitive 하기 때문에 붙여진 이름이다. 하지만 실제로 돌려보면 이 방식을 지원하는 JDBC 드라이버는 많지 않고 그 일부도 제각각 특정 조건을 만족해야만 동작하는 경우가 많다.(이를테면 단일테이블 억세스일때만..) 그리고 만약 사용자 업데이트가 키 컬럼을 수정하였다면 역시 Keyset은 제대로 동작하지 않는다.


단점은
  - WAS에서 필요한 양과 상관없이 해당 쿼리의 모든 Key셋 데이타를 읽어야 한다.
  - DB에서 읽은 모든 Row를 KeySet을 Snapshot 형태로 저장해야 하기 때문에 Static보다는 적지만 공유 자원인 DB 메모리는 여전히 많이 소모된다.
   - 매 fetch마다 DB Read가 일어난다.
   - 동기적으로 작동하기 때문에 Client의 fetch 요구시마다 row단위의 데이타 전송이 이루어진다.(패킷의 크기가 작다.)
   - cursor를 해당 프로그램이 종료될때까지(DB close 할때까지) 유지해야 하고 더 많은 정보를 관리해야 한다.
   - 첫번째 Row가 Client에 전송되는 시간도 Static 보다는 나을지라도 여전히 느리다.
   - RR의 동시성 문제로 다중 사용자 환경에서 느리다.

장점 
   - Client가 필요한 양만 네트워크에 전송한다. 
   - Repeatable Read가 가능하다.
   - 일부만 억세스한다면 세번째 단점에도 불구하고 Static 보다는 더 빠르다.







마지막으로 Dynamic 커서 방식을 보자. 위 그림을 보면 먼가 이상하다고 생각하겠지만 이는 실수가 아니다. 

  1. WAS는 쿼리 실행을 요청한다.  
  2-3 DB는 쿼리 결과의 메모리에 로드한후 첫번째 Row의 정보를 WAS에게 보낸다. 
  WAS는 fetch를 요청할때마다 해당 SQL은 다시 실행되서 다시 메모리에 올린후 cursor 정보대로 x번째 로우를 보내준다.


몰랐던 사람에게는 당황스럽겠지만 Dynamic 커서로 실행되면 rs.next()때마다 매번 쿼리가 다시 실행된다. 2번 load Data때 전체를 올리느냐 혹은 일부만 올리는가는 역시 DB 마다 구현이 다르지만 보통은 전체를 다시 올린다. 이런 제정신이 아닌듯해 보이는 짓을 하는 이유는 Pantom을 read하기 위해서이다. 앞의 Keyset 커서는 쿼리가 실행된 이후 다른 사용자가 업데이트 한 정보는 볼수 있지만 사용자가 새로이 insert한 row(일명 pantom row)는 보지 못한다. 다이나믹 커서는 매 패치시마다 쿼리가 다시 실행되기 때문에 사용자가 insert한 정보를 억세스 가능하게 한다.

사실 만약 당신이 DB 전문가가 아니라면 Dynamic 커서는 쓸 필요도 없고 써서도 안된다. Dynamic Cursor가 사용하는 잠금 수준은 Serializable 이고 이는 극히 제한적이며 특수한 용도로 사용되며 이 모드는 워낙 DB마다 구현이 천차만별이기 때문에 전용 드라이버의 API를 사용하지 않으면 JDBC API로 이 cursor 방식을 실행시키는 방법도 없다.




여러번 강조했다시피 단순히 설정한다고 해당 커서 모드로 동작하는게 아니다. 위에서 말한 것 말고도 MSSQL의 경우 Select문이 트리거가 있는 테이블과 트리거가 없는 테이블을 조인하면 커서가 static 커서로 변환된다. Forward Only 커서가 읽기 전용이 아니라면 Dynamic 커서로 변환된다. 원격 테이블을 참조하는 분산쿼리라면 커서는 Keyset 방식으로 바뀐다 등등 수많은 예외 사항이 존재한다.

대부분의 프로그래머가 자신도 모르게 사용하는 커서 수준은 Forward Only, Static, Keyset 정도이다. 현재 프로그래밍에서 사용하는 커서 모드를 모른다면 몇가지 확인 방법이 있다. 

  - 첫째 rs.first(), rs.absolute(), rs.last() 같은 메소드가 정상작동한다면 static 아니면 keyset이다. Foward Only는 비동기적으로 작동하기 때문에 위 메소드의 커서이동이 불가능하고 Dynamic는 멤버 row가 고정되어 있지 않기 때문에 fetch absolute가 지원되지 않는다. 커서 용어로 Scroll이 가능한 커서는 static과 keyset이다.
  - 둘째 SQL에 group by 나 max 혹은 union, minus 같은 집계함수나 구문을 사용하였다면 static 이다. 집합의 레벨에 변형이 있다면 Forward Only로 작동할 수 없다. 
  - 셋째 구문에 Order by를 사용했다면 그리고 Where 구문을 위해 인덱스가 사용되었다면 static일 확률이 높다. 정렬정보를 유지하기 위해 Set정보를 유지해야 하기 때문이다. 임시 테이블을 만들 필요가 있는 Select문은 암시적으로 static 커서로 변환되는 경우가 많다.
  - 넷째 static과 keyset 모두 가능성이 있다면 아마도 static 일 것이다. keyset은 잘못 사용했을때 다중사용자 환경에서 잠금으로 인한 속도저하가 있기 때문에 대부분의 Driver는 static을 선택한다.

사실 몇가지 더 있겠지만 원리를 이해하였다면 쿼리만 보고도 이게 어떤 커서로 실행될 것인지 짐작할 수 있다. 단순하게 말하자면 아주 초보적이고, 하나의 테이블만을 대상으로 하고, 집계함수나 Order by 등의 구문을 쓰지 않으면 Forward Only로 동작할 확률이 높고 그 밖의 경우는 대부분 static 으로 동작한다고 해도 거의 틀리지 않는다.

사실 여기까지 소개한 Forward Only나 Static 모두 심각한 단점이 있기 때문에 - 라기 보단 대부분 적절히 피할 방법을 알지 못하기 때문에 - 어느 모드든 그리 좋아 보이지는 않는다. 그리고 이 말은 사실 대부분의 환경에서 심각한 문제를 겪고 있다는 뜻이기도 하다. 만약 정확하게 커서에 대한 지식을 쌓고 적절하게 프로그래밍 한다면 DB 서버는 다중환경에 훨씬 더 최적화 되어 있기 때문에 DB 사이즈와 상관없이 같은 부하에서 웹서버가 다운되면 다운됐지 DB 서버가 다운되는 일은 결코 없다. 


다음글에는 오라클과 MSSQL의 차이점과 오라클과 MSSQL은 이런 문제를 어떻게 피해가는가에 대하여...

'Framework > Database' 카테고리의 다른 글

프로시저 vs SQL  (0) 2009.03.23
Framework - 커서(Anonymous)  (0) 2009.03.19
Framework (Servant)  (0) 2009.03.12
Framework (DBController)  (0) 2009.03.12
Framework -커서(ForwardOnly, Sensitive(=Static))  (0) 2009.03.12
Posted by bleujin
Framework/Database2009. 3. 12. 21:10


앞에 나온바와 같이 Query의 실행이라는 기본목적외에도 현실에는 여러가지 일들이 있다. 그래서 각각의 일들을 해주는 Servant(하인)들이  있다. 단지 본래의 일(Query의 실행)에 영향을 주어서는 안되기 때문에 별도의 쓰레드로 관리된다. 모든 쿼리는 실행이 완료되면 IQueryable객체와 걸린시간정보를 묶은 AfterTask를 Channel에 넣어두고 종료된다. ServantChannel에 AfterTask를 등록한 시점에서 DC는 더이상 관여하지 않는다.

여러개의 Servant는 Chain 형태로 연결되어 있다. 체인의 가장 선두가 Channel에 새로운 AtferTask가 등록되면 가져와서 isDealWith로 자신이 할일인지 판단하여 자신의 작업을 한후 nextServant에게 Task를 전달해준다. Servant는 별도의 Thread로 등록되어 있기 때문에 dc의 작업에 아무런 영향도 끼치지 않는다.


FileServant :
설정된 시간보다 느린 모든 타입(Command, Procedure, Batch 등등)의 쿼리를 잡아서 특정 파일에 기록을 남긴다.

NoneServant :
아무 일도 하지 않는다. Null Object Pattern.


StdOutServant :
특정 타입의 쿼리가 실행될때 걸린 시간등의 정보와 함께 화면에 출력한다.

TraceOfLateProcServant :
설정된 시간보다 느린 모든 타입(Command, Procedure, Batch 등등)의 쿼리를 잡아서 특정 테이블에 기록을 남긴다. 로그파일보다 Table에 기록을 남기면 좀 더 편하게 문제점을 분석할 수 있다.



다이어 그램이 Thread때문에 복잡해지긴 했지만 그닥 어렵지도 않고 특별한 의미를 가진 부분도 아니다.
단 Servant는 프레임워크의 확장 포인트의 일부이고 시스템을 새로 시작하지 않아도 런타임중에 새로운 Servant의 등록과 삭제가 가능하기때문에 여러가지로 응용이 가능하다. 이를테면 XML 파일에 설정된 쿼리가 실행될때 특정 사용자에게 메일을 보내도록 하는 Servant도 있다. 화면 모니터링을 통해 실시간으로 문제 쿼리의 유무를 감시하는데 사용하기도 한다.



'Framework > Database' 카테고리의 다른 글

Framework - 커서(Anonymous)  (0) 2009.03.19
Framework - 커서(Keyset, Dynamic)  (0) 2009.03.13
Framework (DBController)  (0) 2009.03.12
Framework -커서(ForwardOnly, Sensitive(=Static))  (0) 2009.03.12
Framework (Handler)  (0) 2009.03.08
Posted by bleujin
Framework/Database2009. 3. 12. 17:21




파란색을 실험실로 간주했을때 dc는 실험실의 연결창구 역할을 하게 된다. Client는 기본적으로는 DBController(이하 dc)를 통해서 메시지 교환을 한다. 

DBController는 Facade 패턴의 예중 하나로 Facade(건물의정면) 패턴은 복잡한 서브 시스템에 통일된 인터페이스를 제공함으로써 복잡한 API를 단순화 시켜준다. 시스템을 서브 시스템 단위로 나누어 구성하는 것은 시스템의 복잡도를 낮춰주지만, 동시에 서브 시스템 사이에서의 통신 부하와 결합도가 증가하게 된다. 이러한 서브 시스템 사이의 의존도를 낮추고, 서브 시스템의 사용자 입장에서 사용하기 편리한 인터페이스를 제공하고자 하는 것이 facade 객체이다.

Facade 객체는 실생활에서의 고객 서비스 센터와 유사하다. 가령, 어떤 상품을 구매하는 과정에서 문제가 생겼다고 가정할 때, 고객이 문제의 성격에 따라 해당 부서에 직접 연락하는 것이 아니라 고객 서비스 센터를 통하는 것은 Facade 패턴에 대한 좋은 유추 사례가 될 수 있다.

여기서 DBController Facade 역할이 하는 일은 무엇일까 ?

첫째 복잡한 것을 단순하게 보여준다. 실험실 내부에서 작동하고 있는 내부에서 작동하고 있는 많은 클래스들의 관계나 사용법을 의식하지 않도록 해 준다. Java 접근자는 구조적인 이유로 public을 쓸수밖에 없는 경우가 종종 있는데 그중에 일부만 - 즉 개념적인 published 메소드 - 만을 노출시킴으로써 보이는 API를 적게 해 주고 단순하게 해 준다.

둘째 실험실의 컴포넌트로부터 클라이언트를 격리하여 실험실과 클라이언트 사이의 의존성을 낮춘다. 이런 캡슐화는 클라이언트와 서브시스템의 생존 기간이 다르기 때문에 필수적이다. Facade 패턴을 사용한다고 해도, 필요한 경우 실험실의 클래스에 직접 접근할 수도 있다.(이러한 것을 터널링이라고 부른다.) 즉, 일반화 정도(generality)와 개발의 편의성 사이에서의 적당한 합의점을 찾아야 한다.

Facade 패턴의 일반적인 특징에 추가로 위 DBController은 2가지를 더하고 있다.(SRP에는 위반되지만 ..) 첫번째는 실험실에서의 checked exception을 runtime exception으로 바꾼다. exception framework에서 말한대로 첫째로 대부분의 SQLException을 client에서는 잡아서 현명한 처리를 하지 않으며 둘째로 기껏해야 로그 기록하는 정도라면 본래 코드를 작성하는데 오히려 방해가 되기 때문이다. 이런 예외 처리가 현재 상황에서 적합하지 않다면 DC를 상속받아 해당 메소드를 재정의 하여야 한다.(실제 필요한적이 한번도 없었기 때문에 미리 만들어 두지 않았다.)

다른 한가지 역할은 Servant Filter 등록을 관리한다. 비실험실인 client는 현실의 요구사항이 있다. 이를테면 특정 쿼리의 실행을 모니터 하고 싶을 수도 있다.(느린 쿼리, 배치형 쿼리같은 특정 타입, 특정 테이블을 사용하는 쿼리 등등) 그리고 이 부분은 모두 필터로 관리되어 런타임중에 추가 삭제가 가능해야 한다. (나중에 다시 언급)


보통의 경우 Facade 객체는 Singleton 패턴으로 사용되는 경우가 많지만 DC의 경우 실제 커넥션 풀링등의 리소스 관리는 DBManager에게 맡기고 있으므로 배치 처리용으로 Servant가 없는 별도의 BatchDBController를 사용해도 되고 시간이 오래 걸리는 쿼리의 경우 비동기적으로 작동하는 AsyncDBController를 사용해도 된다.

'Framework > Database' 카테고리의 다른 글

Framework - 커서(Keyset, Dynamic)  (0) 2009.03.13
Framework (Servant)  (0) 2009.03.12
Framework -커서(ForwardOnly, Sensitive(=Static))  (0) 2009.03.12
Framework (Handler)  (0) 2009.03.08
Framework (Rows)  (0) 2009.03.07
Posted by bleujin