Framework/Database2009. 3. 6. 04:57

DB는 서비스와 개체가 아닌 개념으로서 접근해야 한다는 얘기는 앞에서 강조하였다. 서비스로서 접근하는 의사 코드는 이를테면 아래와 같다.

Case1 )
Message msg = DBService.newMessage("messageName") ;
msg.setValue("userId", "bleujin") ;
msg.setValue("passwd", "pwd") ;
ResultValue result =  DBService.execute(msg) ;

여기서 중요한 것은 Message와 ResultValue는 JDBC 객체야 아니여야 한다는 점이다. JDBC 객체가 아니기 때문에 모든 문장에서 SQLException을 처리해야 할 필요도 없고(이는 나중에 다시..) Connection을 꼭 닫아야 하는 강박관념이 휘말리지 않아도 된다.

비슷하게 JDBC 구문을 작성해 보자.
Case 2)
Connection conn = DBConnectionManager.getConnection() ;

PrepareStatement pstmt = conn.prepareStatement("query") ;
pstmt.setString(1, "bleujin") ;
pstmt.setString(2, "pwd") ;
ResultSet rs = pstmt.execute();



비슷해 보이는가? 언뜻 보면 비슷하게 보이지만 사실 전혀 비슷하지 않다. 개체니 개념이니를 떠나서 실제적인 문제를 지적해보자.

첫째로 Case2는 지저분한 예외처리를 반복해야 한다.
둘째로 Case2는 리소스 정리를 잊지 말고 해주어야 한다. -> DB는 중요한 리소스인데 일부 코드의 실수가 전체 서비스를 중단시킨다.
셋째로 클라이언트 쪽 코드가 너무 많은걸 알고 있기 때문에 이후 유지보수에 어려움이 많다.
넷째로 실제 트랜잭션등의 코드가 들어가기 시작하면 비지니스 로직이 아닌 JDBC 코드로 지저분해진다.

문제점을 요약하면 이전글에서 말한대로 매번 구조적인 중복이 발생한다는 사실이다.
그에 비해 Case1은 JDBC와 중립적인 Java 객체를 사용했기 때문에 JDBC의 반복적인 중복으로부터 자유롭다.

그러나 우리는 왜 Case2식의 코드를 작성하는가? 비슷한 코드가 매번 반복된다고 할 때 케이스별로 차이가 발생하는 부분은 pstmt.setString()을 호출하는 부분이다. 다른 부분은 매번 반복된다. 반복되는 줄 알면서도 쉽게 지우기 어려운 이유은 PrepareStatement 객체인 pstmt가 바로 Connection 인스턴스인 conn에서 생기기 때문이다. 다시 말해서 Connection은 PrepareStatement 객체에게 일종의 끊기 어려운 족쇄같은 것이기 때문에 이를 떼어내야 한다.

pstmt가 하는일은 사실 그냥 index와 value값을 가지고 있기만 하면 되는 간단한 일이다. 그렇다는 얘기는 굳이 PrepareStatement를 먼저 생성해야 할 필요가 없다. 비슷한 인터페이스를 가지고 내부에 param을 저장할 수 있는 Map객체를 가진 객체 - 이를테면 Message라고 하자 - 를 만들고 여기에 값을 set 한다음에 실제 execute가 발생할때 그때 PrepareStatement를 생성해서 Message에 저장된 값을 설정한 다음에 실행시키면 그만이다.

의사코드로 작성하면

class Message {
    private Map values = new HashMap() ;
    private String name  ;
    public Message() {
        this.name = name ;
    }
    public void setString(int index, String value) {
       values.add(new Integer(index), value) ;
    }
    ......

   public int execute() {
       Connection conn = getConnection() ;
       PrepareStatement pstmt= conn.prepareStatement(getMessageName()) ;
       for(..){
            pstmt.setValue(....) ;
       }
       int result =  pstmt.execute() ;
       pstmt.close() ;
       conn.close() ;
       return result ;
   }

}
와 같이 Wrapper 객체를 만들어 Laze Execution을 시키면 문제는 간단하다. 이렇게 작성하면 더이상 PrepareStatement처럼 connection에 메이지 않아도 된다.

좀더 생각해야할 문제는 그 다음부터이다. 처음 드는 생각은 그럼 select는 ? 이고 두번째는 그럼 트랜잭션은? 이다.



Select는 ?

