Pattern[1/4]

|

제목

디자인 패턴의 적용(I)

일련번호

 

배포

마소 9월호 연재 기사(1)

작성일

2000-08-14

분류

Public

변경일

2000-08-16

상태

Draft

작성자

윤동빈, 백창현

 

글을 시작하면서

디자인 패턴이란 무엇인가?

프로그래밍을 작성할 때 누구나 나름대로 많은 생각하는 바가 있을 것이다. 수 많은 프로그래머들이 있었고 그들은 유사한 문제점을 나름대로 고민하여 좋은 해결책을 제시하였다. 필자들의 경우에는 다른 프로그래머들의 좋은 소스 코드들을 참조하고 그들이 많은 시간을 들여 구현한 클래스의 구조를 필자들의 코드에 반영하고 싶었다.

좋은 코드를 참조하면 좋은 코드를 만들 수 있다는 것은 당연한 진리이다. 그러나, 진정으로 좋은 코드는 쉽게 접할 수 없다. MS-Word나, Exchange등의 소스코드를 접할 수 있는가? 행여 접한다고 하여도 그 코드를 보고 코드 내부에 담겨있는 철학을 이해할 때까지는 얼마나 많은 시간이 걸리겠는가

그러면, 우리 프로그래머들이 서로의 좋은 기법을 참조하고 사용할 방법은 없는 것인가? 객체 지향 프로그래밍에서 가장 중요하게 여겨지는 것이 "코드의 재활용성"에 있음에도 불구하고, 다른 이들의 코드와 구조를 왜 그리도 참고하기 힘든 것인가?

만약, 우리가 파일을 관리하는 파일 관리 시스템을 구현한다고 생각해 보자. 파일과 디렉토리, 링크를 구현하기 위한 데이터 구조를 어떻게 구성할 것인가? 파일을 캐쉬하는 캐쉬 시스템은 어떻게 구성할 것인가? 윈도우 탐색기에서 파일을 선택하면 왼쪽 부분에 자그마하게 나타나는 미리 보기 부분들은 어떻게 구현할 것인가? 무엇보다 구현한 시스템에 다른 요구 사항들이 발생 하였을 때, 그 부분들을 잘 수용할 수 있을 것인가? 이런 수많은 난제들이 우리의 앞에 도사리고 있다. 과연 어떻게 해쳐나갈 수 있을 것인가?

결론부터 말하자면, 디자인 패턴은 이런 많은 고민들을 직접적으로 해결하여 준다. 파일과 디렉토리, 링크를 구현하기 위한 데이터 구조를 이렇게 가져가면 효과적이면서도 추후에 확장 가능한가에 관한 해답을 클래스 들의 구현방법으로 제시하고 있다.

디자인 패턴을 공부한다라는 의미는 무엇일까? 필자들의 생각에는 좀 더 훌륭한 사람들의 좋은 디자인 형태를 배운다라는 의미로 해석하고 싶다. 2-3년후에 시행착오로 알아낼 지식을 이 디자인 패턴을 공부함으로써 그 기간을 앞질러 나갈 수 있는 것이다. 또한 좋은 디자인 형태를 공부해 둠으로써 자신의 코드를 좋게 다듬어 놓을 수 있고 이러한 코드들이 모여 전체 프로젝트의 품질이 크게 향상될 수 있다.

물론 디자인 패턴의 종류와 구조를 암기하는 것 자체도 쉬운 일은 아니다. 그러나 이것은 시간만 들이면 다 할 수 있는 일이다. 실제 어려운 일은 우리가 접하는 문제 영역(Problem Domain)의 특정 컨텍스트(Context)에서 적용되어질 수 있는 디자인 패턴이 무엇인지를 알아내는 일이다. 실제 문제에서는 여러 디자인 패턴이 혼합되서 등장하며 또한 거의 대부분 패턴 책에 소개되어 있는 내용이 약간씩 변형되어 적용되게 되어 있다. 우리가 바둑을 두더라도 책에 나온 정석대로 두어오는 상대방은 없는 것처럼 말이다. 또한 우리는 패턴을 잘 못 적용했을때의 부작용도 항상 염두에 두어야 한다.

디자인 패턴이란  바로 변화에 대한 대비이다.

우리가 학교에서 나온 숙제를 가지고 디자인 패턴을 적용할 필요는 없다. 마찬가지로 그 프로젝트가 일회성이고 스펙의 변화가 없다면 패턴이 적용될 필요가 없다. 그러나 우리는(제품 개발로 먹고사는 대부분의 소프트웨어 개발자가 여기에 속한다) 대부분 프로젝트 완료후의 매우 긴 기간의 유지 보수 기간을 가진다. 스펙은 심지어 개발기간에도 변하며 사용중에도 부지런히 변한다. 코딩을 하다가도 스펙의 잘 못된 점이 발견되기도 한다. 즉 스펙은 언제나 변할 수 있는 여지가 있다.

개발자들이 개발을 할때는 스펙이  문제 영역(Problem Domain)의 컨텍스트가 된다. 그안에서 최적화된 솔루션을 내놓는 것이 개발자의 할 일이다. 그러나 이 스펙은 계속 변하므로 개발자는 항상 이에 대비하여 디자인을 해야 한다. 바로 개발자의 연륜은 여기에서 판가름된다. 경험있는 개발자는 자신의 코드를 항상 일반적으로 만들려고 노력한다.

디자인 패턴은 변화되어야 할 클래스의 수를 최소화시켜 준다. 커스터마이징되는 부분과 뼈대가 되는 부분이 디자인때에 이미 분리가 되기 때문이다.  많은 디자인 패턴의 캐털로그를 하나 하나 음미해 보자. 모든 디자인 패터들의 일관된 주제는 바로 이 변화라는 것을 알 수 있을 것이다.

디자인 패턴을 적용할 때 제일 중요한 세가지 규칙

디자인 패턴을 공부하다 보면 그 기저에 흐르는 규칙이 있음을 알게 된다. 필자들은 그것을 다음의 세가지로 정의하고 싶다.

è     구현 클래스가 아니라 인터페이스를 가지고 프로그래밍한다.

[인터페이스를 바탕으로 하는 클래스 호출]

caller 오브젝트는 callee 오브젝트를 직접 보는 것이 아니라 항상 interface를 통해 보도록 한다.  Callee의  내부 구현이 변화하더라도 caller는 영향을 받지 않도록 하기 위해서이다. 이때 interface는 가급적 변화하지 않아야 하므로 interface의 디자인은 최대한 일반적으로 고안되도록 신경을 써야 한다. 

모든 구현이 이처럼 interface를 통할 필요는 없다. 예를 들어 utility 클래스 같은 경우는 직접 구현 클래스를 써도 무방하다. 그러나 callee 클래스가 복잡한 비즈니스 로직을 담는 비즈니스 객체인 경우에는 중간에 interface를 디자인해 두는 것이 시간이 흐를수록 좋다라는 것을 많이 느낄 수 있다.

è     상속(Inheritance)이 아니라 Delegation을 사용한다.

OO의 기본 개념에 상속이 있다. 그럼 이 상속을 쓰지 말라는 것은 또 무슨 말인가? 상속은 공통된 특징이 있는 여러 객체들을 묶을 때 많이 사용한다. 그러나 Sub Class의 입장에서 보면 불필요한 메소드를 단지 Super Class에 존재한다라는 사실만으로 접근가능하게 되어 버리는 경우가 생기게 된다. 즉 Sub Class와 Super Class의 구조가 컴파일시에 서로 엮이게 되어 런타임시에는 이를 바꿀 수가 없다.

