Site Search :
Standard Enterprise XML Methodology Pattern Setting Tunning Other
Article Contributors
GuestBook
Javapattern Maven
XSourceGen Dev
JetSpeed Test
JLook Image
jLook Family Site


Reflection을 이용한 Value Object비교
 
VO패턴을 사용하였을 경우 변경이력등의 기록을 위한 VO를 비교하여 데이터베이스에 변경된 사항을 반영할 수 있도록 간단하게 고안한 내용이다. ( 2003/07/01 ) 327
Written by ienvyou - 최지웅
1 of 1
 

안녕하십니까? 놀새~입니다.
이번 아티클에서는 제가 한 프로젝트시에 필요했었던 변경이력에 대한 로그를 남기기 위하여
만들었던 VO 비교기입니다.


▶ 생성동기

일전 프로젝트는 XX사이트의 콜센터업무구축을 하는 것이었는데 (사실 생각만 하면 치가 떨림다~)
고객에 대한 마스터정보를 누가 언제 어떻게 바꾸었는지를 기록을 해놓아야 다음에 해당
고객의 문의가 다시 들어왔을 경우의 응대를 쉽게 하기 위하여 만들었던 내용입니다.

실력이 아닌 자존심에 가득찬 S모 회사의 사람들이 짰던 내용이 있었는데 pro*C로 해당 데이터
베이스를 읽어서 TableBean을 만들어 모든 필드를 변수로 선언하여 썼습니다.

가령 EMP라는 table에 EMPNO, EMP_NAME 등등이 있을때 생성됐던 Bean측은
// 구 데이터보존용
private String old_emp_no;
private String old_emp_name;

// 변경된 데이터
private String new_emp_no;
private String old_emp_name;

처럼 작성했었습니다.

흠~ 아무리 봐도 변수선언에 불필요한 내용까지 있었던 지라 변경을 해보고자 놀새~는 결심했습니다.

▶ 전략짜기

흠~.. 변경된 데이터만을 알아내어 해당 필드의 X->Y로 전이된 결과값을 어떻게 보존할수 있겠습니까?
한번 생각을 해봅시다.
여러분들 같으면 어떻게 만드는 것이 최선의 방법일까요?

메모리? 파일? DB reload?

쩝~ 쉽게 결정할 만한 사항이 아니겠네요.
메모리로 하자니 많은 수의 콜센터직원이 하루에 직원이 내용을 바꾸는데만 하더라도 어마어마한
양의 데이터가 쌓일수가 있으니 위험하기도 할것 같습니다.

그리고 만약 해당 고객에 대한 실제 update가 이루어지지 않았을 경우의 대책을 어떻게 세울것인지도
궁금하네요.

그러면 배치작업을 돌릴수 있도록 filtering engine을 탑재한 파일시스템에 저장을 하자?
어라...이건 실시간 업무이기 때문에 가능성을 있으나 업무에 대한 적합성 측면에서 약간은 
무리수일듯도 합니다.

그렇다면 DB select를 한번 더 ?
흠.. 이건 또한 메모리처럼 DB가 걱정되긴 하지만 그리 심각할 것 같지는 않고..

호~ 무엇으로 결정을 해야 할까요? 더 좋은 방법을 알고 계신가요?
그러면 방명록에좀 남겨주심이~~ ^^

결국은 시간이 없어서 가장 쉽게 갈수 있는 데이터베이스를 이용하기로 하였습니다. 

자. 현업의 요청사항은 다음과 같습니다.

1. 고객에 대한 모든 변경정보를 이력으로서 화면에 보여질수 있어야 합니다.
2. 변경이 된다하더라도 기존의 데이터베이스와 비교하여 실제 바뀌어진 필드만 찾아내야 합니다.
3. 각 테이블에 대한 정보 및 필드에 대한 정보를 함께 보고 싶습니다.
4. 코드 값이 있을때 상담원이 식별할 수 있는 형태의 실제 값으로 변환되어져야 합니다.

즉 
테이블        이름          필드명         필드설명         기존      변경       누가     언제
============================================================================
XXX1     고객마스터     CUST_CD      고객유형       개인        법인     놀새~   07-01 2시

요구사항은 위와 같은데 매우 간단하죠?
그렇다면 모든 애플리케이션에서 사용할 수 있는 형태로 만들어내야 할터인데..

쩝..즉 간단한 변경이력용 공통 모듈정도가 되겠군요...

