태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

디자인패턴 연습 6

연습 5의 핵심은  PreparedStatement 처리 소스를 분리하는 일이다.
public MyEtt getMyEtt(String field1, int field2) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rset = null;
    MyEtt ett = null;
    try {
        conn = daoContext.getConnection("1st");
        stmt = conn.prepareStatement("SELECT * FROM tables WHERE filed1=? AND filed2=?");
        stmt.setString(1,field1);
        stmt.setInt(2,field2);
        rset = stmt.executeQuery();
        if ( rset.next() ) {
            ett = new MyEtt();
            ett.setFiled1( rset.getString("field1") );
            ett.setFiled2( rset.getInt("field2") );
            ett.setFiled3( rset.getString("field3") );
        }
        return ett;
    } catch(Throwable t) {
    } finally {
         try { rset.close(): } catch(Throwable ignore) {}
         try { stmt.close(): } catch(Throwable ignore) {}
         try { conn.close(): } catch(Throwable ignore) {}
    }
}

위의 소스에서 진한 부분은 가변적인 부분이고, 나머지는 항상 동일하게 처리하는 부분이 된다.
가변적인 부분도 다시 나누어보면 클래스 작성시 결정되어 지는 부분과 동작시 결정되는 부분으로 나눌 수 있다.
클래스 작성시 결정되는 부분은 sql 문과 ? 에 치환될 파라메터의 갯수와 형식...
그리고 결과값을 담을 Object (MyEtt) 에 값을 넣는 부분이 된다.
클래스 동작시 결정되는 부분은 파라메터의 값이 해당될 것이다.
ConnectionId 는 상황에 따라 좀 달라질 것 같다.

공통되는 부분을 작성한 추상 클래스를 두고 각 sql 별로 상속해서 구현하는 형태를 쉽게 생각할 수 있다.
그러면 일단 위 소스를 단계별로 나누어본다.
1. Connection 생성
2. PreparedStatement 생성 - sql 필요
3. 파라메터 바인딩 - 파라메터 필요
4. executeQuery, executeUpdate 실행 - sql 에 따라 변하기 때문에 객작성시 결정되는 부분...
5. ResultSet 에서 적절한 Object 로 변환 - executeUpdate 에서는 필요없음.
6. 결과 Object 반환 - executeQuery 에서는 List 형태, executeUpdate 는 int 값을 반환..

executeUpdate와 executeQuery 라는 결과가 차이가 나는 둘을 모두 충족시키기 위해 결과 Object 를 만들었다.
public class PreparedSqlResult {
    private final int count;
    private final List list;
    // executeUpdate 에서 사용할 Constructor.
    public PreparedSqlResult(int count) {
        this.count = count;
        this.list = null;
    }
    // executeQuery 에서 사용할 Constructor.
    public PreparedSqlResult(List list) {
        this.count = list.size();
        this.list = list;
    }
    public List getList() { return this.list; }
    public int getCount() { return this.count; }
}
이제 단계별로 처리를 진행하는 슈퍼클래스를 하나 만들어본다.
public PreparedSqlResult execute(Object[] parameters) {
    Connection conn = getConnection(); // 1번.
    PreparedStatement stmt = conn.prepareStatement( getSql() );
    for( int i=0; i< parameters.length; i++ ) {  // 3번. 파라메터 바인딩
        stmt.setObject(i+1,parameters[i]); // setObject 으로 웬만한 Type은 해결이 가능하다.
    }
    // 4번. 실행
    PreparedSqlResult  result = null;
    if ( useRowObject() ) {
        ResultSet rset = stmt.executeQuery();
        ArrayList list = new ArrayList();
        while( rset.next() ) {
             list.add( bindRowData(rset) );  // 5번. Row별 Object 변환
        }
        result = new PreparedSqlResult(list); // 6번. 결과 반환
    } else {
        int rst = stmt.executeUpdate();
        result = new PreparedSqlResult(rst);  // 6번. 결과 반환
    }
    return result;
}

public abstract Connection getConnection(); // 1번. 서브클래스에서 connection 을 제공
public abstract String getSql();  // 2번. 서브클래스에서 sql 을 제공
public abstract boolean useRowObject(); // 4번. 서브클래스에서 execute 형식을 제공
public Object bindRowData(ResultSet rset) { // 5번. 서브클래스에서 ResultSet Row별 Object 생성
    return null;
}

