'분류 전체보기'에 해당되는 글 140건

  1. 2009.03.10 exception framework
  2. 2009.03.08 Framework (Handler)
  3. 2009.03.07 AL - 배경
  4. 2009.03.07 Framework (Rows)
  5. 2009.03.07 Class Design
  6. 2009.03.07 Database - Oracle Hint
  7. 2009.03.07 Database - Plan 이해
  8. 2009.03.07 checked vs runtime
  9. 2009.03.06 Framework - AL(개요)
  10. 2009.03.06 Framework (IQueryable)
Framework/예외처리2009. 3. 10. 02:25


예외 처리에 관해 알아야 할 중요한 사실 중 하나는 예외 처리는 비즈니스 로직이라는 것이다.
if (출금액() > 임금액) throw new 충분한돈이없음예외 와 같은 태생이 비즈니스 로직뿐 아니라 일반적으로 많이 나오는 IOException, SQLEXception등의 java Exception도 어떻게 예외 처리를 하는가는 프로그램마다 다르고 비지니스 로직마다 다르다.

그 전에 프로그래밍에서 예외란 에러와 달리 Alternative Path의 한 종류이고 이도 정상적인 하나의 프로그래밍 로직으로 인정해야 한다.

try {
    ....
} catch(IOException ex){
   log.warn(ex) ;
}

위 구문에서 예외 처리는 중복일까 아닐까? 프레임워크의 시야로 본다면 위 구문이 여러번 나온다면 그건 중복이다. 앞서 다른 글에서 밝힌바와 같이 두 줄이상의 코드가 동일할때만 중복이라고 하는게 아니다. 위와 같은 코드는 구조적인 중복의 2번째 케이스이다. 그런데 단 한줄인데 더 이상 어떻게 하라는 말인가? 한줄을 줄여봤자 한줄인데 이게 왜 중복이고 더 이상 어떻게 줄일 수 있다는 건지 의문을 가질 수 있다.

아마도 객체지향 책을 몇권 읽었다면 구체적인 것에 의지하지 말아라 라는 말을 듯었을 것이다. 구체적인 것에 의존하지 않기 위해 우리는 컨크리트 클래스보다는 추상클래스를 추상클래스보다는 인터페이스를 써야 한다는 것을 이미 알고 있다. 그리고 여기서의 log은 아마도 Apache의 common log같은걸 사용했다면 인터페이스 일테니 충분하지 않은가? 그리고 물론 충분하지 않다. 객체지향에서 구체적인 것이란 단순히 컨크리트 클래스만을 의미하는 것이 아니다. 처음에 언급한대로 예외처리는 비지니스 로직이다라는걸 인정한다면 log를 남긴다는 것 자체가 충분히 구체적이다.

보통의 경우 비지니스 로직과 일반 로직은 수명과 변화의 속도가 다르기 때문에 가능하면 분리해서 구성해야 한다. 잘 알다시피 프로그래머는 CPU가 아니기 때문에 멀티 테스킹의 CPU처럼 유연한 상태전환이 쉽지않고 인터럽트에 취약하기 때문에 로직 중간 catch 코드에 log를 쓸지 warn을 쓸지 info를 쓸지 LogLevel은 몇레벨을 쓸것인지 따위에 고민하게 만들어서는 안된다.

function xxx() throws IOException {
     ,,,,,,,
}

대부분의 코드에서 catch문을 작성하지 않는다면 어느순간 예외들을 던지는 구문이 메소드 내용보다 길어질 것이라고 걱정할지 모르지만 앞서의 조언들을 잘 따라서 인프라를 담당하고 있는 시스템 프레임워크들이 runtime exception을 던지게 하거나 걱정할 정도로 많은 예외를 던져야 한다면 코드를 다시한번 변화의 속도를 기준으로 나눠서 한번정도 Wrapping하는것도 고려한다면 그다지 큰 문제는 아니다.

그마저도 싫다면 혹은 어렵다면
try {
    ....
} catch(IOException ex){
   exManager.resolve(ex) ;
}
와 같이 처리 하는게 낫다 한다. resolve이란 메소드는 log 메소드 보다 더 추상적이고 확장과 구성의 포인트가 많다.

계획대로 모든 예외처리를 미루었다면 FrontController에서
,,,,,
catch (Throwable e) {
     Throwable ex = e ;
     if (e instanceof ServletException && ((ServletException) e).getRootCause() != null) {
        ex = ((ServletException)e).getRootCause() ;
     }
     getExceptionHandler().resolve(new HttpExceptionInfo(request, form, ex));
}
와 같이 한번에 처리해 버릴수 있다.

실제로 예외 처리의 예을 다이어그램으로 그려본다면



와 같이 Chain Of Responsibility 패턴 정도가 나올것이다. Chain Of Responsibility는 어떤 사람에게 요구가 들어왔을때 그 사람이 그것을 처리할 수 있으면 처리하고, 처리할 수 없으면 다음사람에게 넘긴다. 다음 사람이 그것을 처리할 수 있으면 처리하고, 처리할 수 없으면 또 다음 사람에게 넘긴다.

이 패턴은 요구를 하는 사람과 요구를 처리하는 사람을 느슨하게 연결한다. 만약 이 패턴을 사용하지 않으면 이 요구는 이 사람이 처리해야 한다. 라는 지식을 누군가 중앙 집중적으로 가지고 있거나 개별적으로 각각 처리해야한다. 이 지식을 요구를 하는 사람(Client)에게 맡기는 것은 현명하지 않다. 요구를 하는 사람이 처리자들의 역할 분담까지 상세하게 알아야 한다면 부품으로서의 독립성이 손상되고 자신의 일에 집중할 수 없게 되기 때문이다.

Client가 예외를 던지면 예외는 자신의 어떤 종류의 예외인지 예외 코드는 무엇인지 그리고 예외가 발생한 클래스와 메소드의 위치 등 예외와 관련한 모든 정보를 자체적으로 담고 있기 때문에 Handler들은 내가 담당할 예외인지를 판단해서 맞다면 자신이 처리하고 그렇지 않다면 다음 핸들러에 넘겨주는 방식을 취한다. 실제로 변할 가능성이 높은 - 혹은 변화의 속도가 다른 -  구체적으로 파일에 로그를 남길것인지 따라 중복된 예외들을 제외하고 에러데이타베이스를 통해 관리할 것인지 혹은 자고 있을 가엾은 개발자에게 문자를 날릴것인지는 각각의 Handler Class 들이 결정하게 된다. 그리고 컨피그 파일 등에서 아래와 같이 구성파일에서 핸들러의 종류와 순서를 지정할 수 있게 한다면 예외 처리의 변동에 유연하게 대처할 수 있다.

 <!--
  Exception Handler
 -->
 <exception-handler>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.DupValOnIndexExceptionHandler</class-name>
            <constructor>
                <constructor-param>
                    <description>contact Message</description>
                    <type>java.lang.String</type>
                    <value> Please, check ID value</value>
                </constructor-param>
            </constructor>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.InterMediaExceptionHandler</class-name>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.ReferenceExceptionHandler</class-name>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.LockExceptionHandler</class-name>
            <constructor>
                <constructor-param>
                    <description>contact Message</description>
                    <type>java.lang.String</type>
                    <value> Please, Confirm Lock Tab Of Admin's Menu</value>
                </constructor-param>
            </constructor>
        </configured-object>
        <configured-object>
            <class-name>com.bleujin.webapp.exception.LastOutExceptionHandler</class-name>
        </configured-object>
 </exception-handler>

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
checked vs runtime  (0) 2009.03.07
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/Database2009. 3. 8. 20:15

MDL을 구문을 실행하는 execUpdate와 Select를 실행하는 execQuery에 추가로 Select지만 Java의 일반 객체로 바꿔주는 execHandlerQuery가 있다.

앞서 서비스와 서비스의 교환은 중립적인 메시지 교환이라고 했는데 Rows 객체는 실질적으로 중립적인 객체라고 하기 어렵다. JIT 연결과 SQL Exception을 없에는 등 몇가지 점을 수정하긴 했지만 기본적으로 java.sql.ResultSet을 상속받았기 때문이다.

그래서 잠시 딴 생각을 해보자. ResultSet이 왜 필요한가? 클라이언트 서비스에서 DB서비스에 접근할때 response 될때 정말 필요한것은 Value 그 자체이지 특별히 ResultSet 이라는 인터페이스가 필요하지는 않다. 그리고 이러한 중립적인 Value 객체는 java.util에 많이 있다. 또는 EJB 처럼 POJO를 사용할 수도 있다.