▶ 어떻게 짤것인가?
최초의 아키텍쳐는 JSP-Model1도 아니고 아주 고전적인 JSP였는데 데이터베이스 접속부터
모든 것을 JSP코드 안에서 모두 처리하게 한 너무나도 뛰어난(?) 프로그램이었습니다.
그래서 Model-2까지는 아니더라도 최소한 bean을 이용하여 코딩을 하도록 개발자들에게
요구를 했고, Front tier단의 VO(Value Object)와 DAO(Data Access Object)패턴을 이용하여 코딩을
하도록 주문했었습니다.

여기서 놀새~가 생각한것은 "흠~ VO를 쓰는군~ 보통 VO는 database table schema의 반영이지"
였습니다.

데이터베이스의 필드에 대한 내용은 해당 데이터베이스가 주석이 잘 달린경우는 문제가 
없겠는데 주석이 안달린 필드들이 많아서 자바측에서 metadata를 쓴다해도 못가져올 확률이 
많아 필드설명에 대한 것을 처리하기가 상당히 애매모호한 측면을 보이고 있습니다.

그래서 하는 수 없이 해당 필드를 처리할수 있는 데이터베이스 테이블을 추가하게 됩니다.

테이블명은 HT001(실제 변경이력정보), HT002(테이블설명), HT003(필드)으로 간단하게 명명하고

HT001 : HISTORY_NO, HISTORY_TYPE, CUSTOMER_NO, TABLE_NAME, 
            TABLE_COMMENT, COLUMN_NAME, COLUMN_COMMENT, 
            BEFORE_DATA, AFTER_DATA, UPDATE_DATE, WORKER

HT002 : TABLE_NO,  TABLE_NAME,  VO_NAME, TABLE_COMMENTS

HT003 : TABLE_NO, COLUMN_NAME, METHOD_NAME, COLUMN_COMMENT

HT002테이블의 테이블은 실제 어떤 VO와의 연결이 되어 있는지를 표현하며, HT003테이블은
해당 VO가 어떤 column이름의 method에 매핑이 되는지를 살펴본 것입니다.

즉 어떠한 애플리케이션이던지 해당 comparator를 호출하게 되면 자동으로 VO의 형태를
찾아내어 reflection을 이용하여 method를 이용하여 뽑아내면 쉽게 될것 같죠?

CM_001 테이블이 고객마스터이고 필드는 CUST_NAME, CUST_UUID이라고 가정해보죠

HT002의 데이터는 다음과 같습니다. 고객마스터 테이블의 테이블명이 CM_001이라고 했을때

TABLE_NO -> 1
TABLE_NAME -> CM_001
VO_NAME -> CM001VO
TABLE_COMMENTS -> 고객마스터

또한 HT003의 데이터는 다음과 같습니다.

RECORD-1
====================================
TABLE_NO -> 1
COLUMN_NAME -> CUST_NAME
METOHD_NAME -> getCustName
COLUMN_COMMENT -> 고객이름

RECORD-2
====================================
TABLE_NO -> 1
COLUMN_NAME -> CUST_UUID
METOHD_NAME -> getCustUuid
COLUMN_COMMENT -> 고객유일ID

자. 위와 같이 해당 table에 대한 field및 vo에 대한 mapping정보를 입력합니다.

이제 비교할 수 있는 코드를 간단하게 보도록 하겠습니다.


package com.javapattern.util;

import java.sql.*;
import java.lang.reflect.*;
import java.util.*;
import javax.transaction.*;

import com.javapattern.call.vo.HT003VO;
import com.javapattern.call.vo.HT002VO;
import com.javapattern.call.vo.HT001VO;

public class HistoryUpdator extends ComponentDAO{
    private static HistoryUpdator updator;

    private HistoryUpdator() {}

