'SE'에 해당되는 글 10건

  1. 2008.07.25 Pattern[2/4]

Pattern[2/4]

|

제목

마소 연재 10월호(Request Dispatcher)

일련번호

 

배포

 

작성일

2000-09-12

분류

Public

변경일

2000-09-12

상태

Draft

작성자

윤동빈

소프트웨어의 제작

소프트웨어를 제작하는 일은 영화의 제작과 유사한 점들이 많이 있다. 영화를 만들기 위해서 어떠한 것들이 필요한가? 제작자, 시나리오, 감독, 조감독, 촬영 기사, 플롯, 배우 등의 모든 구성 요소들이 영화를 결정한다. 타이타닉과 같은 대작을 만들기 위해서는 더욱 정교하고 알찬 기술 요소들이 필요하다. 소프트웨어의 제작도 영화와 비슷한 면모들이 많이 있다. 아키텍터(전제 아키텍쳐를 구성하는 사람)의 역할이 감독의 역할과 비슷할까. 각각의 부분을 맞은 이들의 역할이 어우러져 영화를 구성하듯, 각각의 역할을 구성하는 이들의 역할이 어우러져 소프트웨어를 만든다. 여러분이 영화를 만든다고 생각해보라. 어떤 것을 배우고 어떻게 구성할 것인가? 촬영기법 / 영화의 시나리오 구성법, 이야기(플롯)의 구성법등의 여러 가지를 배우고 구성할 것이다.

소프트웨어를 제작한다고 하면 여러분은 어떻게 시작할 것인가? 이전에 하던 대로, 컴퓨터를 키고, Visual C++이나 Delphi 혹은 JAVA의 편집을 위해서 Edit Plus등의 프로그램을 작성하는 도구를 화면에 하나 띄운 다음, 바로 코딩을 시작할 것인가?

이 글을 읽는 독자라면 위와 같은 식으로는 제대로 된 소프트웨어를 만들어 낼 수 없다는 사실을 너무나 잘 알고 있을 것이다.

바로 그러하다. 그러한 방식으로는 아무것도 만들 수 없다.

 

영화를 제작할 때는 여러 가지 요소들이 사용된다. 컴퓨터 그래픽을 이용한 영상과 컴퓨터 그래픽(CG)의 결합, 카메라 워킹, 여러 촬영기법 [ 필자도 영화의 제작 과정은 잘 모르지만 대략 이러하리라], 필름을 이용한 영상 처리 등의 기법들이 사용된다. 이들이 좋은 영화에 반드시 필요한 요소라고는 할 수 없지만, 이들로써 영화의 표현 능력과 예술성을 향상시킬 수 있다는 점은 주지의 사실이다.

프로그램의 제작도 그러하다. 디자인 패턴이 좋은 프로그램의 제작에 반드시 필요한 요소라고는 할 수 없지만, 이들로써 프로그램의 성능과 안정성과 확장성을 향상시킬 수 있다는 점은 주지의 사실이다.

이번 호에서는 지난 호에 이어 웹 어플리케이션 서버의 설계 과정 중에서 디자인 패턴이 사용되었던 설계의 일부분과 애플리케이션 서버에서의 핵심 부분인 Request  Dispatcher 구현 사례를 소개하고 이들에서 패턴이 어떻게 사용되었는지 설명하려 한다.

 

사례 1 . – 설계 과정에서 사용된  디자인 패턴

필자들도 웹 어플리케이션 서버를 구현하고 싶었다. 그러나 작년까지만 하여도 웹 어플리케이션 서버를 디자인 한다는 생각은 있었으나 구체적으로 무었을 어떻게 해야 할 지 전혀 구상 할 수 없었다.

웹 어플리케이션 서버들의 구상을 어느 정도 완성한 것은 필자가 많은 경험을 겪고 난 올해 5월 이후의 일이었다. 마지막으로 필요했던 것은 사용자 권한관리를 구성하는 접근 권한 관리 (Access control List)에 관한 것이었으며, 이를 필두로 대체적인 웹 어플리케이션 서버의 디자인이 구상 되었었다. 이번 호에서는 당시의 디자인 중에서 가장 핵심 파트에 속하는 이벤트 위주의 웹 처리 기법에 대해서 패턴을 이용하여 소개하고자 한다. 이 이벤트 위주의 웹 처리 기법은 조금 특화 된 서버 프레임 워크로 생각하면 이해에 도움이 될 것이다.

상황 설정.

구성.

어디서부터 어떻게 접근하여 나갈 것인가? 영화를 만들 때 시나리오부터 시작하듯, 프로그램의 제작에서도 시나리오부터 완성하여 나아간다. UML에서는 대략적인 시나리오의 완성을 위한 합리적이고 효과적인 방법론을 제시하고 있는데, 이것이 바로 유즈 케이스 다이어그램(Use Case Diagram)이다.

1.        그림 1 - System Use Case

 