그렇다고
bean.setName(rows.getString("name") ;
bean.setEmpNo(rows.getString("empNo") ;
bean.setSal(rows.getString("sal") ;
와 같이 반복적으로 직접 값을 셋팅하는 것은 불편하다.

개인적으로 어려운 걸 쉽게 만들기는 어렵다고 생각한다. 효율적인 SQL을 만드는 것은 어렵다. 이를 단순히 하이버네트식의 객체 인터페이스로 바꾼다고 해서 어려운 SQL을 쉽게 만들기는 아주아주 어렵다고 생각한다. 다만 복잡한걸 간결하게 만들기는 그렇게까지 어렵지 않다. 복잡하다는 것은 과정이 길다는 것을 말하고 이전의 여러 단계로 거쳐야 하는 과정을 몇단계를 줄여줄 수만 있으면 된다. 아직 이에 대한 연구가 행해진 걸 본적은 없지만 대부분의 버그는 어려운 로직보다는 반복적인 로직에서 좀더 많은 버그가 발생하지 않을까 생각한다. 인간은 기본적으로 반복적인 작업에 최적화 되어 있지 못하다. 그런 것은 컴퓨터에 맡기는 게 좀더 좋다.

ResultSet을 특정 Bean의 List로 바꾸는 것은 리플렉션을 이용하면 간단하다. 그리고 운이 좋게도 Apache Open source인 dbutils에 이미 대부분 구현되어 있어서 베껴왔다. 어느 것이나 마찬가지라고 생각하지만 기존의 사례를 살펴보는 것은 항상 도움되는 일이다.

예컨대 이런식으로 사용한다.
 public void testHandler(){
  IQueryable query = dc.createUserProcedure("employee@listBy(?)").addParam(10);
  ObjectWrapper wrapper = dc.getObjectWrapper(query, new BeanListHandler(Employee.class));
  List<Employee> list = (List<Employee>)wrapper.getObject() ;
  for(Employee e : list){
   System.out.println(e);
  }
 }



ResultSetHandle 인터페이스의 handle(ResultSet rs) 메소드 하나만 구현하면 되기 때문에 구조는 매우 간단하다. 앞에서 구조적 중복을 없에주는게 프레임워크라고했는데 여기서의 중복은 소스 증복에 가깝기 때문에 알아채는 것은 쉬운일이다.


ArrayHandler :
ResultSet의 첫번째 Row를 Object[]로 바꿔준다.

ArrayListHandler :
ResultSet의 모든 Row를 Object[]의 List로 바꿔준다.

BeanHandler :
첫번째 Row를 POJO Bean으로 바꿔준다.

BeanListHandler :
모든 Row를 POJO Bean의 List로 바꿔준다.

MapHandler :
첫번째 Row를 key(columnName), value의 Map으로 바꿔준다.

MapListHandler : 
모든 Row를 Map의 List로 바꿔준다.

LimitMapListHandler :
일부 범위의 Row만 Map의 List로 바꿔준다.

ScalarHandler :
첫번째 Row의 특정 column을 값으로 바꿔준다.

SimpleXMLStringBufferHandler :
모든 Row를 XML 형태의 StringBuffer로 바꿔준다.

CSVStoreHandler :
모든 Row를 CSV 파일 형태로 저장한다.


다시 말하자면 프레임워크는 어렵운걸 쉽게 만들어 주기 보단 복잡한걸 간결하게 만들어주는게 우선이다. 어려운걸 쉽게 만들기가 이론적으로 불가능은 아니지만 무척 어렵기 때문에 - 아이러니 하지만 쉽게 만들 수 있다는건 어려운게 아니다. 물론 혁신의 가능성을 부정하는 것은 아니지만.. - 그런것은 쉽게 도전할 성질의 것이 아니다. 복잡한걸 간결하기 만들기 위해서는 단계를 줄이고 순서를 없애야 한다.

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

Framework (DBController)  (0) 2009.03.12
Framework -커서(ForwardOnly, Sensitive(=Static))  (0) 2009.03.12
Framework (Rows)  (0) 2009.03.07
Framework (IQueryable)  (0) 2009.03.06
Framework (DBManager)  (0) 2009.03.04
Posted by bleujin
Framework/Another Lore2009. 3. 7. 20:43


AL의 필요성을 인식하기 위해서는 먼저 코드보다는 배경 사상이 먼저 이해되어야 한다. 오래전에 쓴 글이지만 원본이 없는관계로 다시 쓴다 -ㅅ-

1. 컨텐트는 타입을 가져야 한다.



2. 컨텐트는 관계를 가진다.
흔히 형사 영화에서 형사들이 여러 종류의 파일을 책상위에 짝 펼쳐놓고 담배피면서 인상을 찌뿌리는 장면을 본 적이 있을 것이다. 그때의 형사가 이마의 주름살을 지며 찾는 것은 다름아닌 널려져 있는 사건 정보들간의 관계이다. 관계는 이처럼 눈에 쉽게 보이지 않지만 매우 중요하다. RDBMS에서 관계는 속성으로 취급되는데 몇년전에 이를 좀더 확대해서 관계도 컨텐트다 라는 개념으로 접근하여 코드를 작성했었는데 수개월후 여기저기 개념상의 혼란이 많이와서 관계는 관계일뿐이고 그 자체로 의미가 있다로 수정하였다. (분석단계의 한 문장이 가지는 위력을 다시한번 체감했던 기억이다. )

3. 그룹도 컨텐트다.
보통은 게시판이라는 틀이 있고 게시글이라는 내용이 들어간다고 생각한다. 즉 틀과 내용은 구분되어 있다고 생각한다. 이제 생각을 바꿔보자. 게시판틀 컨텐트와 게시글 컨텐트는 사실 include라는 관계를 맺고 있을뿐 동일한 컨텐트라고 추상화시켜보자. 그렇게 보면 무언인가를 담는 틀 따위같은건 사실 없고 컨텐트와 컨텐트의 관계만 있을 뿐이다. 이 추상화를 통해 보면 여러 로직들이 상당히 간결해진다. 보통 게시판의 리스트와 View화면에서 댓글 리스트 모두 특정 컨텐트와 특정 관계를 가지고 있는 컨텐트의 리스트 일 뿐이다.


4. 컨텐트는 복합적이다.
3조건과 중복되는 얘기처럼 보이지만 여기서 복합적이란 한개 이상의 컨텐트가 동시에 Create되거나 Update되거나 Delete 되는  컨텐트로서 컨텐트지만 항상 다른 컨텐트의 inner컨텐트로 존재하는 컨텐트가 있다는 것을 말한다. 따라서 컨텐트가 교환될때 이를 항상 단수라고 가정해서는 안된다. 예컨데 파일이 첨부된 글의 경우 파일은 자체적인 속성 리스트를 가지고 있기 때문에 파일 자체가 하나의 컨텐트이고 글이란 컨텐트의 inner로 존재하지만 컨텐트의 교환시 항상 같이 참여하게 된다. 이러한 복합 컨텐트를 쪼개다 더이상 쪼갤수 없는 컨텐트를 Leaf 컨텐트라고 하고 Leaf 컨텐트는 속성과 속성값의 리스트를 가지고 있다.


5. 컨텐트는 참조관계수로 중요도를 측정한다.
구글의 페이지 랭크개념과 마찬가지로 컨텐트가 가지는 관계 수는 그 컨텐트의 중요도를 나타내는 지표중의 하나이다. 컨텐트는 상위 include 컨텐트를 가지고 있기 때문에 상위 컨텐트의 관계 수를 토대로 사이트의 중요도도 표시할 수 있다. (사이트도 하나의 컨텐트 이므로)


6. 관계는 타입을 가진다.
컨텐트끼리의 관계는 매우 여러개이며 이중 몇가지는 미리 지정하여 재사용이 가능하다. 예컨대 모든 컨텐트는 최상위 root 컨텐트를 제외하고 이미 존재하는 Parent 컨텐트로부터 create되는데 이때 자동적으로 parent-child 관계를 가지게 된다. 어떤 켄텐트가 다른 컨텐트를 포함하는 틀 개념이라면 이때의 관계는 include 관계라고 할 수 있다. 물론 사용자는 임의의 관계를 맺을 수 있지만 대부분의 관계는 이처럼 기 정의된 관계 타입을 사용하게 된다.


7. 관계는 동적이다.
RDBMS든 ODB든 기존에는 컨텐트간에 맺은 관계는 거의 변하지 않았다. 위키같은 프로그램이 있으니 본질적으로 이들이 정적이다라고 할 수는 없겠지만 AL에서는 기본적으로 관계는 동적이다 라고 정의한다. 어떤 타입의 컨텐트와도 관계를 맺을 수 있으며 생성시기와 상관없이 관계를 맺을 수 있다. 다시말해서 어떤 컨텐트가 생성될때 맺어진 관계뿐 아니라 이후 생존기간 내내 어느 컨텐트와도 다시 관계 설정이 가능하다는 뜻이다.


8. 컨텐트는 다중 관계를 가진다.
컨텐트가 단지 하나의 관계만을 가진다면 이를테면 이전의 게시판 컨텐트와 게시글 컨텐트는 하나의 관계만을 가진다. 그렇기 때문에 게시글은 쉽게 잊혀진다. 접근할수 있는 통로가 단 하나이기 때문이다. "Java로 구현하는 데이타베이스 아키텍쳐" 라는 책이 있다면 이는 자바 책일까? 데이타베이스 책일까? 아키텍쳐 책일까? 아니면 지하철에서 오고가며 읽을 수 잇는 책일까? 정답은 이 모든 분류에 속할 수 있다. 분류라는 것 자체가 철저히 주관적이기 때문에 하나의 분류에 종속이 되면 접근성이 극히 제한된다. 


... 아 생각이 안난다 -ㅅ-;; 몇개 더 있었는데..


간단한 Lore 창조 시나리오 코드를 보면 아래와 같다.
 public void init() throws Exception {
  AnotherLore anotherLore = new AnotherLore() ;   // 새로운 Service Lore가 창조됨
  UserSession session = anotherLore.login(new TestCredential("bleujin"), "com.bleujin.www") ;  // 사용자가 로그인
  Workspace workspace = session.getWorkspace() ;  // 작업 공간을 할당받음
  IGlobalPersistence globalPersistence = workspace.getGlobalPersistence(); // 공용 작업공간을 가져옴

  NodeType object = globalPersistence.createNodeType(NodeType.NO_SUPERTYPE, "_object", new PropertyDefinition[0]) ;  // 0개의 속성타입을 가지는 최상위 노드타입 생성
  Node root = session.createNode(Node.NO_PARENT, OBJECT_TYPE, "/") ;  // 최상위 노드 생성
  session.save() ;
 }

'Framework > Another Lore' 카테고리의 다른 글

와~~~  (0) 2009.04.09
AL - Extreme  (0) 2009.04.04
AL - Code Example  (0) 2009.03.20
AL - Abstraction & Model  (0) 2009.03.13
Framework - AL(개요)  (0) 2009.03.06
Posted by bleujin
Framework/Database2009. 3. 7. 19:27


테스트를 위해 오라클에 패키지를 만들자.
Procedure 구문을 몰라도 그냥 대충 의도만 보면 된다.

--// select 는 cursor type으로 return 하므로 사용자 cursor type을 만든다.

CREATE OR REPLACE PACKAGE SCOTT.Types
    as type cursorType is ref cursor ;
    FUNCTION dummy return number ;
END ;

CREATE OR REPLACE PACKAGE BODY SCOTT.Types
as FUNCTION dummy return Number
is
 BEGIN
   return 1 ;
 End dummy ;
END ;

--// 그냥 dummy 함수 만들어 둔다.


-- // test에 사용할 Package
CREATE OR REPLACE PACKAGE Employee
is
    FUNCTION  listBy(v_deptNo in number) return Types.cursorType ;
    FUNCTION  addDeptWith(v_deptNo in number, v_dname in varchar2, v_loc in varchar2) return number ;
    PROCEDURE addEmpBatch(v_empno in number, v_ename in varchar2, v_deptno in varchar2) ;
END Employee;


CREATE OR REPLACE PACKAGE BODY employee
is
    FUNCTION listBy(v_deptno IN Number)
    return types.cursorType is rtn_cursor types.cursorType ;
    begin
        Open rtn_cursor For
        Select empno, ename, job from emp where deptno = v_deptno ;
       
        return rtn_cursor ;
    end ;

    FUNCTION  addDeptWith(v_deptNo in number, v_dname in varchar2, v_loc in varchar2)
    return number is
    begin
        insert into dept values(v_deptno, v_dname, v_loc) ;
        return SQL%ROWCOUNT ;
    end ;
   
    PROCEDURE addEmpBatch(v_empno in number, v_ename in varchar2, v_deptno in varchar2)
    is
    begin
        insert into emp(empno, ename, deptno) values(v_empno, v_ename, v_deptno);
    end ;

end ;

employee package에는
listBy : 부서번호를 받아서 해당 부서의 사원들의 리스트
addDeptWith : 부서를 추가하는 function
addEmpBatch : 사원을 추가하는 procedure

정도만 있다고 이해하면 된다.


package test.db;

import junit.framework.TestCase;

import com.bleujin.framework.db.DBController;
import com.bleujin.framework.db.Rows;
import com.bleujin.framework.db.manager.OracleCacheDBManager;
import com.bleujin.framework.db.procedure.IQueryable;
import com.bleujin.framework.db.servant.StdOutServant;

public class TestRows extends TestCase {

  private DBController dc;

  public void testSelect() {

    IQueryable query = dc.createUserProcedure("employee@listby(?)").addParam(20);
    // IQueryable query = dc.createUserCommand("select empno, ename, job from emp where deptno = ?").addParam(20);
    Rows rs = dc.getRows(query);
    System.out.println(rs);
  }

  public void tearDown() throws Exception {
    // 원래는 시스템 종료시 단 한번 호출
    dc.getDBManager().destroyPoolConnection();
  }

  public void setUp() throws Exception {
    // 원래는 시스템 시작시 단 한번 호출
    dc = new DBController("test"new OracleCacheDBManager("jdbc:oracle:thin:@novision:1521:bleujin""scott""tiger"5)new StdOutServant(StdOutServant.All));
    dc.getDBManager().initPoolConnection();
  }
}


간단한 코드를 작성하면 위와 같다.
위의 코드에서는 junit에서 setUp()가 tearDown()은 모든 테스트 메소드 시작과 종료시 호출되므로 테스트 메소드 마다 풀을 만들었다 없앴다 해야 하지만 테스트 코드이므로 신경끄자. 

   
IQueryable query = dc.createUserProcedure("employee@listby(?)").addParam(20);
   Rows rs = dc.getRows(query);
   System.out.println(rs);


OracleCacheDBManager는 UserProcedure의 @앞의 employee는 packageName으로 @뒤의 listBy는 function 혹은 procedure로 해석한다. (앞 글에서 말한대로 이는 DBManager가 결정하는 것일뿐 위의 문장이 항상 PL/SQL을 요구하는것은 아니다.)

난 도저히 프로시저 따위는 못쓰겠다 한다면 
   IQueryable query = dc.createUserCommand("select empno, ename, job from emp where deptno = ?").addParam(20);
와 같이 UserCommand를 사용해도 되긴 한다.



여기서 말하고자 하는 객체는 리턴값인 Rows로서 이 프레임워크에서 중요한 역할을 하고 있다.

위 프로그램을 실행하면
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE RowSet PUBLIC '-//Sun Microsystems, Inc.//DTD RowSet//EN' 'http://java.sun.com/j2ee/dtds/RowSet.dtd'>

<RowSet>
  <data>
    <row>
      <EMPNO>129848</EMPNO>
      <ENAME>m7qpklg</ENAME>
      <JOB>SALESMAN</JOB>
    </row>
    ......
    <row>
      <EMPNO>139387</EMPNO>
      <ENAME>zvk6yp6</ENAME>
      <JOB>SALESMAN</JOB>
    </row>
  </data>
</RowSet>

가 나온다.


Rows는 java.sql.ResultSet을 상속받고 있으므로 기존의 JDBC 처럼 다루면 된다. java.sql.ResultSet과 주요한 차이점은 두가지가 있다.

첫째 close를 하지 않는다.(쿼리 실행시에 getConnection()도 하지 않는다.) 
java.sql.ResultSet과는 달리 Rows는 HTTP와 마찬가지로 JIT식으로 처리를 하고 connectionless 한 환경에서 동작하기 때문이다. - 이 방식의 장단점은 좀 길기 때문로 다른 글에서 언급 - 클라이언트 코드를 작성하는 쪽에서는 수백번-혹은 수천번의 연결하고 실행하고 끊고 예외처리하고 등을 해야 할 필요가 없다. 단지 사원의 리스트를 던져줘라고만 하면 충분하고 또 그래야만 한다. 나머지 과정은 너무 귀찮고 반복적이라 신경쓰고 싶지 않는 것이다.

HTTP는 규약상 connection less 한 규약이지만 실제 사용되는 HTTP 1.1은 내부적으로는 한 페이지에 포함된 글 뿐만 아니라 여러개의 그림도 같은 connection을 통해 가져온다. 좀더 빠르기 때문이다. DB Framework는 풀링 처리를 하기 때문에 그다지 차이가 나진 않지만 그러함에도 2개이상의 Query를 실행시키고 싶다면

  
public void testSelect2(){
    UserProcedures upts = dc.createUserProcedures("two query";
    IQueryable query1 = dc.createUserProcedure("employee@listby(?)").addParam(20);
    IQueryable query2 = dc.createUserCommand("select * from dept where deptno = ?").addParam(10);
    
    upts.add(query1).add(query2;
    Rows rs1 = dc.getRows(upts);
    Rows rs2 = rs1.getNextRows();
  }
와 같이 여러개의 Query를 UserProcedures 객체에 담아서 실행시키면 Rows는 chain 식으로 연결되어 리턴한다.



둘째 SQL실행시에 SQLException을 던지지 않는다.

이전 글인 예외처리의 예외에서 언급한바와 같이 실험실의 바깥에서는 현실적으로 대부분의 SQLException에 대해 특별한 처리를 할게 없다. 그래서 실제로는 runtime exception은 RepositoryException을 던진다. DB가 다운됐거나 SQL오류등으로 혹은 해당 컬럼명이 없어서 SQLException이 발생했을경우 에러 페이지를 보여주고 로그에 기록하는 것 말고 대체 무엇을 할수 있을까? 다른 무언가를 해야 한다고 하더래도 아마도 그 상대적인 확률은 아주 낮을 것이다.

여기서 중요한건 클라이언트 코드 작성자가 선택권을 가지는 것이다. 99%의 A와 1%의 B가 있다면 A를 기본값으로 하고 B도 필요하다면 할수 있는 방법이 상식적이지 않을까? 그리고 만약 반대로 1%의 A와 99%의 B 환경이 있다면 그에 대한 대책도 준비해 줘야 한다. checked exception을 처리할 확률이 작다면 그냥 runtime exception을 catch 하는게 좋지만 오히려 checked exception으로 해야할 경우가 더 많다면 Facade인 DBController을 재정의 하면 된다.

그러나 Facade인 DBController가 쿼리 실행시 SQLException을 감춰버릴 수 있었지만 리턴값의 Rows의 상위 인터페이스인 java.sql.ResultSet은 모든 메소드가 SQLException을 던지도록 되어 있고  이 규약을 깨는것은 혼란을 가져올수 있으므로 좋지 못하다. 다른 어떤 방법이 있을까?

여기서는 다시 두가지 대안을 제시한다
하나는 역시 ResultSet을 상속받지만 모든 메소드에 SQLException을 반환하지 않는  new NRows(Rows rows, RuntimeException ex) 의 컴포지션 객체를 제공하는 방식이고
두번째는 handler를 통해 java의 일반 객체로 바꾸는 방식이다. (이는 다음에 설명)
   


이쯤 되면 슬슬 프레임워크가 기본적으로 갖춰야 할 미덕이 보인다.
첫째 간결한 API가 좋다. 만약 1-2-3 단계를 거쳐서 무언가를 해야한다면 한단계로 줄이는 걸 고민해라.
둘째 빈도의 차이가 발생한다면 확률이 높은 쪽을 기본값으로 한다.
셋째 그래도 클라이언트는 항상 선택할 수 있어야 하고 때에 따라서는 확률이 바뀌는 환경도 고려해야 한다.

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

Framework -커서(ForwardOnly, Sensitive(=Static))  (0) 2009.03.12
Framework (Handler)  (0) 2009.03.08
Framework (IQueryable)  (0) 2009.03.06
Framework (DBManager)  (0) 2009.03.04
Framework (구조적 중복 제거)  (0) 2009.02.21
Posted by bleujin



클래스와 멤버에 대한 접근은 최소화하라

정보은폐는 시스템을 구성하는 모듈 사이의 의존성을 줄여, 각 모듈을 따로 개발하고, 테스트 하고, 최적화하고, 쓰고, 이해하고, 수정할 수 있게 해주기 때문에 매우 중요한 개념이다.


private
package-private
protected
public
[published] - SRP


public 클래스는 public 인스턴스 변수를 가지지 않아야 한다. (쓰레드에 대해 안전하지 않다.) 접근성은 최대한 줄여야 한다. 아주 조심스럽게 최소의 public API를 설계한 다음, 쓸데없는 클래스, 인터페이스, 멤버가 API의 일부가 되지 않도록 해야 한다. public static final 필드가 아니라면 클래스에 public 필드가 있어선 안된다. 또, public static final 필드라 해도 이 필드는 기본타입이거나 불변 객체만 참조해야 한다.

public static final String[] VALUES = {"1", "2", "3"} ;
이를테면 위와 같은 코드는 실제로 final 하지 않다. 배열은 레퍼런스 참조이므로 실제 값이 변동되는걸 막을 수는 없다.

private static final String[] PRIVATE_VALUE = {"1", "2", "3"} ;
public static final List VALUES = Collections.unmodifiableList(Arrays.asList(PRIVATE_VALUE)) ;
만약 정말로 final 하게 유지하려면 위와 같이 unmodifiableList 를 사용해야 한다.


상속보다 컴포지션을 써라

상속은 비록 강력한 기능이긴 하지만 캡슐화를 위반하기 때문에 문제를 발생시킬 수 있다. 상위클래스와 하위 클래스가 정말 서브타입 관계가 있을 때만 상속을 쓸 수 있다. 하지만 이런 경우라도 하위 클래스와 상위 클래스가 다른 패키지에 있거나 상위 클래스가 상속을 위해 설계되지 않았다면 상속받은 하위 클래스에 문제가 생기기 쉽다. 컴포지션과 포워딩을 쓰면 상속의 폐해를 막을 수 있다. 특히 적절한 인터페이스가 있어서 Wrapper 클래스를 구현할 수 있다면 금상첨화이다.

Case1)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;

public class InstrumentHashSet extends HashSet {
  private int addCount = 0;

  public InstrumentHashSet() {
  }

  public InstrumentHashSet(Collection c) {
    super(c);
  }

  public InstrumentHashSet(int initCap, float loadFactor) {
    super(initCap, loadFactor);
  }

  public boolean add(Object o) {
    addCount++;
    return super.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return super.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }
  
  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet() ;
    s.addAll(Arrays.asList(new String[]{"A""B""C"})) ;
    System.out.println(s.getAddCount()) ;
  }
}

위의 main함수를 실행시키면 기대와는 달리 3이 아니라 6이 나온다. 왜냐하면 Hashset의 addAll은 Collection의 수만큼 반복하여 add함수를 실행시키기 때문에 count가 2씩 올라가기 때문이다. 이렇게 인터페이스가 아닌 컨크리트 클래스의 상속을 사용하면 슈퍼클래스의 구현에 대해 알고 있지 않으면 이처럼 실수를 하기 쉽다.


Case2) Wrapper Class(Decorator Pattern)

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class InstrumentHashSet implements Set {

  private final Set s;
  private int addCount = 0;

  public InstrumentHashSet() {
    s = new HashSet();
  }

  public InstrumentHashSet(Set s) {
    this.s = s;
  }

  public boolean add(Object o) {
    addCount++;
    return s.add(o);
  }

  public boolean addAll(Collection c) {
    addCount += c.size();
    return s.addAll(c);
  }

  public int getAddCount() {
    return addCount;
  }

  // foward method
  public void clear() {
    s.clear();
  }
  
  ......
  

  public static void main(String[] args) {
    InstrumentHashSet s = new InstrumentHashSet();
    s.addAll(Arrays.asList(new String[] { "A""B""C" }));
    System.out.println(s.getAddCount());
  }
}

좀더 좋은 구현은 위와 같다. Case2)는 인터페이스를 구현해서 자신이 구현할 메소드는 직접 구현하고 나머지 메소드는 컴포짓을 사용해서 위임을 사용한다.

정말 모든 B가 A인가?는 LSP 이야기에서 나오겠지만 지키기기 생각만큼 쉽지는 않다. Java API에도 이런 실수가 보이는데 이를테면 Properties는 Hashtable을 상속받고 있다. Properties의 경우 String만 key와 value로 받아들인다는 불변규칙이 있지만 상위 클래스인 hashtable에는 이런 조건이 없다.

이럴경우....
  Hashtable hs = new Properties();
  hs.put(new Integer(1), Calendar.getInstance()) ;

와 같은 코드 작성시 혼란이 올수 있다. 이때  hs는 Properties 임에도 불구하고 key와 value로 Object를 받아버린다.


컨크리트 클래스보다는 추상클래스를, 추상클래스보다는 인터페이스를 써라

이미 존재하는 클래스가 새로운 인터페이스를 구현하도록 고치는 것은 어려운 일이 아니다. 하지만 이미 존재하는 클래스가 새로운 추상 클래스를 상속받도록 고치는 것은 이미 배포된 프레임워크에서는 거의 불가능한 일이다. Sun도 Properties가 Hashtable을 상속받는건 잘못이라는 걸 알았지만 이미 고칠수가 없다.

다양한 구현이 가능한 타입을 정의할때 인터페이스를 쓰는 것이 가장 좋다. 단, 쉽게 기능을 추가할 수 잇는 것이 유연성과 강력함보다 더 중요한 경우에는 추상 클래스를 쓰는 것이 좋다. 하지만 그 한계를 이해하고 수용할 수 있을때만 추상 클래스를 사용해야 한다. 외부에 제공하는 중요한 인터페이스에 대한 기본 뼈대 구현을 제공하는 것이 좋다. public 인터페이스는 신중하게 설계해야 하고, 다양하게 구현을 통한 철저한 시험을 거친후 발표해야 한다. 인터페이스와 추상 클래스의 장점만 결합해서, 외부에 제공하는 중요한 인터페이스들에 대해 기본 뼈대 구현(Skeletal implementation_을 해놓은 추상 클래스를 제공할 수 있다.(AbstractCollection, AbsgtractSet, AbstractList....)

여기서 본질적인 질문을 해보자.
추상클래스와 인터페이스는 무슨 차이가 있는가 ? 문법상으로 추상 메소드만 있다면 인터페이스와 비슷한 역할을 할 수 있다. 인터페이스를 쓰면 Mixin 타입을 정의할 수 있고 계층 구조가 없는 타입 프레임 워크를 만들 수 있지만 그런 차이를 넘어서 본질적인 차이는 무엇일까? 인터페이스를 써야할까 추상 클래스를 써야 할까 를 고민할때 선택하는 기준은 무엇인가? 극히 개인적인 의견이긴 하지만 클래스 이름이 명사는 추상클래스를 동사와 형용사는 인터페이스를 사용한다. 명사는 보통 속성을 가지고 있고 실체가 있는 경우가 보통이지만 동사와 형용사는 보통 실체가 없는 동작이나 관점이기 때문에 변할 소지가 다분하다.


인터페이스 타입으로 객체를 참조하고 리턴해라

// better
public List newSubscriber(){
    return new Vector() ;
}

// bad
public Vector newSubscriber(){
    return new Vector() ;
}

적절한 인터페이스가 없다면, 인터페이스 타입이 아닌 클래스 타입으로 객체를 참조해도 괜찮다. 예를 들어 String이나 BigInteger와 같은 값 클래스가 다양한 구현체를 가진다는 것은 상상하기 어렵다. 이 클래스들은 대부분 final이고, 해당하는 인터페이스를 가진 경우는 겨의 없다. 따라서 값 클래스는 인자, 변수, 리턴타입으로 쓸수 있다.

어떤 프레임워크의 기반타입이 인터페이스가 아니라 클래스라면 클래스 타입으로 객체를 선언할 수 밖에 없을 것이다. 하지만 이런 클래스 기반 프레임워크에 속하는 객체라도 구체적인 클래스 타입이 아닌 추상 클래스인 기반 클래스 타입으로 참조하는 것이 좋다. 


클래스의 상태를 요약하는 toString()을 작성해주면 편하다.

public String toString(){
   return "a:" + a + ", b:" + b + ", c:" + c;
}
대부분의 JVM은 최적화 작업을 해주기 때문에 위의 String 연산은 보이는만큼 많이 일어나지 않는다.

# Bad
public String toString(){
   String s = "a:" + a ;
             s += ", b:" + b ;
             s += ", c:" + c ;
   return s ;
}
그러나 위 코드는 JVM이 최적화 작업을 해주기 사실상 어렵다.



 

'Framework > 아키텍쳐 일반' 카테고리의 다른 글

아키텍쳐 패턴 - Pipes and Filter 패턴  (0) 2009.03.11
아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
나쁜 디자인의 징후  (0) 2009.02.22
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
Posted by bleujin
Database 일반2009. 3. 7. 12:56


역시 마찬가지 교육 자료.

# 오라클 Hint 구문의 사용

힌트 구문은 SQL문장(가령 SELECT, INSERT, DELETE, UPDATE)의 첫단어 이후에 /*+ hint */와 같이 나타낸다. 주석과 다른점은 /* 뒤에 스페이스 없이 '+' 기호가 있다는 점이다.

select /*+ rule*/ * from emp where sal > 3000

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=HINT: RULE
   1    0   TABLE ACCESS (FULL) OF 'EMP'


주의할점 )
1. Hint는 대소문자를 구분하지 않는다. 아래의 구문은 모두 유효하다.

Select /*+ FULL(dept) */ * from DEPT where deptno > 10 ;
Select /*+ full(dept) */ * from DEPT where deptno > 10 ;
Select /*+ FULL(DEPT) */ * from dept where deptno > 10 ;
Select /*+ Full(Dept) */ * from DEPT where deptno > 10 ;


2. Table Alias를 사용했을 경우 힌트 구문이 테이블 명을 인수로 취해야 한다면, 반드시 인수부분에 알리어스 명을 적어주어야 한다.
Select /*+ full(Dept)*/*  from dept d where deptno > 10               ................. X

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=4 Bytes=120)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'DEPT' (Cost=1 Card=4 Bytes=120)
   2    1     INDEX (RANGE SCAN) OF 'PK_DEPT' (UNIQUE) (Cost=2 Card=1)