    public static HistoryUpdator newInstance() {
        if( updator == null ) {
            synchronized(HistoryUpdator.class) {
                updator = new HistoryUpdator();
            }
        }
        return updator;
    }
    /**
    *  상태변경이력을 관리하기 위하여 테이블코드, 작업자와 원본 데이터와 수정데이터에 
    *  대한 reflection
    *  비교를 한후 변경된 데이터의 컬럼에 대하여 저장하도록 한다.
    *  select시에는 제외되면 insert, update, delete transaction이 발생했을 경우에 처리가 
    *  되도록 한다.
    */
    public boolean historyUpdate(int customerNo, 
                                              int tableCode, 
                                              String historyType, 
                                              int workerNo, 
                                              Object before, 
                                              Object after) throws Exception {
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rset = null;

        ArrayList reflectionMethod = new ArrayList(30);

        boolean result = false;
        int tableNo = 0;
        String tableName = null;
        String voName = null;
        String tableComment = null;
        try{
            StringBuffer query = new StringBuffer();

            // 해당테이블에 어떠한 VO와 매핑된지 확인
            query.append("SELECT TABLE_NO,  TABLE_NAME,  VO_NAME, ");
            query.append(" TABLE_COMMENTS FROM HT002 ");

            conn = getConnection();
            pstmt = conn.prepareStatement(query.toString());
            rset = pstmt.executeQuery();
            if( rset.next() ) {
                tableNo = rset.getInt(1);
                tableName = rset.getString(2);
                voName = rset.getString(3);
                tableComment = rset.getString(4);
            }
        }catch(Exception e) {
            e.printStackTrace();
        }finally{
            close(rset);
            close(pstmt);
            close(conn);
        }
        // 1. 먼저 입력받은 원본과 데이터베이스의 vo가 같은지 확인한다.
        if( !before.getClass().getName().equals(after.getClass().getName()) ) 
            throw new Exception("원본VO와 변경VO가 같지 않습니다");
        

        if( !before.getClass().getName().equals(voName) ) 
            throw new Exception("테이블관련 VO와 입력하신 VO가 같지 않습니다");

        // 해당 테이블에 대한 reflection을 호출할 수 있도록 테이블과 매핑된 vo method를 
        // 가져오도록 한다.
        try{
            StringBuffer query = new StringBuffer();
            query.append("SELECT COLUMN_NAME, METHOD_NAME, COLUMN_COMMENT ");
            query.append(" FROM HT003 WHERE TABLE_NO = ?");

            conn = getConnection();
            pstmt = conn.prepareStatement(query.toString());
            pstmt.setInt(1, tableNo);

            rset = pstmt.executeQuery();
            HT003VO ht003 = null;
            while(rset.next()) {
                ht003 = new HT003VO();
                ht003.setColumnName(rset.getString(1));
                ht003.setMethodName(rset.getString(2));
                ht003.setColumnComment(rset.getString(3));
                reflectionMethod.add(ht003);
            }
            System.out.println(reflectionMethod);
        }catch(Exception e) {
            e.printStackTrace();
        }finally{
            close(rset);
            close(pstmt);
            close(conn);
        }

        // 3. 변경된 대상의 리스트를 찾아낸다.
        ArrayList changeList = compareObject(reflectionMethod, before, after);
        System.out.println(changeList);
        insertToHT001Table(customerNo, 
                                      historyType, 
                                      workerNo, 
                                      tableName, 
                                      tableComment, 
                                      changeList);
        return result;
    }

    public void insertToHT001Table(int customerNo, 
                                                  String historyType, 
                                                  int workerNo, 
                                                  String tableName, 
                                                  String tableComment, 
                                                  ArrayList changeList){
        Connection conn = null;
        PreparedStatement pstmt = null;
        try{
            StringBuffer query = new StringBuffer();
            query.append("insert into HT001(history_no, history_type, customer_no, table_name, ");
            query.append(" table_comment, column_name, column_comment, before_data, ");
            query.append(" after_data, update_date, worker ) ");
            query.append(" values ");
            query.append(" ( (select max(history_no) + 1 from HT001) , ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )");
            
            conn = getConnection();
            conn.setAutoCommit(false);

            pstmt = conn.prepareStatement(query.toString());
            
            HT001VO ht001 = null;
            for( int i = 0 ; i < changeList.size() ; i++) {
                ht001 = (HT001VO) changeList.get(i);
                pstmt.setString(1, historyType);
                pstmt.setInt(2, customerNo);
                pstmt.setString(3, tableName);
                pstmt.setString(4, tableComment);
                pstmt.setString(5, ht001.getColumnName());
                pstmt.setString(6, ht001.getColumnComment());
                pstmt.setString(7, ht001.getBeforeData());
                pstmt.setString(8, ht001.getAfterData());
                pstmt.setTimestamp(9, new Timestamp(System.currentTimeMillis()));
                pstmt.setInt(10, workerNo);
                pstmt.execute();
            } 
            conn.commit();
        }catch(Exception e) {
            e.printStackTrace();
            try{
                conn.rollback();
            }catch(Exception e2) {}
        }finally{
            close(pstmt);
            close(conn);
        }
    }

