스포츠 경기에 세트 플레이라는 것이 있습니다. 미리 약속한 방식으로 공격을 진행하는 것입니다. 하지만, 상대가 어떻게 나올 지 확신 할 수가 없는데, 약속 된 방식만으로 공격이 성공 할 수 있을까요?
이제 객체들의 세트 플레이를 생각해 보겠습니다.
승리라는 하나의 목표를 위해서 여러 명의 선수들이 경기에 나서듯이, 여러 개의 객체들이 하나의 목표를 위해 팀워크를 이룬다면 효율적인 개발을 할 수가 있습니다.
"복잡한 문제를 객체들로 나눠서 쉽게 해결 할 수 있는 상태를 만든다."
좋은 아이디어입니다. 그런데 어떻게 해야 하는 걸까요?
이제 간단한 예제를 통해서 지금까지 설명한 내용을 자세하게 살펴보도록 하겠습니다. 하마티(http://www.himytv.com/) 개발을 통해서 실제 있었던 개발 상황을 설명하기 쉽도록 간략하게 다듬어서 진행하겠습니다.
문제: PC 화면을 실시간으로 동영상 압축하는 라이브러리 구축 (DesktopEncoder)
해결해야 할 문제가 비교적 간단합니다. 이제 기능 목록을 작성하여 어떤 객체들로 나눠야 할 지를 고민해보겠습니다.
[그림 1] 기능 목록 작성
목록들을 보니, 유사한 기능들이 있습니다. 이넘들을 묶어 보겠습니다.
[그림 2] 유사한 기능끼리 묶어 보기
이렇게 기능 분석을 통해서 해결해야 할 문제를 3 개의 객체로 나눠서 작업을 하도록 하겠습니다. 그리고, 이들 객체를 하나로 묶어 줄 DesktopEncoder 라는 객체와 이것을 사용하는 외부의 객체를 Client 라고 하겠습니다.
이제는 이들 사이의 최소한의 약속을 찾아 낼 시간입니다.
[그림 3] Job Flow
일단 [그림 3]과 같이 외부의 객체 Client와 DesktopEncoder 사이의 약속을 찾아냈습니다. 제가 만든 Job Flow 라는 문서 형식을 사용하고 있습니다. 플로우 차트를 확장한 형태입니다. (http://ryulib.tistory.com/164)
외부 객체 Client와 DesktopEncoder 사이의 약속들이 너무 단순해서 특별히 문제가 발생 할 시나리오를 찾지 못했습니다. 이제 요청받은 일들을 나눠주려고 합니다. 그런데! 비동기로 압축을 진행해야 하기 때문에 스레드에 대한 안정성의 문제가 발생합니다. 이런 경우가 자주 발생하는 편이라 저는 TaskQueue 라는 클래스 라이브러리를 만들어서 쓰고 있습니다. 이번에도 TaskQueue 를 사용하도록 하겠습니다.
TaskQueue 는 큐에 데이터를 넣으면, 스레드에 의해서 데이터가 순서대로 이벤트로 발생합니다. 작업을 요청하는 쪽에서는 요청만하고 기다리지 않습니다. 실제 작업은 TaskQueue 내부 스레드에 의해서 진행하도록 합니다.
[그림 4] Job Flow
[그림 4]에 보시면 새로운 객체 TaskQueue 가 늘어났습니다.
DesktopEncoder.Start() 메소드가 실행되면 TaskQueue.Add(Start) 로 인해서 큐 안에 Start 메시지가 쌓이고, 내부의 스레드를 깨웁니다. 스레드는 깨어나면 큐에 있는 모든 메시지를 하나씩 차례로 이벤트로 발생시킵니다. 이로 인해서 Start 이벤트가 발생합니다.
Start 이벤트가 발생하면, DesktopCapture(화면 캡쳐 객체)와 FrameCompare(화면 간 비교 객체)의 Clear 메소드를 실행하여, 이전에 실행 된 압축 중간 데이터를 삭제합니다.
Stop 이벤트에서는 아무것도 표시되지 않았으나, 내부적으로 플래그에 Stoped 처리가 되어 이후 외부에서 화면데이터를 읽으려고 하면 nil을 반환하도록 합니다. 전체적인 시나리오에 크게 영향을 주지 않는다고 판단하여 생략하였습니다. 실제 개발에서도 너무 시시콜콜한 내용들은 생략하고 있습니다.
Start와 Stop의 경우에는 굳이 비동기를 사용하지 않아도 되겠으나, 압축이 일어나는 스레드와 분리 되는 것보다 같은 스레드에서 작업하는 것이 편리한 지라, 모든 작업을 TaskQueue 를 통해서 하고 있습니다.
이번에는 DesktopEncoder.Get() 메소드에 대해서 고민을 해보도록 하겠습니다. 압축을 하면 데이터가 조각으로 나오며, 이것이 하나씩 읽혀지게 됩니다. 만약 네트워크 속도의 한계가 오면 외부 객체가 더 이상 Get을 하지 않게 되고, 압축은 잠시 멈추게 됩니다. 이 조건을 살펴보면 압축 된 데이터가 잠시 어딘가에 보관되어야 한다는 것을 알 수가 있습니다. 아직까지 설계에서 나타나지 않은 버퍼 객체가 필요한 것 입니다. [그림 5]는 버퍼 객체를 추가한 Job Flow 입니다.
[그림 5] Jow Flow
화면 압축 데이터를 저장할 PacketBuffer라는 새로운 객체가 늘어 났습니다. Start와 Stop 이벤트가 발생하면 저장 된 데이터를 모두 소거하도록 되어 있습니다.
DesktopEncoder.Get() 메소드가 실행되면 PacketBuffer.Get()을 실행하여, 저장 된 데이터를 가져와서 돌려줍니다. 데이터를 돌려준다는 것은 해당 메소드가 void 형태가 아니고 리턴 값이 존재한다는 의미입니다. 그리고, 만약 PacketBuffer가 비어 있다면, 다음 화면을 압축하라는 의미로 TaskQueue.Add(NextScreen)을 통해서 TaskQueue에 메시지를 추가합니다.
조건에 대해서는 아래처럼 마름모 기호를 사용 할 수도 있지만, 간략하게 화살표 위에 표시하였습니다.
[그림 6]은 모든 조건이 충족 된 최종 Job Flow 입니다.
[그림 6] Jow Flow
이렇듯 각 객체 간의 약속이 정해지면, 구현 알고리즘을 캡슐화한 각 객체들에 대한 개발은 분업해서 작업하기가 쉬워집니다. 또한 약속이 변경되지 않는한 내부 구현의 변화에 의해서 다른 객체들이 영향을 받지 않게 됩니다. 그리고, 전체적인 프로세스 흐름을 쉽게 파악하고 이해하기 쉬워지기 때문에 개발 효율면에서도 도움이 됩니다.
실제 개발 했던 과정을 소설처럼 모두 설명해드리고 싶었지만, 오늘은 여기까지로 참겠습니다 ^^;
실제 개발 과정에서는 Job Flow가 바로 바로 나오지 않고, 여러 시나리오를 상상해 가면서 수정을 반복합니다. 명확한 해답이 필요한 경우에는 중간에 작은 테스트 프로그램을 통해서 시나리오들을 검증하기도 합니다.
아래는 위의 Job Flow로 표현했던 아이디어를 코드로 나타낸 일부입니다. 코딩을 하면서 추가 및 변경 된 부분들이 있습니다.
unit DesktopEncoder; interface uses DesktopEncoder.FrameCompare, DesktopEncoder.FrameEncoder, DeskCamUtils, DeskCamInterface, TaskQueue, PacketBuffer, DesktopCapture, ScreenSlice, Windows, SysUtils, Classes, SyncObjs; type TDesktopEncoder = class (TComponent, IDesktopEncoder) private // implementation of IDesktopEncoder function GetIsPaused:boolean; procedure SetIsPaused(AValue:boolean); private FStarted : boolean; FIsPaused : boolean; FPacketBuffer : TPacketBuffer; FDesktopCapture : TDesktopCapture; procedure add_DeskCamInfo; private FTaskQueue : TTaskQueue; procedure on_Task(Sender:TObject; AData:pointer; ASize:integer; ATag:pointer); procedure on_Terminate(Sender:TObject); procedure do_Start; procedure do_Stop; procedure do_Clear; procedure do_GetNextScreen; private FFrameCompare : TFrameCompare; procedure on_ChangedFrame(Sender:TObject; AThreadNo,AIndex:word; AData:Pointer; ASize:integer; ...); procedure on_FrameCompare_Completed(Sender:TObject); private FFrameEncoder : TFrameEncoder; procedure on_DeskFrame(Sender:TObject; AData:pointer; ASize:integer); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure Start; procedure Stop; procedure Clear; function Get(var AData:Pointer; var ASize:integer):boolean; published property DesktopCapture : TDesktopCapture read FDesktopCapture; end; implementation type TTaskType = (ttStart, ttStop, ttClear, ttGetNextScreen); { TDesktopEncoder } procedure TDesktopEncoder.add_DeskCamInfo; var Packet : TDeskCamInfoPacket; begin Packet.Header.FrameType := ftDeskCamInfo; Packet.Header.Index := 0; Packet.DeskCamInfo.ScreenWidth := (FDesktopCapture as IDesktopCapture).GetScreenWidth; Packet.DeskCamInfo.ScreenHeight := (FDesktopCapture as IDesktopCapture).GetScreenHeight; FPacketBuffer.Add( @Packet, SizeOf(Packet) ); end; procedure TDesktopEncoder.Clear; begin FTaskQueue.Add( Pointer(Integer(ttClear)) ); end; constructor TDesktopEncoder.Create(AOwner: TComponent); begin inherited; FStarted := false; FIsPaused := false; FPacketBuffer := TPacketBuffer.Create; FDesktopCapture := TDesktopCapture.Create(Self); FFrameCompare := TFrameCompare.Create; FFrameCompare.OnChangedFrame := on_ChangedFrame; FFrameCompare.OnCompleted := on_FrameCompare_Completed; FFrameEncoder := TFrameEncoder.Create; FFrameEncoder.OnDeskFrame := on_DeskFrame; FTaskQueue := TTaskQueue.Create; FTaskQueue.OnTask := on_Task; FTaskQueue.OnTerminate := on_Terminate; end; destructor TDesktopEncoder.Destroy; begin FTaskQueue.Clear; FreeAndNil(FTaskQueue); inherited; end; procedure TDesktopEncoder.do_Clear; begin FPacketBuffer.Clear; FFrameCompare.Clear; end; procedure TDesktopEncoder.do_GetNextScreen; var ScreenSlice : TScreenSlice; begin if FStarted = false then Exit; if FIsPaused then Exit; try ScreenSlice := FDesktopCapture.GetScreenSlice; if ScreenSlice <> nil then FFrameCompare.Execute(ScreenSlice); except // TODO: 에러 처리 end; end; procedure TDesktopEncoder.do_Start; begin if FStarted then Exit; FStarted := true; FPacketBuffer.Clear; FDesktopCapture.Clear; FFrameCompare.Clear; add_DeskCamInfo; FDesktopCapture.Start; end; procedure TDesktopEncoder.do_Stop; begin if not FStarted then Exit; FStarted := false; FDesktopCapture.Stop; FPacketBuffer.Clear; FFrameCompare.Clear; end; function TDesktopEncoder.Get(var AData: Pointer; var ASize: integer): boolean; begin AData := nil; ASize := 0; Result := FPacketBuffer.GetPacket( AData, ASize ); if FPacketBuffer.IsEmpty then FTaskQueue.Add( Pointer(Integer(ttGetNextScreen)) ); end; function TDesktopEncoder.GetIsPaused: boolean; begin Result := FIsPaused; end; procedure TDesktopEncoder.on_ChangedFrame(Sender: TObject; AThreadNo,AIndex:word; AData: Pointer; ASize, ...); begin FFrameEncoder.Execute( AIndex, AData, ASize, ... ); end; procedure TDesktopEncoder.on_DeskFrame(Sender: TObject; AData: pointer; ASize: integer); begin FPacketBuffer.Add( AData, ASize ); end; procedure TDesktopEncoder.on_FrameCompare_Completed(Sender: TObject); var Packet : TEndOfFramePacket; begin Packet.Header.FrameType := ftEndOfFrame; Packet.Header.Index := 0; Packet.CountOfFrames := 0; Packet.SizeOfFrames := 0; FPacketBuffer.Add( @Packet, SizeOf(Packet) ); end; procedure TDesktopEncoder.on_Task(Sender: TObject; AData: pointer; ASize: integer; ATag: pointer); begin case TTaskType(Integer(ATag)) of ttStart: do_Start; ttStop: do_Stop; ttClear: do_Clear; ttGetNextScreen: do_GetNextScreen; end; end; procedure TDesktopEncoder.on_Terminate(Sender: TObject); begin FreeAndNil(FPacketBuffer); FreeAndNil(FDesktopCapture); FreeAndNil(FFrameCompare); FreeAndNil(FFrameEncoder); end; procedure TDesktopEncoder.SetIsPaused(AValue: boolean); begin FIsPaused := AValue; FDesktopCapture.IsPaused := AValue; end; procedure TDesktopEncoder.Start; begin FTaskQueue.Add( Pointer(Integer(ttStart)) ); end; procedure TDesktopEncoder.Stop; begin FTaskQueue.Add( Pointer(Integer(ttStop)) ); end; end.
TaskQueue 에 대한 소스는 아래 링크를 참고하시기 바랍니다.
Episode #1 - 어려운 문제 조각내서 해결하기 (0) | 2019.11.24 |
---|---|
프로그래밍 칠거지악 (0) | 2014.11.21 |
재귀 호출 (2) | 2013.12.17 |
클래스 상속과 인터페이스 구현의 차이 #2 (6) | 2013.12.12 |
PC 화면 압축과 실시간 전송에 대한 연구 (0) | 2013.10.20 |