3. 여러개의 힌트가 한 SQL 문장에 동시에 쓰일수 잇으며 이때 공백문자로 구분해 주도록 한다. (','가 아니다. ',' 이후의 힌트 문장은 무시된다.)

Select /*+ full(d) full(e)*/ * from dept d, emp e where e.deptno = d.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   HASH JOIN (Cost=5 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)
   3    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)

힌트 구문간이 아니라 인자를 취하는 힌트구문에서 인자의 구분값으로 ,를 사용할 수는 있으나 가능한 사용하지 말기를 권한다.
select /*+ index(e pk_emp) */ * from emp e
select /*+ index(e, pk_emp) */ * from emp e

위 두문장 모두 유효하다.


4. Schema가 명시되었거나 링크에서의 힌트는 Alias를 사용한다.

Select /*+ full(e) */ from scott.emp e where empno = 7839 ;
Select /*+ full(e) */ from emp@remote e where empno = 7839 ;

# Optimizer

 - rule, first rows, all rows, choose
10i 이후 부터는 의미 없다.

# Join Order
- ordered

- leading

Leading 힌트 구문은 테이블 조인시에 첫번째 테이블(드라이빙 테이블)로 사용될 테이블 명이나 알리아스를 인수로 취한다.

select /*+ leading(d) use_hash(e)*/ * from dept d, emp e where e.deptno = d.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   HASH JOIN (Cost=5 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)
   3    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)