    public ArrayList compareObject(ArrayList reflectionMethod, Object before, Object after) {
        ArrayList changeList = new ArrayList(10);
        Class [] argsType = {};
        Object [] argsValue = {};
        Method beforeMethod = null;
        Method afterMethod = null;
        HT003VO ht003 = null;
        
        Object beforeObject = null;
        Object afterObject = null;

        try{
            HT001VO ht001 = null;
            for( int i = 0 ; i < reflectionMethod.size(); i++ ) {
                ht003 = (HT003VO) reflectionMethod.get(i);
                beforeMethod = before.getClass().getDeclaredMethod(ht003.getMethodName() , 
                                                                                               new Class[0]);
                afterMethod = after.getClass().getDeclaredMethod(ht003.getMethodName() , 
                                                                                            new Class[0]);

                beforeObject = beforeMethod.invoke(before, argsValue);
                afterObject = afterMethod.invoke(after, argsValue);

                if( beforeObject != null && afterObject != null && 
                    beforeObject.getClass() == String.class ) {
                    String str1 = (String)beforeObject;
                    String str2 = (String)afterObject;

                    if( ! str1.equals(str2) ) {
                        ht001 = new HT001VO();
                        ht001.setColumnName(ht003.getColumnName());
                        ht001.setColumnComment(ht003.getColumnComment());
                        ht001.setBeforeData(str1);
                        ht001.setAfterData(str2);
                        changeList.add(ht001);
                    }
                } else if( beforeObject != null && afterObject != null && 
                              beforeObject.getClass() == Integer.class ) {
                    int int1 = ((Integer)beforeObject).intValue();
                    int int2 = ((Integer)afterObject).intValue(); 
                    if( int1 != int2 ) {
                        ht001 = new HT001VO();
                        ht001.setColumnName(ht003.getColumnName());
                        ht001.setColumnComment(ht003.getColumnComment());
                        ht001.setBeforeData("" + int1);
                        ht001.setAfterData("" + int2);
                        changeList.add(ht001);
                    }
                } else if( beforeObject != null && afterObject != null && 
                             beforeObject.getClass() == Long.class ) {
                    long long1 = ((Long)beforeObject).longValue();
                    long long2 = ((Long)afterObject).longValue();
                    if( long1 != long2 ) {
                        ht001 = new HT001VO();
                        ht001.setColumnName(ht003.getColumnName());
                        ht001.setColumnComment(ht003.getColumnComment());
                        ht001.setBeforeData("" + long1);
                        ht001.setAfterData("" + long2);
                        changeList.add(ht001);
                    }
                } else if( beforeObject != null && afterObject != null && 
                             beforeObject.getClass() == java.util.Date.class ) {
                    java.util.Date date1 = (java.util.Date)beforeObject;
                    java.util.Date date2 = (java.util.Date)afterObject;
                    if( date1.getTime() != date2.getTime() ) {
                        ht001 = new HT001VO();
                        ht001.setColumnName(ht003.getColumnName());
                        ht001.setColumnComment(ht003.getColumnComment());
                        ht001.setBeforeData(date1.toString());
                        ht001.setAfterData(date2.toString());
                        changeList.add(ht001);
                    }
                } // end of if
            
                
            } // end of for

        }catch(Exception e) {
            e.printStackTrace();
        }
        return changeList;
    }
}
위의 코드를 보게 되면 historyUpdate(int customerNo, int tableCode, String historyType, int workerNo, Object before, Object after) 의 메소드를 일반 애플리케이션에서 호출하도록 하고 있습니다. 어떤 고객의 정보, 테이블의 코드, INSERT, UPDATE, DELETE여부, 작업자, 이전VO, 변경VO를 인수로 취하고 있습니다. 실행되는 순서를 보게 되면 1. 들어온 VO의 reflection을 통하여 같은 VO인지를 확인한다. 2. 같은 VO가 같다면 해당 테이블코드를 이용하여 그 VO에 어떠한 메소드가 있는지 확인한다. 3. 기존VO와 변경된 VO를 이용하여 어떠한 필드의 값이 변경되었는지를 reflection을통하여 확인한다. 4. 실제 변경된 내용을 저장하고 있는 ArrayList객체의 내용을 히스토리 마스터테이블에 삽입한다. 실제 핵심적인 비교부분은 compareObject라는 메소드안에 존재하여 객체의 reflection을 이용하여 데이터타입 및 변수의 값들을 이용하여 테스트하고 있습니다. 단순 객체비교와 같은 경우 Comparator인터페이스를 사용할 수 있지만 위와 같은 경우 상황이 애매모호하므로 실제 값비교에만 중점을 두었습니다. 클래스 하나로 아주 간단하게 만들었으므로 이해하는데 별로 어려움이 없을 듯합니다. 궁금하신 사항이 있으시면 메일주세요.. ^^ 밥먹으러 갈시간이네요.. ^^ 아싸..
 
1
References
 
Copyright ⓒ 2003 www.javapattern.info & www.jlook.com, an jLOOK co.,LTD