[상속]

 

delegation은 super class가 내부 필드로 존재하게 되고 필요할때마다 이 필드에 접근하게 된다. 이렇게 되면 런타임에 내부 필드를 자유롭게 바꿀 수 있게 되어 해당 필드를 접근함으로써 얻어지는 행위또한 바뀌어지게 된다.  즉 delegation을 통해 런타임의 행위가 바뀌게 되는 것이 최대 이점인 것이다.

[Delegation]

 

이러한 이유로 GoF에서는 상속을 white-box reuse라 하고 delegation을 black-box reuse라고 한다.

 

è     Coupling을 최소화함으로써 추후의 변화를 국부화한다.

이 대목은 어떻게 보면 원칙론에 가까운 말이다. 어느 하나의 기능 변화가 전체 클래스 구조를 바꾸거나 혹은 많은 부분에 걸친 변화를 야기한다면 뭔가 디자인이 잘 못 되어진 것이다. 그리고 여러 부분을 중복되서 수정하고 있다면 이또한 뭔가 잘 못 되고 있는 것이다. 모든 디자인 패턴들이 일관되게 말하는 있는 것 그것은 Coupling을 최소화하라이다. 아직도 무슨 말인지 감이 안온다면 그것이 바로 Design Pattern을 구체적으로 공부해야 하는 동기가 될 것이다.

 

실제 개발과 디자인 패턴의 연결

이 글은 여지껏 디자인 패턴의 책들이 해 오지 못했던 실제 개발과 디자인 패턴의 연결을 모색해 보고자 하는 시도이다. 디자인 패턴은 실제로 디자인 단계에서 발견되어 적용되기도 하지만 더 많은 경우에는 문제가 분명해 지고 수 많은 불합리성이 드러난 후에 비로소 발견되기도 한다. 즉 디자인 패턴은 bottom-up으로 적용되는 경우가 더 많다.

이처럼 어떠한 이유로 해서 좀 더 효율적인 코드로 변화하는 것을 Refactoring이라 하며 실제 개발에서는 이 Refactoring단계에서 디자인 패턴이 적용되는 경우가 많은 것이다. Refactoring을 당연시 여기자. 어차피 주어진 스펙에서 최적의 해를 찾는 것이 개발자의 할 일이라면 스펙이 변화할 때 디자인도 바뀌는 것이 당연한 것이다. 또한 구현하기 전에는 보이지 않았던 많은 문제들 때문에 디자인 자체가 잘 못 된 경우도 많기 때문이다. 꾸준한 Refactoring을 통한 합리적인 디자인 패턴의 적용, 이것이 진정한 정-반-합의 개발 프로세스일 것이다.

 

 

 

 

 

 

 

 

 

 

 

 

Problem Domain

Class Design

Design Patterns

Refactored Design

Refactoring Rules

Refactoring

빈번히 변함

 개발

1.        그림 1 디자인 패턴의 적용을 통한 개발 프로세스

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


[ BOX 5 Refactoring ]

[ BOX 6 Anti-Pattern ]

이 글에서 사용하는 것들

이글은 기본 언어로 자바를 사용하며 여러 다이어그램은 Rational Rose의 UML을 사용해서 표현한다. 필자들은 자바로 된 디자인 패턴 책은 Patterns in Java를 주로 참조하였으며 UML에 대해서는 Applying UML and Patterns를 참조하였다.

이 글은 총 4편으로 전개되는데 이번 호에서는 웹 애플리케이션을 개발하면서 겪었던 실례를 중심으로 해서 디자인 패턴의 적용에 대해 논해 보았다. 실례에 등장하는 디자인 패턴에 대해서는 따로 박스 기사로 간단히 설명해 나가기로 하겠다.

 

 

 

 

 

 

 

 사례 연구 첫번째 : 데이터베이스 처리 객체

여기서 소개되는 디자인 패턴의 적용사례는 웹 애플리케이션 개발에 있어서 데이터베이스 처리 객체 개발의 사례이다.  데이터베이스 처리 객체란 데이터베이스 연결을 맺고 SQL을 데이터베이스에 전달하며 그 결과를 얻을 수 있도록 하는 객체를 의미한다. JDBC를 통해 구현되어질 수 있고 JDBC API들의 Wrapper라고 할 수 있다.

초기 개발시에는 디자인 패턴이 적용되지 않았었고 후에 패턴이 적용되었다. 이러한 과정에서 디자인 패턴이 적용되지 않은 코드가 후에 얼마나 많은 문제를 일으킬 수 있는지를 잘 경험할 수 있었고 만일 초기부터 패턴에 입각한 엄밀한 설계가 이루어졌다면 보다 쉽게 기능들이 추가되었을 것이라는 생각이 들었다. 나개발은 데이터베이스 처리 객체의 개발자고 후에 컨설턴트로 나오는 나모델러는 필자가 고안해낸 가상의 인물이다.  처음에는 디자인 패턴에 입각하지 않은 나개발의 개발 사례를 이야기하고 이후 나모델러의 조언 형식으로 디자인 패턴을 적용하는 개발을 이야기하겠다. 중요한 것은 요구 사항이 변하면서 디자인 패턴에 입각하지 않은 개발과 디자인 패턴에 바탕을 둔 개발이 어떻게 대처하는 가를 살펴보는 것이다.

여기서 적용되는 디자인 패턴은 모두 세가지로서 Façade Pattern, Factory Method Pattern, Strategy Pattern이다. 각각의 패턴들에 대한 설명은 글중에 별도로 설명될 것이다.

 

1. 나개발에게 주어진 문제 : 데이터베이스 처리 객체를 만들어라.

나개발은 입사한지 1년되는 신참개발자이다. 이번에 자바를 기반으로 하는 웹 애플리케이션 개발에 참여하게 되었다. 나개발이 맡은 모듈은 주로 데이터베이스에 관련된 부분이었다.

나개발이 맡은 데이터베이스처리 부분에 대한 초기 요구사항은 다음과 같이 정리할 수 있었다.

l         데이터베이스에 대한 조회(select), 변경(update), 추가(insert)가 가능할 것

l         비즈니스 로직에서 JDBC 객체를 직접 다루는 것을 허용하지 말 것.

l         오러클만 지원해도 무방

JDBC 객체를 직접 다루지 않고 데이터베이스 처리 객체에서 이를 흡수하는 것이 사실 나개발이 맡은 업무의 핵심이었다. 독자들중에는 EJB의 CMP(Container Managed Persistence) Entity Bean과 여기서 만드는 DB 처리 객체가 어떻게 틀리는지 궁금해 하실 수도 있다. 이전의 많은 프로젝트에서도 스스로 CMP Entity Bean에 해당하는 클래스를 직접 제작하고 있으며 EJB에서는 그것이 표준화되었다. 그러나 아직 CMP Entity Bean이 안정화되어 있다고 보기는 어렵고 또한 외부키(FK)관계를 쉽게 기술하기 어렵기 때문에 많이 쓰이지는 않고 있다. 그리고 BMP Entity Bean으로 만든다면 내부 JDBC 처리 로직을 구현해야 하는데 이때 쓰일 수 있는 객체가 바로 여기서 만들어지는 데이터베이스 처리 객체라고 할 수 있겠다. 즉 테이블에 직접 매핑되는 그런 상위수준의 객체가 아니라 JDBC의 처리를 대신하는 낮은 수준의 객체라고 보면 정확하다.