# Join Operation
- use_nl(table..)
보통 use_nl 구무은 ordered 힌트구문과 보통 같이 사용된다. use_nl에 인수로 취하는 테이블 명은 from 절에서 두번째로 나오는, 즉 테이블 명(inner table)을 명시해 주어야 한다.

CASE 1) select /*+ ordered use_nl(e)*/* from dept d, emp e where d.deptno = e.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=166 Card=82 Bytes=9K)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=2 Card=1 Bytes=87)
   2    1     NESTED LOOPS (Cost=166 Card=82 Bytes=9K)
   3    2       TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)
   4    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE) (Cost=1 Card=1)

CASE 2) select /*+ ordered use_nl(d)*/* from emp e, dept d where d.deptno = e.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=84 Card=82 Bytes=9K)
   1    0   NESTED LOOPS (Cost=84 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (BY INDEX ROWID) OF 'DEPT' (Cost=1 Card=1 Bytes=30)
   4    3       INDEX (UNIQUE SCAN) OF 'PK_DEPT' (UNIQUE) (Card=1)

- use_merge(table)
성능 측면에서 보면 머지 조인에서는 from절에 나오는 테이블 순서가 NESTED LOOP만큼 중요하지 않다. 즉 SM에서는 드라이빙 테이블의 의미가 없다.

select /*+ ordered use_merge(d)*/* from emp e, dept d  where d.deptno = e.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=8 Card=82 Bytes=9K)
   1    0   MERGE JOIN (Cost=8 Card=82 Bytes=9K)
   2    1     SORT (JOIN) (Cost=5 Card=82 Bytes=7K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   4    1     SORT (JOIN) (Cost=4 Card=82 Bytes=2K)
   5    4       TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)

- use_hash(table)
테이블중 하나를 기반으로 해시 테이블을 메모리에 생성한 후에, 나머지 테이블을 스캔하면서 해싱 함수를 적용하여, 메모리에 로드된 해치 테이블과 비교하면서 매칭이 되는 걸 뽑아내는 조인이다. 해시 조인이 제 성능을 내기 위해서는 사이즈가 작은 테이블이 메모리에 로드되는게 좋다. 즉 사이즈가 작은 테이블이 드라이빙 테이블이 되어야 한다.

특히 해시 테이블이 메모리내에 생성되면(그렇지 않으면 내부적으로 임시테이블을 만든다) 성능이 좋다. 그리고 두 테이블의 크기가 확연히 다를수록 성능이 좋아진다. 해시 조인은 또한 안티조인과 병렬 처리 기능과도 궁합이 잘 맞는다. 그리고 범위 조건이 아닌 동등비교 조인에 쓰인다.

select /*+ ordered use_hash(큰 테이블)*/ .. from 작은 테이블, 큰 테이블 where ...
select /*+ ordered use_hash(e) */ d.deptno, e.empno, e.job from dept d, emp e where e.deptno = d.deptno

--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=4K)
   1    0   HASH JOIN (Cost=5 Card=82 Bytes=4K)
   2    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=1K)
   3    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=3K)

- hash_aj
not in 쿼리에 사용되어, 안티 조인을 해시 조인을 이용해서 풀리도록 하는 힌트 구문이다. 힌트구문의 명시는 아래와 같이 서브 쿼리에 명시해 주어야 한다. 또한 조인 조건의 컬럼에 NOT NULL 조건도 명시해 주어야 한다. NOT NULL 조건이 명시되지 않으면 HASH_AJ 힌트 구문은 무시된다.

Select * From emp Where empno is not null and ename is not null
   and (empno, ename) not in (select /*+ hash_aj*/ empno, ename from emp_sub_20 where empno is not null and ename is not null)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=4 Bytes=428)
   1    0   HASH JOIN (ANTI) (Cost=5 Card=4 Bytes=428)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=4 Bytes=348)
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=1 Bytes=20)

참고로 위의 코리는 다음과 같이 NOT EXIST를 이용하여 풀수도 있다.

Select * From emp e Where not exists (select /*+ hash_aj*/ 1 from emp_sub_20 where empno = e.empno and ename = e.ename)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=6 Card=4 Bytes=348)
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=4 Bytes=348)
   3    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP_SUB_20' (Cost=1 Card=1 Bytes=20)
   4    3       INDEX (RANGE SCAN) OF 'EMP_SUB_EMPNO' (NON-UNIQUE) (Cost=1 Card=1)

그러면 not in과 not exist 중에 각각 어느 상황에 사용하는 것이 더 효율적일까 ? 일단 inner table(서브쿼리에 존재하는 테이블)에 인덱스가 없을때는 hash_aj를 이용한 not in 구문이 효과적이다 inner table에 인덱스가 존재하지만, outer table의 로우수가 많을때는 hash_aj를 이용한 not in이 더 효과적이다.  inner table에 인덱스가 존재하고 아우터 테이블의 수가 적고, inner table의 로우수가 많을때는 not exists가 더 효과적이다. 기본적인 원칙일뿐 상황에 따라 조금 다르다.

- hash_sj
주로 서브 쿼리의 컬럼상에 인덱스가 존재하지 않는 Correlated Exists 쿼리에 사용되는 힌트 구문이다.

select count(*) from emp e where exists(select /*+ Hash_SJ */ 1 from dept d where e.deptno = d.deptno)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=1 Bytes=26)
   1    0   SORT (AGGREGATE) (Card=1 Bytes=26)
   2    1     HASH JOIN (SEMI) (Cost=5 Card=82 Bytes=2K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=1K)
   4    2       TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=1K)

