LazarusCompleteGuide:8.2

From 흡혈양파의 번역工房
Jump to navigation Jump to search

파일 작업하기

어떤 타입의 데이터든 그것을 읽고 쓰는 데 있어 라자루스에서 공급하는 컴포넌트만 사용한다면 파일 처리는 매우 수월하게 이루어진다. 일반 컨트롤은 파일 열기에 LoadFromFile 메소드를, 파일 저장에 SaveToFile 메소드를 가질 것이다. 메모 필드나 복잡한 SynEdit 컨트롤을 다룰 때 우리는 텍스트 파일에 lines 를 처리하고, TImage를 다룰 때는 해당 Picture 프로퍼티를 처리하고 있다:

if OpenPictureDialog1.Execute then
  Image1.Picture.LoadFromFile(OpenPictureDialog1.FileName);


Load... 와 Save... 메소드를 좀 더 상세히 살펴봐야 할 것이다. TBitmap 이나 TJpegImage 와 같은 간단한 기억영역 클래스(storage class) 또한 LoadFromFile 과 SaveToFile 메소드를 가진다. 그들은 TGraphic 클래스의 추상 메소드로부터 상속된다. 이 중요한 기반 클래스는 Graphics 유닛에서 정의된다:

TGraphic = class(TPersistent)
  private
   // omitted
  protected
   // omitted
  public
    procedure Assign(ASource: TPersistent); override;
    constructor Create; virtual;
    procedure Clear; virtual;
   {$IF declared(vmtEquals)}
    function Equals(Obj: TObject): Boolean; override; overload;
   {$ENDIF}
    function LazarusResourceTypeValid(const AResourceType: string):
                                                   boolean; virtual;
    procedure LoadFromFile(const Filename: string); virtual;
    procedure LoadFromStream(Stream: TStream); virtual; abstract;
    procedure LoadFromMimeStream(AStream: TStream; const AMimeType:
                                                    string);virtual;
    procedure LoadFromLazarusResource(const ResName: String);
                                                            virtual;
    procedure LoadFromResourceName(Instance: THandle; const ResName:
                                                   String); virtual;
    procedure LoadFromResourceID(Instance: THandle; ResID: PtrInt);
                                                            virtual;
    procedure LoadFromClipboardFormat(FormatID: TClipboardFormat);
                                                            virtual;
    procedure LoadFromClipboardFormatID(ClipboardType:
               TClipboardType; FormatID: TClipboardFormat); virtual;
    procedure SaveToFile(const Filename: string); virtual;
    procedure SaveToStream(Stream: TStream); virtual; abstract;
    procedure SaveToClipboardFormat(FormatID: TClipboardFormat);
                                                            virtual;
    procedure SaveToClipboardFormatID(ClipboardType: TClipboardType;
                               FormatID: TClipboardFormat); virtual;
    procedure GetSupportedSourceMimeTypes(List: TStrings); virtual;
    function GetResourceType: TResourceType; virtual;
    class function GetFileExtensions: string; virtual;
    class function IsStreamFormatSupported(Stream: TStream): Boolean;
                                                             virtual;
public
  property Empty: Boolean read GetEmpty;
  property Height: Integer read GetHeight write SetHeight;
  property Modified: Boolean read FModifed write SetModified;
  property MimeType: string read GetMimeType;
  property OnChange: TNotifyEvent read FOnChange write FOnChange;
  property OnProgress: TProgressEvent read FOnProgress
                                      write FOnProgress;
  property Palette: HPALETTE read GetPalette write SetPalette;
  property PaletteModified: Boolean read FPaletteModified
                                    write FPaletteModified
  property Transparent: Boolean read GetTransparent
                                write SetTransparent;
  property Width: Integer read GetWidth write SetWidth;
end;


정의부(definitions)는 가상으로 정의되므로, 자손들이 구현부를 제공해야 함을 의미한다. 본문에 나타낸 정의부는 private 및 protected 부분을 제외한 것이다. 일반 파일 외에도 다른 가능한 소스와 대상이 있음을 볼 수 있다.


파일 읽기와 파일 쓰기

라자루스 내부 함수들은 이미 많은 작업을 할 수 있도록 해주는데, 특히 텍스트 파일과 관련해 많은 기능을 제공하고 있다. 하지만 데이터를 편집 필드에 표시하기 전에 어떤 처리가 필요하다면 우리는 RTL의 표준 파스칼 함수에 의존해야 할 것이다. 이는 읽기와 쓰기에 모두 해당하겠다. 프리 파스칼과 라자루스는 File 과 Textfile 데이터 타입을 제공한다. File 로 선언된 파일들은 선택적으로 관련 타입을 가질 수 있다. 이런 경우 선언부를 이용해 명시한다. 파일의 작업을 시작하기 전에 우리는 이름을 file handle 과 관계시킬 필요가 있는데, 이 때 AssignFile 프로시저를 실행한다 (또는 오래된 버전의 경우 Assign과 동일).


바이너리 파일 접근

파스칼은 text 파일과 binary 파일을 구분한다. 텍스트 파일은 Text 타입이나 Textfile 타입으로 정의된다. 사실 Text나 Textfile 은 동일한 대상을 참조하지만 당신은 항상 Textfile 을 사용해야 한다. 델파이는 Text 프로퍼티를 가진 다른 많은 컴포넌트들로부터 텍스트 파일을 명확하게 구분하기 위해 Textfile 이라는 이름을 도입하였다. 라자루스에서도 이용 가능한 이름이다. 그 외 모든 파일, 즉 바이너리 파일 등은 File 타입에 해당한다.


각 파일은 그것과 관련된 handle 을 가질 필요가 있다. 하지만 운영체제에서 할당하지 않고 파스칼이 직접 할당한다. 가장 간단한 예제를 들어보자면 아래와 같다:

var  f: File of Byte;
begin
// convert filename encoding:
  AssignFile(f, UTF8ToSys(OpenDialog1.FileName));


File of Byte 타입으로 된 파일 내 데이터는 한 번에 1 바이트씩 읽거나 쓸 수 있다. 이 호출이 끝나면 파일은 더 이상 이름에 의해 어드레싱(address)되지 않고, 할당된 핸들을 통해서만 어드레싱된다. 하지만 파일은 아직 열리지 않은 상태이며, 먼저 접근 모드를 설정해야 할 것이다. 파스칼은 기본적으로 후에 파일이 어떻게 사용되는지와 상관없이 타입이 정해진(typed) 파일과 타입이 정해지지 않은(untyped) 파일을 읽기 및 쓰기용으로 열 수 있다. 접근 모드는 FileMode 라는 시스템 변수에 저장되며, 3가지 값을 가질 수 있다:

0 모든 파일이 읽기 전용으로 열린다.
1 모든 파일이 쓰기 전용으로 열린다.
2 모든 파일이 읽기와 쓰기용으로 열린다.


파일을 열기 전에는 FileMode 변수에 올바른 값을 할당하는 것이 중요한데, 값의 수정이 허용되지 않은 상태에서 쓰기 접근용으로 파일을 열면 오류 또는 예외가 발생하여 trap 되기 때문이다 (아니면 프로그램이 충돌). FileMode 기본 값은 2이므로, 이를 변경하지 않는다면 운영체제는 쓰기 가능한 파일을 만들 것을 요청받는데, 쓰기가 허용되지 않는 경우 이는 불가능해진다. 파스칼에서는 기존 파일들을 Reset 명령으로 연다. Rewrite 는 새 파일을 생성하거나 기존 파일을 덮어쓴다. Append 는 기존 파일의 끝에 데이터를 추가 시 사용된다. 존재하지 않는 파일에 Reset 또는 Append 를 호출하면 오류가 발생한다.


운영체제가 새 파일을 생성할 수 없을 경우 Rewrite 또한 오류가 야기될 것이다. 파일을 열 때 발생할 수 있는 오류를 포착하는 방법에는 두 가지가 있다. 가장 간단한 방법은 컴파일러 지시어 {$I-}를 이용해 입출력 연산에 대한 검사(checking)를 끄는 방법이다:

AssignFile(f, ...);
{$I-}                 //turn off I/O checking
Reset(f);
if IOResult <> 0 then // the file could not be opened!
  ...
{$I+}                 // I/O checking is turned on again


좀 더 현대적 접근법은 파일 루틴에 대한 예외 핸들러를 생성하는 것이다. 이를 실행하려면 입출력 연산의 검사를 활성화시켜야 한다 (기본 설정으로 되어 있음):

AssignFile(f, ...);
try
  Reset(f);
except
  ShowMessage('File cannot be opened!');
  Exit;
end;


하지만 미리 FileMode 를 올바르게 설정하면 많은 파일 접근 실패를 막을 수 있다.


위의 AssignFile 문에서 우리가 연 파일로부터 모든 데이터를 차례로 읽어오기 위해서는 루프(loop)를 적용해야 한다:

FileMode := 0;                // file is read-only
Reset(f);                     // file is opened for reading
while not EoF(f) do begin ... // while the file end has not been reached...


EoF ("End of File")은 핸들 f 로 파일의 끝에 도달할 때 True 를 리턴하는 파일 함수이다. 파일을 역으로 읽을 때 사용할 수 있는 이와 비슷한 함수도 있다. 이 함수 BoF ("Begin of File")은 파일의 시작에 도달 시 알려준다.


파일의 크기는 FileSize 함수로 검색할 수 있다. 해당 함수는 경로를 포함한 파일명을 파라미터로 간주하고, 파일의 크기를 64-bit 정수로 리턴한다. FileSize를 비롯해 파일 처리를 위한 다른 많은 함수들은 FileUtils 와 SysUtils 프리 파스칼 유닛에서 이용할 수 있다. 이 분야에서 또 흥미로운 함수 한 가지는 System 유닛의 FileSize 이다. 이 함수는 동일한 이름을 갖고 있고 처음에 포함되어 있기 때문에 가져오기(import)를 할 때는 먼저 자격을 부여(qualify)할 필요가 있다. 이 함수는 크기를 바이트로 리턴하지 않고 records 로 리턴한다. File of Byte 의 경우 동일한 값이지만 File of Word 에는 다른 값인데, 각 레코드는 1이 아닌 2바이트 크기를 가지기 때문이다.


이렇게 아래의 방법을 통하면 다이얼로그로 경로를 받은 파일의 크기를 알 수 있다.

I := FileSize(UTF8ToSys(Dialog1.FileName));


그리고 다음과 같이 쓸 수도 있다

I := System.FileSize(f)

하지만 이 경우 f는 앞전의 예제와 같은 파일이어도 다른 파일사이즈를 반환한다.


레코드는 흥미로운 동시 매우 복잡한 데이터 타입이다. 따라서 System.FileSize 는 레코드의 파일에 레코드 수를 리턴하고, FileUtils.FileSize 는 전체 크기를 레코드의 파일에 대한 바이트로 리턴할 것이다. typed files 외에도 untyped files 또한 존재한다. 타입이 정해지지 않은 파일을 읽을 때는 데이터 타입에 상관없이 메모리 버퍼에 내용이 적힌다. 이 버퍼는 힙(heap)이나 데이터 영역 중 하나에 존재할 것이다. 읽거나 쓰게 될 데이터 크기는 읽기 또는 쓰기 당시에 명시된다. 크기가 클수록 프로그램이 더 빠르게 작동하고 더 많은 작업 메모리가 필요할 것이다. 여기 파일 복사 루틴의 전형적인 예를 들어보겠다. 소스와 목적지(destination)는 열기 대화창과 저장하기 대화창에 미리 설정되어 있어야 한다.

