실험실 밖에서는 개체로서가 아닌 개념으로서의 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 |