'Framework'에 해당되는 글 84건

  1. 2009.03.07 AL - 배경
  2. 2009.03.07 Framework (Rows)
  3. 2009.03.07 Class Design
  4. 2009.03.07 checked vs runtime
  5. 2009.03.06 Framework - AL(개요)
  6. 2009.03.06 Framework (IQueryable)
  7. 2009.03.04 Framework (DBManager)
  8. 2009.02.22 나쁜 디자인의 징후
  9. 2009.02.22 Design Principle - SRP
  10. 2009.02.21 Framework (구조적 중복 제거)
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
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
Framework/Database2009. 3. 4. 16:48

실험실 밖에서는 개체로서가 아닌 개념으로서의 DB를 인식해야 한다는 걸 명심하고 이제 실험실 안의 코드를 보자. 일단 실험실 밖에서야 어떻게 돌아가든 실험실 안에서는 경제적 효율성이 아니라 순수한 수학적 효율성을 가지는 코드를 작성해야 한다.(공학적 효율성 = 경제적 효율성 + 수학적 효율성) 따라서 비록 추상 메소드이긴 하지만 DBManager가 가지는 getConnection 등의 db 식의 메소드에 대한 추상적인 접근은 이후에 하기로 하고 일단 추상클래스인 DBManager를 볼모로 잡아둔다는 가정하에 실험실 안에서는 repository service가 아니라 general db로 인식이 가능하다.

DBManager Class Diagram






DBManager 클래스는 상속이 잘못 사용된 대표적인 예와 닮아 보인다. 브랜든 골드패더의 책에서처럼 햄버거를 상속하여 치즈 햄버거를 만들고 양파 추가한 치즈 햄버거를 만들고 토마토를 추가한 치즈 햄버거를 만들고 양상추를 추가한 치즈 햄버거를 만들고 물론 양파만 추가한 햄버거도 만들고... 등등 데코레이터 패턴을 설명할때 잘못된 사용으로 나오는 상속의 부적절한 사용예와 무척 닮았다. 

게다가 JDBC라는 규약을 만든 이유가 통일적인 인터페이스를 위해서이기에 jdbc와 사용자 정보만 넘기면 어느 DB든 연결이 가능하므로 클래스 하나면 될텐데 지나치게 복잡해 보인다.

위 CD를 보면 DBManager를
MSSQLDBManager
   MSSQL2000 용 표준 연결 관리 클래스이다. Pool을 사용하지 않는다.

OracleDBManager 
   Oracle 용 표준 연결 관리 클래스이다. Pool을 사용하지 않는다. 아주 기본적인 코드이므로 8i이상에서는 모두 동작한다.

WASDBManager
   WAS에 등록된 DB 연결정보를 사용한다. Pool을 사용하는지의 여부는 WAS에게 맡긴다. 아주 기본적인 코드이므로 WAS종류에 상관없이 동작한다.

HSQLDBManager
   HSQL용 표준연결 관리 클래스이다.

MYSQLDBManager
   MYSQL용 표준 연결 관리 클래스이다.

H2DBManager
  H2용 표준 연결 관리 클래스이다. 

DBManager를 상속받은 각 DB별 Manager를 보면 DB의 종류만 틀릴뿐 큰 차이가 없어보인다. 물론 WAS의 경우같이 jdbcURL대신 DSN등을 받는 등의 조그만 인터페이스 차이가 있겠지만 이렇게 많은 클래스를 만들 필요가 있었을까? 복잡해 보인다고 할 수 잇지만 실제로는 그닥 별 차이 없다. 왜냐하면 대부분의 프레젝트에서 사용하는 Manager는 대부분 하나이기 때문이다. 단지 하나의 Manager를 사용한다면 이렇게 많은 Class를 만들어야 할 이유는 더더욱 없어보이지만 일단 넘어가자. 