procedure TForm1.Button1Click(Sender: TObject);
  var SourceF, TargetF: File;           // file handles
      Buffer: array[1..2048] of Char;   // (any) buffer
      NumRead, NumWritten: Int64;       // counter for Blockread/write
begin
  AssignFile(SourceF, UTF8ToSys(OpenDialog1.FileName));   // source file
  try
    Reset(SourceF, 1);          // you must specify the record size of untyped files!
  except
    ShowMessage('Error when opening '+ OpenDialog1.FileName + '!');
    Exit;// source file could not be opened
  end;

  AssignFile(TargetF, UTF8ToSys(SaveDialog1.FileName));   // destination file
  try
    Rewrite(TargetF, 1);        // a new file ー the record size for reading and writing
                                // must necessarily be mde equal
  repeat
    BlockRead(SourceF, Buffer, SizeOf(Buffer), NumRead);  // read ... and
    BlockWrite(TargetF, Buffer, NumRead, NumWritten);     // write
    until (NumRead = 0)         // there is nothing left to read
    or (NumWritten <> NumRead); // last block was shorter -> end of file
  finally                       // and even if it goes wrong:
    CloseFile(SourceF);         // the two files are closed and
    CloseFile(TargetF);         // their handles are released
  end;
  ShowMessage(OpenDialog1.FileName + ' was copied to ' +
                                        SaveDialog1.FileName);
end;


함수명 설명
procedure Append(var F:TextFile|File); Append 는 기존 파일을 연다. F로 적힌 모든 데이터는 파일의 끝에 추가된다. Append 모드에선 텍스트 파일만 열 수 있다. Append 를 호출하고 나면 파일 F는 최종 CloseFile 전까지 쓰기 보호된다. Append 를 호출하고 난 후 다른 프로그램에 의한 동시적 접근은 불가능하다. Append 는 기존 파일에만 사용할 수 있다. 파일이 존재하지 않을 시 런타임 오류가 발생한다.
procedure AssignFile(var F:TextFile|File; const s:string); Assign 과 AssignFile 은 파일에 이름을 할당한다. 어떤 파일 타입이든 작동하지만 이 호출은 파일을 열지는 않는다. 이름과 파일 변수를 관계시키고, 파일을 닫힌 것으로 표시할 것이다.
procedure CloseFile(var F:TextFile|File); 파일을 닫기 위한 Close 또는 CloseFile.
function EoF(var F:TextFile|File); boolean; End-of-File(파일 끝)에 도달했는지 검사한다.
procedure Erase(var F:TextFile|File); 파일 시스템에서 열리지 않은 파일을 제거한다. 파일에 Assign가 할당되어야 하며, Reset 이나 Rewrite 로 열리지 않았을 것이다.
procedure Flush(var F:TextFile|File); 플러시(flush)가 열린 파일 F의 메모리 버퍼를 비우고 내용을 기억 매체에 쓴다. 파일을 닫지 않는다.
procedure ReadLn(var F:Text; Args: Arguments); 하나 또는 이상의 값을 파일 F로부터 읽어와 결과를 명시된 파라미터 Args에 저장한다. 이후 파일 포인터는
procedure ReadLn(Args: Arguments); 파일 내 다음 행으로 진행된다. 텍스트 파일에서 행의 끝은 (플랫폼 의존적) 행끝 문자(들)로 표시된다. Windows에선 CRLF, Unix에선 CR, MacOS에선 LF가 해당한다. 행의 끝 표시는 행의 일부로 간주하지 않으므로 무시된다. 명시된 파일 F가 없다면 표준 입력이 읽힐 것이다.
procedure Rename(var F:TextFile|File; const s: string); 재명명은 관련 파일 F의 이름을 변경한다.
procedure Reset(var F:TextFile|File); 파일을 읽기용으로 연다.
procedure Rewrite(var F:TextFile|File); 파일을 쓰기용으로 연다.
procedure Seek(var F:TextFile|File; Pos: int64). 파일 읽기/쓰기 위치를 설정한다.
function SeekEOF(var F:TextFile|File); 부울 (Boolean); 파일 위치를 파일 끝으로 설정한다.
procedure Truncate(var F:TextFile|File); 현재 파일 위치에서 (열린) 파일 F의 길이를 자른다.
procedure Write(Args: Arguments);
procedure Write(var F:TextFile|File; Args: Arguments);
Args 파라미터의 내용 쓰기를 텍스트 파일 F로 쓴다. F 파라미터가 누락 시 텍스트는 표준 출력에 쓰일 것이다. 숫자형 타입은 변환되고, 문자열과 문자열 포인터는 메모리에 저장된 대로 쓰일 것이다.
procedure WriteLn(Args: Arguments);
procedure WriteLn(var F:Textfile; Args: Arguments);
Write와 동일한 기능을 하지만 플랫폼 특정적 행 끝 문자(들)를 문자열 끝으로 추가한다.
procedure Reset(var F[:File; RecSize : Word]); 타입이 정해지지 않은 파일을 연다. 어떤 크기도 명시되지 않은 경우 128로 가정한다.
procedure Blockread(var F:file; var Buf; Count: Integer[; var AmtTransferred: Integer]); 타입이 정해지지 않은 파일로부터 버퍼 Buf로 blockwise 읽기. 레코드 읽기 최대 수는 Count이다. 모든 읽기가 성공적으로 끝나면 AmtTransferred는 호출 후 Count와 동일할 것이다.
procedure BlockWrite(var f: file; var Buf; Count: Integer[; var AmtTransferred: Integer]); 기억 영역에서 타입이 정해지지 않은 파일로 데이터의 blockwise 쓰기. 써야 할 레코드 최대 수는 Count이다. 모든 쓰기가 성공적으로 끝나면 AmtTransferred 는 호출 후 Count와 동일해질 것이다.
표 8.5: 타입이 정해지지 않은(untyped) 파일에 가장 중요한 함수들