나개발은 이전에도 자바로 짧은 프로그램을 만들어 본 경험이 있었기 때문에 자바의 문법에는 익숙해져 있는 상태였고 간단한 SQL과 JDBC 프로그래밍도 어느정도는 해 보았었다.

단계 1 ) JDBCHelper의 디자인

 

나개발은 웹 사이트에서 JDBC API 문서를 받아 읽어 본후 다음과 같은 구현 전략을 세웠다.

JDBC API는 하나의 SQL을 실 행해서 결과를 얻기까지 Connection, Statement, ResultSet을 차례대로 얻어야 한다. 이 얼마나 불편한가? 나는 이러한 JDBC 객체에 대한 JDBC Helper 클래스를 만들어서 다른 개발자들이 오직 이 JDBC Helper만 보고 데이터베이스 처리를 할 수 있도록 해야 겠다.

[JDBCHelper.java 첫번째 버젼의 Class Diagram]

 

나개발이 개발한 모듈을 사용하는 다른 개발자들도 복잡한 JDBC 오브젝트의 사용법을 다 몰라도 사용가능했기 때문에 긍정적인 반응을 보였다. 나개발은 자신감이 생겼다. 이정도 개발쯤이야.

JDBCHelper 생성자에서는 JDBC의 Database Connection Pooling을 내부적으로 사용하여 Connection을 매번 새로 연결하지 않아도 되도록 구현했다.

JDBCHelper의 사용예는 다음과 같다.

JDBCHelper helper = new JDBCHelper(host, ip, );

helper.connect();

helper.select(select name from table where id = 1’”);

if(helper.next()) {

         String name = helper.getString(name);

}

helper.close();

단계 2) PreparedStatement의 지원

개발 프로그램이 기업내의 대규모 인원이 사용하는 웹 애플리케이션인 이상 스트레스 테스트를 실시하기로 했다.  오러클에 트레이스를 걸고 모든 개발자들이 달라 붙어 여러 기능에 대해서 동시 실행을 해 보았다. 사실 일정만 좀 여유가 있었다면 스트레스 테스트 프로그램도 좀 만들어 보련만 프로젝트 막판이라 노동력으로 때울 수 밖에 없었다. 테스트 결과는 별로 좋게 나오지 않았다.

이유를 살펴 보니 모든 SQL이 동적 SQL이라는 것에 있었다. 동적 SQL이란 SQL에 속한 값들이 매번 달라지는 것을 의미한다. 우리가 통상적인 SQL 문장을 만든다면 이것은 동적 SQL이 된다. e.g select name from member where id = A123

정적 SQL은 precompile된 SQL을 이용하는 것으로 값은 따로 주게 된다.  e.g select name from member where id = ?

나개발은 다소 당황했지만 JDBC API를 다시 살펴 본 결과  PreparedStatement를 사용해서 위의 문제를 해결할 수 있음을 발견했다.

나개발은 기존의 JDBCHelper의 소스를 찬찬히 다시 들여다 보면서 PreparedStatement 기능을 어떻게 넣는 것이 좋은가에 대해서 고민했다.  나개발은 최대한 빨리 끝내서 프로젝트의 완료일에 영향을 주고 싶지 않았다. JDBCHelper에 모든 PreparedStatement관련 기능들을 메소드로 추가하기로 했다. SQL은 PreparedStatement를 만들 때 필요하므로 preparedStatement(String sql)이라는 메소드를 추가시켜서 내부적으로 PreparedStatement를 만들도록 했다. 그후 pstmtSetXXX()라는 메소드를 이용해서 패러미터를 설정한 후 pstmtSelect()나 pstmtUpdate()를 실행한다. ResultSet이 얻어진 후에는 Statement에서 했던 방식으로 GetType(int index)나 GetType(String column)을 사용하면 된다.

나개발은 이 단계에서  슬슬 JDBCHelper라는 클래스가 이해하기 어렵게 되었음을 느끼게 되었다. PreparedStatement와 Statement가 같은 클래스에 있기 때문에 다른 개발자들이 Statement에서 패러미터 설정을 하기도 하고 PreparedStatement를 설정해 놓고 Statement의 select를 호출하는 실수를 종종 했다. 이러한 실수를 원천적으로 막을 수는 없을까 잠깐 생각해 보았지만  더 많은 기능추가 사항 때문에 나중에 생각해 보기로 했다.

[JDBCHelper.java 두번째 버젼의 Class Diagram]

 

 

 

 

 

 

 

 

 

 

 

단계 3) CallableStatement의 지원

나개발의 JDBCHelper를 사용하던 채대리는 Stored Procedure 기능이 없음을 알게 되고 곧 이 기능을 요청했다. StoredProcedure는  JDBC의 CallableStatement에 의해서 쉽게 구현되어질 수 있었고 PreparedStatement와 CallableStatement는 기능적으로 아주 흡사했다. 나개발은 개발자들의 금도끼(Golden Hammer)라고 할 수 있는 CUT&PASTE 개발방법론을 사용하기로 맘먹었다.

CallableStatement는 입력 패러미터가 있고 출력 패러미터가 있다. 입력 패러미터는 PreparedStatement에서 했던 방식대로 cstmtSetType()형태로 만들고 내용은 다른 메소드에서 배껴 왔다. 출력 패러미터는 cstmtRegisterOutParameter()를 통해서 등록된다. 실행은 cstmtUpdate()를 통해서 된다.

[JDBCHelper.java 세번째 버젼의 Class Diagram]

 

 

이미 많은 모듈들이 JDBCHelper를 사용하고 있었고 기존 소스에 영향을 안주면서 CallableStatement 기능을 추가했으므로 상당히 흐뭇함을 나개발은 느낄 수 있었다. Statement, PreparedStatement, CallableStatement를 하나의 클래스에 처리하는 JDBCHelper는 얼마나 막강한가를 다시 한번 머리속에 뒤새겼다. 몇몇 개발자들은 이 거대한 클래스의 내부 소스가 너무 이해하기 어렵다고 투덜되었지만 나개발의 모듈에 대한 강한 자부심으로 인해 뭐라 말할 수는 없었다.

드디어 프로젝트는 성공적으로 릴리즈되었고 고객의 반응도 괜찮았다. 나개발도 오랜 기간의 고생을 끝내고 유쾌한 회식자리에 동참할 수 있었다.

단계 4) US7ASCII의 처리

점차 많은 수의 제품이 사이트에 설치되기 시작했고 적지 않은 고객이 다음의 사항을 요청해 왔다. 가장 중요한 요구 사항은 기존에 오러클 DB가 있고 이 오러클 DB를 이용해서 제품을 사용하고 싶다는 요청이었다.

문제는 기존에 설치된 오러클들은 모두 NLS_LANG이 US7ASCII였고 기존에 작성된 MIS 프로그램 때문에 NLS_LANG을 바꿀 수 없는 경우가 많았다. 오러클의 NLS_LANG이 US7ASCII에서는 나개발이 개발한 제품에서 한글이 깨져 보였다. 영업 부서에서는 이미 기계약된 사이트에서는 US7ASCII를 사용하고 있어서 꼭 지원이 되어야 하는 기능이라고 했고 기술지원 부서에서도 사이트 담당자의 이러한 요구사항을 전달해 왔다.

며칠의 디버깅과 뉴스 그룹등을 뒤져서 나개발은 한글 코드 변환하는 방법을 알아 내었다. 

// Java Unicode -> US7ASCII DB

String oldSQL = select id from member where name = 홍길동’”;