- merge_aj
Select * From emp Where empno is not null and ename is not null
   and (empno, ename) not in (select /*+ merge_aj*/ empno, ename from emp_sub_20 where empno is not null and ename is not null)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=4 Bytes=428)
   1    0   MERGE JOIN (ANTI) (Cost=7 Card=4 Bytes=428)
   2    1     SORT (JOIN) (Cost=4 Card=4 Bytes=348)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=4 Bytes=348)
   4    1     SORT (UNIQUE) (Cost=4 Card=1 Bytes=20)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=1 Bytes=20)

- merge_sj
select count(*) from emp e where exists(select /*+ merge_sj*/ 1 from dept d where e.deptno = d.deptno)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=1 Bytes=26)
   1    0   SORT (AGGREGATE) (Card=1 Bytes=26)
   2    1     MERGE JOIN (SEMI) (Cost=7 Card=82 Bytes=2K)
   3    2       SORT (JOIN) (Cost=4 Card=82 Bytes=1K)
   4    3         TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=1K)
   5    2       SORT (UNIQUE) (Cost=4 Card=82 Bytes=1K)
   6    5         TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=1K)

#  병렬처리
- parallel(table [degree [instance]])
- noparallel(table)
- append
insert 문장에 대해 append 힌트 구문을 사용하게 되면 insert 되는 테이블에 존재하는 블럭들의 기존의 빈 공간들은 사용되지 않고, 데이타들이 Append 되게 된다. 모든 insert 구문에 사용할 수 있는 힌트 구문이 아니라, 아래와 같이 'insert .. select.. ' 패턴의 문장에서만 사용할 수 있는 힌트 구문이다. 이 힌트 구문이 사용되었을 때는, 새로이 입력되는 데이타들은 데이터 버퍼 캐쉬를 거치지 않고 바로 삽입되게 된다.

alter table emp nologging ;
insert /*+ append*/ into emp select * from emp_dummy ;

nologging 모드의 테이블에 append 모드 상태로 데이터를 insert 하게 되면, 그 작업은 리두 로그에 기록이 되지 않으므로 작업시간의 단축을 가져올수 있다. 사실상 insert 시 nologging의 효과를 보려면 반드시 /*+ append*/ 힌트 구문을 사용해야 한다. 하지만 복구시에 문제가 발생할 수 있는 여지가 있으므로 크리티컬한 테이블에 대해 nologging모드에서 append로 데이터를 삽입했다면 일단 logging로 전환한후 바로 데이타베이스 핫백업을 받아두는 것이 좋다.

alter table 테이블 명 logging ;

이와 비슷한 방식으로 sqlload x/s control=xx direct=true 도 해당된다.

- noappend
- parallel_index(table [,index [,degree [,instance]]])

#기타

- cache

보통 풀스캔되어 SGA의 DB의 버퍼 캐시 영역에 올라간 데이타 블럭들은 사용후에 바로 사라지게끔 알고리즘화 되어 있다. 하지만 CACHE 힌트를 쓰게되면 그 풀스캔된 데이터들을 SGA의 버퍼캐시 영역에 보다 오래토록 보존해 준다.
그러므로 이러한 힌트 구문은 테이블 사이즈가 작으면서 자주 억세스 되는 테이블에 사용하면 유용하다.

select /*+ Full(dept) cache(dept) */ * from dept where deptno > 0

그러나 실행계획만으로 Cache 여부를 알수는 없다.

- nocache

Cache의 반대 구문으로 보통의 경우 NoCache 특성은 테이블 풀스캔시의 디폴트 속성이므로 굳이 이러한 구문을 명시할 필요는 없다.

- merge(viewname)
뷰를 나머지 쿼리와 융합시켜주는 역할을 해주는 힌트 구문이다. Oracle 8i에는 버그가 있어 제대로 작동하지 않는다. (히든 파라미터인 _COMPLEX_VIEW_MERGING=TRUE로 설정되면 가능) Oracle 9i부터 COMPLEX_VIEW_MERGING=TRUE로 세팅되면 View Merge가 가능하다.

CASE 1)
select /*+ merge(v)*/ * from emp e, (select deptno, dname from dept where loc = 'CHICAGO') v
where e.deptno = v.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=1 Bytes=117)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=2 Card=1 Bytes=87)
   2    1     NESTED LOOPS (Cost=4 Card=1 Bytes=117)
   3    2       TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=1 Bytes=30)
   4    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE) (Cost=1 Card=1)

CASE 1-1)
select * from emp e, (select rownum, deptno, dname from dept where loc = 'CHICAGO') v
where e.deptno = v.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     NESTED LOOPS
   3    2       VIEW
   4    3         COUNT
   5    4           TABLE ACCESS (FULL) OF 'DEPT'
   6    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

CASE 2)
select * from emp e,  (select deptno, avg(sal) avg_sal from emp e group by deptno) v
where e.deptno = v.deptno and e.sal > v.avg_sal

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     NESTED LOOPS
   3    2       VIEW
   4    3         SORT (GROUP BY)
   5    4           TABLE ACCESS (FULL) OF 'EMP'
   6    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

CASE 3)
select /*+ merge(v)*/ * from emp e,  (select deptno, avg(sal) avg_sal from emp e group by deptno) v
where e.deptno = v.deptno and e.sal > v.avg_sal

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=7 Card=82 Bytes=10K)
   1    0   FILTER
   2    1     SORT (GROUP BY) (Cost=7 Card=82 Bytes=10K)
   3    2       HASH JOIN (Cost=5 Card=82 Bytes=10K)
   4    3         TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=2K)
   5    3         TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=8K)

- no_merge(viewname)

CASE 1-2)
select /*+ no_merge(v)*/ * from emp e,  (select deptno, dname from dept where loc = 'CHICAGO') v
where e.deptno = v.deptno

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=4 Card=1 Bytes=109)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=2 Card=1 Bytes=87)
   2    1     NESTED LOOPS (Cost=4 Card=1 Bytes=109)
   3    2       VIEW (Cost=2 Card=1 Bytes=22)
   4    3         TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=1 Bytes=30)
   5    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE) (Cost=1 Card=1)

- push_pred(viewname)
ver 8i에서는 버그로 인해 제대로 작동되지 않았으며, ver 9i부터 적용 가능하다. 테이블이 뷰와 Outer Join을 할 경우에 적용할 수 잇는 힌트 구문이다. 이 경우 보통 뷰안의 쿼리가 독립적으로 실행이 된후에 조인이 일어나기 마련이지만 PUSH_PRED 힌트 구문을 사용하면 뷰안에 조인 조건이 녹아 들어가 영향을 주게 된다.

CASE 1)
Select * From emp_sub_20 s, (Select e.empno, d.dname, e.ename from emp e, dept d Where e.deptno = d.deptno) v
Where s.empno = v.empno(+)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   MERGE JOIN (OUTER)
   2    1     SORT (JOIN)
   3    2       TABLE ACCESS (FULL) OF 'EMP_SUB_20'
   4    1     SORT (JOIN)
   5    4       VIEW
   6    5         TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   7    6           NESTED LOOPS
   8    7             TABLE ACCESS (FULL) OF 'DEPT'
   9    7             INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

CASE 2)
Select /*+ PUSH_PRED(v)*/* From emp_sub_20 s,  (Select e.empno, d.dname, e.ename from emp e, dept d Where e.deptno = d.deptno) v Where s.empno = v.empno(+)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=84 Card=82 Bytes=4K)
   1    0   NESTED LOOPS (OUTER) (Cost=84 Card=82 Bytes=4K)
   2    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=82 Bytes=2K)
   3    1     VIEW PUSHED PREDICATE (Card=1 Bytes=29)
   4    3       NESTED LOOPS (Cost=2 Card=1 Bytes=55)
   5    4         TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=1 Card=1 Bytes=33)
   6    5           INDEX (RANGE SCAN) OF 'PK_EMP' (UNIQUE) (Cost=1 Card=1)
   7    4         TABLE ACCESS (BY INDEX ROWID) OF 'DEPT' (Cost=1 Card=1 Bytes=22)
   8    7           INDEX (UNIQUE SCAN) OF 'PK_DEPT' (UNIQUE) (Card=1)

- no_push_pred

- push_subq

보통 correlated subquery까지 포함되어 있는 테이블 조인에서는, 조인이 먼저 일어난 후에 subquery가 마지막에 실행되기 마련이다. 서브쿼리의 실행 비용이 고비용이고, 반환하는 로우도 상당하다면, 서브 쿼리를 먼저 실행시키는 것이 성능에 도움이 될 것이다.

CASE 1)
Select * from emp e, emp_sub_20 s Where e.empno = s.empno and exists(select 1 from dept d where deptno = s.deptno)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     NESTED LOOPS
   3    2       TABLE ACCESS (FULL) OF 'EMP_SUB_20'
   4    2       TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   5    4         INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)
   6    1     INDEX (UNIQUE SCAN) OF 'PK_DEPT' (UNIQUE)

CASE 2)
Select /*+ push_subq */* from emp e, emp_sub_20 s Where e.empno = s.empno
    and exists(select 1 from dept d where deptno = s.deptno)

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=84 Card=88K Bytes=11M)
   1    0   NESTED LOOPS (Cost=84 Card=88K Bytes=11M)
   2    1     NESTED LOOPS (SEMI) (Cost=2 Card=82 Bytes=4K)
   3    2       TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=82 Bytes=3K)
   4    2       INDEX (UNIQUE SCAN) OF 'PK_DEPT' (UNIQUE) (Card=82 Bytes=1K)
   5    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=1 Card=88K Bytes=7M)
   6    5       INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE) (Card=1)

- no_push_subq
- star
- star_transformation
- driving_site




 


 

'Database 일반' 카테고리의 다른 글

Database Quiz - sub query  (0) 2009.03.12
Database - Plan 이해  (0) 2009.03.07
Database Quiz - count  (0) 2009.02.13
Database Quiz - 함수의 활용  (0) 2009.02.07
Database Quiz - 실행계획 유도  (0) 2009.02.07
Posted by bleujin
Database 일반2009. 3. 7. 01:30

예전에 교육 목적으로 작성한 자료..

실행계획의 해석 순서

1. 실행계획상에서 가장 안쪽으로 들여쓰기된 문장이 가장 먼저 수행된다.

2. 각 문장은 들여쓰기가 한단계 덜한 부모 문장에 종속된다.

3. 안쪽으로 동일하게 들여 쓴 문장이 여럿 있을때는 가장 위쪽의 문장이 먼저 수행된다. (단 예외적으로 인덱스를 경유하여 테이블 억세스하는 두개의 실행계획 문장은 하나로 취급한다. 이 경우는 가장 안쪽으로 들여쓰기된 문장이 먼저 수행된다는 룰을 따르지 않는다.)


4. NestedLoop인 경우 NESTED LOOPS 바로 밑에 나온 문장이 드라이빙 테이블(DRIVING | OTER TABLE)에 대한 억세스를 나타내며, 그 아래 문장은 비드라이빙 테이블(PROBED | INNER TABLE) 테이블의 억세스를 의미한다.


Case1 )
Execution Plan
--------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   NESTED LOOPS
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     TABLE ACCESS (BY INDEX ROWID) OF 'DEPT'
   4    3       INDEX (UNIQUE SCAN) OF 'PK_DEPT' (UNIQUE)


위 예제의 경우 EMP의 각 로우마다 추출된 DEPTNO를 이용해서 PK_DEPT 인덱스를 억세스 한후에 인덱스에서 얻어진 ROWID를 근거로 DEPT 테이블을 억세스하여 원하는 로우를 추출하는 실행계획을 보여주고 있다.

Nested Loop는 (Filter도 동일) 아래의 문장(Table Access By Index RowId of Dept)이 위 문장의 Row수 만큼 반복이 된다. 즉 3-4번은 2번의 행수만큼 반복 실행이 되었다는 뜻이다.