위의 시나리오는 사용자가 웹에서 상품을 구매할 때 발생하는 상황(유즈케이스)를 간단하게 그린 것이다. 사용자가 웹 브라우저를 통해서  하는 일은 크게 2가지, 상품의 목록을 보는 일과, 상품을 구매하는 일이다. 두 가지의 상황 중에서 서버에 로드가 걸리는 일은 상품의 구매이다. 상품의 구매는 전자 상거래의 트랜잭션의 처리가 포함되어 있고, 데이터 베이스에 구매 결과를 반영해야 함에 근거한다.

디자인된 웹 어플리케이션

디자인을 위한 시나리오의 제작을 위한 설정으로 쇼핑 몰에서의 웹 어플리케이션 사용을 예로 든다. 쇼핑 몰을 구성할 때 심각하게 고려해야 하는 사항중의 하나이다. 웹 어플리케이션을 설계할 때 언제나 그렇듯이, 다음 2가지의

1.       기능 향상 및 확장성의 고려

2.       서버 과부하의 처리

의 문제가 있다. 이 문제들을 풀 수 있는 서버구조에는 어떤 방향이 있을까. 정답은 물론 한가지가 아니다. 수많은 웹 어플리케이션 서버들이 나름대로의 독자적인 방법을 제공한다. 이번에 기술하는 내용도 이러한 방법 중의 하나이며, 이벤트의 큐 처리를 위한 생산자-소비자 패턴[1]에 근거하여 위의 가용성을 보장하고, 동적 연결 패턴을 이용하여 확장성을 보장하려 한다. 이벤트를 처리하기 위해서 쓰레드를 직접 생성하기 보다, 큐에 저장하여 여유가 있는 서버가 이벤트를 큐에서 획득하여 처리하는 생산자-소비자 패턴의 방식으로 단시간에 서버 로드가 집중되더라고

서버 구조.

전체 구조 설명.

서버쪽에서 발생하는  모든 작업은 웹 폼의 제출(SUBMIT)과 같은 이벤트에 의해서 통제된다. 쇼핑 몰을 이용하는 사용자가 웹 페이지 상의 구매라는 버튼을 클릭하게 되면 이 구매 버튼에 의해서 현재의 웹 페이지가 제출(SUBMIT)된다.

SUBMIT된 WEB PAGE는 서버쪽으로 제출되고, 서버쪽에서는 제출된 웹 페이지가  구매 행위라는 정보를 파악하고 구매를 처리하는 구매 처리기를 수행한다.

처리기를 통해 수행된 결과는 다시 클라이언트로 반환된다.

서버상에서 발생하는 구매 처리 행위의 수행은 이벤트 처리기의 서버 로직을 통해서 수행한다.  이벤트 처리기는 이렇게 사용자가 발생시킨 웹 이벤트들을 처리한다.

 

가용성확장성을 생각해 보자. 이벤트 처리기는 여러 서버에 분산되어 존재할 수 있다. 동시 다발적으로 발생하는 클라이언트의 요청은 이벤트 처리기에 의해서 분산되어 처리된다.

 

2.        그림 2전체 구조

 

이벤트 큐의 처리

이벤트 큐는 생산자-소비자 패턴을 이용하여 구현된다.

이벤트가 발생하면 웹 서버 쪽에서 JSP나 SERVLET을 이용하여 사용자가 요청한 작업을 이벤트로 전환하여 이벤트 큐에 전달한다.

 

3.        그림 3 생산자-소비자 패턴을 이용한 이벤트 큐의 구현

생산자-소비자 패턴에서는 이벤트가 발생하면 이를 이벤트 큐에 저장한다. 저장된 이벤트는 이벤트 소비자(Consumer)가 받아서 액션 매니저에서 이벤트를 수행하도록 넘겨준다. 이벤트 소비자는 단지 하나의 서버에만 존재하는 것이 아니라, 여러 개의 서버에 분산되어 존재할 수 있다. 이때는 큐에 누적된 이벤트를 여러 개의 서버가 하나씩 가져가 수행하는 역할 분담의 기능을 구현할 수 있다.

하나의 서버 내에서도 소비자는 일정한 개수만큼만 만들어지도록 조절되어 서버의 성능과 기타 환경에 따라 적절히 처리량을 조절할 수 있다.

이벤트 처리기의 설계 [구매 처리기]

이벤트를 처리하기 위한 처리기는 우선 서로 다른 이벤트 처리기를 나타내기 위한 범용적 객체가 존재해야 한다. 여기서는 그러한 객체를 액션 객체로 설정하였다. 구매 행위 등의 각각의 행위를 처리하기 위한 비즈니스 로직이 담겨 있는 이벤트 처리기 객체는 모두 액션(Action)이라는 추상객체를 상속 받아 구현된다. 이 부분은 동적 연결 패턴(Dynamic Linkage Pattern)을 참조하여 설계 되었다.

4.        그림 4 액션 객체의 클래스 다이어그램

 

액션 객체의 등록

