[그림 2] Jow Flow 기본 구성 요소


[그림 1]에서 Jow Flow를 구성하는 기본 요소를 나열하였습니다.  상세한 설명은 아래와 같습니다.
  • 이벤트: 객체에 의미있는 변화를 의미합니다.  또한, 프로세스의 진입점을 의미하기도 합니다.
  • 프로세스: 객체가 제공하는 또는 해야하는 기능을 의미합니다.  함수 또는 메소드에 해당합니다.
  • 메시지: 객체간에 전달되는 데이터를 의미합니다.
  • 판단: 조건 제어문인 if 또는 case에 해당합니다. 
  • 흐름: 프로세스 흐름의 방향 또는 의존성의 방향을 의미합니다. 
     

[그림 2] Jow Flow의 예
 
[그림 2]는 실제 개발에서 사용됐었던 문서의 예 입니다.  PC 화면 압축 솔루션인 DeskCamEncoder라는 시스템을 DeskCamCapture, Repeater, FrameEncoder 세 가지의 객체로 분업하여 설계되었습니다.  시스템을 구성하는 하위 객체들은 한 칸 아래에 그리는 것으로 표현합니다.
  • 상위 시스템인 DeskCamEncoder는 Start(), Stop() 메소드를 제공하고, OnDeskFrame이라는 이벤트를 발생시킨다는 의미 입니다.  (On은 생략하고 있음)
  • DeskCamEncoder.Start()가 실행되면, PC 화면의 캡쳐를 담당하는 DeskCamCapture 객체의 Start() 메소드를 실행합니다.  이후, 쓰레드를 래핑한 객체인 Repeater의 Start() 메소드를 실행하고, 각 프레임을 실제 압축하는 FrameEncoder 객체 내부의 버퍼를 Clear() 메소드로 지워서 이전 작업에 대한 영향을 없애도록 합니다.
  • DeskCamCapture.Start() 이후에는 객체 내부의 타이머를 통해서 주기적으로 화면을 캡쳐해서 데이터를 가지고 있습니다.  이전에 캡쳐한 것이 아직 있으면 무시합니다.
  • Repeater 객체 내부에 있는 쓰레드를 통해서 주기적으로 반복이 일어납니다.  이것을 Repeat 이벤트로 표현했습니다.  즉, 쓰레드가 반복될 때마다 실행되는 이벤트 입니다.  OnRepeat 이벤트가 발생하면, DeskCamCapture.GetScreenSlice() 메소드를 호출합니다.  결과를 받아와서 nil이면 아무런 동작을 하지 않고, ScreenSlice 데이터가 리턴되면 FrameEncoder 객체의 Execute() 메소드를 통해서 압축을 실행합니다.  이 부부은 판단 요소를 추가해서 조건문이 사용됨을 알려야 하나, 필자의 경우 단순한 도면을 선호하기 때문에 판단 요소를 생략하고 흐름 요소만으로 분기가 일어나고 있음을 표현하고 있습니다.
  • 압축된 결과는 DeskFrame 이벤트로 발생됩니다.  이것은 곧장 상위 시스템인 DeskCamEncoder의 DeskFrame 이벤트로 연결되어 추후 외부에서 사용 할 수 있도록 합니다. 

Jow Flow를 그리는데에는 시스템의 어느 정도의 깊이를 다루느냐가 중요한 변수가 되는데, 그것은 추후 다루기로 하겠습니다.



[소스 1] DeskCamEncoder.pas
unit DeskCamEncoder;

interface

uses
  DeskCamUtils, DeskCamCapture, ScreenSlice, ThreadRepeater, FrameEncoder,
  Classes, SysUtils;

type
  TDeskCamEncoder = class (TComponent)
  private
    FDeskCamCapture : TDeskCamCapture;
    FFrameEncoder : TFrameEncoder;
  private
    FRepeater : TThreadRepeater;
    procedure on_Repeat(Sender:TObject);
  private
    function GetDeskCamCapture: IDeskCamCapture;
    function GetOnDeskFrame: TDeskFrameEvent;
    function GetOnEndOfFrame: TNotifyEvent;
    procedure SetOnDeskFrame(const Value: TDeskFrameEvent);
    procedure SetOnEndOfFrame(const Value: TNotifyEvent);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure Start;
    procedure Stop;
  published
    property DeskCamCapture : IDeskCamCapture read GetDeskCamCapture;
    property OnDeskFrame : TDeskFrameEvent read GetOnDeskFrame write SetOnDeskFrame;
    property OnEndOfFrame : TNotifyEvent read GetOnEndOfFrame write SetOnEndOfFrame;
  end;

implementation

{ TDeskCamEncoder }

constructor TDeskCamEncoder.Create(AOwner: TComponent);
begin
  inherited;

  FDeskCamCapture := TDeskCamCapture.Create(Self);
  FFrameEncoder := TFrameEncoder.Create;
  FRepeater := TThreadRepeater.Create(Self);
end;