일단 대충 구성해 보았는데... 어느 정도 사용이 가능할 것으로 보인다.
물론 try~catch~finally 도 필요하고 close도 시켜야 한다.

위의 예처럼 어떠한 로직의 일정 부분을 서브클래스에서 구현하도록 하는 것을
[develop/java patterns] - 디자인패턴 - TemplateMethod 라고 한다.

여기서 파라메터 바인딩 부분을 조금 더 고급스럽게 바꿔보자..
위의 예에서 파라메터값을 Object[] 로 전달하고 있는데, 아무래도 개발시에 불편할 것 같다.
키, 값 을 넣는 Map 형태로 받는다면 더 좋을 것 같고, 파라메터를 다루는 데도 명확할 것 같다는 판단이다.
그렇다면 파라메터키와 바인딩 순서를 맞추어줄 무엇인가가 필요하게 된다.
파라메터 바인딩 순서는 sql 이 결정될 때 함께 결정되어 지는 부분이기 때문에 객체 작성시의 요건이다.
private Vector parameterKeys = new Vectory();
protected void appendParameterKey(String key) { this.parameterKeys.addElement(key); }
public Enumeration getParameterKeys() { this.parameterKeys.elements(); }
// ... insertParameterKey, removeParameterKey 등도 구현해놓으면 좋을 듯...

public PreparedSqlResult execute(Map parameterMap) {
    ... 생략...
    for( int i=0; i< parameterKeys.size(); i++ ) {
        stmt.setObject(i+1, parameterMap.get( parameterKeys.get(i) ) );
    }
    ... 생략...
}
다시 한번 위에서 PreparedStatement 의 setObject(int parameterIndex, Object value) 메쏘드를 사용했는데..
이보다 setObject(int parameterIndex, Object value, int sqlType) 을 사용하는 것이 더 좋을 것 같다.
그 이유는 javadoc 에서 PreparedStatement 두 메소드의 설명을 보면 알 수 있을 것이다.
sqlType 은 java.sql.Types 에 int 형식으로 각 타입별로 정의되어 있다.
그러면 위의 소스에서 parameterKeys 라는 Vector 에 키가 아니라 이러한 정보 객체를 담는 것으로 한다.
public class PreparedSqlParameter {
    private final String name;
    private final int sqlType;
    public PreparedSqlParameter(String name, int sqlType) {
        this.name = name;
        this.sqlType = sqlType;
    }
    public String getName() { return this.name; }
    public int getSqlType() { return this.sqlType; }
}
private Vector parameters = new Vectory();
protected void appendPreparedSqlParameter(PreparedSqlParameter p) { this.parameters.addElement(p); }
public Enumeration getParameterKeys() { this.parameters.elements(); }
// ... insertParameterKey, removeParameterKey 등도 구현해놓으면 좋을 듯...

public PreparedSqlResult execute(Map parameterMap) {
    ... 생략...
    for( int i=0; i< parameters.size(); i++ ) {
        PreparedSqlParameter p = (PreparedSqlParameter) parameters.get(i);
         stmt.setObject(i+1, parameterMap.get( p.getName() ), p.getSqlType() );
    }
    ... 생략...
}
자 이를 구현한 서브클래스를 예로 든다면
private String sql;
public ${SubClassname}() {
    this.sql = "select * from table1 where field1=? and field2=? ";
    appendPreparedSqlParameter("field1",Types.INTEGER);
    appendPreparedSqlParameter("field2",Types.VARCHAR);
}
처럼 정의할 수 있다.
처음에 이렇게 만들고 나서 나중에 Spring DAO 쪽을 보게 되었는데..같은 방법이었다.... 제길....
흉내를 낸게 아닌데, 흉내낸 것처럼 보일까 속이 상해서... 그때 프레임워크에 저 방법을 안 써 버렸다.. ^^;

처음 목표한 공통 로직의 분리라는 관점에서는 어느 정도 해결이 된 것같다.
이제 이것을 깍고 다듬어 더 유용한 구조를 만들어 보도록 한다.