MSSQLDBManger를 상속받는 MSSQLCacheDBMaanger, MSSQLPoolDBManger을 보자. 이 두개의 클래스의 차이는 커넥션 풀링 구현의 차이이다. 하나는 객체 풀링을 사용하고 다른 하나는 DBCP의 풀링을 사용한다. DBCP의 커넥션 풀링이 일반적으로 쓰이는 방식이므로 중복되지 않도록 PoolHelper에 캡슐화하여 사용한다. 커넥션 풀링에 완전 무결한 방법이 있어 그것 하나만 사용하면 좋겠지만 사실 그렇게 간단한 문제는 아니다. 

이상과는 다르게 JDBC는 완벽하게 통일된 인터페이스를 보장해 주지는 못한다. 아니 그렇게 하기 위해서는 얼마간의 무언가를 희생해야 한다. 

첫번째로 JDBC Dirver의 버그 문제이다. JDBC는 인터페이스 일뿐으므로 실제 벤더 혹은 일반 IT기업이 구현한 Driver에는 버그들이 종종 발견된다. 물론 버그를 돌아갈 꼼수 코드는 있다. 다만 돌아가는 순간 이미 JDBC의 표준 인터페이스를 위반하게 되고 해당 클래스의 재사용은 불가해진다. 

두번째로 JDBC Driver 각각의 효율성을 최대한 살려주기 위함이다. JDBC의 표준 코드와 다른 방식으로 효율적인 방법을들 사용할 수 있다. 오라클은 사실 Driver 버전마다 조금씩 다른 풀링 방법을 제시하는데 이때 JDBC의 인터페이스를 사용하지 않는다 .수백번의 다양한 테스트를 해본결과 일반적인 DBCP의 커넥션 풀링보다 약 10-15% 정보 빠르다라는 결과를 얻었다. 그러나 그렇다고 해서 항상 OracleCacheDBManager를 쓸수는 없는 일이다. DB가 오라클이 아닐수도 있고 오라클이 제공하는 커넥션 풀링은 버전을 타니까 말이다. 


세번째로 테스트와 안전한 연결을 위해서이다. 예를 들어 MSSQL의 연결을 담당하는 클래스 3개는 모두 다른 방식으로 작성되었다. 이는 2개의 컴퓨터가 동시에 다운되기 어려운 것처럼 3개의 클래스 모두가 잘못되었을 확률을 줄여준다. 연결 관리는 너무 중요하고 짧은 코드지만 여기에 문제가 없다라고 자신하기는 힘들다. 단순히 코드의 문제가 아니라 지역적인 문제가 얽히면 얼마든지 버그는 끼어들 수 있다.

네번째로 지역적인 문제가 있다. 예컨데 Oracle9iCacheDBManagerWithRetry의 경우 DB가 같은 LAN상이 아닌 방화벽 너머에 있었는데 이때 방화벽이 지속적으로 모니터링을 하여 연결이 오래된 경우 자동으로 연결을 끊어버리기 때문에 Pooling을 사용할 수 없었다. 그렇다고 매번 연결을 맺는 방식은 너무 느려서 사용할 수 없었다. 그리고 방화벽의 정책을 바꿀수도 없다고 클라인트가 주장하였기 때문에 방화벽이 연결을 끊으면 기존의 풀링 객체를 무효화시키고 다시 생성해서 Retry를 하는 클래스이다. 우습지만 이런 비슷한 일은 현실에서 비일비재하게 일어난다. 만능 커넥션 풀링하는 클래스 하나만을 가지고 쓸 수 없는 이유다.




CD 자체는 DBManager를 상속받아 DB마다 혹은 풀링 정책마다 따른 차이를 구현한 정도이므로 매우 간단하다. 그러함에도 DBManger를 별도로 분리해서 설명하는 다른 이유가 있다. 

보통의 경우 DB Library는 대부분
  +getConnection(String jdbcURL, String userId, String passwd)
  +execute(String sql)
  +query(String sql)
  +freeConnection(conn)
