사례 연 구 두번째 : 캐쉬 관리 패턴(Cache Management Pattern)의 도입
웹 어플리케이션을 작성할 때, 데이터 베이스 억세스는 매우 빈번하게 발생한다. 절대 다수의 웹 어플리케이션들이 정보를 저장하기 위해서 데이터 베이스를 이용한다. 사용자가 폭주하는 상황이라면 웹 어플리케이션의 성능은 데이터 베이스 억세스 속도와 직결된다. 지금도 많은 개발자들이 데이터 베이스 억세스 속도의 향상을 위해서 방법을 배우고, 기술을 공부하고 있다. 데이터 베이스 억세스 속도를 향상시키는 기법 중의 하나로 캐쉬(Cache) 기법이 있다. 캐쉬는 작은 용량의 빠른 기억장치를 사용해서 대용량의 느린 기억 장치의 억세스 속도를 개선시키는 방법으로, 데이터 베이스의 경우에는 데이터 베이스에 존재하는 데이터들을 서버 상에 메모리 캐쉬 기법을 사용하여 일부 보관하는 방법이 많이 사용된다.
다음에 소개하는 사례는 캐쉬 패턴의 적용 사례를 기술한 것으로, 인트라넷 시스템 개발 과정에 있었던 일을 약간 각색하였다. 이 사례는 처음부터 패턴을 적용하여 디자인 한 사례는 아니다. 오히려 기존의 시스템의 구성을 디자인 패턴을 적용하여 개선한 리팩토링(Refactoring)[1]의 사례라는 점을 염두에 두고 읽기를 바란다.
단계 1 - 문제의 발생
인트라넷에서 사용하는 전자 결재 시스템은 문서 객체를 데이터 베이스에서 읽어서 생성한다. 결재 흐름이 시작되면, 사용자들은 결재가 진행되는 문서를 계속해서 보고 수정과정을 거쳐 결재한다. 여기서 문제로 지적되는 것은 문서 객체를 데이터 베이스에서 가지고 오는 일이 너무 빈번하게 일어난다는 점이다. 문서 객체는 40개 이상의 컬럼을 가지는 방대한 객체로 데이터 베이스에서 정보를 읽어 문서 객체를 생성할 때 마다 상당한 시간이 소모된다. 서버의 성능을 향상시켜 동시에 지원할 수 있는 사용자 수를 늘이기 위해서는 데이터 베이스 접근에 소모되는 시간을 최소화 하여야 한다.
성능 개선 준비 단계에서 열린 SUB팀 내부회의에서 최초로 데이터 베이스 억세스가 너무 빈번하게 발생한다는 문제가 제기 되었다. 팀에 보고된 이후로 팀 내에서는 이 문제가 수차에 걸쳐 논의 되었다. 억세스 시간을 줄이기 위한 방안들이 회의에서 논의되었으며 다음의 방법들이 추천되었다.
1. SQL Query의 최적화로 적절한 인덱스를 사용하게 한다.
2. Static SQL을 사용하여 SQL Parsing에 소모되는 시간을 최소화한다.
3. 문서 객체를 사용하는 로직을 변경하여 트랜잭션 내부에서 데이터 베이스 억세스를 최소한으로 줄이자.
4. 캐쉬를 이용하여 메모리 상에 존재하는 객체는 데이터 베이스에서 읽어오지 않도록 하자.
당시의 상황에서 고려할 수 있는 방법은 Static SQL의 사용과 캐쉬의 사용이었다. SQL Query는 이미 최적화 되어 있었고, 문서 객체를 사용하는 서버 로직을 변경하는 것은 너무나 많은 작업량을 필요로 하였다.
Static SQL의 사용은 문서 객체를 가져오기 위한 데이터 베이스 억세스 문장을 일부 수정하여 간단하게 해결되었다. 그러나 Static SQL을 사용하여 얻어진 성능향상은 매우 미미하였으며, 진정한 성능향상을 위해서는 캐쉬가 도입되어야 했다.
캐쉬를 이용한 기법은 객체를 획득하고자 할 때 캐쉬 저장소(Repository)에 존재하면 캐쉬 저장소에서 가지고 오며 존재하지 않으면 데이터 베이스에서 읽어와서 캐쉬 저장소에 넣고 객체를 요청한 어플리케이션에 읽어온 객체를 넘겨준다. 빈번하게 사용되는 객체들은 메모리에서 직접 억세스 하는 개념이다.
[ 캐쉬 사용의 다이어그램 : 직접 억세스 vs 캐슁 억세스 ]
이 캐쉬의 구현은 간단하지 않다. 캐쉬에 객체를 저장하는 부분인 캐쉬 저장소와 저장소의 관리를 위한 기능, 그리고 캐쉬 저장소의 객체수가 지정된 수를 초과할 때 하나의 객체를 저장소에서 제거하는 킥오프(Kick-Off)를 위한 알고리즘 등을 구현해야 한다.
단계 2 - 해결 방안의 고민
팀장은 일단 담당자를 정하고 담당자가 캐쉬를 구현하는 부분을 맞아 수행하기로 결정하였다. 담당자는 팀 내부에서 회의를 거쳐 선정하였다. 이제 캐쉬 시스템을 구성하는 일은 우리의 친구인 Z군이 담당하게 되었다. 과연 그는 어떻게 안정적이고 뛰어난 성능을 가진 캐쉬 시스템을 구현할 것인가?
‘캐쉬를 어떻게 만들어야 하지? 캐쉬에는 어떤 구성 요소들이 어떻게 동작하는 것일까?’
한참을 고민한 우리의 Z군! 그러나 답은 쉽게 나오지 않는다. Z군은 물론 캐쉬가 무엇인지 잘 알 고 있었다.
‘캐쉬 란 속도가 빠르고 용량이 적은 저장 장치(A)를 이용하여, 용량이 크고 속도가 느린 저장장치(B)의 성능을 개선하기 위한 시스템 아닌가? 데이터 베이스에 억세스 할 때의 A는 메모리에 해당되며, B는 데이터 베이스에 해당되는데, 이를 어떻게 구현하지?’
이론과 실제는 다르다. 캐쉬의 이론은 위의 말처럼 간단하지만, 이를 프로그래밍하는 일은 그리 간단하지 않다. Z군은 다음의 문제들을 고민하기 시작했다.
‘어떤 부분들을 클래스로 만들어져야 하며, 클래스들에는 어떠한 인터페이스들을 구현하여야 할까? 그리고, 내가 설계한 클래스 들이 최선의 답이 될 수 있을까?’
스스로 생각해 보아도 정답은 없어 보인다. 이 때 옆에서 고민하던 Z군을 지켜 보고 있던 P군이 한마디 한다.
“어이, 이번에 공부한 디자인 패턴 중에 캐쉬 패턴이 등장하던데, 한번 써보지 않겠어?”
이전부터 P군과 Z군과 더불어 팀 내의 몇몇 사람들이 함께 디자인 패턴을 스터디를 하여 왔었다. 그러나 Z군은 패턴은 공부해 왔지만 이를 직접 적용해 보지 못하던 때였다. 디자인 패턴에 상당한 관심을 가지고 있던 P군과 Z군은 함께 캐쉬 패턴을 이번 작업에 도입하는 것을 검토하였다.
단 여기서 한가지 고려할 점은 캐쉬를 적용하는 작업은 Z군에게 배정되었으며, P군은 디자인 상의 조언을 위해 임시적으로 참여하였다는 점이다. P군은 코드를 직접 작성하지 않으며 조언만을 할 뿐이다. 모든 코딩 작업은 Z군이 담당하였다.
[ Box 4 ? Cache Management Pattern ]
단계 3 - 캐쉬 패턴의 도입
캐쉬 패턴을 적용하기 위해서 P군과 Z군이 최초에 수행한 것은 아이러닉 하게도 캐쉬 패턴에 대한 상세한 공부였다. 잘 알려진 디자인 패턴이 모두 40개가 넘으며 이들 하나하나가 각기 다른 의미를 지니고 각기 다른 부분에 적용되기 때문에, 이들을 모두 이해한다는 것은 매우 어려운 일이었다.
캐쉬 패턴이 구체적으로 어떠한 구조를 가지고 있는가? 우리(P&Z군)가 하고자 하는 일에 정확하게 적용 가능한가? 이에 대한 답을 찾기 위해서 우리는 “Pattern in JAVA”책을 참조하였다. 마크 그랜드가 쓴 “Pattern in Java” 는 GOF[2] 책 이후로 패턴에 관련된 명저로 평가되고 있는 책이다. 이 책에 나온 캐쉬 관리 패턴(이하 캐쉬 패턴)의 설명을 참고하자.
캐쉬 패턴은 접근에 시간이 많이 소모되는 객체들의 빠른 접근을 가능하게 한다. 이는 생성에 비용이 많이 소모되는 객체들을 사용 완료된 후에도 별도의 저장장치에 계속 복사본을 간직한다. 객체는 생성시에 데이터 베이스 테이블을 읽어 오거나, 복잡한 연산을 수행하는 등의 이유에 의해 생성에 많은 비용이 소모될 수 있다.
여기서 설명하는 캐쉬 패턴은 바로 P군과 Z군이 구현하고자 했던 캐쉬 패턴과 정확히 일치한다. 이 책의 패턴을 공부하고 P 군과 Z군은 패턴을 도입하기로 결정하였다.
P군과 Z군이 최초에 수행한 작업은 어떤 부분을 대체하여 캐쉬로 만들 것인가를 판별하는 부분이었다. 데이터 베이스 억세스 부분을 캐쉬로 변경한다는 전제 아래, 데이터 베이스를 억세스 하는 부분의 클래스를 캐쉬를 이용하도록 변경하여 다른 클래스들에는 영향을 주지 않으면서 속도를 향상시킬 수 있도록 작업을 진행하였다.
캐쉬 패턴을 적용하기 위해서 데이터 베이스에서 데이터를 가져오는 부분의 로직을 살펴보았다. 데이터 베이스에서 레코드를 읽어 들여 결재 문서로 만드는 로직은 RecvSancDocApi, SancDocApi2개의 클래스에 집중되어 있었다. 이들 클래스에 해당하는 문서 객체는
ⓒ RecvSancDoc : RecvSancDocApi용
ⓒ SancDoc : SancDocApi용
의 2가지 객체로 구성되어 있다. 이 둘은 모두 동일한 테이블을 접근하는데 사용된다. 그러나 어플리케이션에서 접근하는 방법이 다르며 이때마다 2개 종류의 다른 방식으로 사용되기 때문에 이를 표현하는 객체 자체를 2가지 클래스로 정의하고 있었다.
P군과 Z군은 여기에서 심각한 고민을 하지 않을 수 없었다. 캐쉬에는 객체의 키 값을 이용하여 동일한 객체인가를 판별한다. 하나의 데이터베이스 레코드에 해당되는 객체가 둘이기 때문에 한쪽만이 저장되어서는 항상성(Consistency)이 깨어져 버린다. 더구나 이들 Object의 ID체계는 2개의 키를 이용한 체계로 구성되어 있어 범용적으로 사용되는 String객체를 이용한 ID지정을 할 수 없었다.
고민 끝에 둘은 캐쉬의 키는 1종류 만이 존재할 수 있으나, 객체의 상속을 이용하면 2가지 종류의 객체를 모두 1가지로 표현할 수 있다는 사실에 착안하여, 다음과 같은 키 구조를 가져가기로 결정하였다.
[ Object ID 및 SancDoc, RecvSancDoc Structure에 대한 구조]
다른 부분에서 RecvSancDocApi를 이용하여 데이터 베이스를 억세스 하는 코드는 다음과 같다. 이는 RecvSancDocApi가 데이터 객체인 RecvSancDoc을 상속받기 때문에 가능한 코드이다.
RecvSancDocApi recvSancDocApi = new RecvSancDocApi();
recvSancDocApi.szDocID = szDocID;
recvSancDocApi.szRecvDeptID = szRecvDeptID;
recvSancDocApi.read(query);
이제 P군과 Z군이 작업을 해야 하는 부분은 RecvSancDocApi.read부분을 캐쉬를 사용하는 로직으로 변경하는 것이다.
단계 4 - 캐쉬 패턴의 적용
캐쉬 패턴을 코드에 반영하기 위해서 P군과 Z군은 우선 캐쉬 패턴을 적용하기 위한 기반 클래스들을 정의하였다. 캐쉬 패턴에 정의된 클래스 구성을 참조하여, 최상위의 캐쉬를 관리하는 클래스를 CacheManager로 정의하고 데이터 베이스에서 데이터를 가져오는 부분을 CacheFetcher로 정의하였다. 캐쉬에 포함될 객체는 ObjectID라는 객체로 정의하였다.
[ Cache 적용을 위한 Class Diagram ]
캐쉬 패턴을 참조하여 정의한 클래스의 목록은 다음과 같다.
l CacheManager.java : 캐쉬를 관장하고 외부에 인터페이스를 제공
l CacheConstant.java : 캐쉬 시스템에서 사용되는 상수들을 정의하는 부분
l CacheFetcher.java : Cache에 객체가 존재하지 않을 때, Fetcher를 이용하여 객체를 생성한다. a CacheFetcher는 다시 SancCacheFetcher를 호출한다.
l SancCacheFetcher : 데이터 베이스에서 문서 레코드를 읽고 하나의 객체로 생성하는 부분.
l ObjectID.java : 캐쉬에 저장하기 위한 키 객체
l SancDocID,RecvSancDocID : 2가지 종류의 객체를 구별하기 위한 객체
l Hashtable,Vector : Caching되고 있는 객체들은 Hashtable에 저장되며, 이들의 키 값은 저장 순서를 보존하기 위해서 Vector에 별도로 저장된다.
이제 이들 클래스들이 어떻게 동작할 것인가? P군과 Z군의 이 부분에서도 캐쉬 패턴에 포함되어 있는 코드를 참조하였다. 클래스의 상호작용을 UML의 Sequence Diagram으로 모델링 하였다.
[ Sequence Diagram Case 1 ? Cache Hit ]
캐쉬의 저장소(Repository)에 문서객체가 존재하는 경우에는 저장소에서 문서 객체를 가져와 요청한 어플리케이션에 반환한다.
[ Sequence Diagram Case 2 ? Cache Fail ]
캐쉬의 저장소에 문서 객체가 존재하지 않은 경우에는 건네 받은 ID를 이용하여 데이터 베이스에서 문서 객체를 생성한다. 그리고, 이를 저장소에 저장하고 어플리케이션에 반환한다.
문서들을 메모리에 저장하는 캐쉬 저장소(Cache Repository)에서
1. 지정한 개수 만큼의 개체 수 유지.
2. 새로운 개체를 저장소에 저장할 경우, 특정 알고리즘에 의하여 Repository에서 가장 덜 유용한 개체를 삭제
의 2가지 기능을 지원해야 한다.
그랜드(Grand)의 Pattern in JAVA에서는 Doubly Linked List를 사용하여 캐쉬 저장소를 구성하였지만 우리가 구현할 때는 Vector와 Hashtable을 이용하는 구조로 변경하여 코드를 간결하게 만들었다. 저장소에서 가장 덜 유용한 객체로 가장 오래된 객체를 선택하여 저장소에서 삭제하는 알고리즘을 사용하였다.
if( CacheRepository.size() < MAX_CACHE_SIZE ) {
CacheRepository.put( id, obj );
CacheRepositoryIndex.addElement( id );
}
else {
// 맨위에 있는 id와 object를 삭제한다
ObjectID oid = (ObjectID)CacheRepositoryIndex.elementAt(0);
CacheRepositoryIndex.removeElementAt(0);
CacheRepository.remove( oid );
// 새로운 id와 object를 추가한다
CacheRepository.put( id, obj );
CacheRepositoryIndex.addElement( id );
}
캐쉬의 사이즈보다 적을 때 경우에는 저장소(Repository)에 저장되지만, 캐쉬 사이즈보다 큰 경우에는 저장소에서 가장 덜 유용한 객체를 삭제하고 저장소에 저장한다.
최종적으로 RecvSancDocApi.read 함수를 이용한 접근 방식근이 다음과 같이 캐쉬 객체를 이용한 접근 방식으로 변경되었다.
CacheManager cacheManager = CacheManager.getInstance();
RecvSancDocID id = new RecvSancDocID( szDocID, szRecvDeptID );
Object obj = cacheManager.fetchCache( id, query );
캐쉬 저장소에 객체가 존재하지 않을 때, 데이터 베이스에서 객체를 가지고 오는 부분인 CacheFetcher는 기존의 데이터 베이스 억세스 로직을 가장 널리 알려진 “Cut & Paste” 방식으로 잘라 붙여 구현하였다.
캐쉬가 드디어 구현되었다. 디자인 패턴을 이용하여 작업을 시작한지 이틀째의 일이다. P군과 Z군은 내심 기뻤다. 캐쉬는 지금까지 구현한 것들 중에서 상당히 복잡한 수준의 코드였기 때문에 코드 작성에만 4일 정도를 예상하고 있었으나, 패턴의 도입으로 예정된 시간의 절반수준에 작업을 일단락 지을 수 있었기 때문이다.
코딩 작업은 여기서 일단락되었다. 필요한 코드는 이틀간에 걸쳐 모두 작성되었다. 이제는 컴파일 과정을 거쳐 실행시켜 볼 때가 다가 왔다. 코드를 담당하고 있는 Z군의 손에 의해 자바 파일은 클래스 파일로 컴파일 되었다. 컴파일 하고 시스템을 다시 기동하자 정상적으로 작업이 수행되는 것이 아닌가?
“놀라운 걸, 오류 없이 바로 수행되는데..” 라고 P군과 Z군은 즐거웠다. 프로그래머라면 누구나 스스로가 만든 코드가 오류 없이 수행되는 상황이 매우 즐거울 것이다..^^;
단계 5 ? 성찰
앞으로 다른 부분에도 패턴을 도입하기로 결정한 P군과 Z군은 함께 패턴의 도입에서 얻어진 결과를 정리하여 패턴 도입 시에 요구되었던 부분과 도입으로 인한 효과를 나열하여 보았다.
캐쉬 패턴을 도입하여 얻어진 긍정적인 효과들
l 캐쉬 패턴의 클래스 관계를 적용하여 클래스 디자인 시간 및 개발 시간의 최소화 되었다.
l 고려해야 할 이슈 사항들을 캐쉬 패턴의 도큐먼트에서 미리 발견하고 검토하여 프로그래밍 로직에 반영함으로써 코드의 신뢰성 증대 되었다.
l 캐쉬 패턴의 문서를 이용하여 P군과 Z군의 상호간에 캐쉬에 대한 공통적인 개념과 VIEW를 제공하여 개발 의견 교환이 쉽게 이루어 졌다.
l 캐쉬의 클래스 들이 패턴을 도입하여 완전한 캡슐화가 가능하였다. 즉, 캐쉬에서 킥오프 시키는 부분인 가장 이전에 사용되었던 객체를 제거하는 부분과 객체를 데이터 베이스에서 가져오는 획득하는 부분들이 완전히 캡슐화 되어 분리되었다. 추후 성능개선을 위해서 새로운 알고리즘을 도입하려 할 경우에도 캡슐화 된 부분들만 새로운 모듈로 교체함으로써 개선이 가능하다.
l P군이 Z군에게 캐쉬의 로직을 설명할 때, 패턴 문서를 참고하여 설명하여 Z군이 이해하기 수월하였다. 추후 모듈의 개선이나 담당자가 변경되어도, 패턴을 이용한 부분의 코드는 패턴의 설명을 참조하여 쉽게 이해할 수 있다.
캐쉬 패턴을 도입하려 하였을 때 필요했던 부분들.
l 캐쉬 패턴을 도입하는 것은 단순한 작업이 아니었다. 캐쉬 패턴에 나타난 클래스들을 그대로 P군과Z군의 로직에 반영할 수 없었다. 반영하기 위해서는 많은 부분의 알고리즘과 로직을 수정하여야 하였다. 디자인 패턴에 관한 이해 또한 사전에 충분히 되어 있어야 했다. 그렇지 않았다면 패턴을 도입할 생각조차 하지 못했을 것이다.
l 클래스들이 구조화가 상당 부분 진행되어 있어야만 패턴을 도입할 수 있다. P군과 Z군의 이번 작업에서도 데이터 베이스를 억세스하는 로직이 RecvSancDocApi와 SancDocApi의 두 부분에 집중되어 있었기 때문에 패턴을 도입하는 것이 가능하였다. 집중되지 않고 곳곳에 산재 되어 있었다면, 도입과정에서 시일이 매우 소모되었을 것이며, 경우에 따라서는 도입이 불가능 할 수도 있다.
l 클래스들의 역할과 상호작용이 분명하게 결정되어 있어야 한다는 것이다. 이로써 기존의 로직에서 어느 부분에 어떻게 적용할 것인가를 결정할 수 있다.
l 이번 사례는 디자인 패턴 도입의 단편적인 사례이다. 도입 시에 프로그래밍 로직이 복잡하면 복잡할수록 구성이 힘들어 질수록 패턴의 적용이 어렵다. 코드가 복잡해지면 이해하기 힘들어진다. 이러한 코드일수록 단순화 하는 작업이 필요하다.
캐쉬 패턴 도입의 최종 결론
P군과 Z군은 이번 패턴의 도입을 통해서 패턴은 유용하였으며 노력과 시간을 들여 공부할 가치가 있음을 새삼 느꼈다. 무엇보다 새롭게 추가한 캐쉬 기능이지만 캐쉬 패턴에 동작 원리가 명확하게 기술되어 있어 오류를 발생시키지 않을 것이라는 확신을 코드 완성 시점부터 가질 수 있었다. 이전의 P군과 Z군이 작성했던 많은 부분들이 안정화 되기까지 상당한 시간과 노력이 소모되었음을 고려하였을 때 이는 매우 고무적인 일이었다. 작업 수행기간 조차 단축되지 않았던가?
글을 마치면서
프로그래밍의 문제는 항상 원칙을 벗어나지 않는다. 가장 근본적인 원칙은 Coherence와 Coupling의 문제이다. Coherence를 높이고, Coupling을 얼마나 적게 만드는가? 에 따라 소프트웨어 개발의 승패가 좌우된다고 하여도 과언이 아니다. Coupling이 적은 코드는 유지 보수 작업과정에서 필요한 인력과 시간이 Coupling이 많은 코드에 비해 현저하게 줄어든다. 안정성과 신뢰성도 Coupling이 적은 코드에서 최대로 발휘된다.
간단하게 생각하여 보자. 사소한 기능하나를 추가하기 위해서 이곳 저곳 시스템의 전반적인 부분을 모두 고쳐야 한다면, 이 조그마한 기능 하나의 추가로 인해서 발생하는 부작용이 얼마나 될 것 인가를 상상해 보라.
Coupling을 줄이고 Coherence를 높이는 수단은? 정확한 디자인과 논리 정연한 프로그램의 구조, 그리고 고정되어 있는 인터페이스의 도입도 수단의 한 종류이며, 이책에서 이야기하는 디자인 패턴도 한가지 수단이다. 객체의 도입에 있어서 패턴이라는 개념에 근거한 클래스들의 역할의 지정과 상호관계의 설정은 어떤 클래스들이 무엇을 담당하고, 어떤 순서로 상호작용이 발생하는지를 명백하게 밝혀준다.
좋은 코드를 만들고 좋은 시스템을 만들고 프로그래머 스스로가 자신이 만든 코드에 자부심을 느끼기 위해서는 언제나 노력이 필요하다. 필자들은 디자인 패턴을 처음 접하였을 때부터 확신할 수 있었다. 바로 우리가 공부해야 하는 것이며, 노력한 부분 이상의 보답이 있을 것이라는 점을…
그리고, 그 신념은 공부가 계속될수록 더욱 확고해지고 있다.
참고 문헌
l Design Pattern : Elements of Resuable Object-Oriented Software,1995 : Erich Gamma, Rechard Helm, Ralph Jonson, Jojn Vlissides.
l Pattern in JAVA, volume 1,1999 : Mark Grand
l Anti Patterns : Refactoring Software,Architectures and Projects in Crisis 1998 : William H. Brown & Others
l Applying Uml and Patterns : An Introduction to Object-Oriented Analysis and Design,Craig Larman
l Refactoring : Improving the Design of Existing Code,1999 : Martin Fowler
l Object Solutions,1996 : Grady Booch
l www.antipatterns.com
'Programming > Design Pattern' 카테고리의 다른 글
[펌] The Service Locator Pattern (0) | 2006.01.21 |
---|---|
[펌] The Service Locator Pattern (0) | 2006.01.21 |
[펌] The Message Façade Pattern (0) | 2006.01.21 |
[펌] The Session Façade pattern (0) | 2006.01.21 |
[펌] The Business Delegate pattern (0) | 2006.01.21 |