일단 맘에 안드는 점은 executeQuery 와 execueUpdate 처리를 나누는 if절 제어 부분이다.
디자인패턴에 대해 이야기 하는 자료에서 제어보다는 구조를 사용하라는 말이 나온다.
그래서 구조를 만들기로 한다.
먼저 위에서 만든 슈퍼 클래스에서는 처리하는 부분은 일단 제거하고, 값을 실어 나르는 형태로 전환시킨다.
public abstract class PreparedSql {
    private Vector parameters;
    protected PreparedSql() {
        this.parameters = new Vector();
    }
    protected void appendPreparedSqlParameter(PreparedSqlParameter parameter) {
        this.parameters.addElement(parameter);
    }
    ... PreparedSqlParameter 설정 관련 메소드 중략 ...
    public abstract String getSql();
    public abstract Object getConnectionId();  // getConnection 을 getConnectionId 로 변경
    public Object bindRowData(ResultSet rset) { return null; }
   
public abstract PreparedSqlHandler getPreparedSqlHandler();  // 실제 처리를 수행할 객체를 반환

    PreparedSqlResult execute(DaoContext context,Map parameterMap) throws SQLException {
        PreparedSqlHandler handler = getPreparedSqlHandler();
        return handler.service(context, this, parameterMap);
    }
}
위의 소스에서 execue 부분을 보면 실제 처리를 할 놈을 일단 PreparedSqlHandler 라는 놈으로 하고 있고,
handler의 execue 메소드를 실행해서 결과를 반환하도록 되어 있다.
public interface PreparedSqlHandler {
    PreparedSqlResult service(DaoContext context, PreparedSql sql, Map parameterMap)
        throws SQLException;
}
그리고 execueUpdate 와 execueQuery 를 각각 구성한다.
public class ExecuteUpdateHandler implements PreparedSqlHandler {
    // Singleton 으로 낭비를 줄이자...
    private static ExecuteUpdateHandler singleton = new ExecuteUpdateHandler();
    public static final PreparedSqlHandler getInstance() { return singleton; }
    private ExecuteUpdateHandler() {}
   
    public PreparedSqlResult service(DaoContext context, PreparedSql sql, Map parameterMap)
             throws SQLException {
        Connection conn = null;
        PreparedStatement stmt = null;
        try {
            conn = context.getConnection(sql.getConnectionId());
            stmt = conn.prepareStatement( sql.getSql() );
            int idx = 0;
            for(Enumeration e = sql.getPreparedSqlParameters(); e.hasMoreElements();) {
                PreparedSqlParameter param = (PreparedSqlParameter)e.nextElement();
                stmt.setObject(++idx, parameterMap.get(param.getName()), param.getSqlType() );
            }
            int rst = stmt.executeUpdate();
            return new PreparedSqlResult(rst);
        } catch( SQLException se) {
            throw se;
        } finally {
            try { stmt.close(); } catch(Throwable ingore) {}
            try { conn.close(); } catch(Throwable ingore) {}
        }
    }
}
public class ExecuteQueryHandler implements PreparedSqlHandler {
    // Singleton 으로 낭비를 줄이자...
    private static ExecuteQueryHandler singleton = new ExecuteQueryHandler();
    public static final PreparedSqlHandler getInstance() { return singleton; }
    private ExecuteQueryHandler() {}
   
    public PreparedSqlResult service(DaoContext context, PreparedSql sql, Map parameterMap)
             throws SQLException {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rset = null;
        try {
            conn = context.getConnection(sql.getConnectionId());
            stmt = conn.prepareStatement( sql.getSql() );
            int idx = 0;
            for(Enumeration e = sql.getPreparedSqlParameters(); e.hasMoreElements();) {
                PreparedSqlParameter param = (PreparedSqlParameter)e.nextElement();
                stmt.setObject(++idx, parameterMap.get(param.getName()), param.getSqlType() );
            }
            ArrayList list = new ArrayList();
            if ( rset.next() ) {
                list.add( sql.bindRowData(rset) );
            }
            return new PreparedSqlResult(list);
        } catch( SQLException se) {
            throw se;
        } finally {
            try { rset.close(); } catch(Throwable ingore) {}
            try { stmt.close(); } catch(Throwable ingore) {}
            try { conn.close(); } catch(Throwable ingore) {}
        }
    }
}
위처럼 구성하니 if 절을 쓰지 않고 구조적으로 해결이 된다.