형태의 구조를 가지는게 일반적이다. 열고 실행하고 닫고... 의 클래스가 무난해 보이지만 역으로 생각해보자. 위 방식이 하나의 책임을 가지고 있다면 execute할때 innerMethod로 getConnection과 freeConnection을 호출해서 사용해도 되지 않을까?

다시 말해서
+execute(String sql){
   getConnection() ;
   // 원래 execute Code
   freeConnection(conn) ;
}
와 같이 바꿀 수 있는가?

단순히 물리적으로 public 메소드를 하나만 가져야 한다는 의미는 아니다. 접근자의 경우 외부에서 호출 가능한 접근자는 public 하나뿐이므로 SRP의 원칙에 따라 public 메소드 하나만을 가지는 클래스를 만들기는 사실상 쉽지 않다. 다만 SRP에서 말하는 하나의 책임이란 개념적인 published Method 개념에 대한 상상이다.몇몇 다른 언어는 published라는 접근자를 사용하지만 자바에는 없기 때문에 추상적인 접근이 필요하다.


이렇게 바꿀 수 없는 이유는 여러가지가 있다.

첫째 항상 하나의 sql만 실행하리라는 보장이 없다.

둘째 sql 실행방법은 다양하다. 단순히 select와 MDL(insert, delete, update 등의 Modify Definition Language)의 차이뿐 아니라 batch 처리해야 sql도 있다.

셋째 연결과 실행사이에는 부가적인 작업이 있다. 대표적으로 트랜잭션 처리가 있고 로그나 정책에 따른 예외처리등의 다른 작업이 끼어들게 된다.

이와같은 이유로 무난해 보였던 열고.실행하고.닫고는 이 경우 하나의 책임이라고 할 수 없다. sql을 실행하는 것은 열고 닫고와 다른 책임이므로 별도의 클래스로 분리하는게 좋아보인다.

물론 이는 실험실 안에서의 이야기이다.
실험실 바깥에서 실험실은 단수이므로 추상화의 정도가 좀더 크지만 실험실 안에서의 추상화의 수준은 좀 더 낮다. SRP가 하나의 클래스 뿐 아니라 메소드 혹은 패키지 혹은 컴포넌트 혹은 서비스 그 자체에도 적용될 수 있는 것처럼 책임이란 뜻도 그 놓여진 곳에 따라 추상화 정도가 다르다. 실험실 바깥에서는 실험실을 개념으로서 접근하고 있으므로 getConnection, execute, freeConnection으로 접근하지 않아야 한다. (그리고 이게 이 프레임워크의 주 목적이기도 하다. ) 바깥에서 접근할때는 이게 물리적 DB로 인지하는게 아니므로 이런 정보를 주세요라는 request 하나로 표현되어야 한다.


# 에릭 감마의 public & published interface

http://lambda-the-ultimate.org/node/1400
A key challenge in framework development is how to preserve stability over time. The more miles a framework gets the better you understand how you should have built it in the first place. Therefore you would like to tweak and improve it. However, since your framework is heavily used you are highly constrained in what you can change. At this point it is crucial to have well defined APIs and to make it clear to the clients what is published API and what internal code is. For published APIs you should commit to stability and for internal code you have the freedom to change it.



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

Framework (Rows)  (0) 2009.03.07
Framework (IQueryable)  (0) 2009.03.06
Framework (구조적 중복 제거)  (0) 2009.02.21
Framework (실험실 코드)  (0) 2009.02.20
Framework (개요)  (0) 2009.02.20
Posted by bleujin


Rigidity ? 변경하기 어렵다.

Fragility ? 깨지기 쉽다.

Immobility ? 재사용하기 어렵다.

Viscosity ? 올바른 일을 하기 어렵다.

Needless Complexity ? 과도한 디자인

Needless Repetition ? Copy&Paste?

Shotgun surgery 수정을 하려 보니 여러 클래스를 수정해야 하더라. 하나의 책임이 여기저기 흩어져 있는 경우