다시 일반적인 JDBC 코드를 보자
ResultSet rs = pstmt.execute();
  ....  // Case별로 다른 코드
rs.close() ;
psmts.close() ;
conn.close() ;


중간 .... 부분의 코드는 비지니즈 로직마다 다르다. 앞의 setParam때와 마찬가지로 이 부분의 매번 다르기 때문에 어쩔수 없는 족쇄 코드인 close 구문이 - 그리고 예외 처리 구문이 - 뒤 따라 온다. 이 부분은 먼저 전체범위 처리와 부분범위 처리에 대한 이해와 커서에 대한 이해 글을 먼저 읽어야 하지만 간단하게 요약하면 클라이언트 커서를 사용하는 중립적인 객체를 사용하면 된다. 우리가 필요한건 ResultSet에 있는 값이기 때문에

class Message {
    ....

    Rows execQuery(){
        ......
        ResultSet rs = pstmt.execute();
        Rows rows = new Rows() ;
        rows.populate(rs) ;          // rs의 값을 rows에 담는다. rows는 일종의 커다른 맵이라 생각하자.
        rs.close() ;
        psmts.close() ;
        conn.close() ;
        return rows ;
    }
}
와 같이 JDBC의 ResultSet 인터페이스를 impl하였지만 실제 connection less 한 상태에서도 값을 얻을 수 있는 Rows에 값을 담아서 던지면 클라이언트 코드는 커넥션에 대해 신경쓸 필요없다.

클라인트 코드는 대충 이렇게 작성하면 된다.
Rows rows = msg.execQuery() ;
..... rows를 가지고 지 할일 한다.

그리고 범위를 벗어나면 rows는 알아서 가비지 컬렉션 된다.



Transaction은 ?

트랜잭션은 좀더 쉽다. Message 배열을 가지는 Messages 객체를 만들어서 여러개의 메시지를 담은다음 각 메시지의 execute()를 호출하는 execute를 만들면 된다. 대충 아래와 같다.

Messages extends Message {

     public int execUpdate() {
        int result = 0 ;
        Connection conn = getConnection() ;
        for(Message msg : messages){
            result += msg.execute(conn) ;
        }
        return result ;
     }
}

이 기종의 트랜잭션의 처리와 조건에 따른 트랜잭션 처리는 좀더 어려운 주제이므로 여기서는 생략하자. 코드가 조금 어려울 뿐 인터페이스 자체가 중립적이므로 이것도 굳이 상관없다.


DB를 서비스로 접근해야 한다는 얘기를 하기 위해 객체 이름을 Message로 했을뿐 실제 클래스 이름으로 바꿔보면 대충 아래와 같다.


DBController dc = new DBController() ;
IUserProcedure upt = dc.createUserProcedure("findCustomer(?,?)") ;
upt.addParam(1, "bleujin").addParam(2, "pwd") ;
Rows rows = dc.execQuery(upt) ;


여기서 주목해야 할 것은 findCustomer이란 문자열은 무언인가? 하는 점이다. 대부분의 프로그래머들은 Stored Procedure에 안좋은 감정을 가지고 있다. 싫어하는 첫번째 이유는 SP가 DB 종속적이고 두번째로 비지니스 코드를 종속적인 SP에 담아서는 안된다는 이유다. 속도가 빠르다라던가 보안에 좋다 등의 장점도 있지만 아마 그런점은 애써 무시한다.

장점은 그렇다치더라도 단점은 과연 타당한가?

첫번째 이유는 이전에도 강조했다 시피 일부 DB만 SP를 지원하기 때문에 SP를 쓰지 말아야 한다는 것 자체가 종속에서 벗아나지 못했다는 뜻이다. DB종속에서 진정 벗어나고 싶으면 DB가 SP를 지원하든 안하든 Client 코드는 그에 영향 받지 않아야 한다가 좀더 유연한 코드이다. 위의 findCustomer를 어떻게 해석하냐는 앞에서 얘기한 DBManager에 달렸다. 이를테면 OracleDBManager는 execUpdate가 호출될때 findCustomer이라는 SP를 찾아서 실행한다. MSSQLDBManager는 findCustomer이라는 function을 찾아서 실행한다. 그마저도 없는 HSQL이나 H2같은 Manager들은 지정된 xml에서 findCustomer이라는 이름으로 설정된 SQL을 얻어와서 실행한다. 다시 말해서 DB가 지원하면 쓰면 그만이고 안 지원하면 안쓰면 되고 지원하는데도 안쓰고 싶으면 안 쓸 방도도 만들어 주어야 한다는 것이다.