Case 2)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   HASH JOIN (Cost=5 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)
   3    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)

그에 반해 위 Hash Join의 실행계획은 2번과 3번은 한번씩 실행이 되었다는 뜻이다.



# TABLE

TABLE ACCESS BY ROWID
조건으로 주어진 컬럼의 인덱스에 존재하는 ROWID를 값을 근거로 테이블을 억세스하여 해당로우를 추출해내는 로우 연산. ex) select * from emp e where empno = 7369
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)


TABLE ACCESS FULL
ex)  select * from emp
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (FULL) OF 'EMP'




# INDEX

INDEX UNIQUE SCAN
ex) select * from emp e where empno = 7876
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)


INDEX RANGE SCAN [DESCENDING]
where절의 인덱스 구성 컬럼이 <, >, Between, Like 혹은 복합인덱스의 선두컬럼중의 일부만 사용되었을때 나타난다.  기본적으로 index scan은 single block io를 한다.

ex) select * from emp where deptno >= 20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)


보통의 경우 인덱스 생성시 자동으로 그 컬림이 오름차순으로 정렬된 형태의 인덱스가 생성된다. 그러므로 인덱스를 경유하게 되면, 그 컬럼 값들은 자동으로 오름차순 정렬 형태로 추출되게 된다. 그러나 INDEX_FFS와 같은 힌트구문을 이용하여 INDEX FULL SCAN을 행해서 얻어진 결과 셋의 경우에는 Multi Index Block Scan을 할수 있으므로 사용 인덱스 컬럼을 기준으로 볼때 정렬되지 않은 결과를 보여줄수도 있다.

ex) select /*+ index_desc(e emp_deptno_idx)*/* from emp e where deptno >= 20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=1 Card=4 Bytes=348)
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=1 Card=4 Bytes=348)
   2    1     INDEX (RANGE SCAN DESCENDING) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE) (Cost=2 Card=1)


INLIST ITERATOR

ex) select /*+ all_rows*/* from emp e where empno in (7876, 7900, 7902)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=HINT: ALL_ROWS (Cost=1 Card=1 Bytes=87)
   1    0   INLIST ITERATOR
   2    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP' (Cost=1 Card=1 Bytes=87)
   3    2       INDEX (RANGE SCAN) OF 'PK_EMP' (UNIQUE) (Cost=2 Card=1)

ex) select * from emp e where empno in (7876, 7900, 7902)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   CONCATENATION
   2    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   3    2       INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)
   4    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   5    4       INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)
   6    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   7    6       INDEX (UNIQUE SCAN) OF 'PK_EMP' (UNIQUE)

AND-EQUAL

ex) select * from emp x1 where job = 'CLERK' and deptno = 20
Execution Plan
--------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     AND-EQUAL
   3    2       INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE)
   4    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)





# JOIN

NESTED LOOPS
조인되는 컬럼중 적어도 하나의 인덱스가 존재하며 그 인덱스를 사용할때 나타나는 로우 연산이다. 실행계획상에서 상위에 위치하는 테이블을 드라이빙 테이블이라 하고, 하위에 위치한 테이블을 비드라이빙 테이블이라고 한다.

ex) select * from emp x1, dept x2 where x1.deptno = x2.deptno
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     NESTED LOOPS
   3    2       TABLE ACCESS (FULL) OF 'DEPT'
   4    2       INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

만약 Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=166 Card=82 Bytes=9K)
   1    0   NESTED LOOPS (Cost=166 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=1 Bytes=30)
위와 같은 실행계획이 보여진다면 성능측면에서 좋지 못할 것이다. 보통 이러한 실행계획은 힌트 구문을 사용하여 억지로 만들지 않는한 오라클에 의해서 만들어 지지는 않을 것이다.(두테이블이 아주 작으면 만들어질수도 있다.) 이 경우 오라클은 자동적으로 성능을 생각해서 MERGE나 HASH가 발생하는 실행계획을 세운다.

두 테이블 모두 조인컬럼에 인덱스가 있고 모두 사용 가능할 경우 Rule Base Optimizer는 뒤에 테이블을 드라이빙 테이블로 선정한다. 물론 한쪽에만 인덱스가 존재할 경우 인덱스가 존재하는 쪽이 드라이빙이 된다. Cost Base에서는 특별히 힌트를 사용하지 않는 이상 통계정보에 의해 드라이빙 테이블이 결정된다.

MERGE JOIN
ex) select *  from emp e, dept d where d.deptno +0 = e.deptno +0
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   MERGE JOIN
   2    1     SORT (JOIN)
   3    2       TABLE ACCESS (FULL) OF 'DEPT'
   4    1     SORT (JOIN)
   5    4       TABLE ACCESS (FULL) OF 'EMP'


HASH JOIN
ex) select /*+ use_hash(x1 x2) */ * from dept x1, emp x2 where x1.deptno = x2.deptno
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   HASH JOIN (Cost=5 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)
   3    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)

상위에 위치한 DEPT 테이블이 메모리에 로드되게 되고, 오라클은 해싱 함수를 이용해서 EMP 테이블의 로우들을 메모리에 로드된 값들과 비교하여 원하는 값을 추출한다.

[HASH | MERGE] ANTI JOIN

anti join의 예) not in / not exists / minus

HASH ANTI-JOIN
ex) create table emp_sub_20 as select empno, ename from emp where deptno = 20 ;
create index emp_sub_empno on emp_sub_20(empno) ;

CASE 1)
select * from emp where (empno) not in (select empno from emp_sub_20)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20'

CASE 2)
select * from emp x1 where not exists(select 1 from emp_sub_20 where empno = x1.empno)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     INDEX (RANGE SCAN) OF 'EMP_SUB_EMPNO' (NON-UNIQUE)

CASE 3)
select empno, ename, job from emp x1 minus select empno, ename, job from emp_sub_20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   MINUS
   2    1     SORT (UNIQUE)
   3    2       TABLE ACCESS (FULL) OF 'EMP'
   4    1     SORT (UNIQUE)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20'

CASE 4)
select * from emp where  empno is not null
   and (empno) not in (select /*+ hash_aj*/ empno from emp_sub_20 where empno is not null)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=77 Bytes=8K)
   1    0   HASH JOIN (ANTI) (Cost=5 Card=77 Bytes=8K)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=4 Bytes=52)


MERGE ANTI-JOIN
ex) select * from emp where  empno is not null
   and (empno) not in (select /*+ merge_aj*/ empno from emp_sub_20 where empno is not null)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=8 Card=77 Bytes=8K)
   1    0   MERGE JOIN (ANTI) (Cost=8 Card=77 Bytes=8K)
   2    1     SORT (JOIN) (Cost=5 Card=82 Bytes=7K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   4    1     SORT (UNIQUE) (Cost=4 Card=4 Bytes=52)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=4 Bytes=52)


[HASH | MERGE] SEMI JOIN
semi join은 보통 exists절을 이용하는 correlated exists 구문에서 나타난다.

BEFORE) select * from emp x1 where  exists (select 1 from emp_sub_20 where empno = x1.empno)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     INDEX (RANGE SCAN) OF 'EMP_SUB_EMPNO' (NON-UNIQUE)

drop index emp_sub_empno ;

AFTER 1) select * from emp x1 where
   exists (select 1 from emp_sub_20 where empno = x1.empno)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20'

AFTER 2) select * from emp x1 where   exists (select /*+ hash_sj*/1 from emp_sub_20 where empno = x1.empno)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=8K)
   1    0   HASH JOIN (SEMI) (Cost=5 Card=82 Bytes=8K)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=82 Bytes=1K)

create index emp_sub_empno on emp_sub_20(empno) ;


HASH SEMI-JOIN
ex) select * from emp x1 where
    empno is not null
    and exists(select /*+ hash_sj*/ 1 from emp_sub_20 where empno = x1.empno and empno is not null)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=5 Bytes=500)
   1    0   HASH JOIN (SEMI) (Cost=5 Card=5 Bytes=500)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=4 Bytes=52)


MERGE SEMI-JOIN
ex) select * from emp x1 where  empno is not null
    and exists(select /*+ merge_sj*/ 1 from emp_sub_20 where empno = x1.empno and empno is not null)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=8 Card=5 Bytes=500)
   1    0   MERGE JOIN (SEMI) (Cost=8 Card=5 Bytes=500)
   2    1     SORT (JOIN) (Cost=5 Card=82 Bytes=7K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   4    1     SORT (UNIQUE) (Cost=4 Card=4 Bytes=52)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=4 Bytes=52)


OUTER JOIN
Outer join은 Nested Loops, Merge Join, Hash Join과 수반되어 나타나는 옵션의 성격을 가지는 조인방법이다.
아우터 조인에서는 (+)가 붙지 않는 컬럼을 가진 테이블이 무조건 드라이빙 테이블이 된다.

ex) select * from emp x1, emp_sub_20 x2 where x1.empno = x2.empno(+)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   NESTED LOOPS (OUTER)
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP_SUB_20'
   4    3       INDEX (RANGE SCAN) OF 'EMP_SUB_EMPNO' (NON-UNIQUE)

ex) select /*+ use_merge(x1 x2)*/*  from emp x1, emp_sub_20 x2  where x1.empno = x2.empno(+)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=8 Card=82 Bytes=9K)
   1    0   MERGE JOIN (OUTER) (Cost=8 Card=82 Bytes=9K)
   2    1     SORT (JOIN) (Cost=5 Card=82 Bytes=7K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   4    1     SORT (JOIN) (Cost=4 Card=82 Bytes=2K)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=82 Bytes=2K)

ex) select /*+ use_hash(x1 x2)*/*  from emp x1, emp_sub_20 x2  where x1.empno = x2.empno(+)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   HASH JOIN (OUTER) (Cost=5 Card=82 Bytes=9K)
   2    1     TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   3    1     TABLE ACCESS (FULL) OF 'EMP_SUB_20' (Cost=2 Card=82 Bytes=2K)



# ROW OPERATION

CONCATENATION
ex) select * from emp x1 where job = 'CLERK' and deptno in (10, 20)
-- where (job = 'CLERK' and deptno = 10 ) or (job = 'CLERK' and deptno = 20 )

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   CONCATENATION
   2    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   3    2       AND-EQUAL
   4    3         INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)
   5    3         INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE)
   6    1     TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   7    6       AND-EQUAL
   8    7         INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)
   9    7         INDEX (RANGE SCAN) OF 'EMP_JOB_IDX' (NON-UNIQUE)

CONNECT BY

ex) create index emp_mgr_idx on emp(mgr, empno) ;
select * from emp x1 connect by mgr = prior empno start with mgr = 7839
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   CONNECT BY (WITH FILTERING)
   2    1     NESTED LOOPS
   3    2       INDEX (RANGE SCAN) OF 'EMP_MGR_IDX' (NON-UNIQUE)
   4    2       TABLE ACCESS (BY USER ROWID) OF 'EMP'
   5    1     NESTED LOOPS
   6    5       BUFFER (SORT)
   7    6         CONNECT BY PUMP
   8    5       TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   9    8         INDEX (RANGE SCAN) OF 'EMP_MGR_IDX' (NON-UNIQUE)

ex) select * from emp x1 connect by mgr + 0 = prior empno start with mgr = 7839
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   CONNECT BY (WITH FILTERING)
   2    1     NESTED LOOPS
   3    2       INDEX (RANGE SCAN) OF 'EMP_MGR_IDX' (NON-UNIQUE)
   4    2       TABLE ACCESS (BY USER ROWID) OF 'EMP'
   5    1     NESTED LOOPS
   6    5       BUFFER (SORT)
   7    6         CONNECT BY PUMP
   8    5       TABLE ACCESS (FULL) OF 'EMP'