String newSQL = new String(oldSQL.getBytes(), 8859_1);

ResultSet resultSet = statement.executeQuery(newSQL);

//US7ASCII DB -> Java Unicode

if(resultSet.next()) {

String oldStr = resultSet.getString(1);

String newStr = new String(oldStr.getBytes(8859_1));

}

 

핵심 아이디어는 데이터베이스에 넣을 때는 시스템 디폴트 인코딩 형태로 바이트 배열을 얻어낸 후 이를  ISO8859_1로 바꿔 넣어야 하고 데이터베이스에서 값을 얻어 올때는 반대로 ISO8859_1형태를 시스템 디폴트 인코딩 형태로 바꾸어 주면 된다.

SQL 문장 자체와 CallableStatement, PreparedStatement에서 사용되는 패러미터에 대해서는 코드 변환이 일어나야 하며 이 NLS_LANG에 대한 정보를 보고서 각각 다르게 처리해야 된다는 결론을 얻었다.

나개발에게 떠오른 아이디어는 JDBCHelper 생성자에 NLS_LANG정보를 넘긴다음 코드 변환이 필요한 부분마다 switch문을 넣어 NLS_LANG이 US7ASCII인 경우에는 코드 변환을 하자는 것이었다. 아주 적은 부분만의 수정으로 소기의 목적을 달성할 수 있을 것 같았다.

그런데 문제가 생겼다. 코드를 보았더니 Statement, PreparedStatement, CallableStatement를 처리하는 부분에 대해서 모두 수정을 해 주어야 했기 때문이다. 결국 모든 코드 변환이 필요한 부분에 다음의 switch 문을 넣어 주었다.

// sql = SQL 문장

switch(NLS_LANG) {

         case US7ASCII :

                     sql = new String(sql.getBytes(KSC5601), 8859_1);

                     break;

         case KSC5601 :

         default :

                     // do nothing

}

모든 경우에 대해 테스트가 너무 힘들다고 판단한 나개발은 일단 내보내고 보자는 식으로 패치 릴리즈를 했지만 CallableStatement의 String 패러미터 설정이 잘 못 되어서 한글이 깨지는 문제가 보고되었다. 에러는 재빨리 수정을 해서 다시 내보냈지만 이 에러 원인을 찾아 내기까지 기술지원담당자와 나개발 본인이 하루를 꼬박 보낼 수 밖에 없었다. Switch문이 들어가는 문장이 너무 많아서 깜빡 잊고 cstmtSetString(int index, String string)에는 코드 변환 로직을 넣지 않았던 것이다. 나개발은 뭔가 효율적인 방법이 있을 것 같은 생각이 들었지만 다른 기능 추가 스케줄에 쫓겨 곧 이일은 잊어 버렸다.

단계 5) MSSQLServer의 처리

JDBCHelper는 계속적으로 버그가 수정되어졌지만 그 디자인 골격은 쉽게 바뀌어지지 않았다. 왜냐면 JDBCHelper를 사용하고 있는 클래스들이 많은 관계로 JDBCHelper의 하나의 변환은 다른 개발자들의 수많은 모듈들이 변화해야 한다는 것을 의미했기 때문이다.

제품이 더 많은 사이트를 확보해 나가면서 영업부는 좀 더 공격적인 영업을 개시하기로 했다. 중소규모의 사이트도 공략을 해 나가면서 MSSQLServer를 기반으로 하고 있는 곳이 늘어났다. 특히나 하반기 영업의 교두보라 할만큼 전략적으로 중요하다고 생각되어진 사이트가 MSSQLServer를 사용하고 있었다.

이제 개발팀에게는 MSQLServer의 지원이 당면과제가 되었고 여기에는 크게 세가지 문제가 있었다. 첫째로 MSSQLServer를 지원하도록 Connect() 메소드를 수정하는 것이었다. 둘째는 MSSQLServer용 JDBC Driver를 선정하는 일이었다. 셋째는 비즈니스 로직에서 쓰인 SQL들을 MSSQLServer용으로 바꾸는 일이었다.

첫번째 문제는 Connect()내에서 switch를 만들어 주어 간단히 해결할 수 있고 둘째 문제는 제품 선정의 문제이므로 이것또한 하루정도의 웹 서핑으로 적절한 벤더를 선택할 수 있었다. 제일 어려운 것은 세번째 문제인데 SQL이 비즈니스 로직에 의해 생성되고 있고 오러클에 의존적인 SQL을 사용하고 있는 것이 문제였다. 이 오러클에 의존적인 SQL을 MSSQLServer용으로 변환하기 위해서는 각각의 기능 개발자가 MSSQLServer 담당자와 상호협의를 하여야 하는 번거로움이 있었다.

모든 SQL을 하나의 클래스나 파일로 빼자는 의견도 나왔지만 이것은 너무나 큰 Refactoring이었기 때문에 개발일정상 그대로 골격은 가지고 가기로 했다. 이것은 오러클에 의존적인 SQL은 몇 부분이 안된다라는 개발자들의 의견을 바탕으로 이 부분만 손보면 쉽게 MSSQLServer의 지원이 가능해 보였기 때문이다.

기본 디자인은 오러클에 의존적인 SQL을 꼭 써야만 하는 곳에서는 현재 DB가 오러클이면 그대로 유지를 MSSQLServer인 경우에는 따로 MSSQLServer용으로 코드를 만들어 넣었다. 다행히 MSSQLServer 담당자가 적극적인 성격의 소유자라 쉽게 마무리될 수 있었다.

 

String sql = null;

If(conf.DB_TYPE == MSSQLServer) {

         sql = .

}

else {

         sql =  .

}

 

그러나 나개발은 뭔가가 잘 못 된 것 같다라는 생각을 가지게 되었는데 앞으로 또 다른 제 3의 DB를 지원하게 될때에 지금과 같은 팀전체의 노동(?)을 반복하지 않기 위해서는 뭔가 구조적인 변경을 가할 필요가 있는 것처럼 느껴졌기 때문이다. 그러나 변경해야 되는 범위가 JDBCHelper를 사용하는 모든 비즈니스 객체까지 포함되기 때문에 refactoring을 하기가 쉽지가 않게 되었다. 바로 이점이 나개발을 한숨 짓게 만든 요인이었다.

2. 새로 영입된 아키텍트인 나모델러의 조언

나모델러는 지난 4년간  자바 개발과 프로젝트 컨설팅을 해왔었고 그 이전에도 OO관련 프로젝트를 진행해 본 경험이 있는 중견 개발자이다. 이번에 새로 회사에 영입되어 개발팀이 진행하는 프로젝트에 대한 기술적 컨설팅을 해주는 임무를 부여받았다.

나개발은 나모델러에게 현재 자신이 구현한 JDBCHelper와 여러 데이터베이스의 SQL을 처리하는 데에서 오는 오버헤드에 대해서 말했다. 나모델러는 나개발에게서 커피한잔을 얻어 먹은 다음 자신이 느끼는 사항들을 말하기 시작했다. 나모델러의 주요 관점은 디자인 패턴의 적용을 통한 객체간의 coupling의 최소화였다. 나개발은 모두 다섯 단계에 걸쳐 JDBCHelper에 큰 변화를 일으켰는데 각 단계별로 알맞은 디자인 패턴을 적용하게 되면 코드/디자인에 걸쳐 얼마나 생산성이 높아지는 지를 지적했다. 다음부터는 나모델러의 지적사항을 각 단계별로 정리한 것이다.

단계 1 ) JDBCHelper의 디자인 -> Façade Pattern의 적용