각각의 액션 객체들은 팩토리에 의해서 생성되어 해쉬 테이블에 등록된다. 해쉬테이블은 전체 시스템에서 1개 존재하며, 언제나 이 해쉬 테이블을 참조하여 작업을 수행한다.

액션 객체의 등록과정은 다음과 같이 이루어 진다.

Action action = ActionFactory.getAction(BUY); // BUY라는 액션명을 가지는 액션 객체를 생성한다.

EventHandlerHashtable.put(action.getActionName(),action);

등록은 추상객체를 이용한다. 여기서 각각의 객체를 직접 생성하지 않고 클래스 팩토리와 추상 객체를 이용하여 등록하는 것은 코드의 상호 연관성을 최소화 하기 위한 것이다. 추상 객체를 이용하면, 액션 객체들이 더더욱 추가되더라고 등록 과정의 로직이 변화될 필요가 없다.

 

동적 연결 패턴의 사용 시나리오

1.       초기화 시에 이벤트 처리 객체들은 각각의 이벤트 명을 키 값으로 해쉬테이블(HASHTABLE)에 등록된다.

2.       이벤트가 발생하면 이벤트 명을 키 값으로 해쉬테이블에서 이벤트 처리 객체를 찾아 온다.

3.       이벤트 처리객체의 추상 클래스인 액션 객체의 메쏘드를 호출하여 비즈니스 로직을 수행한다.

 

5.        그림 5 이벤트 처리 시퀀스 다이어그램.

이벤트 처리 내부의 코드를 살펴보자.

Void actionManager.handleEvent(Event event){

           Strting eventName = event.getEventName();

 Action action = eventHandlerHashtable.get(eventName);

 action.preprocess();

 action.do();  // 실제 비즈니스 로직의 수행이 발생한다.

 action.postProcess();

}

 

여기서 왜 이벤트 처리 객체를 직접 호출하지 않고, 상위의 추상 객체를 받아 호출하였는가라는 의구심을 가지는 독자도 있을 것이다. 이 부분이 바로 이른바 객체 지향의 마법이다. 상위 객체의 메쏘드를 호출하여도 실제 수행되는 부분은 상속 받은 하위 객체의 오버라이드(Override)된 메쏘드에서 호출된다.

 

동적 연결 패턴을 이용하면 이벤트와 이벤트를 처리하는 이벤트 처리기의 추가 시에 서버 로직이 변화될 필요가 없다는 매우 중요한 장점이 있다. 예를 들어, 사용자가 발송 내역을 확인하는 기능을 원한다고 할 때,

 

1.       발송 내역 이벤트 명을 지정.

2.       발송 내역을 확인하기 위한 비즈니스 로직을 담고 있는 이벤트 처리기 등록

3.       발송 내역 확인 이벤트 처리기를 발송 내역 확인 이벤트 명을 키 값으로 해쉬테이블에 등록.

3가지 작업으로 발송 내역 확인 기능을 추가할 수 있다.

 

따라서 이러한 동적 연결 패턴의 사용은 서버의 확장성을 극대화하여 기능의 추가시에도 다른 기능들을 추가 할 필요가 없다.

 

액션의 수행

액션의 수행은 전처리(preprocess), 수행(do), 후처리(postProcess) 의 3가지 단계로 구분된다. 이러한 3가지 단계의 개념은 다른 시스템(워크플로우)에서 도입되어 만들어진 개념으로 각각의 단계에서는 다음의 역할을 담당한다.

-          전처리(preprocess) : 전처리 단계는 액션의 처리를 위한 정보들을 수집하고, 여러 관련 환경들을 초기화한다. 예를 들어, 세션이나 다른 정보들을 이용하여 흐름의 문맥을 설정하고, 적절한 트랜잭션을 시작시키는 작업이 이에 포함된다.

-          수행(do) : 비즈니스 로직이 담겨진 부분으로 데이터 베이스에 업데이트 하고, 파일에 저장하는 등의 대부분의 로직이 담겨져 있다.

-          후처리(postProcess) : 비즈니스 로직이 수행되고 난 후, 트랜잭션을 종료하고 기타 사용한 리소스들을 반환하는 등의 로직이 포함되는 부분이다.

이렇게 3가지 부분으로 나누어 객체를 설계한 것은 비즈니스 수행 객체의 제작을 좀더 명확하게 하기 위해서이다. 하나의 액션 객체는 하나의 액션을 담당하며, 액션의 전처리와 후처리 과정이라는 개념이 명시되어 있으면 이에 따른 적절한 액션 객체를 구현하는 과정이 더욱 수월해 진다.

설계에서 디자인 패턴

지금까지 설계된 웹 어플리케이션 서버에서 웹 이벤트 처리 부분의 설계를 집중적으로 설명하였다. 이 부분에서 사용된 패턴은 생산자-소비자 패턴과 동적 연결 패턴의 2가지 였다. 전자와 더불어 서버의 가용성과 신뢰성을 어느 정도 이상으로 보장할 수 있는 설계를 할 수 있었고, 후자와 더불어 확장성을 가지는 설계를 할 수 있었다.

