상세 컨텐츠

본문 제목

HiMyTV 방송 서버 구조 #2 - DB에 대한 비동기 처리

소프트웨어 공학

by ryujt 2013. 7. 22. 07:54

본문


[그림 1]


이번에는 방송 서버의 다른 기능을 좀 부각해서 설명을 이어 갑니다.  바로 데이터베이스를 비동기로 처리하는 내용입니다.  지난 포스트의 클래스 다이어그램에서는 표시되지 않았지만, 룸서버는 데이터베이스를 처리하는 TDatabase 클래스와 상호 협조를 하고 있습니다.  


TDatabase는 일반적인 데이터베이스 시스템일 수도 있고, 현재 서비스에서처럼 http 프로토콜을 이용해서 사용자 정보 등을 쿼리해 올 수도 있습니다.


일단 예제로 로그인 과정을 한 번 살펴보도록 하겠습니다.


[소스 1] 로그인 과정

procedure TRoomServer.rp_Login(AConnection: TConnection;
  ACustomData: DWord; AData: pointer; ASize: integer);
var
  ValueList : TValueList;
  pPacket : PRoomPacket absolute AData;
begin
  if AConnection.IsLogined then Exit;

  AConnection.UserName := 'Loging...';

  ValueList := TValueList.Create;
  try
    ValueList.Text := pPacket^.ToString(ASize);

    if ValueList.Integers['Version'] <> ROOM_SERVER_VERSION then begin
      sp_ErVersion(AConnection);
      Exit;
    end;

    AConnection.LocalIP := ValueList.Values['LocalIP'];

    FDatabase.Execute(TDatabaseRequestLogin.Create(Self, AConnection, ValueList.Values['LoginString']));
  finally
    ValueList.Free;
  end;
end;

22: 라인에 보면 FDatabase(TDatabase의 객체 레퍼런스)의 Execute() 메소드를 이용해서 로그인 요청을 하고는 결과를 처리하지 않고 패킷 처리를 완료합니다.  그리고 파라메터로 TDatabaseRequestLogin의 객체를 넘겨주고 있습니다.  그리고 TDatabaseRequestLogin 클래스의 생성자의 맨 앞에 있는 파라메터는 자기 자신을 가르키도록 되어 있습니다.


TDatabaseRequestLogin는 로그인 결과를 Self 객체에게 피드백을 해주는 방식을 취합니다.  콜백 함수와 같은 것 입니다.




[그림 1] Job Flow - DB에 대한 비동기 처리


[그림 1]의 Job Flow를 설명하면 다음과 같습니다.

  • rp_Login(이벤트): 클라이언트로부터 로그인 요청을 받았습니다.  
  • Database.Execute: Database 객체에게  DB 처리를 요청합니다.
  • Add: 작업 요청을 큐에 저장합니다.
  • WakeUp: 워크 스레드를 깨 웁니다.
  • WakeUp(이벤트): 워크 스레드들이 일어납니다.  
  • Get: 작업 요청을 하나씩 꺼내옵니다.  꺼내 온 결과 nil 이면 무시합니다.  
  • Worker. Execute: 요청 작업이 있으면 실제 작업을 처리하고, TDatabaseRequestLogin의 첫 번 째 파라메터에 해당하는 객체에게 Call Back을 실행하여 결과를 알려 줍니다.

[소스 2] 실행 결과를 Call Back 받아 처리 하는 부분
procedure TRoomServer.DatabaseRequestLogin(AConnection: TConnection;
  AID: integer; AResult: TValueList);
var
  RoomUnit : TRoomUnit;
  ErrorMsg : string;
  UserLimit : integer;
  IsLogined : boolean;
begin
  if AConnection.IsLogined then Exit;

  IsLogined := AResult.Booleans['result'];

  AConnection.RoomID := AResult.Values['room_id'];
  AConnection.UserID := AResult.Values['user_id'];
  AConnection.UserName := AResult.Values['user_name'];
  AConnection.UserLevel := AResult.Integers['user_level'];

  // TODO: 임시 코드 예전 웹 서비스에 맞춰서 추가 함
  if AResult.Booleans['owner'] then AConnection.UserLevel := TUserLevel.ADMIN;

  ErrorMsg := AResult.Values['error_msg'];
  UserLimit := AResult.Integers['user_limit'];

  AConnection.UserID := TIdURI.URLDecode(AConnection.UserID);
  AConnection.UserID := StringReplace(AConnection.UserID, '%20', ' ', [rfReplaceAll]);

  ErrorMsg := TIdURI.URLDecode(ErrorMsg);
  ErrorMsg := StringReplace(ErrorMsg, '%20', ' ', [rfReplaceAll]);

  if not IsLogined then begin
    sp_ErLogin(AConnection, ErrorMsg);
    Exit;
  end;

  RoomUnit := FRoomList.FindRoomByRoomID(AConnection.RoomID);
  if RoomUnit = nil then begin
    sp_ErLogin(AConnection, '강의실에 접속하는 중 에러가 발생하였습니다.'#13#10'잠시 후 다시 시도하시기 바랍니다.');
    Exit;
  end;

  if (UserLimit > 0) and (RoomUnit.ConnectionList.Count >= UserLimit) then begin
    sp_ErLogin(AConnection, '강의실 정원을 초과하였습니다.');
    Exit;
  end;

  if (AConnection.UserLevel < TUserLevel.ADMIN) and RoomUnit.CheckBlackList(AConnection) then begin
    sp_ErLogin(AConnection, '강퇴 된 사용자는 당분간 강의실을 이용하실 수가 없습니다.');
    Exit;
  end;

  AConnection.IsLogined := IsLogined;

  RoomUnit.UserIn(AConnection);
end;


[소스 2]에 구현된 DatabaseRequestLogin 메소드는 IDatabaseRequestLogin 인터페이스의 메소드입니다.  즉, TRoomServer는 DB 처리에 대한 Call back을 받기 위해서, IDatabaseRequestLogin 상속 받아서 구현해야 합니다.


[소스 3]은 IDatabaseRequestLogin와 TDatabaseRequestLogin의 선언부 입니다.


[소스 3]

type
  IDatabaseRequestLogin = interface
    ['{9B615467-9024-41C8-8E92-CD40B64AF68E}']

    {*
      로그인 요청에 대한 데이터 처리 결과 통보
      @param AConnection 처리를 요청한 (연결) 객체
      @param AID 처리를 요청한 객체의 아이디
      @param AResult 처리 결과 데이터
    }
    procedure DatabaseRequestLogin(AConnection:TConnection; AID:integer; AResult:TValueList);
  end;

  TDatabaseRequestLogin = class (TDatabaseRequest)
  protected
    procedure Execute; override;
  public
    Receiver: IDatabaseRequestLogin;
    Connection : TConnection;
    ConnectionID : integer;
    LoginString : string;  /// JSon 포멧의 RoomID, UserID, UserPass, etc

    {*
      @param AReceiver 처리를 요청한 그리고 결과를 메시지를 받을 객체
      @param AConnection 처리를 요청한 (연결) 객체
      @param ALoginString 로그인을 위한 정보
    }
    constructor Create(AReceiver:IDatabaseRequestLogin; AConnection:TConnection; ALoginString:string); reintroduce;
  end;


데이터베이스 처리는 로그인 뿐 아니라, 게임 등에서는 승패 정보 등을 저장하고 순위 정보를 쿼리해오는 등의 다양한 일을 하게 됩니다.  저는 모든 요청 사항에 대하여 따로 클래스와 인터페이스를 작성하여 처리하고 있습니다.


관련글 더보기