나모델러가 보기에 JDBCHelper의 의도는 좋았다고 평가했다. JDBC 객체들은 이해도 쉽지 않을 뿐더라 그 사용법도 배우기 어렵지만 보다 간략화된 기능을 가진 JDBCHelper를 통해 개발자들은 여러가지 잡다한 문제들을 신경쓰지 않아도 된 것이다. 이러한 잡다한 일에는 DB Connection의 문제들, 그리고 Statement와 ResultSet의 생성문제들이 있다. 이처럼 여러 객체들이 가지는 비즈니스 로직을 하나의 객체로 집중시키는 것을 Façade Pattern이라 하며 다른 말로는 Wrapper 혹은 Helper라고도 한다.

나개발은 의식을 하지 않더라도 이미 Façade Pattern을 JDBCHelper에 적용한 셈이었고 그 위력을 경험적으로 알고 있었다. 나모델러는 이 JDBCHelper의 사용의 편리성에 덧붙여서 비즈니스 로직이 JDBC에 바로 의존하지 않게 됨으로써 SQLJ의 지원이나 OODB의 지원 같은 일들이 독립적으로 벌어질 수 있는 기틀이 마련되었다고 평가했다. 나개발은 다소 우쭐했다. 뭐 이정도야 보통이죠.

 

[Box 1 Façade Pattern에 대한 설명 삽입]

단계 2) PreparedStatement의 지원 -> Factory Method Pattern의 도입

 

나모델러는 PreparedStatement가 도입된 시점에서 JDBCHelper를 두가지로 분할(decomposition)했어야 했다고 지적했다. Statement의 행위를 가지는 클래스 하나, PreparedStatement의 행위를 가지는 클래스 하나 이렇게 두개의 클래스를 만들어야 했다는 것이다. 클라이언트입장에서 이 두 클래스를 하나로 다루기 위해서 인터페이스를 또 하나 만든다. 그리고 이러한 두 개의 클래스를 만들어주는 Factory 클래스도 만들어져야 한다. 이렇게 되면 바로 Factory Method Pattern이 된다. 다음의 클래스 디자인을 보자.

 

 

 

 

 

1.        그림 2 PreparedStatement 지원하는 JDBC Helper - Factory Method Pattern 적용

 

 

JDBCHelper는 인터페이스로서 Statement와 PreparedStatement의 기능을 하는 메소드들을 동시에 갖는다. JDBCStatementHelper는 패러미터 설정 메소드(setType 메소드)가 호출되면 InvalidMethodException을 발생시키도록 디자인하며 Statement의 기능들을 처리한다. JDBCPreparedStatementHelper는 Connection, Transaction, Out Parameter Getter는 JDBCStatementHelper의 메소드를 그대로 사용하며 In Parameter Setter만 새로 정의하면 되기 때문에 JDBCStatementHelper를 상속받도록 한다.

이전에는 JDBCHelper를 생성자를 통해서 생성한후 connect()를 호출해서 사용하였다. 여기서는 JDBCHelperFactoryIF 인터페이스를 통해서 JDBCHelper를 생성되게 된다. JDBCHelperFactoryIF를 호출할 때 생성하려고 하는 JDBCHelper의 형태(HELPER_STATEMENT 혹은 HELPER_PREPARED_STATEMENT)를 지정해 준다.

다음은 JDBCHelperFactoryIF를 통해 JDBCHelper를 얻어오는 과정에 대한 예제 코딩이다. 구현과정에서 바뀐 점은 이전 코드에서는 prepareStatement(String sql)을 호출하여 먼저 PreparedStatement 객체를 내부적으로 만들었으나  바뀐 코드에서는 먼저 setType() 메소드를 사용해서 설정한 후 그후 select(String sql)이나 update(String sql)을 하도록 하였다. 이것은 Statement와 PreparedStatement의 사용법간의 일관성을 유지하기 위한 변경이다.

 

JDBCHelperFactoryIF factory = JDBCHelperFactory.getFactory();

JDBCHelper helper = factory.createJDBCHelper(host, port, SID, , JDBCHelperFactoryIF.HELPER_STATEMENT);

JDBCHelper helper = factory.createJDBCHelper(host, port, SID, .., JDBCHelperFactoryIF.HELPER_PREPARED_STATEMENT);

JDBCStatementHelper와 JDBCPreparedStatementHelper클래스를 분할함으로써 Statement와 PreparedStatement의 경계가 코딩시와 런타임시에 명확해 진다. 이렇게 되면 나중에 기능추가하기도 쉽게 되고 그만큼 에러가 날 확률도 적어지게 된다.

또한 클라이언트는 JDBCHelper라는 인터페이스를 사용하게 됨으로써 JDBCHelper의 내부 구현 코드와의 Coupling을 최소화하므로 추후 JDBCHelper을 상속하는  내부 구현 클래스들이 변화하더라도 영향을 받지 않게 되는 장점이 생겼다. 이 장점은 다음의 CallableStatement의 구현을 보면 명확해 진다.

[Box 2 Factory Method Pattern에 대한 설명 삽입]

단계 3) CallableStatement의 지원 -> Factory Method Pattern의 연속

나모델러는 다소 상기된 모습으로 세번째 단계에서 벌어질 일을 생각하며 잠시 슬쩍 웃음을 짓는다. 그리고 말을 이어나가기 시작했다.

나개발이 개발할때의 단계 3에서는 CallableStatement의 구현은 JDBCHelper에의 메소드 추가를 의미하였다. 그러나 이것은 Statement, PreparedStatement, CallableStatement라는 기능들이 한 클래스에 공존함으로써 개발시, 혹은 런타임시에 에러를 유발할 확률이 많아진다. 그러나 Factory Method Pattern이 도입됨으로써 CallableStatement의 구현은 JDBCCallableStatementHelper를 독립적으로 하나 추가함으로써 해결될 수 있다. 나모델러는 Rational Rose를 사용해서 멋지게 다음과 같은 클래스 다이어그램을 그려 보였다.

JDBCCallableStatementHelper는 JDBCPreparedStatementHelper를 상속하게 되어 있다. java.sql.CallableStatement는 java.sql.PreparedStatement를 상속하므로  JDBCCallableStatementHelper 입장에서는 대부분의 JDBCPreparedStatementHelper내의 메소드를 재사용가능하다.

 

 

 

 

 

 

 

 

 

단계 4) US7ASCII의 처리 -> Strategy Pattern과 Factory Method Pattern의 도입

 

이단계에서 나모델러가 지적한 것은 많은 switch문이었다. 코드 변환을 위해서 NLS_LANG을 검사해서 매번 코드 변환을 해주는 것은 소스 코드의 가독성을 해칠뿐만 아니라 에러를 유발할 소지가 많다는 지적이었다. 나개발도 이대목에서 고개를 끄덕였다.

나모델러가 제시한 해법은 CodeConverter클래스를 Stragety Pattern과 Factory Method Pattern을 사용해서 만들어 놓고 이를 JDBCHelper가 가지고 있게 하는 것이다. 코드 변환이 필요한 시점에서는 이 CodeConverter클래스를 호출해 주면 된다.

 

 

 

 

 

 

 

public  class JDBCStatementHelper implements JDBCHelper {

         private CodeConverter converter;

        

         public JDBCStatementHelper(, int NLS_LANG, ) {

                     // CodeConverter.getInstance()에서는 NLS_LANG을 보고서 어떤 CodeConverter를 생성할지를 결정한다.

                     converter = CodeConverter.createConverter(NLS_LANG);

         }

        