설계 시점에서 패턴을 이용할 때 우리에게 몇 가지 이점이 있다.

1.       디자인 패턴의 개념을 이용하여 설계의 범위를 확장할 수 있다. 이는 보다 좋은 도구와 재료가 있으면 좋은 제품을 만들어 낼 수 있다는 것과 동일한 이치이다.

2.       디자인 패턴을 통해서 도입된 부분 파트의 설계에 대해서 확신을 가질 수 있다. 추상화 된 개념의 도입으로 설계자는 패턴 내부의 상세한 부분의 설계에서 벗어날 수 있을 뿐만 아니라, 이들로써 구성된 코드들이 정상적으로 동작할 것이라는 점을 확신할 수 있다.

3.       패턴이라는 좋은 재료와 도구의 사용으로 설계 시간이 단축되며, 여러 명이 함께 설계할 때도 상호간의 의사소통을 위한 시간을 줄 일 수 있다.

필자는 이러한 점을 몇 번의 설계 과정을 통해서 인식하였으며, 그때마다 더욱 디자인 패턴의 소중함을 깨닫고 있다. 이번의 설계 과정에서도 생산자-소비자 패턴과 동적 연결 패턴을 적용하여 서버의 확장성과 가용성이 어느 정도 보장된 설계라는 점을 확신할 수 있다.

위의 설계는 웹 어플리케이션의 한 부분을 들었을 뿐이다. 이는 전체 웹 어플리케이션에서의 부분에 불과할 뿐이며 범용적으로 사용할 수 있는 어플리케이션의 골격을 만들기에는 턱 없이 부족하다. 하지만, 천리길도 한걸음부터 라는 옛말처럼 이제 한 걸음 내 딛었을 뿐이다.


1.                    사례 2 : 애플리케이션 서버에서의Request  Dispatcher 구현

웹 애플리케이션을 작성할 때는 많은 경우 웹 애플리케이션 서버를 사용하게 된다. 웹 애플리케이션 서버는 웹 서버와 애플리케이션 서버를 한데 묶어 놓은 형태를 취하는 경우가 많다. 이미 도입된 레퍼런스 사이트를 많이 가지고 있는  웹 애플리케이션 서버를 사용하게 되면 개발의 위험이 그만큼 줄어 들게 되고 빠르게 만들 수 있는 장점이 있다. 그러나 어쩔 수 없는 이유로 인해서 직접 애플리케이션 서버를 만들어야 하는 경우도 있다. 여기에는 웹 애플리케이션 서버가 원하는 기능을 만족시키 못한다든지 사용 하드웨어와 맞지 않다든지 하는 여러가지 이유가 있을 수 있다. 이 애플리케이션 서버의 예제로는 채팅서버나 메신저 서버, 팩스 게이트웨이, 레게시 시스템에 대한 Adapter 등등이 있을 수 있다. 이 예제에서는  애플리케이션 서버를 작성할 때 발생했던 필자의 실제 체험을 약간 간략화시켜 들어 보이고  발생한 문제점을 해결하기 위해 사용한 해결 방법과 적용 디자인 패턴을 이야기해 나가 보기로 하겠다. 여기서 언급되는 모든 소스 코드와 UML 모델은 이달의 디스켓에 실도록 하겠다.

 

애플리케이션 서버의 설계

나개발이 개발해야 되는 이번 업무는 애플리케이션 서버의 개발이다. 개발 언어는 자바로 하기로 한다. 자바에는 Java RMI라고 해서 자바 분산 객체의 표준이 있고  Java 1.2 SE 버전에 포함되어 있다. 그러나 Java RMI는 동시 요청이 몰렸을 때 성능이 매우 저하되기 때문에 나개발이 속한 개발팀은 Java RMI를 애플리케이션 서버의 백본으로 사용하지 않기로 결정했다.

EJB Container와 MTS등은 모두 애플리케이션 서버의 일종이라고 볼 수 있다. 특히 이 두가지는 기존의 ORB(Object Request Broker)의 기능에 TP Monitor 기능이 더한 CTM(Component Transaction Monitor)이라고 불린다. CORBA의 ORB는 TP Monitor이 없으므로 CTM이라고 부르지는 않는다.

TP Monitor 기능이 들어가게 되면 문제가 복잡해 지므로 애플리케이션 서버의 가장 간단한 기능인 ORB기능만을 담당하도록 애플리케이션 서버를 구성하기로 했다.

클라이언트는 자바로 작성되어지며 특정 명령 객체를 애플리케이션 서버에 직렬화(Serialization)해서 전달한다. 애플리케이션 서버는 이 직렬화된 명령 객체를 해석해서 이 명령 객체의 특정 메소드를 실행한다. 이 특정 메소드에서 리턴된 리턴 객체를 서버는 다시 클라이언트에게 직렬화해서 전달하게 된다. 만일 서버에서 실행중에 에러가 났다면 이 에러 객체를 직렬화해서 전달한다.