사실 개인적으로 SP의 가장 큰 장점으로 생각하는 부분은 속도나 보안등의 이유가 아니라 SP나 MSSQL의 function은 컴파일이 되는 그 자체에 장점이 있다고 생각한다. xml 파일에 저장된 query문은 그냥 String일뿐이다. 따라서 slect empNo, ename from emp where deptNo < 20 이라는 SQL문이 저장되어 있을때 정말 emp라는 테이블이 있는지 emp라는 테이블에는 empNo라는 컬럼이 있는지 그리고 select를 slect로 잘못 쓰지는 않았는지는 확인해 주지 못한다. 컴파일 하지 않는 언어인 Javascript를 사용하면서 우리가 얼마나 많은 초보적인 문법 실수를 저지르는지 그리고 유지보수에 문제를 많이 가지고 있는지 생각해 보면 이는 자명한 일이다. SP나 Function은 실시간으로 검사하기 때문에 DBA가 테이블 이름을 잘못 rename한다면 이를 바로 알려준다. xml 파일에 저장되어 있다면 사용자가 에러를 일으킨후 톰캣이 수천라인의 에러 로그를 토해내고야 알수 잇는 반면에 말이다.

두번째 비지니스 코드는 종속적인 SP에 담아서는 안된다라는 주장에 대해 밝혀보자.
이는 먼저 비즈니스 코드란 무엇인가에 대한 개념이 잡히지 않는 소리다. 추상화를 적절히 사용했다면 비즈니스 코드란 findCustomer이란 이름 그 자체이지 결코 SQL Query가- select cname from customer where custNo = ? 식의 -  비지니스 코드가 아니다. 비즈니스 코드에 대해 알고있는 사람들은 이 버튼을 누르면 findCustomer가 실행된다라고 알아야지 이런 저런 테이블에서 이런저런 쿼리를 던집니다 라고 알아야 하는게 아니다. 물론 나도 IF문으로 도배된 수천라인의 SP로 작성된 코드를 감싸는게 아니다. 문제는 지나치게 과잉 처리된 SP에 문제가 있는 거지 SP 자체를 사용하는게 종속적인 비지니스 로직을 사용하는게 아니라는 말이다.


다시 실제적인 문제로 돌아와서 Query의 타입은 실제로 여러가지가 있다.




최상위 인터페이스인 IQueryable는 MDL을 실행하는 execUpdate와 Select를 담당하는 execQuery 3개의 메소드가 있다.
execUdpate : MDL 구문 실행
execQuery : Select 형 구문
execHandlerQuery : HandlerQuery


UserProcedure :
UserProcedure는 String 하나를 생성자로 받는데 이 String이 어떻게 해석되는가는 저 앞쪽의 DBManager에 달려 있다. 같은 오라클 DB라도 이걸 프로시저 이름으로 해석하느냐 아니면 매핑되어 있는 SQL로 해석하느냐는 다르다. 앞의 이야기는 주로 이 클래스를 기준으로 설명하였다. DBManager는 이 클래스를 상속받는 OracleUserProcedure와 MSSQLProcedure 등을 상황에 따라 사용한다.

UserCommand :
테스트나 기타 간단한 확인을 위해서 Query문 그 자체를 받는다. 실제로는 잘 사용하지 않는다. 사실 UserCommand를 만든 이유는 UserCommandBatch를 위해서다.
UserCommandBatch :
10000개의 insert를 해야 할때 만개의 PrepareStatement를 생성하는 건 바보같은 짓이기 때문에 JDBC의 배치처리 구문을 사용한다. setParam시 배열을 사용한다. 실제 10000개의 인서트시 DB Call을 한번 사용하기 때문에 반복적인 배치처리를 해야 할 때 사용한다. 배치작업은 JDBC 규약대로 하나의 트랜잭션으로 자동 처리된다.

UserProcedureBatch :
테스트 결과 Procedure를 배치로 사용하는 것은 일반 SQL의 배치보다 효율이 많이 떨어진다. 그러나 건수가 아주 많지 않다면 굳이 새로 만들필요없이 대충 사용한다.