DaoContext 에 이것을 실행할 수 있는 메소드를 추가해보자...
public interface DaoContext {
    .... 중략 ....
     PreparedSqlResult parparedSqlExecute(PreparedSql sql, Map parameterMap) throws SQLException;
}
public class GenericDaoContext implements DaoContext {
    .... 중략 ....
    public PreparedSqlResult parparedSqlExecute(PreparedSql sql, Map parameterMap) throws SQLException {
        return sql.execute(this,parameterMap);
    }
PreparedSql 을 상속하여 sql 정보와 handler 등을 설정해서 객체를 만들고, DaoContext 에서 이를 실행시킬 수 있게 되었다.

이 구성을 클래스 다이어그램으로 보면...
사용자 삽입 이미지

이런 그림이 된다.
바로 [develop/java patterns] - 디자인패턴 - Command 패턴에 해당된다.

실제 sql 동작을 위해 작성되는 코드는 다음과 같다.
public class TestPreparedSql1 extends PreparedSql {
    private final String sql;
    private final Object connectionId;
    public TestPreparedSql1(Object connectionId) {
        this.connectionId = connectionId;
        this.sql = "SELECT field1,field2,field3 from table1 where field1 = ? and field2 = ? ";
        appendPreparedSqlParameter(new PreparedSqlParameter("field1",Types.INTEGER));
        appendPreparedSqlParameter(new PreparedSqlParameter("field2",Types.VARCHAR));
    }
    public Object getConnectionId() { return this.connectionId; }
    public PreparedSqlHandler getPreparedSqlHandler() { return ExecuteQueryHandler.getInstance(); }
    public String getSql() { return this.sql; }
    public Object bindRowData(ResultSet rset) throws SQLException {
        TestBean bean = new TestBean();
        bean.setField1(rset.getInt("field1"));
        bean.setField2(rset.getString("field2"));
        bean.setField3(rset.getString("field3"));
    }
}
의 형태가 된다.
처음 그냥 풀어서 코딩할 때 보다 더 어려워졌나...?
하지만 프레임워크들을 살펴보다 보면 이와 유사한 방법을 사용한다는 것을 볼 수 있을 것이다.

물론 지금까지의 코드들로 실무에 적용시키기에는 제약이 많이 따른다.
우선 sql 이 고정되어 있고 동적으로 조건절이 추가되거나 하는 부분이 없어서 사용상의 제약이 있다.
bindRowData 메소드도 ResultSet 이 넘어가기 때문에 개발자가 next() 해버린다거나 하면 문제가 될 수도 있다.
기타 이런 저런 여러가지 더 필요한 사항들이 있다.
그래도 틀은 만들었으니 하나하나 해결해 나가면 된다.
프레임워크나 API는 처음부터 모든 걸 구현하지는 못한다. 사용하면서 계속 발전되 나가는 것이다.
실제 내가 만들어 제공할 프레임워크는 실무에 다양하게 사용하기 위해 더 많은 확장점을 제공하고 있다.

DaoContext 에 PreparedSql 을 또 하나의 상품군으로 해서 FlyWeight 패턴을 적용하면... 더 좋을 것이다.
첨부된 소스 코드에는 이 부분도 포함되어 있다.




    연습6.
  • 실무에서 PreparedSql 은 많은 수가 생기게 된다. 1차원 병렬 key 구조는 관리가 힘들 수도 있다.
  • PreparedSql 을 관리하기 편한 구조로 디자인 해본다.

    힌트
  • 계층 구조는 우리에게 익숙한 구조이다.
  • 각각의 PreparedSql  을 여러개 묶어서 하나의 PreparedSql 로 서비스하는 방법도 가능하다.

    더 생각해 볼 것
  • PrepardSql 을 async 로 돌릴 수 있는 방법을 생각해 보자. (구현은 하지 않아도 된다.)
    일단 백그라운드로 실행시키고 collback 을 받을 수만 있으면 async 가 된다.
    우리가 이미 연습한 패턴을 사용하면 쉽게 해결이 가능하다.
  • PrepardSql queing 도 방법을 생각해 보자. (구현은 하지 않아도 된다.)

'develop > java patterns' 카테고리의 다른 글

디자인패턴 연습 6  (0) 2008/06/25
디자인패턴 - Command  (6) 2008/06/24
디자인패턴 - TemplateMethod  (0) 2008/06/24
디자인패턴 연습 5  (0) 2008/06/18
디자인패턴 - Flyweight  (3) 2008/06/18
디자인패턴 - Singleton  (1) 2008/06/17
Trackback 0 Comment 0
prev 1 ... 124 125 126 127 128 129 130 131 132 ... 244 next