[시스템 구성도]

 

 

애플리케이션 서버

명령

객체

 

 

클라이언트

 

Exception

리턴

객체

 

 

 

 

 

 


스펙 정리도 끝났고 문제도 없어 보인다. 자, 이제 직접 개발을 해 보자.

 

나개발의 1차 개발 애플리케이션 서버

애플리케이션 서버는 자바로 개발되어지기 때문에 자연스럽게 쓰레드 모델을 사용하게 되었다. 다른 언어의 경우 OS마다 사용하는 쓰레드 라이브러리가 상이하므로 아직까지도 프로세스 모델을 많이 사용하는 경향이 있다. 쓰레드 모델에서는 쓰레드의 처리 형태에 따라 크게 네가지로 나뉜다.

l        Single Threaded Execution 동시에 단 하나의 쓰레드만을 실행한다.

l        Thread per Request 요청이 발생할때마다 쓰레드를 생성한다. 가장 일반적인 처리 형태이다.

l        Thread per Session 하나의 세션에는 하나의 쓰레드를 할당하게 되므로 한 세션의 동시 요청들은 모두 순차적으로 처리되게 된다.

l        Thread Pooling 쓰레드 풀을 사용해서 일정 개수의 쓰레드를 순환시켜 사용한다. 요즘 많이 사용된다.

 

속도나 리소스 면에서는 Thread Pooling이 가장 이상적이나 이번 1차 개발에서는 구현의 편이성을 감안해서 Thread per Request방식을 따르기로 했다.

나개발은 자바를 사용함으로써 애플리케이션 서버를 의외로 쉽게 구축할 수 있었다. 자바는 쓰레드와 소켓 라이브러리가 워낙 강력하고 편하게 되어 있어서 서버 프로그래밍을 만드는 데에는 적격이라고 할 수 있다. 가장 하단의 네트워크 프로토콜은 Serialization을 사용해서 쉽게 해결할 수 있었다.

개발된 애플리케이션 서버의 Sequence Diagram을 살펴 보기로 하자.

[애플리케이션 서버의 Sequence Diagram]

애플리케이션 서버는 모두 세개의 클래스(RequestDispatcher, MasterThread, ServerProtocolHandler)와 두개의 인터페이스(ServerProtocolHandlerIF, Command)로 구성되어 있다. RequestDispatcher는 클라이언트의 요청을 받아 들여 MasterThread에 전달한다.  Thread Per Request 로 동작하므로 쓰레드인 MasterThread는 요청이 있을때마다 매번 생성된다. 다음은 RequestDispatcher의 골격 코드이다. 아래의 코드처럼 Socket을 얻은 후에 별도의 쓰레드(MasterThread)에서 수신 바이트를 해석하지 않고 Main Thread에서 수신 바이트를 해석하게 되면 경우에 따라 Block이 되는 경우가 발생하므로 주의를 요한다.

ServerSocket serverSocket = new ServerSocket(port);

System.out.println("Request Dispatcher( " + port + " ) \t... OK");

while (true) {

         try {

            Socket socket = serverSocket.accept();

                     if (socket != null) {

                                 (new MasterThread(socket)).start();

                     }

         } catch (Exception e) {

                     System.out.println("Socket Exception");

                     continue;

         }

}

 

MasterThread객체는 ServerProtocolHandlerIF의 getCommand()를 사용해서 클라이언트의 요청을 해석해서 Command 객체를 얻어 낸다. 이때는 Serialization을 사용한다.  얻어진 Command 객체의 execute() 메소드를 호출해서 임의의 Object를 돌려 받은 후 다시 이 Object를 ServerProtocolHandlerIF의 send()를 사용해서 클라이언트에 Response를 전달한다. 다음은 MasterThread의 골격 코드이다. Command 객체를 실행한후 Exception이 발생하면 이 Exception을 클라이언트에 전달한다.

[MasterThread의 코드 일부]                        

socket.setSoTimeout (SOCKET_TIME_OUT);

socketIS = socket.getInputStream();

socketOS = socket.getOutputStream();

ServerProtocolHandlerIF handler = ServerProtocolHandler.getInstance();

Command cmd = handler.getCommand(socketIS);

try {

         System.out.println("get " + cmd.getName());

         Object object  = cmd.execute();

         handler.send(socketOS, object);

} catch(Exception e) {

         handler.send(socketOS, e);

}

위의 코드에서 socket.setSoTimeOut()을 반드시 해야 됨을 유의한다. 기본적으로 자바의 소켓 라이브러리는 blocking I/O를 기본으로 하므로 read()의 경우 read byte가 생길때까지 무한히 대기하게 된다. 이를 방지하기 위해 socket.setSoTimeOut()을 실행해서 time-out 시간이 지정된 non-blocking I/O형태를 띄게 만든다.