TimeOutQuery :
다른 IQueryable를 생성자로 받아서 별도의 Thread로 해당 Query를 모니터하면서 지정된 시간이 지나면 query를 캔슬시킨다. 기본적으로 JDBC API에는 cancel API가 있지만 DB 벤더에 따라 구현 여부가 다르다. Decorator 패턴의 활용

UserProcedures :
여러개의 다른 IQueryable를 하나의 Transaction으로 처리한다. Composite 패턴의 기본 활용이다. 만약 select 하는 IQueryable를 UserProcedures에 여러개 담아서 실행하면 어떻게 될까? 이런 경우 여러개의 sql을 모두 실행시켜서 return값인 Rows를 chain 형태로 엮는다. firstRows.getNextRows() 형태로 다음 결과 셋을 얻을 수 있다.

XAUserProcedure :
앞의 UserProcedures는 여러개의 IQuery를 하나의 DB에 트랜잭션 처리하는 것에 반해 두개 이상의 DBManger를 사용하면서 이기종간(이를테면 오라클과 MSSQL)의 IQueryable들을 하나의 트랜잭션으로 처리하고자 할때 사용한다.

CombinedUserProcedures :
위의 다이어그램에는 표시되지 않았지만 아주 가끔 먼저 select 를 해보고 특정 조건에 따라서 해야할 일이 달라지는 경우가 있다. 이런 케이스는 경우의 수가 천차 만별이지만 - 이를테면 먼저 insert를 하고 커밋을 하지 않는 상태에서 다음 select를 던져보고 update를 할건지 delete를 할건지 아니면 rollback를 선택한다. - 최대한 일반적인 상황을 가정해 만들어본 클래스이다. 아주 없다고는 할수 없지만 확률상 많이 나타나지 않으므로 아주 독특한 경우는 상속받아서 새로이 구현하는 것도 고려해볼 수 있다.


쿼리의 종류에는 여러가지가 있지만 아직 10년간 프로그래밍하면서 위의 케이스를 벗어나는 타입은 아직 없었다. 사실 있더래도 상관없다. 이는 모두 실험실 안의 코드이고 실험실 바깥에서는 상관하지 않으니 새로운 타입의 IQueryable를 구현하면 그만이기 때문이다.



여기서 다시 햄버거 가게가 생각나겠지만 왜 복잡하게 IUserCommand 인터페이스를 사용하고 MSSQLUserCommand와 OracleUserCommand 를 따로 만들어야 하는지에 대해서 말하자면 JDBC에 표준 API가 있음에도 사실상 표준 API만으로 JDBC 코드를 만들기는 어렵다.

이는 달리 말해서 단순히 DB의 특정 기능을 사용하지 않는다고 DB로의 독립이라는 목표를 달성하기가 매우 어렵다는 것을 뜻한다.  이는 Framework 차원에서 주의깊게 접근해야 한다. 예컨데 Clob 처리를 보면 Oracle과 MSSQL은 처리 방식이 전혀 다르다. 해당 Framework를 사용하는 Client는 그딴거에 상관하고 싶어하지 않으므로 실제 impl하는 컨크리트 클래스들이 여러개씩 존재하고 어떤 컨크리트 클래스들을 사용할 것인가는 DBManager가 판단한다. (좀더 정확히 말하자면 DBManager의 RepositoryService 객체가 판단한다. )


이글의 요지는 이렇다. 많은 프로그래머들이 알고 있듯이 JDBC의 표준 API만으로 JDBC 프로그래밍을 하는건 무척 어렵다. 이상과는 다르게 DB벤더마다 조금씩 다른 API를 사용해야 하고 단순히 특정 기능을 사용하지 않는다고 소위 DB에 독립적인 코드를 만든다는 것은 어렵다. - 그 밖에도 어려움은 많다 - 그리고 DB Framework에서는 이런 문제에 대하여 클라이언트 코드는 전혀 JDBC 코드를 사용하지 않도록 Wrapper Object 등을 통해 차단막을 설치해버리면 Framework 안의 실험실 코드는 철처히 벤더 의존적인 코드로 만들어도 상관이 없다. 실험실 바깥과는 Service와 독립적인 객체로 통신하는 느슨한 연결을 통해 달성할 수 있는 장점이다.


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

Framework (Handler)  (0) 2009.03.08
Framework (Rows)  (0) 2009.03.07
Framework (DBManager)  (0) 2009.03.04
Framework (구조적 중복 제거)  (0) 2009.02.21
Framework (실험실 코드)  (0) 2009.02.20
Posted by bleujin