         public int select(String sql) {

                     sql = converter.convertFromDefault(sql);

                    

         }

         public String getString(int index) {

                     String ret = resultSet.getString(index);

                     ret = converter.convertToDefault(ret);

                     return ret;

}

}

 

이처럼 CodeConverter라는 클래스가 도입되면 코드가 아주 이해하기 쉬워지며 추후 벌어질 수도 있는 어떠한 코드 변환에도 유연하게 대응할 수 있는 구조를 가지게 된다고 나모델러는 역설했다. 나개발은 switch문이 없어져서 코드를 읽기 쉽게 된 점이 너무 기뻤다.

[Box 3 Strategy Pattern에 대한 설명 삽입]

 

단계 5) MSSQLServer의 지원 -> Factory Method Pattern의 도입

나모델러는 MSSQLServer에 대한 지원은 앞으로 벌어질 Informix, Sybase,  MySQL과 같은 다양한 상용 데이터베이스 지원을 예상해서 확장성있게 설계되는 것이 중요하다는 점을 지적해 주었다. 또한 한 사람의 개발자가 이 모든 데이터베이스의 특성을 전부 이해하는 것은 거의 불가능하기 때문에 다양한 사람들이 독립적으로 각 데이터베이스에 대한 SQL을 작성할 수 있는 기반을 만들어 주는 것이 중요했다. 나개발이 속한 개발팀이 만든 제품은 비즈니스 로직에 SQL이 섞여 있어서 포팅이 신속하게 벌어질 수가 없었다.

여기서 개발팀은 중대한 결단이 필요한데 위의 나모델러가 제시한 대로 확장성있게 재설계에 들어가느냐(Big Refactoring) 아니면 계속 다른 기능추가를 계속해 나가느냐를 결정해야 한다. 나모델러는 다양한 데이터베이스에 대한 포팅이 쉽게 이루어지게 해야 할 뿐만 아니라 튜닝또한 현재구조로는 쉽게 이루어질 수 없기 때문에 재설계가 반드시 필요하다고 지적한다. 때로는 직접적인 영업상의 이득으로 연결되지 않는, 후퇴로 보이는 재설계가 향후에 몇배의 영업상의 이득으로 연결될 수 있음을 경영진들에게 설득하는 것이 중요하다.

 

만약에 재설계에 들어간다면 어떠한 구조가 가장 효율적일 것인가? 나모델러가 제시하는 그림은 다음과 같다.

우선 여러 곳에 분산되어 있는 SQL을 하나의 클래스나 파일로 묶는다. 개중에 어떤 SQL들은 많은 조건들에 의해 다양하게 생성되므로 (특히 검색화면을 통해 생성되는 SQL이 이런 경우가 많다.) 모두 메소드를 통해 SQL을 가져 올 수 있도록 한다. 변하지 않는 SQL이라 하더라도 추후에는 변할 가능성이 있기 때문에 이처럼 메소드를 통해SQL을 접근하도록 하는 것이 확정성이 있다.

 

 

ANSI SQL로 해결될 수 있는 SQL들은 AnsiSQLFactory로 해결하고 DB 의존적인 부분들만 XXXXSQLFactory에서 구현하도록 한다.  다음은 이렇게 Factory Method Patter을 사용해서 디자인되었을때의 클라이언트 코드 예이다.

 

SQLFactory factory = SQLFactory.createSQLFactory(DB_TYPE);

String sql = factory.getSearchSQL(int condition, );

SQLFactory클래스는 상태를 가지지 않으므로(stateless) 구체적인 구현시에는 singleton pattern으로 구현해도 좋을 듯 싶다.

 

3.  나모델러의 조언 그후

나모델러와 나개발의 대화가 가능했었던 이면에는 UML이라는 모델링 표준이 있었기 때문에 가능했다. UML은 대략적인 클래스의 정적 관계(Class Diagram)를 쉽게 기술할 수 있을 뿐만 아니라 클래스간의 동적 관계(Sequence Diagram, Activity Diagram)도 기술하는데 유용하게 쓰인다. 데이터베이스의 ERD처럼 코더들사이에서도 UML이 의사소통의 언어가 될 날이 오리라 생각된다.

다시 이야기의 초점은 나개발로 돌아간다. 나개발은 그후 디자인 패턴을 공부해 보자는 오기가 생겼다. 1995년 첫 판이 나온 이래 명작으로 손꼽히는 GoF의 Design Patterns : Elements of Reusable Object-Oriented Software Mark Grand의 Patterns in Java를 읽고 난후 나모델러의 이야기들이 의미있게 와 닿기 시작했다.

나개발은 나모델러의 조언을 계속 얻어 가면서 JDBC Helper에 대한 Refactoring을 시작했다. 클래스 다이어그램으로 아이디어만 접했을때보다 실제 구현은 훨씬 어려웠고 해결해야 될 문제들도 많았다. 그러나 만들어진 코드/디자인은 훨씬 이해하기도 쉽고 기능을 추가하기도 용이했다.

다양한 데이터베이스를 구축하기 위한 클래스 디자인은 전체 팀원들의 공감을 얻어서 잘 진행되었다. 추후 Informix, Sybase같은 다른 데이터베이스로의 포팅이 다른 개발자들에 의해서 독립적으로 진행되어졌으며 이는 좀더 빠른 time-to-market 제품을 가능하게 해주었다.

 