ServerProtocolHandler 객체는 InputStream에서 Command를 읽어 들이고( getCommand() )  OutputStream으로 Object를 출력하는 역할을 한다 ( send() ). ServerProtocolHandler는 이전호에서 기술한 Factory Method 패턴과 Singleton 패턴을 사용해서 만들어졌기 때문에 내부적으로 자신에 대한 인스턴스를 단 하나 가지고 있으며 생성자가 private으로 되어 있어서 반드시 getInstance() 메소드를 통해서만 생성된다.

ServerProtocolHandler는 팩토리 역할을 하며 ServerProtocolHandlerIF가 생성된 객체 역할을 하는데 본 구현에서는 ServerProtocolHandler가 ServerProtocolHandlerIF의 구현도 되기 때문에 약간 간략화된 Factory Method 패턴이라고 할 수 있겠다.

public class ServerProtocolHandler implements ServerProtocolHandlerIF  {

private static ServerProtocolHandlerIF handler = null;

 

private ServerProtocolHandler() {

}

 

public static ServerProtocolHandlerIF getInstance() {

         if(handler == null) {

                     handler = new ServerProtocolHandler();

         }

         return handler;

}

 

public Command getCommand(InputStream inputStream) throws Exception {

         ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);

         Command cmd = (Command)objectInputStream.readObject();

         return cmd;

}

 

public void send(OutputStream outputStream, Object object) throws Exception {

ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);

         objectOutputStream.writeObject(object);

         objectOutputStream.flush();

}

}

위의 코드를 보면 알 수 있듯이 본 구현에서는 객체를 직렬화(Serialize)시켜서 전달함으로써 통신 프로토콜 처리기가 상당히 깔끔해 졌음을 알 수 있다.

Command 인터페이스는 애플리케이션 서버에 의해 실행되는 대상이며 따라서 애플리케이션에 수행되는 행위들은 이 Command 인터페이스를 구현해야 한다. Command 인터페이스의 execute() 메소드는 MasterThread에 의해서 실행된다.

애플리케이션 서버 구현을 정리해 보면 다음과 같은 클래스 다이어그램을 얻을 수 있다.

[애플리케이션 서버의 클래스 다이어그램]

 

 

나개발의 1차 개발 클라이언트

 

이번에는 자바 클라이언트에서 애플리케이션 서버에  요청하는 클라이언트 모듈을 만들어 보기로 한다. 클라이언트에서는 ClientProtocolHandler와 ClientProtocolHandlerIF를 사용해서 서버와 통신을 하게 된다. 통신은 서버와 대응하여Serialization을 사용한다. 아래 다이어그램에서는 클라이언트에서 애플리케이션 서버에 요청하는 Sequence Diagram을 표현하였다.

[ 클라이언트에서 애플리케이션 서버에 요청하는 Sequence Diagram]

이 클라이언트에서는  특정 스트링을 애플리케이션 서버에 전달시켜 다시 이 스트링을 전달받는 EchoCommand를 실행시킨다. 이처럼 모든 비즈니스 로직 객체는 Command를 상속해야 한다.

public class EchoCommand implements Command {

         private String msg;

 

         public void setString(String msg) {

                     this.msg = msg;

         }

 

         // 애플리케이션 서버에서 실행되는 부분

         // 클라이언트로부터 전달받은 msg를 그대로 돌려준다.

         public Object execute() throws Exception {

                     return msg;

         }

 

         public String getName() {

                     return "EchoCommand";

         }

}

 

Client에서는 다음의 코드를 사용해서 EchoCommand를 서버에서 실행시킨다.

// EchoCommand를 예제로 들어 본다.

Class c = Class.forName("cmd.EchoCommand");

EchoCommand cmd = (EchoCommand)c.newInstance();

cmd.setString("I'm fine.");

 

Socket socket = connect();

ClientProtocolHandlerIF handler = ClientProtocolHandler.getInstance();

// EchoCommand를 애플리케이션 서버에 전송한후 String을 돌려 받는다.

String echo = (String)handler.sendCommand(socket.getInputStream(), socket.getOutputStream(), cmd);

System.out.println("echo from server : " + echo);

 

ClientProtocolHandlerIF의 sendCommand()는 EchoCommand를 서버에 전달한 후 Response로 온 객체를 해석하는 일을 한다. 이 ClientProtocolHandlerIF에서 하는 일은 서버의 ServerProtocolHandlerIF가 하는 일과 유사하므로 이에 대한 자세한 구현 코드는 적지 않겠다.

ServerException은 Java RMI에서의 RemoteException, EJB의 EJBException과 거의 같은 역할을 한다. 애플리케이션 서버에서 생긴 Exception을 대리(delegation)하는 역할을 수행한다.

클라이언트쪽의 Component Diagram은 다음과 같다.

[EchoCommand를 실행시키는 클라이언트쪽의 Component Diagram]

 

그러나 문제는 ..