Divergent Change 한 클래스가 이런 저런 이유로 자주 수정되어야 하더라. SRP의 위반

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

아키텍쳐 패턴 - Layer 패턴  (0) 2009.03.11
Class Design  (0) 2009.03.07
Design Principle - SRP  (0) 2009.02.22
Method Design  (0) 2009.02.11
몇가지 프로그래밍 조언  (0) 2009.02.10
Posted by bleujin


객체지향에서 디자인이란 객체(클래스)들에게 적절히 책임(responsibility)을 분배한 뒤 객체들 간의 의존 관계를 관리(dependency management)하는 작업이다. 이때 객체지향 원리는 책임 분배와 의존관계 관리의 일종의 가이드 라인 역할을 하게 된다.

객체지향에 잘 알려진 원리는 SRP, DIP, LSP, OCP, ISP가 있으며 이 원리는 결국 DRY 성배를 추구하게 된다.
DRY는 객체지향뿐 아니라 모든 프로그래밍 분야에 적용될 수 있는데.

- DRY는 하나의 요구사항은 한곳에 두어야 한다는 원리이다.
- 사실 각 기능과 요구사항을 한 번만 구현하려고 노력하는것이다.
- DRY는 시스템의 각 정보와 기능을 말이 되는 하나의 장소에 두는 것을 의미한다.
- 어떤 지식 한 조각도 하나의 시스템 안에서는 모호하지 않고, 권위 있고, 단 하나뿐인 표현을 가져야 한다.

DRY 이야기는 많이 했으니 원리 얘기로 다시 돌아가서 그중에 SRP을 먼저 보자.

SRP는 Single Responsibility Principle의 약자로 클래스(혹음 메소드 혹은 패키지 혹은 컴포넌트)는 하나의 책임만을 맡아야 한다는 원리다. 이때 책임(responsibility)이란 뜻은 변경의 원인(reason to change)을 말하며 결국에 "클래스 변경의 원인은 하나이어야 한다."라고 할 수 있다.

시스템의 모든 객체는 하나의 책임만을 가지며, 객체가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중되어 있어야 한다. DRY는 하나의 기능을 한 곳에 두자는 내용이고 SRP는 클래스가 한가지 일만 잘하게 하자는 내용이며 응집도는 사실 SRP의 다른 이름이다.

SRP의 키워드는 책임으로 요약되는데, 그렇다면 책임이란 무엇일까? 책임이란 ‘변경을 위한 이유’이다. 만약 하나의 클래스에 변경을 위한 두 가지 이상의 이유가 있다면 그 클래스는 한 가지 이상의 책임을 갖고 있는 것이다. 그러나 아까 말했듯이 클래스 뿐 아니라 패키지 혹은 컴포넌트에도 같은 원리를 적용할 수 있다. 그러면 두개의 클래스를 가지고 있는 패키지는 이 원리를 위반하기 때문에 하나의 클래스만 가져야 한다는 것인가? 아니다 그렇지 않다. 동등한 추상화 레벨의 책임을 2개 이상 가져서는 안된다는 뜻이다.

SRP는 하나의 클래스에 한 가지 책임을 가르치는 원칙이다. 우리는 설계 관점에서 우리가 인식하지 못하는 SRP 위반을 자주 하게 된다. 이 위반을 경계하기 위해 깊은 통찰력이 필요하지도 않다. 단지 머리에 ‘책임’이란 단어를 상기하는 습관이면 된다

위반 사항에는 대가가 따른다. SRP를 위반할 경우 따르는 재앙은 첫 번째로 ‘왕따’가 발생한다는 것이다. 잔고 클래스가 이율 관리 애플리케이션과 배포됐을 때 확실히 ‘환율 계산’ 메쏘드는 소외된다. 즉 만약 A라는 책임과 B라는 책임을 갖고 있는 클래스가 있을 경우 A만 필요로 하는 애플리케이션은 항상 B를 들고 다녀야 한다.