EoF, AssignFile, CloseFile는 표 8.5를 참고한다.


표준 입력, 표준 출력, 표준 오류

텍스트 파일을 작업할 때는 몇몇 특수 제한이 적용된다. 엄격히 말해 "텍스트 파일"은 저장 메체로부터 데이터를 읽고 기억 매체로 데이터를 쓰는 것으로 제한되지 않는다. 프리 파스칼은 TextFile에 4가지 표준 변수를 제공한다: Input, Output, ErrOutput, StdErr. Input 과 output 은 표준 입/출력 장치를 나타낸다. 보통은 콘솔이 해당된다. Input은 읽기전용이고, Output은 쓰기전용이다. StdErr 는 ErrOutput 과 동일하며, 일반적으로 콘솔을 대상으로 한다. 보통은 디버그 정보를 읽는 데에만 사용된다. 실제로는 매우 드물게 발생하며, 디버그 정보는 Output에 직접 적힌다.


이러한 채널들을 이용할 수 있는 경우 (운영체제에 따라 좌우) 모든 표준 TextFile 변수가 열려 프로그램이 시작하는 즉시 사용 준비가 될 것이다. 예를 들어, Unix와 DOS에선 항상 사실이다. Unix에서는 출력이 콘솔로 전송된다. 프로그램이 콘솔로부터 시작될 경우 그 출력이 눈에 보이는 곳도 콘솔이다. 그렇지 않을 경우, Output으로 전송된 내용은 모두 사라질 것이다.


Windows에서는 애플리케이션이 콘솔 모드에서 컴파일되지 않는 한 표준 TextFile 변수를 사용 시 오류를 발생할 것인데, 콘솔 모드만 콘솔로의 접근을 허용하기 때문이다. 윈도우에서 console mode 를 설정하려면 메인 프로그램 파일에 {$apptype console} 지시어를 사용한다. 파라미터 TextFile 이 ReadLn 또는 WriteLn 의 호출에서 누락될 경우 읽기 연산은 Input 에, 쓰기 연산은 Output 에서 실행될 것이다. 아래 예제는 Write, WriteLn, ReadLn 프로시저를 콘솔과 함께 이용하는 방법을 보여준다. 사용자에게 자신의 이름을 콘솔로 입력한 후 [Enter]를 누르라는 요청을 하게 된다. 이후 프로그램은 그 이름을 콘솔로 출력할 것이다.

program ShowName;
{$apptype console}
  var  Str: String;
begin
 // Writes the specified text without a newline character at the end
  Write('Please write your name and press [Enter]: ');
  ReadLn(Str);     // ReadLn reads text up to the next line break
  WriteLn('Your name is: ' + Str);     // writes a line to the console
end.


표준 채널이 사용될 경우 어떤 파일 할당도 이루어지지 않을 것이다. 하지만 파일로 접근을 원한다면 바이너리 파일로의 접근 시와 동일한 루틴을 사용한다.

프로시저 설명
AssignFile/Assign 타입이 정해진 파일과 마찬가지로 파일명과 핸들을 관계시킨다.
Closefile/Close 타입이 정해진 파일과 타입이 정해지지 않은 파일에 대한 파일 핸들을 해제(release)한다.
Reset and Rewrite 타입이 정해진 파일과 마찬가지로 파일을 연다/생성한다.
Read 다음 글줄 끝내기(line ending) 없이 파일로부터 읽는다.
ReadLn 글줄 끝내기를 포함한 전체 행을 파일로부터 읽는다.
Write 최종 글줄 끝내기 없이 행을 쓴다.
WriteLn 글줄 끝내기를 포함한 전체 행을 파일에 쓴다.
표 8.6: 텍스트 파일을 다룰 때 필요한 기본 함수 (데이터 타입 TextFile의 파일)