모든 것이 문제가 없는 것처럼 보였고 테스트 프로그램은 잘 동작되었다. 그러나 문제는 스트레스 테스트때 발생하였다. 동시 요청의 수를 늘리면 수행 속도가 현저하게 감소하는 것이 아니겠는가? 이것은 Java RMI의 경우에서부터 익히 예상했던 일이었다. 자바 VM은 특정 개수 이상의 쓰레드가 동시에 수행되게 되면 오동작을 일으키거나 성능이 현격히 감소하게 되며 여기서의 특정 개수란 대체적으로 40개정도를 의미하는 것으로 보인다.[2] 여기에 대해서 궁금하신 분들은 매우 긴 트랜잭션을 갖는,  즉 DB 행위와 파일 행위, 복잡한 메모리 행위가 하나의 단위로 묶여서 매우 시간이 오래 걸리는 모듈을 수행하는 쓰레드들을 일정 개수 이상 수행시켜 보시기 바란다. 쓰레드 개수에 따른 수행시간이 선형적이지 않음을 확인하실 수 있을 것이다.따라서 하나의 VM내에서 항상 적정 개수의 쓰레드가 유지될 수 있도록 신경을 써야 한다. 나개발이 속한 개발팀은 이 문제를 해결하기 위해 머리를 맞대고 고민하기 시작했다.

문제의 해결 Guarded Suspension  패턴의 적용

나개발은 동시에 수행되는 쓰레드의 개수를 어떻게 일정 개수 이하로 유지되게 만들 수 있는 가를 고민하였다. 문제의 원인이 되는 코드는 바로 아래의 MasterThread 코드이다. 여기서는 클라이언트에서 들어온 요청인 Command 객체를 무조건 바로 수행시키고 있다.

[문제가 되는 MasterThread 코드]

         try {

System.out.println("get " + cmd.getName());

                     Object object  = cmd.execute();

                     handler.send(socketOS, object);

         } catch(Exception e) {

                     handler.send(socketOS, e);

         }

즉 문제 해결의 열쇠는 무조건 Command 객체를 수행시키지 말고 현재 수행되고 있는 쓰레드를 일정 개수 미만일 경우에만 수행하도록 수정하는 것이다. 쓰레드가 일정 개수 미만이라는 조건을 검사하는 역할을 수행하는 CommandQueue라는 객체를 도입하여 다음과 같이 수정을 하였다. CommandQueue는 Guarded Suspension 패턴을 따르는 객체이다.

         CommandQueue queue = CommandQueue.getInstance();

…….

try {

                     queue.push(cmd);

                     System.out.println("get " + cmd.getName());

                     Object object  = cmd.execute();                          

                     handler.send(socketOS, object);

         } catch(Exception e) {

                     handler.send(socketOS, e);

         } finally {

                     queue.pop();

         }

CommandQueue.push()에서는 일정 개수 이상의 쓰레드가 수행되고 있으면 현재 쓰레드를 wait 상태로 만들고 일정 개수 이하로 떨어졌을 때 그 쓰레드를 wakeup되게 한다. 그리고 작업이 끝나면 CommandQueue.pop()을 통해 작업 쓰레드 목록에서 삭제를 한다. 모든 쓰레드는 FIFO로 처리되므로 CommandQueue라는 이름을 붙였다. CommandQueue의 push()와 pop() 코드는 아래와 같으며 내부적으로 Queue 객체를 사용하고 있다. CommandQueue의 생성자에서 최대 동시 수행가능한 쓰레드 개수를 지정한다.(본 구현에서는 20개로 함.)

 

public class CommandQueue {

         private int activeCommands;

         private Queue queue;

         private int maxCommands;

         private static CommandQueue commandQueue = null;

 

         private CommandQueue(int maxCommands) {

                     this.activeCommands = 0;

                     this.maxCommands = maxCommands;

                     this.queue = new Queue();

         }

 

         public static CommandQueue getInstance() {

                     if(commandQueue == null) commandQueue = new CommandQueue(20);

                     return commandQueue;

         }

 

         public void push(Command cmd) {

                     if(activeCommands < maxCommands) {

                                 activeCommands++;

                     }

                     else {

                                 queue.push(cmd);

                                 synchronized(cmd) {

                                             try {

                                                         cmd.wait();

                                             } catch(InterruptedException ie) {

                                             }

                                 }

                     }

         }

 

         public void pop() {

                     activeCommands--;

                     if(queue.size() > 0) {

                                 Command cmd = (Command)queue.pop();

                                 if(cmd != null) {

                                             synchronized(cmd) {

                                                         cmd.notify();

                                             }

                                             activeCommands++;

                                 }

                     }

         }

         ……….

}

 

 [Box 2  Guarded Suspension Pattern 삽입]

Guarded Suspension 패턴을 적용한 쓰레드 제어 모듈이 적용된 새로운 애플리케이션 서버의 Sequence Diagram은 다음과 같다. 1차 개발시의 Sequence Diagram과 비교해 보면 CommandQueue에 대한 push, pop 기능이 추가되었음을 알 수 있다.

[쓰레드 제어 모듈이 추가된 애플리케이션 서버의 Sequence Diagram]