문제는 여기서 그치지 않는다. 두 번째 재앙은 무관한 메쏘드에 변경이 발생할 경우 불필요한 변경 임팩트가 전달된다. 만약 ‘환율 계산’ 메쏘드가 변경됐을 경우 이율 관리 애플리케이션은 사용하지도 않는 ‘환율 계산’ 메쏘드 때문에 다시 컴파일해야 하고 리테스트해야 하며 재배포해야 한다. 이율 관리와 전혀 무관한데도 불구하고... 사실은 이 임팩트의 영향은 더 심각한데 다음에서 살펴보겠다.

하지만 무조건 책임을 분리한다고 SRP가 적용되는 건 아니다. 가령 데이터 맵퍼 클래스의 메쏘드들이 각각의 insert, delete, update, load 클래스로 분리됐을 경우를 생각해 보자. 마치 절차적 언어에서와 같은 함수 단위의 클래스가 될 것이다. 각 메쏘드 역할에 따른 책임들이 분리되었지만 설계는 장황해지고 관계는 복잡해진다. 그래서 이 문장은 틀린 문장이다. 동일한 책임을 갖는 여러 메쏘드들이 분리된 것이다. 즉 분리의 기준은 책임이며 분리의 목적은 복잡도 감소에 있다.



‘높은 응집도, 낮은 결합도(High Cohesion, Loose Coupling)’의 원리는 1970년대 Larry Constantine과 Edward Yourdon이 정의했던 아주 고전적인 원리이다. 이것은 현재 모든 소프트웨어 시스템 고유의 유지보수성과 적응성을 측정하는 가장 좋은 방법으로 사용되고 있다. 소프트웨어 디자인뿐만 아니라 아키텍처 평가에도 이 원리가 기준이 되는데, 그 이유는 이 원리의 적용 효과가 아주 명백하기 때문이다.

이 원리의 예외는 거의 찾아보기 힘들만큼 보편성을 가지고 있어서 마치 물리학의 엔트로피 법칙처럼 절대적인 기반원리를 제시한다. 낮은 응집도를 갖는 구조는 변경이나, 확장 단계에서 많은 비용을 지불해야 하며 높은 결합도의 경우도 마찬가지이다.

응집도는 ‘하나의 클래스가 하나의 기능(책임)을 온전히 순도 높게 담당하고 있는 정도’를 의미하며 이들은 서로 조화될수록 그 구조는 단순해진다. 응집도가 높은 동네에서 내부 개체가 변했을 때 다른 개체에 충격을 주는 것은 오히려 당연한 징후이다. 이들은 하나의 책임아래 서로 유기적인 관계를 갖고 있기 때문에 내부 개체가 변했을 때 다른 개체의 변경 확률이 높아진다. 마치 예쁜 부츠를 사면 부츠에 어울리는 치마를 입어야 하듯이…


이와 반해 결합도는 ‘클래스간의 서로 다른 책임들이 얽혀 있어서 상호의존도가 높은 정도’를 의미하며 이들이 조합될수록 코드를 보기가 괴로워진다. 이유는 서로 다른 책임이 산만하고 복잡하게 얽혀있기 때문에 가독성이 떨어지고 유지보수가 곤란해지기 때문이다. 이유는 필요 없는 의존성에 있다. 마치 키보드의 자판 하나가 고장나도 키보드 전체를 바꿔야 하는 것처럼. 하나의 변경이 엄청난 민폐를 야기하는 관계이다.



SRP 위반의 악취는 다움과 같다.

1. 여러 원인에 의한 변경
여러 원인에 의한 변경은 한 클래스를 여러 가지 다른 이유로 고칠 필요가 있을 때 발생한다. 즉, 하나의 클래스에 여러 책임이 혼재하고 있어서 하나의 책임의 변화가 다른 책임에게 영향을 준다. 그리고 이 책임이 두 개보다 훨씬 많은 여러 개로 혼재된다면 이 클래스는 심각한 고문관이 된다. 더욱이 이 구조는 더 괴로운 경우로 심화될 수 있다.