COUNT [STOP KEY]

ex) select rownum, ename from emp --rownum이 select column list에 나올때
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   COUNT
   2    1     TABLE ACCESS (FULL) OF 'EMP'

PSEUDO COLUMN CURRVAL, NEXTVAL, LEVEL, ROWID, ROWNUM

ex) select * from emp where rownum <= 3 --rownum이 where 절에 나올때
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   COUNT (STOPKEY)
   2    1     TABLE ACCESS (FULL) OF 'EMP'


FILTER
ex) select * from emp x1 where sal = (select min(sal) from emp t1 where x1.deptno = t1.deptno)
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'EMP'
   3    1     SORT (AGGREGATE)
   4    3       TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   5    4         INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

ex) select * from emp x1 where deptno = 20 connect by mgr = prior empno start with mgr = 7839
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     CONNECT BY (WITH FILTERING)
   3    2       NESTED LOOPS
   4    3         INDEX (RANGE SCAN) OF 'EMP_MGR_IDX' (NON-UNIQUE)
   5    3         TABLE ACCESS (BY USER ROWID) OF 'EMP'
   6    2       NESTED LOOPS
   7    6         BUFFER (SORT)
   8    7           CONNECT BY PUMP
   9    6         TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
  10    9           INDEX (RANGE SCAN) OF 'EMP_MGR_IDX' (NON-UNIQUE)

FOR UPDATE
ex) select * from emp for update ;
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FOR UPDATE
   2    1     TABLE ACCESS (FULL) OF 'EMP'




# SET OPERATION

SORT [JOIN | GROUP BY | ORDER BY | UNIQUE]

SORT JOIN
Merge Join시에 수반되는 집합연산이다. 즉 머지 조인은 항상 SORT 연산을 동반한다.

ex) select /*+ use_merge(e d)*/ * from emp e, dept d where e.deptno = d.deptno
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=8 Card=82 Bytes=9K)
   1    0   MERGE JOIN (Cost=8 Card=82 Bytes=9K)
   2    1     SORT (JOIN) (Cost=5 Card=82 Bytes=7K)
   3    2       TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=82 Bytes=7K)
   4    1     SORT (JOIN) (Cost=4 Card=82 Bytes=2K)
   5    4       TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=82 Bytes=2K)


SORT (GROUP BY)
Group by 절이 사용되어 그루핑 기능이 수행될때 나타나는 집합연산이다.

ex) select job, count(*) from emp group by job
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (GROUP BY)
   2    1     TABLE ACCESS (FULL) OF 'EMP'

SORT(ORDER BY)
Order by절이 사용되여 결과셋을 정렬하는 집합연산이다.

ex) select * from emp e order by deptno
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (ORDER BY)
   2    1     TABLE ACCESS (FULL) OF 'EMP'

참고로 인덱스가 있는 경우 select * from emp e where deptno > 0 와 같이 집합연산을 로우 연산으로 바꿀 수 있다.
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   2    1     INDEX (RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

SORT(UNIQUE)
minus, intersection, union, distinct 등이 사용되는 집합연산으로 결과 셋을 정렬하여 중복 레코드를 제거하는 기능을 한다.

ex) select distinct * from emp e
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (UNIQUE)
   2    1     TABLE ACCESS (FULL) OF 'EMP'

SORT (AGGREGATE)
Group by 절이 쓰이지 않으면서, 쿼리 내에서 max, min, sum, count, avg와 같은 그루핑 함수가 사용됐을때 나타나는 집합연산이다.

ex) select avg(sal) from emp
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (AGGREGATE)
   2    1     TABLE ACCESS (FULL) OF 'EMP'


ITERSECTION
ex) select empno, ename from emp e intersect select empno, ename from emp_sub_20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   INTERSECTION
   2    1     SORT (UNIQUE)
   3    2       TABLE ACCESS (FULL) OF 'EMP'
   4    1     SORT (UNIQUE)
   5    4       TABLE ACCESS (FULL) OF 'EMP_SUB_20'


MINUS
ex) select empno, ename from emp where empno > 7000 minus select empno, ename from emp_sub_20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   MINUS
   2    1     SORT (UNIQUE)
   3    2       TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   4    3         INDEX (RANGE SCAN) OF 'PK_EMP' (UNIQUE)
   5    1     SORT (UNIQUE)
   6    5       TABLE ACCESS (FULL) OF 'EMP_SUB_20'

UNION
union all 연산에 중복레코드를 제거하는 sort unique 연산이 더 추가되어 있는 집합연산이다.

ex) select empno, ename from emp where empno > 7000 union  select empno, ename from emp_sub_20
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (UNIQUE)
   2    1     UNION-ALL
   3    2       TABLE ACCESS (BY INDEX ROWID) OF 'EMP'
   4    3         INDEX (RANGE SCAN) OF 'PK_EMP' (UNIQUE)
   5    2       TABLE ACCESS (FULL) OF 'EMP_SUB_20'


# SPECIAL CASE

REMOTE
ex) select * from emp e, dept@rlink d where d.deptno = e.deptno
Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=82 Bytes=9K)
   1    0   NESTED LOOPS
   2    1     REMOTE*                                                                                 RLINK
   3    1     TABLE ACCESS (BY INDEX ROWID) OF EMP
   4    3     INDEX(RANGE SCAN) OF 'EMP_DEPTNO_IDX' (NON-UNIQUE)

보통 원격지 테이블과 NL로 풀리는 실행계획은 성능측면에서 좋지 못하다

SEQUENCE
ex) select seq_emp.nextval from dual

Execution Plan
--------------------------------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SEQUENCE OF 'SEQ_EMP'
   2    1     TABLE ACCESS (FULL) OF 'SYS.DUAL'


PARTITION(SINGLE / ITERATOR / ALL / INLIST)
TABLE ACCESS CLUSTER
TABLE ACCESS HASH
BITMAP INDEX
BITMAP AND | OR | MERGE
VIEW







'Database 일반' 카테고리의 다른 글

Database Quiz - sub query  (0) 2009.03.12
Database - Oracle Hint  (0) 2009.03.07
Database Quiz - count  (0) 2009.02.13
Database Quiz - 함수의 활용  (0) 2009.02.07
Database Quiz - 실행계획 유도  (0) 2009.02.07
Posted by bleujin
Framework/예외처리2009. 3. 7. 00:36

좀 더 근원적인 질문을 해보자. 예외란 무엇인가?

인터넷을 검색해보니 정보통신용어사전이라는 쓰잘데기 없는 책에는 예외(exception) : 컴퓨터 시스템의 동작 도중 예기치 않았던 이상 상태가 발생하여 수행 중인 프로그램이 영향을 받는 것. 예를 들면, 연산 도중 넘침에 의해 발생한 끼어들기 등이 여기에 해당한다.
라고 등록되어 있다.

Java 프로그래밍에서의 예외는 조금 다르다. 우선 예외란 이상상태가 아니고 예기치 않았던 상황도 아니다. 프로그래밍에서의 이상 상태는 메모리 누수, 왜곡된 데이타 등을 말하며 예외란 프로그래밍에서 발생하는 일반적인 실행의 흐름(일명 Happy Path)을 바꾸는 몇 가지 조건을 처리하도록 설계한 프로그래밍 언어의 개념을 말하며 이는  주 패스가 아니라 대안적인 패스일뿐 이다. 

예외를 가정할수 있는 몇가지 조건의 종류에는 컴퓨터 하드웨어 문제(충분한 공간의 미확보, 할당하지 못하는 기억장치 접근 등), 운영 체제의 설정 실수, 사용자의 입력 실수(없는 파일, 숫자가 기대되는 곳에 문자 입력), 받아들일수 없는 연산(Devide by Zero) 등 아주 많으며 이러한 상황은 이미 예견되는 상황이다.

갑자기 생뚱맞게 정상적인 교육을 받은 프로그래머라면 대부분 알고 있을 예외 정의를 하는 이유는 자바의 공식 정론은 checked는 정상적인 것이며 runtime exception이 프로그래밍 에러를 가르킨다 를 떠올리기 위해서다.

나는 아주 오랫동안 이 논리에 동의해왔다. 그러나 수십만줄짜리 프로그램들을 작성해 보면서 이는 그닥 실용적이지 않다는 생각을 하게 됐다. 그 주제에 대해 Thinking in Java의 Bruce Eckel와 스프링 개발자로 유명한 Rod Jonson도 이 문제에 대해 언급한 바 있다.  

앞의 글에서 checked exception은 먼저 호출자가 예외를 가지고 현명한 무언가를 할수 있다면 체크되는 예외를 사용해야 한다고 했다. 그런데 여기서 중요한건 가능성이 아니라 빈도와 선택 여부이다. 만약 프레임워크 코드를 작성한다면 호출자가 이 예외를 가지고 현명한 처리를 할지 안할지를 예측할 수는 없다. Java API 코드 설계자도 마찬가지이다. 아직 나중에 누군가의 호출자가 예외 처리를 어떻게 할 것인지를 어떻게 미리 할 수 있을까? 따라서 이 경우에는 가능성이 중요한 선택의 기준이 되고 checked exception에 비중을 많이 두게 된다.

하지만 만약 당신이 지금 호출자의 코드를 작성하고 있다면 즉 그렇게 작성된 자바와 프레임워크를 사용하여 비지니스 코드를 작성하고 있는 중이라면 조금 얘기가 다르다. 일반 프로그램을 작성중이라면 가능성의 문제가 아니라 빈도의 정도가 좀 더 중요해진다. 대부분의 비즈니스 코드에서 Database에 접속할때마다 그리고 SQL을 실행할때마다 그리고 rs.getString("")을 할때마다 SQLException을 잡아서 할 수 있는 무언가는 거의 없다. 비록 Framework에서 모든 코드마다 SQLException을 던진다고 모든 코드에서 그걸 모두 잡거나 다시 던지는건 바보같은 짓이다.

Framework에서는 가능성의 문제로 checked exception을 던지지만 일반 클라이언트 프로그램에서 해당 예외를 잡아서 로그나 화면에 출력하는 것 외에 무언가의 다른 처리를 할 빈도가 거의 없다면 그 많은 checked exception은 생산성의 심각한 장애요소로 작용한다. 예외는 기하급수적으로 퍼지기 때문에 함수 하나에서 throw한 예외는 수천줄의 예외 처리를 감당해야 한다. 그래서 일반 클라인언트 코드에서는 잡아봐자 별로 할것도 없는 checked exception보다는 runtime exception이 좀더 적합하다. 대부분이 로그에 기록하는 것 뿐인 예외 처리라면 맨 처음의 프런트코드에서 한번만 잡아주는게 좋다. 빈도는 낮겠지만 현명한 어떤 처리를 해야 한다면 runtime exception도 catch절로 처리 할수 있기 때문에 상관이 없다.


Framework와 일반 코드에서의 예외 처리 원칙이 이와 같이 다르기 때문에 좀 다른 생각을 해야 할 필요가 있다. 실험실과 비실험실은 그래서 직접 만나는 건 좋지 않다. 사실 비실험실에서 실험실 코드를 직접 호출이 된다는 것 자체가 둘의 경계를 희박하게 만들기 때문에 Facade의 중간 Gate를 만든다. Facade가 해야 할일은 실험실의 캡슐화 역할이 가장 중요하지만 예외를 바꾸는 곳으로도 가장 적절한 곳이다.




위 다이어그램에서 Gate의 역할인 DBController의 코드중 일부인 execQuery를 보면

    public Rows execQuery(IQueryable query, int limit) {
        try {
            long start = System.currentTimeMillis();
            Rows result = query.execQuery(dbm, limit);
            long end = System.currentTimeMillis();

            handleServant(start, end, query, IQueryable.EXEC_QUERY);
            return result;
        } catch(SQLException ex) {
            throw RepositoryException.throwIt(ex, query) ;
        }
    }