정리

Concurrency 패턴들을 잘 이해한다면 쓰레드의 제어에 많이 응용할 수 있을 것이다. 비단 본 예제의 Guarded Suspension뿐만 아니라 Single Threaded Execution, Balking, Scheduler, Read/Write Lock, Producer-Consumer, Two-Phase Termination 패턴들은 모두 실제 프로그래밍을 할 때 아주 유용한 코드 템플릿을 제공해 주리라 생각한다.

나개발은 이 Concurrency 패턴을 숙지함으로써 보다 쉽게 동시 수행 쓰레드를 제한하는 기능을 추가할 수 있었다.

 

 

Box 1. Command 패턴

[Command 패턴의 Class Diagram]

AbstractCommand에서는 모든 커멘드가 공통적으로 요구되는 메소드들을 정의해 놓는다. 위의 클래스 다이어그램에서는 doIt()과 undoIt()이 공통적인 메소드가 될 것이다. ConcreateCommand는 이 AbstractCommand에서 정의된 공통 메소드들을 구현해야 한다. 클라이언트는 다양한ConcreateCommand들을 AbstractCommand로 단일하게 다룰 수 있는 이점을 가진다. CommandManager는 ConcreateCommand의 Repository역할을 한다.

 

Box 2. Guarded Suspension 패턴

Guarded Suspension 패턴은 다음의 코드 패턴을 가진다.

class Widget {

         public synchronized void foo() {

                     while(!isOK()) {

                                 wait();

                     }

                     // 실행 코드

         }

         public synchronized void bar(int x) {

                     // 실행 코드

                     notify();

         }

}

foo()가 호출이 되어 실행코드가 수행되기 위해서는 isOK()가 true여야 한다. IsOK()가 false라면 계속 wait상태를 유지하게 된다. 한편 bar()라는 메소드에서는 작업을 마친후 notify()를 수행함으로써 기존의 wait상태에 있는 쓰레드를 동작시켜 준다.

Guarded Suspension은 어떤 전제 조건이 만족되지 전까지 대기해야 하는 상황일 때 많이 사용된다. 조심해야 할 사항은 만일 적절한 notify() 가 없다면 자칫 무한 대기에 빠져 버리는 경우가 종종 생기게 된다는 점이다.

 

맺음말 

커스텀 소프트웨어에서 벗어나 좀더 범용적이고 신뢰성 있는 소프트웨어를 만드는 것.

개발자들이 난점에 부딪히고 어려움을 겪는 과정은 위에서부터 시작된다. 어느 것에서 어떻게 핵심 골격만을 뽑아 내어 설계를 정립하고, 커스텀하게 만들어질 부분과 공용적으로 만들어질 부분들을 잘 설계할 수 있는가?

 

이 부분은 각자의 능력에 전적으로 의존한다. 개발자로써의 경험과 추상화의 역량 등이 이 모든 것을 판가름 짓는다. 물론 이들 역량은 타고 나는 경우도 있으나, 훈련에 의해서 길러진다고 하는 편이 많을 것이다.

 

프로그래머들이 나이가 들면 코딩 능력은 젊은 새로운 개발자들에 밀릴 수 밖에 없다. 마치 운동 선수들이 나이 들면 은퇴하듯이, 프로그래머들도 나이 들면 은퇴할 수 밖에 없을까? 필자들은 오래 전부터 이 질문에 대한 답을 찾아왔었다. 하나하나의 코딩은 새로운 프로그래머들이 더욱 뛰어날지 모르지만, 넓은 시야를 가지고 나무가 아닌 숲을 보고 전체를 생각하는 역량은 결코 단 시간에 갖추어지는 것이 아니다.

 

디자인 패턴은 여러분에 이러한 역량을 기를 수 있는 기회를 제공할 것이다. 패턴은 여러 인간들이 만든 사상과 의견이 포함되어 있는 총체적 산물이므로 이러한 패턴을 도입하는 것은 보다 일반적이고, 핵심에 근접한, 범용적으로 사용할 수 있는 어플리케이션 제작에 한걸음 다가 갔음을 의미한다.  필자의 경험에서도 패턴의 개념들을 도입하기 이전의 코딩 형태들과 도입한 이후의 코딩의 형태들 사이에는 많은 차이가 있었다.

 

훌륭한 코치 아래에서 훌륭한 선수들이 탄생하듯이, 훌륭한 개발 다이렉터 아래에서 훌륭한 프로그래머들이 탄생할 것은 당연한 이치이다.

 

참고 문헌

1.       Patterns in Java Volume 1, Mark Grand

2.       Design Patterns, Elements of Reusable Object-Oriented Software, Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides



[1] Produce-Consumer Model

[2] 동시 사용 가능한 쓰레드 개수는 하드웨어 성능에 크게 의존하므로 40 값이 항상 맞는 것은 아니다.

'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[1/4]  (0) 2008.07.25
And
prev | 1 | ··· | 6 | 7 | 8 | 9 | 10 | next