2. 산탄총 수술
산탄총을 발사하면 하나의 탄환이 부서지면서 여러 개의 탄환으로 확산되어 발사된다. 따라서 (상상하기도 싫지만) 산탄총을 맞은 대상의 총상은 온몸 전체에 퍼지게 된다. 만약 이런 환자를 수술하는 의사는 마치 수십 발의 총을 맞은 환자를 수술하는 것처럼 힘들 것이다.

‘산탄총 수술(shotgun surgery)’은 ‘여러 원인에 의한 변경’과 비슷한 듯 하면서도 정 반대의 내용을 갖는다. ‘여러 원인에 의한 변경’이 하나의 클래스가 여러 변경 원인(책임)을 지니는 반면, 산탄총 수술은 어떤 변경이 있을 때 여러 클래스를 수정해야 하는 증상이다. 즉 어떤 변경의 대상이 여러 곳에 분포되어 마치 산탄총 총상 환자를 수술해야 하는 것 같은 많은 노동비용이 따른다.

‘산탄총 수술’이 괴로운 이유는 단지 수술 부위가 많다는 것만이 아니다. 이 수술을 했음에도 불구하고 혹시 치료하지 못한 상처가 존재할 수 있다는 가능성이 ‘산탄총 수술’의 더 큰 위험성이다. 가령 하나의 테이블을 조작하는 DB 처리문이 애플리케이션 전역에 퍼져 있는 상황에서 DB 테이블의 구조가 바뀌게 됐을 경우에 발생하는 재앙과 같다. 수술도 고되지만 모든 환부를 찾아야 하는 집중력과 긴장감이 개발자를 더욱 힘들게 한다.


산탄총 수술의 냄새는 특히 설정 정보(configuration information), 로깅(logging), DB 처리에서 발생하기 쉬운데 이들을 다룰 때는 항상 산탄총 수술의 악취를 경계해야 한다. 예를 들어 한 곳에서 관리할 필요가 있는 설정 정보를 여러 클래스에서 나누어 처리하고 있다면 이는 산탄총 수술을 할 수 있는 좋은 본보기가 된다.

이를테면 쓰레드, 커넥션, 오브젝트 풀의 크기 값이나 DB, 서버의 주소 정보들을 각각의 클래스에 자체적으로 관리하고 있다면 이들을 설정 파일이나 설정 관리자에게 Move Field하는 것이 바람직하다. 더 나아가 플러그인을 도입해 설정 정보를 통해 동적으로 행위 변화를 통제(Enable Configurable Behavior with Plugin)하는 것도 생각해 볼만하다. 또한 XML 처리나 프로토콜 해석을 담당하는 메쏘드가 여러 곳에 분포되었다면 각각의 유틸성 클래스로 Move Method하는 것이 바람직하다.



다만 주의해야 할 것은
1. 하지만 너무 무리해서 책임을 나누지 말아야 한다. 책임은 변화의 축이며, 하나의 요구사항 변경은 하나의 책임의 변경을 적시하는 경우가 많다. 책임의 입자도가 매우 세밀하다면 변경에 대한 영역이 그만큼 커지게 된다. 변화가 예측되는 곳, 변화에 효율적으로 대응할 수 있는 크기에서 책임을 할당하는 것이 좋다.

2. 하나는 온전한 하나이어야 한다. 하나의 클래스가 여러 개의 책임을 맡는 것도 곤란하지만, 하나의 책임을 여러 클래스로 분할하여 할당하는 것도 곤란하다.





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

Class Design  (0) 2009.03.07
나쁜 디자인의 징후  (0) 2009.02.22
Method Design  (0) 2009.02.11
몇가지 프로그래밍 조언  (0) 2009.02.10
Bleujin Framework 활용 : HTML Parsing  (0) 2009.01.16
Posted by bleujin
Framework/Database2009. 2. 21. 05:21