destructor TDeskCamEncoder.Destroy;
begin
  Stop;
  
  FreeAndNil(FDeskCamCapture);
  FreeAndNil(FFrameEncoder);
  FreeAndNil(FRepeater);

  inherited;
end;

function TDeskCamEncoder.GetDeskCamCapture: IDeskCamCapture;
begin
  Result := FDeskCamCapture;
end;

function TDeskCamEncoder.GetOnDeskFrame: TDeskFrameEvent;
begin
  Result := FFrameEncoder.OnDeskFrame;
end;

function TDeskCamEncoder.GetOnEndOfFrame: TNotifyEvent;
begin
  Result := FFrameEncoder.OnEndOfFrame;
end;

procedure TDeskCamEncoder.on_Repeat(Sender: TObject);
var
  ScreenSlice : TScreenSlice;
begin
  ScreenSlice := FDeskCamCapture.GetScreenSlice;
  if ScreenSlice = nil then begin
    Sleep(5);
    Exit;
  end;

  FFrameEncoder.Execute(ScreenSlice);
end;

procedure TDeskCamEncoder.SetOnDeskFrame(const Value: TDeskFrameEvent);
begin
  FFrameEncoder.OnDeskFrame := Value;
end;

procedure TDeskCamEncoder.SetOnEndOfFrame(const Value: TNotifyEvent);
begin
  FFrameEncoder.OnEndOfFrame := Value;
end;

procedure TDeskCamEncoder.Start;
begin
  FFrameEncoder.Clear;
  FDeskCamCapture.Start;
  FRepeater.Execute(on_Repeat);
end;

procedure TDeskCamEncoder.Stop;
begin
  FDeskCamCapture.Stop;
  FRepeater.Stop;
end;

end.

[소스 1]은 실제 작성된 델파이 소스 코드입니다.  설계 도면에 비해서 추가된 부분들이 있어서 똑 같지는 않습니다.

12, 13, 15: DeskCamEncoder를 구성하는 세 객체가 멤버로 선언되어 있음을 보실 수가 있습니다.  (정확하게는 객체를 참조하는 레퍼런스 변수라고 표현해야하겠지만)

74-85:  Repeater의 OnRepeat 이벤트를 처리하는 코드를 확인하실 수가 있습니다.  79: 라인에서처럼 nil이 리턴되면 무시하고, 그 이외의 경우에는 받아온 ScreenSlice를 FrameEncoder에게 넘겨서 압축을 실행합니다.  ScreenSlice는 캡쳐된 화면을 Frame 단위로 조각낸 데이터 객체입니다.

92-95: DeskCamEncoder의 OnDeskFrame 이벤트 핸들러가 지정될 때, 실제로는 FrameEncoder의 OnDeskFrame 이벤트에 연결하도록 합니다.  따라서, [그림 2]에서 설계한 것과 같이 FrameEncoder의 OnDeskFrame 이벤트가 발생하면 결과적으로 DeskCamEncoder 이벤트가 발생하도록 합니다.

이렇듯 Jow Flow를 통해서 각 객체들의 인터페이스를 정의하고, 이들이 서로 어떻게 호출되는 지는 상위 시스템에서 구현하는 것만으로 전체 시스템의 흐름을 파악할 수가 있게되었습니다.  이후, 하위 객체인 DeskCamCapture, Repeater 그리고 FrameEncoder의 내부 구현을 하는 과정에서는 자신 이외의 다른 객체의 정보를 전혀 모르더라도 문제가 되지 않습니다.  즉, 개별 개발자에게 업무를 나눠서 진행할 때, 서로 방해가 되지 않습니다.  (인터페이스와 구현의 분리)

저는 팀장 또는 설계자의 책임이 바로 여기까지라고 보고 있습니다.  인터페이스 계층까지는 팀장 또는 설계자가 코딩 또는 구현을 직접 관리할 수 있어야 한다고 생각합니다.  그렇지 않으면 설계와 구현이 따로 놀게 됩니다.  그리고, 무엇보다도 가장 중요한 것은 반복과 피드백입니다.  한 번에 모든 설계와 구현이 마무리 될 거라는 것은 환상에 지나지 않습니다. 


Jow Flow를 제대로 활용하려면 소스를 보고 다시 Jow Flow를 그리는 연습도 필요합니다. 



[그림 3] function의 표현

[그림 3]은 델파이에서 function에 대한 표현 방법을 나타내고 있습니다.  C 계열의 언어에서는 리턴 타입이 void가 아닌 함수를 뜻 합니다.  GetScreenSlice는 아래와 같이 선언되어 있습니다.  즉, 리턴 값이 있는 함수라는 의미가 됩니다.  이럴 때 외부에서 호출을 받으면 결과 값(데이터)를 되돌려줘야 하기 때문에 [그림 3]처럼 표현하고 있습니다.  이때, 흐름 요소가 하나가 아닌 것은 리턴 값의 종류에 따라 흐름이 분기 된다는 의미이며, 적황한 표현은 [그림 4]와 같습니다.
function GetScreenSlice:TScreenSlice;

[그림 4] 조건 분기의 정확한 표현




Posted by 류종택


티스토리 툴바