텍스트 파일에 대해 항상 타입을 TextFile 로 선언해야 한다 (Text는 구식). 변수 FileMode 는 텍스트 파일에 영향을 미치지 않는다. 바이너리 파일에서와 마찬가지로 열기와 닫기를 위한 파일명은 UTF8ToSys 와 SysToUTF8 을 이용해 LCL과 시스템 API 간에 변환되어야 한다. 이 함수들은 운영체제 독립적인 방식으로 데이터를 변환 시에도 사용된다 (예: Windows ANSI 또는 FeeBSD ISO 문자열을 UTF8로 변환하거나 그 반대로). 텍스트 파일로 작업 시 발생하는 작은 문제로 글줄 끝내기(line ending)를 들 수 있는데, 이는 전혀 표준화되지 않았다. 윈도우는 항상 CRLF를 사용하고, 유닉스는 LF, MacOS는 CR을 사용한다. 이식 가능한 방식으로 작업하기 위해선 항상 상수 LineEnding 을 이용해 글줄 끝내기를 작성하도록 해야 한다 (#13#10 또는 ^M^J를 이용하는 DOS/Windows 방식을 사용하지 않는다; 5.1장 참고).


스트림 작업하기

파일 접근을 위한 새 시스템은 TFileStream 과 함께 현대 파스칼 런타임 환경에서 소개되었다. 표준 루틴에 비해 좀 더 이해하기 쉬운 메소드 이름, 런타임 오류 대신 예외 처리, 객체 지향이라는 점을 비롯해 몇 가지 이점이 있다. 반대로 표준 시스템에 유용한 애플리케이션도 여전히 많은데, 콘솔을 처리하는 함수와 함께 자연적으로 통합되었으므로 파일뿐 아니라 콘솔로부터 데이터로 접근하는 것도 가능하다 (예: 많은 명령 행 툴들도 이를 실행한다). 두 가지 파일 접근 시스템 모두 동등하며, 둘 사이에 변환은 StreamIO 유닛의 AssignStream 함수로 실행할 수 있다. 아래 함수는 표준 루틴에서 스트림으로 변환이 얼마나 간단한지를 설명한다. 이 예제는 간단한 파일 복사기(file copier)로, 앞서 논한 타입이 정해지지 않은 파일에서 본 내용을 완전히 대체한다:

procedure CopyFile(Source, Target: String);
  var  S: TFileStream = NIL; T: TFileStream = NIL;
begin
  S := TFileStream.Create(Source, fmOpenRead);
  try T := TFileStream.Create(Target, fmOpenWrite or fmCreate);
    T.CopyFrom(S, S.Size);
{$ifdef Windows}
    FileSetDate(T.Handle, FileGetDate(S.Handle)); // file date
{$endif}
  finally
    if T <> NIL then T.Free;
    S. Free;
  end;
end;


함수는 다시 표준 문자열로 작업하므로 소스와 목적지가 LCL을 이용해 결정될 경우 UTF8ToSys 를 이용해 문자열을 변환해야 할 것이다. 해당 함수는 단순히 Blockread/Blockwrite의 대체함수가 아니라 Windows API 내 커널 DLL의 동일한 이름으로 된 루틴의 대체함수이기도 하다. 예제에서 볼 수 있듯이 스트림은 데이터의 소스이기도, 목적지이기도 하다. 모든 스트림은 TStream 클래스로부터 파생되며, 이 클래스는 스트림 데이터 읽기 또는 쓰기와 같이 입/출력 장치의 특성과 독립하여 작용하는 연산을 위한 추상 메소드를 포함한다. TStream 의 올바른 자손이 전달되면 장치에서 입/출력 연산은 파생된 클래스로부터의 메소드에 의해 처리된다. 프리 파스칼 RTL은 Classes 유닛에 많은 TStream 자손들을 제공하는데, 이는 Free Pascal 2 Reference Guide 에 문서화되어 있다. 여기서는 간략하게 개요만 소개하겠다: TFileStream 은 기억 장치 상에 명명된(named) 파일로 데이터를 저장, TMemoryStream 은 메모리에 데이터를 저장, TResourceStream 은 실행 가능 파일의 내부 리소스에 관련, TCustomMemoryStream 은 메모리에 데이터를 유지하는 모든 스트림의 기반 클래스, TStringStream은 문자열 데이터를 저장, THandleStream 은 파일 시스템에 의해 식별된 모든 스크림의 기반 클래스, TOwnerStream 은 다른 스트림을 기반으로 데이터를 저장하는 스트림을 처리하는 기능을 한다. FCL에는 그 외에도 이용 가능한 TStream 자손이 많은데, 그 중 일부는 특히 압축과 소켓(socket)과 관련된다. 중요한 스트림은 본장 뒷부분에서 논할 것이다.

상수 설명
fmOpenRead 파일을 읽기 전용으로 연다.
fmOpenWrite 파일을 쓰기 전용으로 연다.
fmOpenReadWrite 파일을 읽기와 쓰기용으로 연다.
fmShareExclusive 독점적 사용을 위해 파일을 잠근다.
fmShareDenyWrite 다른 프로세스가 읽기만 가능하도록 파일을 잠근다.
fmShareDenyRead 다른 프로세스가 읽을 수 없도록 파일을 잠근다.
fmShareDenyNone 파일을 잠그지 않는다.
fmCreate 새 파일을 생성한다.
표 8.7: TFileStream을 이용해 파일을 열때 사용할 수 있는 모드들


오브젝트 파스칼 스트림은 1990년 Turbo Pascal 6.0과 함께 Turbo Version 1.0에서 소개되었다. 이후 스트림은 새로운 객체 지향 델파이 구문을 위해 업데이트되었다. 이는 Class 유닛 내 프리 파스칼 런타임 라이브러리에서 이용 가능하며, 오브젝트 파스칼 프로그래밍 모델의 매우 중요한 일부를 형성한다. 스트림은 매우 단순한 모델을 기반으로 하고, 두 개의 프로퍼티만 가진다: Size 와 Position. 세 개의 주요 메소드는 Seek, Read, Write 이다. Position 은 스트림에서 읽기 또는 쓰기 연산이 실행되는 위치이다. 스트림이 읽히거나 쓰일 때 현재 위치는 읽히거나 쓰인 항목의 수만큼 증가한다. Size는 현재 스트림 내 바이트 수이다. Seek 를 이용해 현재 위치를 변경할 수 있다. Read 는 스트림으로부터 데이터를 읽고, Write 는 스트림으로 데이터를 쓴다. 스트림의 Size 와 Position 은 주의 깊게 처리해야 하는데, 파이프(pipe), 소켓(socket), FIFO, 압축(compression), 압축해제(decompression) 스트림은 이러한 프로퍼티를 가지지 않기 때문이다.


Tstream

TStream 은 추상 클래스다. 즉, 그의 모든 파생된 클래스에 대한 인터페이스를 명시하는 일만 한다는 의미다. 그로부터 스트림 인스턴스를 생성하는 것은 불가능하다. 스트림 관련 루틴을 작성할 때는 보통 TStream의 모든 파생된 타입을 수락하기 위해 TStream 타입의 파라미터를 사용한다. 올바른 파생 클래스를 전달하는 것은 전적으로 호출자(caller)에게 달려 있다. 아래는 classesh.inc 에서 찾을 수 있는 클래스 정의 그대로이다 (private 과 protected 부분 없이).

TStream = class(TObject)
  protected
    // omitted
  public
    function Read(var Buffer; Count: Longint): Longint; virtual;
    // Read reads data from the stream into a buffer and returns the number of bytes read
    function Write(const Buffer; Count: Longint): Longint; virtual;
    // Write writes data from a buffer to the stream and returns the number of bytes read.
    function Seek(Offset: Longint; Origin: Word): Longint; virtual; overload;
    function Seek(const Offset: Int64; Origin: TSeekOrigin):
       Int64; virtual; overload; // Seek sets the current position in the stream
    // The origin can be: soFromBeginning, soFromCurrent or soFromEnd.
    // Offset must be negative if Origin is soFromEnd. It must be positive for, soFromBeginning can accept both
    // directions and thus both positive and negative values for soFromCurrent.
    procedure ReadBuffer(var Buffer; Count: Longint);
    procedure WriteBuffer(const Buffer; Count: Longint);
    function CopyFrom(Source: TStream; Count: Int64): Int64;
    // CopyFrom copies data from one stream to another.
    function ReadComponent(Instance: TComponent): TComponent;
    // ReadComponent reads a component's state from the stream and transmits this state to the specified Instance
    // instance. If Instance is nil the instance is first created based on the data in the stream.
    // ReadComponent returns the data as read from the stream.
    function ReadComponentRes(Instance: TComponent): TComponent;
    procedure WriteComponent(Instance: TComponent);
    // WriteComponent writes the published properties of the instance, so that they
    // can be read later with TStream.ReadComponent
    // This method is intended for use in an IDE, so that the state of a window
    // or a Datamodule can be stored and then reinstated later in th IDE used to create it.
    procedure WriteComponentRes(const ResName: string;
                                      Instance: TComponent);
    procedure WriteDescendent(Instance, Ancestor: TComponent);
    procedure WriteDescendentRes(const ResName: string;
                Instance, Ancestor:TComponent);
    procedure WriteResourceHeader(const ResName: string;
                                     {!!!:out} var FixupInfo: Integer);
    procedure FixupResourceHeader(FixupInfo: Integer);
    procedure ReadResHeader;
    function ReadByte : Byte;         // reads and returns a Byte from a stream
    function ReadWord : Word;         // reads and returns a Word from a stream
    function ReadDWord : Cardinal;    // reads and returns a DWord from a stream
    function ReadQWord : Qword;       // reads and returns a Qword from a stream
    function ReadAnsiString : String; // reads and returns an AnsiString
    procedure WriteByte(b: Byte);     // writes a Byte to the stream
    procedure WriteWord(w: Word);     // writes a Word to the stream
    procedure WriteDWord(d: Cardinal); // writes a DWord to the stream
    procedure WriteQWord(q: Qword);   // writes a QWord to the stream
    procedure WriteAnsiString (const S : String);// writes an AnsiString
    property Position: Int64 read GetPosition write SetPosition;
    // the current stream position in bytes relative to the starting position
    property Size: Int64 read GetSize write SetSize64;
    // the current size of the stream in bytes
  end;


아래 도표는 각 접근 후 스트림의 상태를 그래프로 설명한다:

procedure WriteMagicNumberToStream(AStream: TStream);
begin
AStream.WriteByte($AA);
Lazarus 8 a.png
AStream.WriteWord($BBBB); Lazarus 8 b.png
end; Lazarus 8 c.png


TMemoryStream

TMemoryStream 은 메모리에 위치한 스트림이다. 이는 파일로부터 가져온 데이터로 채워진 캐시를 생성하는 데 사용할 수 있는 LoadFromFile 메소드를 가진다. 이 캐시는 이후 여러 번 읽을 수 있다. 연산의 실행 속도가 향상되는데, 메모리 접근이 주로 파일 접근보다 훨씬 빠르기 때문이다. SaveToFile 메소드는 메모리 기반의 스트림 내용을 파일로 저장할 것이다.


TStringStream

TStringStream 은 스트림을 문자열로, 문자열을 스트림으로 변환 시 매우 유용하다. 스트림에서 문자열로 변환해야 하는 경우, DataString 프로퍼티로부터 직접 읽어올 수 있다. 문자열은 문자열을 파라미터로 생성자에게 전달함으로써 클래스의 Create 생성자를 이용해 스트림으로 변환된다. 이 클래스는 문자열과의 통신에만 사용 가능하다. 데이터를 저장하기 위해선 문자열을 필요로 한다. 각 Write () 연산에는 SetLength() 가 필요하다. 하지만 이 방법은 TMemoryString 으로부터 메모리 청크(memory chunk)를 기반으로 한 메소드를 이용하는 방법보단 덜 효율적이다. 클래스는 classesh.inc 에서 정의된다:

TStringStream = class(TStream)
// private and protected sections omitted
public
  Constructor Create(const AString: String);
  function Read(var Buffer; Count: LongInt): LongInt; override;
  function ReadString(Count: LongInt): String;
  function Seek(Offset: LongInt; Origin: Word): LongInt; override;
  function Write(const Buffer; Count: LongInt): LongInt; override;
  procedure WriteString(const AString: String);
  property DataString: String read FDataString;
end;


TCompressor와 TDecompressor

프리 파스칼 FCL은 사용 준비가 된 TStream 자손들을 많이 포함한다.


그 중 압축, 소켓, 암호화(encryption) 스트림을 들 수 있는데, zstream 유닛의 TCompressionStream 과 TDecompressionStream 을 먼저 살펴보겠다.


이 클래스들은 데이터를 Zlib 포맷으로 압축 및 압축해제 시 이상적인데 ( http://www.zlib.net/ 참고), 이 포맷은 가장 효율적인 압축방법 중 하나로 꼽힌다. 이 클래스는 오리지널 C 소스 코드에 대한 인터페이스에 그치는 것이 아니라 오브젝트 파스칼에서 완전히 새로운 구현으로서, 라이브러리로 간단한 인터페이스를 제공 시 이식성을 향상시킨다.

TCompressionStream = class(TCustomZlibStream)
protected
  raw_written, compressed_written: longint;
public
  Constructor Create(level: TCompressionlevel; dest: TStream;
                     ASkipHeader: Boolean = False);
  destructor Destroy; Override;
  function Write(const buffer; count: Longint): Longint; Override;
  procedure Flush; // since Free Pascal 2.2.2: immediate compression
  function get_compressionrate: Single;
end;

TDecompressionStream = class(TCustomZlibStream)
protected
 // omitted ...
public
  Constructor Create(ASource: TStream; ASkipHeader: Boolean = False);
  destructor Destroy; Override;
  function Read(var buffer; count: Longint): Longint; Override;
  function Seek(offset: Longint; origin: Word): Longint; Override;
  function get_compressionrate: Single;
end;


데이터를 압축하기 위해선 TCompressionStream 의 인스턴스가 필요하다. 그 생성자는 두 개의 파라미터를 취한다: 압축 수준(표 8.9 참고), 그리고 데이터를 보관해야 하는 대상 스트림(target stream). 데이터는 Write 메소드를 이용해 압축기(compressor)로 전송된다 (아니면 Write 의 변형 중 하나를 이용하거나, CopyFrom 와 같이 그것을 호출하는 다른 메소드를 이용).


단순히 데이터를 전송하면 보관만 될 것이다. TCompressionStream 를 해제(release)했을 때에만 압축된다. 항상 편리한 방법은 아닌 것이, 추가 메소드 Flush가 프리 파스칼 2.2.2에 도입된 후로부터 압축기를 먼저 해제(release)할 필요 없이 데이터를 압축할 수 있게 되었기 때문이다.


데이터의 압축해제 시에는 TDecompressionStream의 인스턴스가 필요하다. 그 생성자는 데이터 소스가 될 파라미터를 가진다. 압축 수준은 제공할 필요가 없는데, 입력 데이터에 인코딩될 것이기 때문이다. 압축되지 않은 데이터는 Read 메소드 혹은 그 변형 중 하나를 이용해 검색할 수 있다. TDecompressionStream 은 Size 프로퍼티를 가지지 않는다는 점을 주목한다. 즉, CopyFrom 메소드를 이용해 다른 스트림을 읽는 듯 압축 데이터를 읽는 것이 불가능함을 의미한다. 따라서 우리는 루프를 생성하여 더 이상 데이터가 리턴되지 않을 때까지 Read 를 호출하거나, 유사한 구현을 가진 프리 파스칼 RTL에서 TStrings.LoadFromStream 와 같은 메소드를 사용해야만 한다. 델파이 프로그래머는 이와 관련해 주의를 기울여야 하는데, 델파이의 TStrings.LoadFromStream 구현부에는 이러한 기능이 없기 때문이다. 본래 이러한 용도로 설계된 것이 아니다.

효과
clNoneNo 압축, 데이터가 단순히 복사된다.
clFastest 이용 가능한 압축 방식 중 가장 빠른 방식을 사용한다.
clDefault 속도와 출력 크기 사이에서 절충. 사용 시 선호되는 값이다.
clMax 가장 느린 압축 방식, 가장 작은 데이터 출력 크기를 야기한다.
표 8.8: TCompressionlevel에 사용이 가능한 값


아래의 짧은 예제 프로그램은 문자열 데이터의 압축(pack)과 압축해제(unpack)를 실행하는 방식을 설명한다. 버튼을 누른 후 첫 번째 텍스트 필드 내 문자열이 압축된 후 다시 압축해제된다. 압축된 문자열의 바이트 값이 메모에 16진 형태로 표시된다. 성공적인 압축 및 압축해제를 설명하기 위해 압축해제된(unpacked) 문자열이 편집 컨트롤에 표시된다.


압축해제는 TStrings.LoadFromString 메소드에 의해 처리된다. 이는 루프에서 올바른 바이트 수를 조회(retrieve)할 때까지 읽는 수고를 덜어준다. 구현은 위에서 제시한 압축 클래스 이용과 관련된 지시를 따른다. 압축된 데이터가 오히려 원본 데이터보다 길어 보인다! 이는 그 입력이 매우 짧으므로 중복 정보가 매우 적다는 사실 때문이다. 문자열을 길게 feed한다면 압축된 데이터의 길이가 어떻게 변하는지 관찰할 수 있다.

uses zstream;
procedure TformCompressor.buttonCompressClick(Sender: TObject);
  var  Compressor    : TCompressionStream;
       Decompressor  : TDecompressionStream;
       SourceString  : TStringStream;
       CompressedData: TMemoryStream;
       DestString    : TStringList;
       i: Integer;
begin
  SourceString := TStringStream.Create(editOriginalString.Text);
  CompressedData := TMemorystream.Create;
  Compressor := TCompressionStream.Create(clDefault, CompressedData);
  Decompressor := TDecompressionStream.Create(CompressedData);
  DestString := TStringList.Create;

(* compress the string *)
  Compressor.CopyFrom(SourceString, SourceString.Size);
  Compressor.Free;

(* copy copressed string into memo field *)
  CompressedData.Position := 0;
  memoResults.Lines.Clear;
  for i := 0 to CompressedData.Size - 1 do
    memoResults.Lines[0] := memoResults.Lines[0] +
                          IntToHex(CompressedData.ReadByte, 2) + ' ';

(* uncompress the string *)
  CompressedData.Seek(0, soFromBeginning);
  DestString.LoadFromStream(Decompressor);
  editResults.Text := DestString.Text;

  Decompressor.Free;
  CompressedData.Free;
  SourceString.Free;
  DestString.Free;
end;


그림 8.2: 압축을 보여주는 예제 프로그램 실행 모습


TResourceStream

TResourceStream 은 실행 가능 파일로 적힌 데이터를 Windows 리소스로서 읽을 때 사용된다. 라자루스는 다른 개발 환경과 마찬가지로 .rc 확장자로 된 리소스 소스 코드를 읽는다. .RC 파일은 순수 텍스트 파일이다. 비트맵과 아이콘은 dump로 저장되거나 외부 바이너리 파일에 대한 참조로 저장된다. 이러한 .rc 파일은 이후 리소스 컴파일러로 처리된다. 라자루스는 이를 위해 WindRes 를 사용한다. Borland의 Resource Workshop이나 Colin Wilson이 만든 무료 XN Resource Editor와 같은 외부 프로그램에 의해 생성된 리소스 파일 또한 라자루스와 호환되는 리소스 파일을 생성한다.


그리고 나면 컴파일된 바이너리 .res 리소스 파일은 실행 가능 Windows 파일로 연결된다. 이를 위해 유닛 파일 또는 프로젝트의 소스 코드에 {$R myresource.rc} 혹은 {$R myresource.res} 지시어가 포함된다. 만약 .RC 파일이 주어진다면, 구문이 WindRes 로 컴파일될 경우 파일이 자동으로 컴파일된다.


플랫폼 독립적 애플리케이션을 설계할 때는 이러한 리소스 파일은 이식이 불가능함을 유념하길 바란다. 이 데이터를 프로그램 자체로 연결하는 대신 실행 파일과 함께 배포 가능한 외부 파일에서 데이터를 읽어오는 편이 더 나을 것이다.


때로는 어쩔 수 없이 내부 리소스와 작업해야 하는 경우도 있는데, 설치되어야 하는 데이터를 포함시켜야 하는 윈도우 설치 소프트웨어를 생성할 때를 예로 들 수 있겠다. 이러한 경우 TResourceStream 이 유용하다. 일반적인 .rc 리소스 파일은 아래와 같은 레이아웃을 가진다:

#include    "resource.h"
// Icons
IDI_ICON1   ICON   "icon1.ico"
IDI_ICON2   ICON   "icon2.ica"
// Bitmaps
IDB_CECAE   BITMAP "cecae.bmp"
// raw data
101 RCDATA
MOVEABLE PURE LOADONCALL DISCARDABLE
BEGIN
  '65 00 02 00 FE FF 74 9C 04 00 10 00 65 9C 00 00'
  'FF FF FE FF 75 9C 04 00 10 00 65 9C 00 00 FF FF'
END


항상 리소스 스트림을 사용해야 할 필요는 없다. 예를 들어, 비트맵을 TBitmap 으로 로딩하려면 TBitmap.LoadFromResourceID 메소드를 이용하면 되는데, 이 메소드는 TResourceStream 보다 더 수월하다. 이 클래스는 소스 파일 용어로 RC_DATA로 불리는 원시 데이터(raw data)를 읽는 데 더 적합하다.


윈도우 리소스 파일은 윈도우에만 제한되지 않는다. 리소스 컴파일러 .rc 파일을 .res 파일로컴파일할 수 있는 WindRes 또한 32 비트 리눅스 시스템에서 이용할 수 있다. 그 외에 .res 파일을 실행 가능 파일로 연결 가능한 .o 파일로 변형시키는 fpcresm 이란 추가 도구도 프리 파스칼에 따라 온다. 하지만 이러한 타입의 리소스가 애플리케이션을 이식 가능하게 만들어주진 않는다. MacOS X에서 그러한 파일들은 완전히 생소하므로 의심이 갈 시에는 사용을 피하는 것이 상책이다.


지속성 객체

많은 프로그램들은 저장된 데이터를 저장하고 저장된 데이터를 변경하기 위해 파스칼 클래스를 필요로 한다. 프로그램이 실행되는 동안 이 정보가 저장되지 않으면 다음에 프로그램을 시작했을 때 이용할 수 없다. 데이터가 객체에 유지될 것인지 확신하는 일반적인 방법은 (데이터 지속이라 부른다), 그것을 SaveToStream 과 LoadFromStream 메소드로 전달하거나, TComponent 자손으로 feed하는 방법이 있다. 그리고 나면 TStream.ReadComponent 와 TStream.WriteComponent 는 이러한 컴포넌트의 모든 published 프로퍼티를 데이터 스트림으로 쓸 수 있고 다시 읽어올 수 있게 된다.


아래 예제는 SaveToStream 과 LoadFromStream 메소드를 이용해 저장 및 복원을 처리하는 그러한 지속 클래스(persistent class)를 보여준다. 클래스는 데이터 스트림에 쓰고 그로부터 읽어올 수 있는 numberData 와 stringData 필드를 가진다.

type
TMyPersistent = class
  public
    numberData: DWord;
    StringData: String;
    procedure SaveToStream(AStream: TStream); Virtual;
    procedure LoadFromStream(AStream: TStream); Virtual;
end;

// ...

TMyPersistent.SaveToStream(AStream: TStream);
begin
  AStream.WriteDWord(NumberData);
  AStream.WriteString(StringData);
end;

TMyPersistent.LoadFromStream(AStream: TStream);
begin
  numberData := AStream.ReadDWord();
  StringData := AStream.ReadString();
end;


아래 클래스 정의는 TComponent 에서 파생된 클래스로, 동일한 작업을 하지만 내장된 기능을 활용한다. 이 클래스는 그 프로퍼티를 지속하는 기능이 있다 (TComponent 에서 파생된 모든 클래스와 마찬가지로). TStream.LoadComponent 와 TStream.WriteComponent 를 이용해 스트림으로부터 이 클래스를 로딩하고 스트림에 클래스를 쓴다:

type
TMyComponent = class(TComponent)
  private
    FNumberData: DWord;
    FStringData: String;
  published
    property numberData: DWord read FnumberData write FnumberData;
    property StringData: String read FStringData write FStringData;
end;