사실 개체로서의 인식과 개념적으로의 인식은 생각보다 큰 차이가 아니다. 손 안에 붕어빵에 대해서 끝업이 사고를 발전시켜보면 붕어빵의 공통점을 생각하게 되고 그 생각에서 좀 더 나아가면 개념으로서의 붕어빵을 생각할 수 있듯 이도 결국 추상화의 정도의 문제라고 생각한다.

개념에 대한 이야기는 잠시 접고 실제적인 이야기를 해보자. 그래. 개념으로서 서비스를 인식하게되면 어떤 잇점이 있는건가? 아니 그 전에 개체로서 인식하면 무슨 문제가 있는건가? 에 대한 답을 먼저 찾아야 한다.

package test.db;

import java.io.Closeable;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import junit.framework.TestCase;

public class InstanceDB extends TestCase{

  public void testCase1()  {
    initPool() ;  // 시스템 시작시 단 한번 호출
    
    
    ///////// start
    Connection conn = null;
    PreparedStatement stmt = null ;
    ResultSet rs = null ;
    try {
      conn = getConnection() ;
      stmt = conn.prepareStatement("select * from emp where rownum <= ?");
      stmt.setInt(15);
      rs = stmt.executeQuery() ;
      
      process(rs;
      
    catch (SQLException e) {
      e.printStackTrace();
    finally {
      try {if(rs != nullrs.close();catch(SQLException ex){ex.printStackTrace() ;}
      try {if(stmt != nullstmt.close();catch(SQLException ex){ex.printStackTrace() ;}
      try {if(conn != nullconn.close();catch(SQLException ex){ex.printStackTrace() ;}
    }
    
    ////// end

    destroyPool() // 시스템 종료시 단 한번 호출
  }

  private void process(ResultSet rsthrows SQLException {
    // rs로 실제 작업을 한다.
  }
  
  private Connection getConnection() throws SQLException {
    String url = "jdbc:oracle:thin:@novision:1521:bleujin" ;
    String user = "scott" ;
    String password = "tiger" ;
    return DriverManager.getConnection(url, user, password;
  }

  private void destroyPool() {
    
  }

  private void initPool() {
    try {
      String driverName = "oracle.jdbc.driver.OracleDriver" ;
      Class.forName(driverName);
    catch (ClassNotFoundException e) {
      e.printStackTrace();
    }
  }
}

보통의 JDBC 예제는 위와 같다. private 함수는 그닥 신경쓸 필요없고 커넥션 풀링 같은 자잘한 문제도 잊어버리기로 하자. 오직 testCase1 함수만 봤을때 언뜻 보기에 별 문제 없어 보인다. 그렇다 별문제 없다. 와~ 이제 집에가서 발딲고 잠이나-ㅅ- 가 아니라.. testCase1 함수는 객체지향의 1원칙 이라고 할 수 있는 DRY를 위반하였다. testCase1 함수 어디에 중복이 있는가? 

예제만을 보면 바로 찾기 어렵지만 사실 testCase함수의 Start와 end 사이가 통째로 중복이다. 왜냐하면 우리는 프로그램 하나를 만들때 testCase와 비슷한 소스를 적어도 수백개-수천개는 만들어야 하기 때문이다. 이를 앞에서 중복얘기할때 말한 구조적인 중복의 문제인데 이는 PMD에 근거한 Code Analyzer같은 툴로 검사해도 나오지 않는 중복이다. (initPool과 destroyPool은 풀링을 제대로 만든다면 프로그램이 뜰때와 내려갈때 한번만 호출이 될것이므로 이부분은 중복이 아니다. )

위 코드는
1. Connection을 얻는다
2. PrepareStatement 객체를 만들어 값을 셋팅한다.
3. 객체를 실행한다.
4. 그 결과값으로 ResultSet을 얻어 무언가 처리를 한다.
5. 사용한 리소스를 닫는다.
6. 예외 처리를 한다.
의 과정을 거치며 위 과정은 2,3번 항목에서 값만 바뀔뿐 수백 혹은 수천번이 반복되는 과정이다.
중복코드를 없애기 위해 getConnection을 함수로 만들어 호출한다지만 호출한다는 과정 자체가 중복이라는 뜻이다.

라이브러리가 단순히 코드의 중복을 없애주는 것과 달리 Framework란 이 과정의 중복을 없애줘야 한다.


package test.db;


import java.sql.SQLException;

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 ConceptDB extends TestCase{

  private DBController dc ;
  public void testCase2() {
    initPool() ;  // 시스템 시작시 단 한번 호출

    IQueryable query = dc.createUserProcedure("emp@list(5)").addParam(5;
    Rows rs = dc.getRows(query;
    process(rs;
    
    
    destroyPool() // 시스템 종료시 단 한번 호출
  }
  private void process(Rows rs) {
    // process rs
  }
  private void destroyPool() {
    try {
      dc.getDBManager().destroyPoolConnection() ;
    catch (SQLException e) {
      e.printStackTrace();
    }
  }
  private void initPool() {
    try {
      dc = new DBController("test"new OracleCacheDBManager("jdbc:oracle:thin:@novision:1521:bleujin""scott""tiger" ,5)new StdOutServant(StdOutServant.All)) ;
      dc.getDBManager().initPoolConnection() ;
    catch (SQLException e) {
      e.printStackTrace();
    }
  }
}

Case2의 과정은 아래와 같다.
1. query 객체를 만든다
2. query 객체를 실행한다.
3. 그 결과값인 Rows를 얻어 무언가 처리를 한다.

Case2의 코드는 Case1과 열고 닫는 과정이 없어져서 단순히 줄이 짧다는 것 이상의 차이가 있다. Case1의 과정은 순차적이고 연속적이다. 1에서 6까지의 과정은 순서대로 이루어져야 하며 어느 과정 하나가 생략되어서는 안된다. 이를테면 5번 과정을 빠뜨리면 유한한 자원인 Connection 리소스의 문제로 이후에는 접속조차 안될것이고 6번 과정에서 예외 처리를 적절히 하지 못해도 마찬가지이다. 하지만 Case2는 과정의 단순화뿐 아니라 불연속적이다.

IQueryable 객체를 만들고 실행을 하지 않더라도 혹은 rs의 close()를 호출하지 않아도 전혀 문제가 되지 않는다. 왜냐하면 위에서 나오는 객체들은 자바의 일반 객체이므로 자바의 가비지 처리 알고리즘에 의해 자동적으로 알아서 처리되기 때문이다. 개체로서의 DB가 아나라 개념으로서 DB를 바라보면 구조적 중복을 상당히 없앨뿐 아니라 과정의 연속성의 필요도 없에버릴수 있다. 그럼으로써 프로그래머는 좀더 언어의 처리보다는 비지니스적인 문제에 집중할 수 있고 실수 가능성을 원천적으로 차단해 버린다. 서비스 코드에서는 오직 개념으로서의 DB만 보이므로 connectionless 같은 개체 문제는 아예 신경쓸 필요도 없게 되는 것이다.

귀찮은 반복코드인 예외처리를 없애는게 우선 좋아보이지만 위같이 아예 없어지면 과연 적절한 예외에 대한 상황처리는 어떻게 할거나? 아니 그보다 당장 트랜잭션 처리는 어찌되는거냐.. 같은 세세한 문제는 일단 다음에...


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

Framework (IQueryable)  (0) 2009.03.06
Framework (DBManager)  (0) 2009.03.04
Framework (실험실 코드)  (0) 2009.02.20
Framework (개요)  (0) 2009.02.20
Framework (블랙박스 증후군)  (0) 2009.02.07
Posted by bleujin