사례 두번째 : 캐쉬 관리 패턴(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를 이용하여 객체를 생성한다. à 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

 

 

 

Box 기사 모음

Box 1 Façade Pattern

흔히 Helper 혹은 Wrapper라고도 불린다. 클라이언트 입장에서 접근해야 되는 클래스가 많을 경우 Façade Class를 하나 만들어서 이 클래스를 통해서 모든 접근을 하게 된다. 장점은 클라이언트와 내부 구현 클래스간의 Coupling을 최소화시켜 주기 때문에 클라이언트에 영향을 주지 않고도 자유롭게 내부 구현 클래스를 바꿀 수 있게 된다.

자바 API 중에 URL 클래스가 대표적인 Façade Pattern의 예가 될 것이다. URL 클래스는 클라이언트로부터 내부의 복잡한 클래스 구조를 숨겨 주는 역할을 한다.

 

 

 

2.        그림 3 Facade Pattern

 

Box 2 Factory Method Pattern

OO언어를 배울 때 맨 처음 배우는 개념이 바로 생성자(Constructor)이다. 모든 객체는 바로 생성자를 통해 생성되게 된다. 그러나 이 생성자를 쓰게 되면 안 좋은 점이 있다. 무엇이 안 좋다는 말인가? 바로 클라이언트가 생성하려는 객체의 구조를 고정시켜 버리게 된다.

필자가 말해 놓고도 필자가 이해가 안되기 때문에 다음의 예를 들어 보기로 한다.

A a = new A();

위의 코드는 A라는 객체를 생성하는 코드이다. 시간이 흐르면서 A는 Object Pooling을 사용해서 생성되게 수정될 수도 있다. 또한 B라는 객체가 새로 등장해서 A를 상속받게 되어 B라는 객체를 생성하도록 수정될 수도 있다. 또한 A가 인터페이스로 바뀌어 Aimpl이라는 새로운 객체를 생성할 수도 있다. B라는 객체가 A를 내부적으로 가져서 B를 생성해야 할 수도 있다. 이처럼 A라는 객체에 변화가 일어 났을 때 위의 코드는 수정되어야 한다. 이것을 클라이언트와 A 객체가 서로 coupling되어 있다고 표현한다.

바로 여기서 Factory Method Pattern의 필요성은 출발한다. Factory라는 중간 클래스를 만들고 여기서 A라는 클래스의 생성을 담당한다. 그리고 A는 Ainterface라는 인터페이스를 상속받게 하고 Factory는 Ainterface를 돌려주게 만든다. 다음의 코드를 보자.

AInterface a = Factory.createA();

Factory를 통해 전달받은 것은 A의 인터페이스이기 때문에 A라는 클래스가 어떻게 변해도 클라이언트의 코드는 전혀 영향을 받지 않는다.  Factory Method Pattern은 이처럼 생성에 관계하기 때문에 크게 Creational Pattern이라고 분류된다. 이 패턴의 일반적인 형태는 다음의 그림과 같다.

필자는 대규모의 조직이 팀을 이뤄 개발하는 환경에서 의외로 이러한 dependency 문제 때문에 불필요한 코딩 작업을 많이 하는 것을 목격해 왔기 때문에 이 패턴의 중요성은 아무리 강조해도 지나치지 않는다고 생각한다.

3.        그림 4 Factory Method Pattern

 

 

 

 

 

Box 3 Strategy Pattern

클라이언트에 행위를 결정하기 위해 지나친 코드들이 들어가고 있지 않은가? 이 형태는 C나 그밖의 Procedural한 언어에 익숙한 개발자들이 처음 OO로 개발할 때 많이 경험하는 코드형태라고 할 수 있다. 보통 if나 switch문을 통해 행위를 결정하게 된다.

만약 if나 switch에 들어가는 조건들이 하나의 전략을 이룰만큼 큰 비중을 차지한다면 이 Strategy Pattern을 고려해 보라.  큰 비중을 차지한다는 것이 좀 애매하다니 예를 들어 본다. 프로그램에 따라 OS의 형태에 따라 상이한 처리를 해 줘야 하는 경우가 많다. 이 경우 많은 클라이언트(이때 클라이언트는 C/S의 Client가 아니라 Class Client를 의미한다) 코드내에서 OS형태에 따라 분기를 해서 처리를 해 주는 경우가 많다. 이 경우 OS형태에 따라 다르게 처리해 주어야 하는 부분들을 하나로 묶어 클래스로 독립시킨다. Windows NT 클래스 하나, Solaris 하나, HP하나. 이것들을 다 Strategy라고 부를 수 있다. 그리고 처음 프로그램이 실행될 때 현재 OS가 무엇이냐에 따라 이Strategy가 정해진다. 클라이언트 코드내에서는 어떤 if, switch와 같은 분기도 존재하지 않은 깔끔한 코드가 자랑이다.

 

4.        그림 5 Strategy Pattern

 

Box 4 Cache Management Pattern

캐쉬에 대해서 들어본 독자 여러분이라면 캐쉬 관리 패턴이  무엇인지   즉시 이해할 수 있을 것이다. 그렇다, 캐쉬 관리 패턴은 캐쉬를 구현하는 필요한 클래스의 구조와 동작 원리를 포함하는 패턴이다.

객체의 생성에 많은 시간과 계산이 소모되며, 한번 생성된 객체는 보관하여 재활용이 가능한 경우에 캐쉬를 적용한다. 캐쉬는 적절하게 적용되면 객체 생성에 소모되는 시간을 대폭적으로 감소시킬 수 있다.

많은 경우 캐쉬 패턴의 도입은 어려운 일이 아니다. 잘 디자인된 프로그램이라면 DB 억세스 혹은 객체를 생성하는 부분의 로직은 하나 혹은 두개 정도의 작은 숫자의 클래스에 집중되어 있다. 이 부분들을 캐쉬를 사용하도록 변경하여 생성 속도를 향상 시킨다.

예를 들어 다음과 같은 함수 호출로 객체가 생성된다고 하자.

ComplexObject co = COFactory.getInstance(coID);

위와 같은 경우 캐쉬 패턴은 COFactory.getInstance()에 도입된다.

ComplexObject getInstance(ObjectID objID) {

         CacheManager cacheManager = CacheManager.getInstance();

         ComplexObject co = cacheManager.fetchObject(objID);

         if( co == null ) { // 캐쉬에 존재하지 않으면

                     co = this.loadFromDB(objID); // DB에서 직접 읽어오는 부분

                     cacheManager.addObject(co);// 캐쉬에 저장

         }

         return(co);

}

위의 코드에서와 같이, 어플리케이션은 캐쉬에서 객체를 가져오며 캐쉬에 존재하지않으면 DB에서 직접 읽어온다. 그리고 읽어온 객체는 캐쉬에 저장한다.

캐쉬 패턴의 클래스 구조를 보자.

[ Cache Management Pattern 클래스 다이어그램 ]

각각의 클래스들이 행하는 역할은 다음의 표를 참조하라.

이름

역할

Object Key

ObjectKey 객체는 캐쉬에서 획득되거나 생성될 객체의 내용을 식별한는 KEY의 역할을 담당한다.

CacheManager

객체를 획득하고자 하는 모든 외부 클래스들은 CacheManager객체의 fetchObject를 호출한다. fetchObject에서는 Cache에 요청된 객체를 얻고자 하며, 실패 시에는 객체를 생성한다.

ObjectCreator

캐쉬에 존재하지 않는 객체를 생성하는 역할을 담당한다.

Cache

캐쉬 객체는 캐쉬에 저장하고자 하는 객체들의 집합을 관장한다. 주어진 ObjectKey에 해당하는 객체를 신속하게 찾아준다. 존재하지 않으면 null을 반환하는 방식 등으로 객체가 존재하지 않음을 알린다.  존재하지 않는 객체는 ObjectCreator에 의해서 생성되어 Cache.addObject 함수 호출로 캐쉬에 저장된다.

 

캐쉬는 위의 4개의 유효한 클래스에 의해서 구성된다. 그러나 캐쉬를 적용할 때는 이외에도 캐쉬에 객체가 존재하지 않을 경우에 어느 객체를 이전 집합에서 제거하고 새롭게 생성된 객체를 할당할 것인가를 결정하는 문제와 객체들이 저장되는 집합의 크기를 어느 정도로 설정할 것인가와 같은 적용시점에 결정해야 할 문제들이 존재한다. 이들에 대한 해답은 여러분이 직접 캐쉬를 적용하여 캐쉬의 적중률(Hit-Ratio)가 어느 정도인지 직접 산출해 보아야 확실이 결정될 수 있다.

 

 

Box 5  Refactoring

앞에서 이야기 안티 패턴의 패턴이 발생하면 어떻게 해야 할까? 잘못된 패턴을 피해 간다고 하지만 안티 패턴이라고 생각되는 부분들이 나타나지 않을 있을까? 그리고 이들 안티 패턴이 개발 중이 코드 상에 나타난다면 어떻게 이를 효과적으로 극복할 것인가?

 

리팩토링(Refactoring) 코드가 동작하는 외부 행위는 변경하지 않으면서 내부적으로 프로그램의 구조를 개선하는 작업이다. 기능이나 성능의 개선이 아닌, 디자인 관점에서 코드를 개선하는 것이다. 현재에 만들어지는 코드들은 우선 디자인 단계를 거친다. 프로그램은 우선 디자인된 코드로 만들어진다. 그러나 시간이 지나면서 코드는 계속 변형된다. 이와 더불어 시스템의 무결성과 초기 디자인의 정신도 사라져간다.

 리팩토링은 이런 관점과는 반대의 입장에 있다. 리팩토링을 통해서 혼돈스럽고 나쁜 디자인의 코드를 정리되고 좋은 디자인의 코드로 변화시킨다. 리팩토링의 단계에서 필요한 변화의 수준은 결코 커다란 것이 아니지만, 작은 변화 하나하나가 모여서 전반적인 디자인을 개선할 있다.

 

다음의 간단한 리팩토링의 예를 보자.

“Extract Method” 리팩토링의 가장 간단한 경우에 속한다.

이는 다음의 코드에서 상세한 내용을 출력하는 코드를 

void printOwing(double amount) {

                printBanner();      

                //printDetails

                System.out.println(“name:”+_name);

                System.out.println(“amount: ”+amount);

}

아래에서처럼, printDetails라는 메소드로 분리하였다.

void printOwing(double amount) {

                printBanner();

                printDetails(amount);

}

void printDetails(double amount) {

                System.out.println(“name:”+_name);

                System.out.println(“amount: ”+amount);

}

 

위의 Extract Method 아주 기초적인 예제에 속하지만 이런 자그마한 개선들이 모여 궁극적으로 좋은 디자인을 이끌어 낸다. 리팩토링은 바로 이런 자그마한 것들에서 시작하는 것이다. 안티 패턴의 개발 상의 안티 패턴 범주에 속하는 것들은 리팩토링 과정을 거쳐서 조금씩 개선될 있다.

무엇보다 필자는 개발지들이 이들 리팩토링의 방법론들을 한번 참조할 있다면 보다 깔끔하고 보기 쉬운 코드들을 만들 있을 것이라 생각한다. 필자가 읽어본 리팩토링은 결코 어렵지 않았다. 오히려아하 그렇구나라고 한번 읽어보면 쉽게 이해되는 부분들이 대다수였으며, 필자의 코딩 기법을 발전시키는데 많은 도움이 되었다.

 

리팩토링에 관한 더욱 심도 있는 이해를 원한다면 마틴 포울러의 저서인

Refactoring : Improving the Design of Existing Code,1999 참조하기를 바란다.

 

Box 6 Anti-Pattern

패턴이 있다. 그리고 패턴의 이면에는 안티 패턴이 있다.

소프트웨어를 만드는 과정 중에서 사람들이 흔히 범하기 쉬운 바람직하지 않는 방법들과 그로 인한 폐해. 안티 패턴(Anti-Pattern) 이들을 의미한다. 

 

디자인 패턴의 출현은 개발자들에게 지금까지 있었던 가장 효과적인 개발 가이드 라인을 제공하여 주었다. 이들 패턴들은 개발 중에 나타나는 복잡한 구조와 생각들을 명백하고 쉬운 용어와 개념으로 정리하여 제공한다. 우리가 소프트웨어를 개발하는 것은 주어진 문제들을 해결하기 위한 것이다. 소프트웨어 개발을 편리하게 도와주는 방안은 지금까지 다양하게 개발되어 왔다. 그러나 이들을 도입하여도 상황은 반드시 좋아지는 것이 아니다. 사실, 연구소와 대학, 그리고 산업에 종사하는 만은 사람들이 소프트웨어를 제작하기 위한 수많은 창의적이고 혁신적인 방법을 생각하고 개발하여 왔다. 수많은 훌륭한 아이디어와 노력에도 불구하고 소프트웨어 개발의 성공 가능성이 낮은 것이 냉혹한 현실이다.

 미국에서 있었던 수백개의 다양한 소프트웨어 개발 사례들의 연구 보고에서는 6개의 프로젝트 중의 5개는 성공적이지 못했던 것으로 조사 되었다. 3분의 1 해당하는 소프트웨어 프로젝트들은 취소되었다. 나머지들도 최초에 예상되었던 예산과 기간의 각기 2씩을 소모하였다. 반복되는 실패들과 긍정적이지 아니한 개발 산출물들은 중요한 가치를 지니고 있다. 왜냐하면 이들이 소프트웨어 산업에 종사하는 이들에게 가지 말아야 방향들을 알려주고 있기 때문이다. 이들이 바로 안티 패턴이다.(from www.antipatterns.com)

 

소프트웨어 산업에 종사하는 이라면 누구나 안티 패턴을 한번 읽어 보고 자신이 그러한 잘못을 저지르고 있지 않은가에 대해서 생각해 봐야 것이다.  글을 쓰고 있는 필자들고 안티 패턴을 읽고 많은 부분들을 공감하였다. 여러분들도 개발 경험이 있다면 안티 패턴의 내용을 읽고 반드시 공감대를 형성할 것이다.

 잘못을 일단 알아야 이를 고칠 있지 않겠는가? 잘못된 것이 무엇인지 안다면 이를 미연에 방지하는 것도 가능하다. 때문에 안티 패턴을 공부할 필요가 있다.

 

안티 패턴은 다음의 3가지 측면에서 나타난다.

-          관리상의 안티 패턴 : 프로세스와 사람들의 잘못된 관리

-          아키텍쳐 상의 안티 패턴 : 설계와 구조상의 잘못된 정의

-          개발상의 안티 패턴 : 코드 상의 잘못된 사례

 

알려진 소프트웨어 안티-패턴들을 다음에 간단히 소개한다. 적당한 용어를 선정하지 못해 직역한 사실에 독자들의 양해를 구한다.

-          스파게티 코드 (Spaghetti Code – 개발) : 개발과정에서 다양한 기능들을 추가하는 과정에서 코드가 복잡하게 꼬여 버린다. 

-          스토브 파이프 시스템 ( Stovepipe System – 아키텍쳐 ) : 다양한 솔루션들을 확실한 추상화의 개념 없이 임의로 묶어 하나의 제품을 만들면 신뢰성이 떨어지며 유지 보수가 힘든 제품이 탄생된다.

-          분석 마비(Analysis Paralysis – 관리 ) : 분석 단계에서 완벽하고 완전한 분석을 꿈꾸면 이는 소프트웨어 개발 진행을 마비 시켜 버린다.

-          스위스 군대 만능 (Swiss Army Knife – 아키텍쳐) : 하나의 객체에 너무 많은 기능과 인터페이스를 담아버리면 복잡하고 유지보수하기 힘들고 상호 의존성이 높은 클래스가 탄생된다.  

-          애매한 관점 ( Ambiguous Viewpoint –개발 ) : 명확하지 않은 관점에서 모델링을 수행하면 객체 모델링에서 객체들을 정확하게 정의할 없다.  

 

안티 패턴을 좀더 이해하고 개발과정에 반영하기를 원하는 분들은 다음의 책을 참조하기 바란다.

-          Anti Patterns : Refactoring Software, Architectures, and Projects in Crisis,1998 by William J.Brown & …

-          Anti Patterns and Patterns in Software Configuration Management, 1998 by William J.Brown & …

 

 

 

 



[1] Refactoring : 코드의 재정비를 가리키는 단어 : 좋지 않은 부분들을 개선하여 바람직한 코드로 변경하는 작업을 의미함.

[2] GOF : Gang of Four : Design Pattern : the reusable Object 책을 4명이 공저하였기 때문에 이책을 가리켜 GOF Book.이라고 칭한다.

'SE' 카테고리의 다른 글

UML[2/4]  (0) 2008.07.25
UML[1/4]  (0) 2008.07.25
Pattern[4/4]  (0) 2008.07.25
Pattern[3/4]  (0) 2008.07.25
Pattern[2/4]  (0) 2008.07.25
And