실험실 코드에서 던지는 checked exception인 SQLException을 rumtime exception으로 바꾸어 버린다. 대부분의 SQLException이 별도의 Path를 가지지 않는다면 클라이언트는 굳이 처리해야 할 부담을 갖지 않는 것이다. 적은 확률이지만 무언가를 해야 한다면 runtime exception인 RepositoryException을 잡는다. 만약 대부분의 코드가 SQL Exception을 처리해야 한다면 - Framework를 이용하는 새로운 Framework를 만들어야 한다면 - 상속등을 이용해 새로운 Gate를 만들면 그만이다.


이제 예외 처리에 대한 문제를 정리해 보자.

모든 호출자들이 이 문제에 대해 알고 싶어하고 처리해야 하는가 ?
   ex) processInvoice() 메소드에서 소비 한도를 초과했다.
   -> checked를 정의하고 사용하여 자바의 컴파일시 지원의 장점을 사용하자.

호출자들 중의 소수만이 이 문제를 처리하길 원하는가?
   ex) getString() throws SQLException
   -> runtime - 이것은 호출자에게 예외를 잡을지에 대한 선택권을 넘겨주면서도 모든 호출자들에게 그것을 잡도록 강요하지 않는다.

먼가 끔직히 잘못되었는가? 그 문제가 치료 불가능한가?
   ex) DB 접속오류
   -> runtime - 오출자가 그 에러에 관한 정보를 통보 받는것 외에 아무것도 할 수가 없다.

아직 잘 모르겠는가?
   -> runtime 고려 - 가능한 runtime을 고려해본다. 두개의 클래스를 제공해 호출자가 잡을지 말지에 대한 선택권을 줘라.

'Framework > 예외처리' 카테고리의 다른 글

GUI TEST  (0) 2009.06.08
예외 수집기  (0) 2009.03.26
exception framework  (0) 2009.03.10
checked exception의 문제  (0) 2009.02.09
예외 처리 격언  (2) 2009.02.09
Posted by bleujin
Framework/Another Lore2009. 3. 6. 05:47

스팸 필터, Validation, Database 에 이어 4번째 Framework AL 개요

AL은 프로젝트 명 Another Lore의 약자로 일명 알-ㅅ-로 이름을 지었다. Validation과 Database가 System Framework인 반면에 AL은 도메인 Domain Framework 이다.

시스템 프레임워크는 프로젝트 종류와 상관없이 해당 시스템 인프라에 밀접한 관련이 있다. 이를테면 DB를 사용한다면 그게 게임이든 웹서비스든 혹은 임베딩 서비스라도 상관이 없다. 그런면에서 시스템 프레임워크는 문제를 단순화 시킬수 있다. 인프라 그 자체는 한정적이고 단순하기 때문에 집중의 장점을 충분히 활용할 수 있고 재 활용도가 높다.

반면에 도메인 프레임워크는 특정 도메인을 가정한 프레임워크이므로 해당 도메인에서 유용한 프레임워크이다. 첫 시도는 4년전쯤의 그리스 창조의 신 이름을 딴 Odin 이라는 프로젝트였다. (공식 명칭은 CAFE -Contetnt Application Framework & Engine) 사용자가 다루는 모든 엔티티를 Node와 Property라는 이름으로 추상화 시켜서 모든 컨텐트를 하나의 구조로 볼 수 있게 하겠다는 시도였다.

이를테면 사원과 부서라는 엔티티를 보자. 이를 다루기 위해서는 Employee 객체와 Dept 객체를 만들고 아마도 DB 겠지만 저장할때는 사원과 부서 테이블을 만들어 저장한다. 이제 로켓을 타고 지상 10000미터쯤에서 Employee객체와 Dept 객체를 바라보자. 아주 콩알만하게 보이기 때문에 우리는 이 두 객체의 차이점을 명확히 볼 수없다. 10000미터 상공에서는 Employee객체나 Dept 객체나 별 차이가 없다. Employee 객체는 empNo, ename, hireDate, sal 이라는 Property가 있고 Dept 객체는 deptNo, dname, loc 라는 Property가 있지만 아주 작은 차이다. (라고 생각하자-ㅅ-) 아주 작은 차이이기 때문에 더이상 굳이 Employee와 Dept로 인터페이스 하지 않아도 되고 최상위 객체로 Node로 인터페이스 처리를 하자. 라는 게 기본적인 아이디어 이다.

처음에는 JSR-170 에서 시작하였다. JSR-170은 Content Repository API를 의미하는데, 각종 컨텐트를 보관, 검색, 버저닝(versioning)하는 방법을 표준화하려는 시도이다. 오픈 소스 구현으로는 Apache Jackrabbit이 있고 현제는 JSR283으로 Extend 2.0 API가 있다.  (http://www.jcp.org/en/jsr/detail?id=170, http://www.jcp.org/en/jsr/detail?id=283) Odin는 소정의 결과도 있었지만 당시의 나는 아직 DB 관점에서 세상을 보는 단점이 있었기 때문에 충분히 성숙한 프레임워크를 만들지 못했다. (라고 나중에 생각했다.)

AL은 간단히 말하면 분산 컨텐트 서비스로 이전의 경험들도 좀더 발전적인 개념을 가지게 되었다. 그전에 AL 따위를 만들어서 무슨 장점이 있는지 부터 살펴보자. 그 자체로 장점을 가지는 시스템 프레임워크와는 달리 도메인 프레임워크는 좀더 제한적이기 때문에 신중히 접근해야 한다.

단기적으로는 Content Service API의 재활용에 있다. 예를들어 대부분의 프로젝트은 로그인 모듈이 필요하지만 그 때마다 로그인 모듈을 만드는건 그닥 좋아 보이지 않는다. 그래서 Open ID라는 게 생겼다. 이전의 Single Sign On 서비스와 비슷하지만 좀더 추상화되었고 MS의 패스포트 서비스와 달리 벤더 종속이 아닌 Open API이고 Open된 표준 규약이 존재한다. 이 OpenID라는 서비스를 사용하면 사용자는 여러군데 동시 가입하지 않아도 여러개의 서비스를 사용할 수 있으며 프로바이더 입장에서는 별도의 로그인 모듈 없이도 인증 서비스를 할 수 있다는 장점이 있다.

물론 정치적인 단점이 없는건 아니지만 그와 비슷한 관점에서 생각해 보면 대부분의 컨텐트 서비스는 컨텐트의 CRUDS(Create, Retrieve, Update, Delete, Search)말고도  
 - Versioning
 - Authorization(Security & ACL)
 - monitoring
 - exception handling
 - filtering 
등과 같은 기능을 공통적으로 사용한다. 그리고 비기능적 요구사항을 infra quality와 application quality(improve productivity, chain service)를 요구한다.  

일종의 메타 서비스(서비스를 만드는 서비스) 같은 것으로 서비스의 공통 부분을 제공해주는 서비스이다. 이런 메타 서비스를 통해 생산성을 향상시킬 수 있는 단기적인 장점이 있고 장기적으로는 AL Framework를 묶은 서비스끼리 chain으로 연결해서 거대한 하나의 Lore를 만들수 있으리라는 기대가 있다.

먼저 도메인에서 사용하는 용어 정의를 하자.
  • Node(노드) : 정보의 최소단위
    * 독립성과 관계성을 가져야 한다.
    * 0개 이상의 Property와 1개 이상의 Link를 가져야 한다.(
    * 한개의 parentNode를 가져야 하며 하나의 nodeType을 가져야 한다. 
    * 자신의 Global Unique하게 인식할 수 있는 한개의 UUID를 가지고 있다.
    * 노드를 인식하고자 할때에는 nodePath를 사용하거나 uuid를 사용한다. (name을 바꿀수 있어야 하나?)
  • Property(속성)
    * PropertyType과는 Class와 instance의 관계이다.
  • PropertyType(속성타입) :  
    * Type[String, Clob, Binary, Long, Double, Boolean, Date, Reference, Undefined] : defines integer constants for the available property types as well as for their standardized type names (used in serialization) and two methods for converting back and forth between name and integer value:
  • Property Constraint
    * Property가 가져야 할 Constraint. 이를테면 RequiredConstraint는 DB의 Not Null 과 비슷
  • NodeType(노드타입) :
    * An important feature of many repositories is the ability to distinguish the entities stored in the repository by type.
    * Supertypes: A node type may extend another node type (or more than one node type, if the implementation supports multiple inheritance.)
  • Property Definitions(속성정의) :
    * A node type contains a set of definitions specifying the properties that nodes of this node type are allowed (or required) to have and the characteristics of those properties.
    * 대소문자 구분하지 않음
  • Link(링크)
    * Node와 Node의 관계
    * Link는 label(name)을 가지며 이 label이 LinkType이다.
    * LinkType과는 Class와 instance의 관계이다.
    * Link는 ... 양방향?
  • LinkType(링크타입)
    * preDefined LinkType : parent-child, member
    * MemberType : A node type contains another memberNodeType specifying the member nodes that nodes of this node type are allowed (or required) to have and the characteristics of those child nodes (including, in turn, their node types)
  • Property Value-Constraint[Validation],
  • Value-Object[Static Value, Reference Value] :
    * Value : this represents the value of a property. The methods of the Value interface are:
  • UserSession,
  • UUID
    * Node Global Unique ID이다.
    * 전 세계에 같은 UUID를 가지는 노드는 있을 수 없다.(같은 UUID는 수학적으로 발생할 수 없는 확률이다.)
    * UUID represents a Universally Unique Identifier per IETF Draft specification

 

NodeType과 Node의 관계는 자바의 클래스와 인스턴스와 관계와 비슷하다. NodeType은 하위로 PropertyType을 가지고 Node는 하위로 Property를 가진다. 예컨데 employee 인스턴스 하나를 만들기 전에 먼저 Employee NodeType을 정의하고 각각의 PropertyType을 선언한 뒤 해당 Value의 Property를 가지는 empNode를 만든다. 이렇게 만들어진 empNode와 deptNode는 다양한 LinkType의 link를 가진다.

... 물리학의 대통합 이론이 떠오를 정도로 정보를 극단적으로 추상화시켰다. 좀더 이해하기 쉽게 말하자면 Database를 Wrapping한 Repository Service이다. 왜 Database를 Wrapping해서 Repository Service 개념으로 접근하는 장점은 무엇일까? 대부분의 프로그래밍은 Model - Process - View 단계를 거친다. 상대적으로 앞에 있을수록 좀더 단단하며 변하기 어렵다. 그래서 테이블 재설계등의 Model의 변동은 프로젝트 전반에 영향을 미치고 그 만큼 파괴력도 크다. AL은 Model의 뒷부분을 숨기고 유연하게 모델을 언제든 재설계를 할 수 있게 하고 오히려 모델 재설계에 장점을 가진다.

마땅한 Tool이 없어서 Class Diagram으로 말하자면




bleujin은 EmployeeNodeType을 인스턴스한 하나의 Node이다.
dev는 DeptNodeType을 인스턴스한 하나의 Node이다. bleujin과 dev는 부서관계라는 link을 가지고 있다.

어찌보면 객체디비나 EJB3모델과 세계관은 비슷하지만 AL은 자체가 서비스인 메타 서비스이므로 좀더 도메인에 치중되어 있다.




'Framework > Another Lore' 카테고리의 다른 글

와~~~  (0) 2009.04.09
AL - Extreme  (0) 2009.04.04
AL - Code Example  (0) 2009.03.20
AL - Abstraction & Model  (0) 2009.03.13
AL - 배경  (0) 2009.03.07
Posted by bleujin
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