구조화저장소기법 (Structured Storage Technique) 만약정해진포맷의파일형식을써야하는것이아니라, 데이터를저장할때대단히유연하고도강력한방법이존재한다면얼마나편리할까? 구조화저장 (structured storage) 이라는새로운방식으로이러한문제를해결할수있다. 구조화저장은 DocFIle 이나 OLE 복합파일 (OLE compound file) 이라는이름으로도불리고있는새로운저장방식이다. 이방식을이용하면 Load 와 Save 를할때파일의일부분만을활용할수있다. 약간의데이터블록만이필요할때전체데이터를모두불러오거나, 저장해야한다면이는상당히비효율적이라고말할수있다. 이를이용하면순차적이면서, 점진적인데이터의접근이가능하다. 기본적으로윈도우에의해서지원되는방식이므로, 쉽게정보를얻을수있다. 이미마이크로소프트에서는이러한구조화저장방식이차세대윈도우제품에서는디폴트파일포맷으로사용할것임을공언하고있다. 또한, 현재 MS 오피스제품군에서는이기술을적용하고있기도하다. 이정도의소개만으로구조화저장의중요성은충분히알고도남음이있을것이다. 그러면, 이제실제로이기술에대한설명과델파이에서이를어떻게구현할것인지에대해서알아보도록하자. 기본적인이해 구조화저장 (structured storage) 이라는용어는 DocFile 이라는용어와혼용되고있다. 사용의편이성을위해지금부터는간단히 DocFile 이라고지칭하도록하겠다. DocFile 을이해하는가장좋은방법은파일내에파일시스템을가지고있는것이라고생각하면된다. 즉, DocFile 에는디렉토리와파일들을가지고있는것이다. 예를들어, Example.ole 라는 DocFile 이있을때여기에 Version, Files 라는디렉토리가있으며 Files 라는디렉토리아래에 File1, File2, File3 와같은파일들을내부적으로포함한다고하자. 이때 File1 과같은 DocFile 내부의데이터블록에다른데이터블록에전혀영향을주지않고접근하는것이가능하다. 참고로앞으로사용하는용어중에서 Storage 라는용어는 DocFile 에서의디렉토리와동격으로생각하면되고, Stream 은 DocFile 에서의파일로생각하면된다. DocFile 생성함수
DocFile 을생성하는함수는 StgCreateDocFile 이다. 이함수는델파이 3 의 activex.pas 유닛에선언되어있으며선언부분은다음과같다. function StgCreateDocfile(pwcsName: POleStr; grfmode: Longint; reserved: Longint; out stgopen: IStorage): HResult; stdcall; 첫번째파라미터인 pwcsname 은파일이름을유니코드로 (OLE 에서는유니코드가표준으로사용된다.) 설정하면되고, grfmode 에는플래그를설정하게된다. 세번째파라미터는현재는사용되지않기때문에보통 0 으로설정하게되며, 실제사용하게될 IStorage 인터페이스가네번째파라미터에서넘어오게된다. 하나의 DocFile 은그자체가 Storage 이다. 그렇기때문에, 이함수에의해서넘어오는 Storage 는파일의루트저장소가된다. 이함수가성공적으로수행되었는지여부를검사할때에는 SUCCEEDED() 함수를사용한다. 그 Pseudo Code 를아래에들어보았다. Hr := StgCreateDocFile(...); if (SUCCEEDED(Hr)) then...; 보통 OLE 를사용할때 HResult 를 S_OK 와비교하는경우가많은데, 이방법은그다지좋은방법이못된다. 그이유는 OLE 가함수가성공적으로수행되더라도미묘하게차이가나는다른여러가지반환값을가질수있기때문이다. 그러므로, SUCCEEDED() 함수를사용하는것이보다효율적인방안이된다. 유니코드 (UniCode) 의사용 앞의함수선언부분에서언급했듯이 OLE 세계에서는유니코드가표준으로사용된다. 그렇지만지금까지의프로그래밍환경에서는안시코드 (AnsiCode) 를표준으로사용해왔기때문에다소간의혼란이있을수있다. 델파이 3 에서는유니코드와안시코드를쉽게변환할수있는방법을제공하기때문에이런변화가커다란문제가되지않는다. WideString 문자열데이터형이유니코드를지원하게된다. 그러면, 간단히안시코드와유니코드를변환하는몇가지방법에대해알아보도록하자. s: string; ws: WideString;
s := abc ; ws := s; 어떤가? 너무나단순하지않은가? 그냥일반문자열을위에서와같이 WideString 형의 대입하는것으로모든것이끝난다. 마찬가지로유니코드문자열을안시코드로변환할때에 도단순히다음과같이하면된다. s: string; ws: WideString; ws := abc ; s := ws; 그렇지만, OLE 함수를사용할때에는보통 WideString 데이터형보다는 PWideChar 데이 터형을파라미터로사용하기때문에이를호출할때에는아래와같은방식으로형변환시켜 사용하면간단히해결된다. ws: WideString; ws := abc ; SomeOLEFunction(PWideChar(ws)); 델파이 2 에서는 WideString 데이터형을지원하지않고, PWideChar 데이터형만을지원하기때문에이를안시코드문자열과호환시키기위해서는해당하는문자열의크기의두배만큼의메모리를할당받고, 실제로문자를유니코드로바꾸기위해서는 API 함수인 MultiByteToWideChar 함수를호출해야했다. 마찬가지로유니코드문자열을안시코드로변환시킬때에도메모리할당과 WideCharToMultiByte API 함수를사용해야한다. 이점이델파이 4 가얼마나 OLE/COM 환경에적합한형태로바뀌었는지를보여주는단적이예가될수있다.
Stream, Storage 이름의제한 DocFile 시스템에서도약간의이름에대한제한을가지고있다. 31 자가넘는이름을가질수는없으며, 이름에!, :, /, \ 등의문자는사용할수없다. 그리고첫번째문자는 ordinal 값이 32 이하인문자가되면안된다. 이러한문자들은특수한목적에사용되게된다. STGM 상수 STGM 상수는 storage, stream 인터페이스에서객체에대한접근모드나객체를실제로생 성, 삭제등을하게되는조건을정의하고있다. 이들상수에대해서알아보도록하자. STGM_READ, STGM_WRITE, STGM_READWRITE Stream 객체에대해서는어떤메소드를허용할것인지를결정하는상수이다. 예를들어, STGM_READ 는 IStream 의 Read 메소드를허용하게된다. Storage 객체에대해서는가능한요소를나열하고, 이들을 open 한다. STGM_WRITE 는객체를저장할수있도록하며, STGM_READWRITE 는 STGM_READ 와 STGM_WRITE 를혼합한것이다. STGM_SHARE_DENY_NONE, STGM_SHARE_DENY_READ, STGM_SHARE_DENY_WRITE, STGM_SHARE_EXCLUSIVE STGM_SHARE_DENY_NONE 은어떤객체를 open 하더라도이것이그객체에대한 read, write 접근에대한제한을가지지않는것을의미한다. STGM_SHARE_DENY_READ 는 open 한객체에대해서 STGM_READ 모드로접근할수없도록제한다. 주로 root storage 객체에대해사용된다. STGM_SHARE_DENY_WRITE 는 STGM_WRITE 모드로접근할수없도록제한하는데, 이것은여러명의사용자가객체에접근했을때생길수있는문제점을해결할수있다. STGM_SHARE_EXCLUSIVE 는 STGM_READ, STGM_WRITE 모드둘다접근할수없도록하는값이다. STGM_DIRECT, STGM_TRANSACTED Direct 모드에서는 storage 요소에대해서변화가일어날경우이값이그대로반영된다. 이모드가디폴트로되어있다. Transacted 모드에서는변화가일어날경우그값이버퍼에저장되었다가 commit 이호출될때객체에반영된다. 만약 IStream, IStorage 인터페
이스에서 Revert 메소드가호출되면이러한변화가무시된다. 그러나, 이모드는현재 OLE 에서는구현되지않고있다. 아마도조만간에는이것이지원될것으로생각된다. STGM_CREATE, STGM_CONVERT, STGM_FAILIFTHERE STGM_CREATE 는현재존재하는 storage, stream 객체가새로운객체가생성될때에는반드시제거되어야한다는것을지정한다. 만약현재의객체가성공적으로제거되지않으면새로운객체가생성되지않는다. STGM_CONVERT 플래그는현재존재하는 stream 의데이터를보존하면서새로운객체를생성하는데, 이때이전객체의데이터는 CONTENTS 라는객체에보존된다. 이때과거의 storage 객체에있던정보는 stream 의형태로변경되어보존되므로, storage 의계층구조정보는망실된다. STGM_CONVERT 플래그는디스크에 storage 객체를생성하려고하는데, 이미그런파일이름이존재하거나, Storage 객체내부에새로운 stream 을생성하려고하는데같은이름의 stream 이있을경우등에서사용하게된다. STGM_FAILIFTHERE 이플래그는만약지정된이름의객체가있을경우에생성과정을취소하는역할을해준다. 이경우에 STG_E_FILEALREADYEXISTS 상수가반환된다. STGM_PRIORITY 이플래그가지정되면현재우선권을가진사용자만이객체의변화를줄수있다. 이를이용하면다른사용자들은이객체에접근해도이를변화시킬수없게된다. 이경우에는반드시 STGM_DIRECT, STGM_READ 가설정되어있어야한다. STGM_DELETEONRELEASE 이플래그는임시파일을사용할때유용하게쓰이는것으로, 부모 storage 객체가해제되 면자동으로그아래의파일들이파괴되도록지정하는것이다. DocFile 을만들어보자. 앞에서설명한 StgCreateDocFile 함수를사용해서실제로 DocFile 을만들어보기로하자. 이미함수선언과파라미터에대해서간단한설명을했지만, 다시한번정리해보자.
함수의선언부는다음과같다. function StgCreateDocfile(pwcsName: POleStr; grfmode: Longint; reserved: Longint; out stgopen: IStorage): HResult; stdcall; 그리고, 각파라미터에는다음과같은내용들을설정하게된다. 1. pwcsname: 유니코드형식의실제파일명. ( 예 ) c:\temp\example.ole 2. grfmode: STGM 플래그가설정된다. ( 예 ) STGM_CREATE or STGM_READWRITE STGM_DIRECT or STGM_SHARE_EXCLUSIVE 3. reserved: 0 4. stgopen: 실제로 storage 를담게될레퍼런스파라미터 그럼, 이제실제 DocFile 을만드는프로시저를하나만들어보자. procedure Create; Hr: HResult; Root: IStorage; Hr := StgCreateDocFile( c:\temp\example1.ole, STGM_CREATE or STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, 0, Root); if (SUCCEEDED(HR)) then end else 아무것도하지않고, Root 라는 IStorage 인터페이스만받아오는프로시저가완성되었다. 사용법이그다지어렵지는않다는것을쉽게알수있을것이다. 마찬가지로 DocFile 을여는것도그다지어렵지않다. 이때에는 StgOpenStorage 라는 API 함수를사용하는데, 이는 DocFile 자체가 root storage 이기때문이다. 이 API 의사용법도거의유사하므로여기에서간단히소개하겠다. procedure OpenDocFile; Hr: HResult; Root: IStorage;
Hr := StgOpenStorage( c:\temp\example1.ole, nil, STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, nil, 0, Root); if (SUCCEEDED(HR)) then end else DocFile 기법을사용한최초의어플리케이션 그럼이제, DocFile 을이용해서그림을저장하고불러올수있는어플리케이션을하나만들어보기로하자. 새로운어플리케이션을하나시작하고, 다음그림과같이폼위에버튼 3 개와이미지컴포넌트, 체크박스, TOpenPictureDialog 대화상자컴포넌트를하나씩얹어서디자인하도록하자. 이때각버튼의캡션을 Open, Save, Load 로정하고, 체크박스에는 Stretch 라고캡션을정하도록한다. 이제간단하게이어플리케이션에대해서설명하면, 구조화저장방법을이용하는방법을익히기위한어플리케이션으로 Open 버튼을누르면대화상자를띄워서, 아무그림파일이나선택하게하고, 이를이미지컴포넌트에보여준다. 이때 Stretch 체크박스가체크되어있을경우에는이미지를 Stretch 해서보여준다. 그리고, 보여주는이미지를 c:\temp\exam1.ole 파일에저장할때에는 Save 버튼을누르고, 저장된이미지를불러올때에는 Load 버튼을누르게한다. 유닛의 implementation 섹션에우리가사용하게될 activex.pas 와 AxCtrls.pas 유닛을 uses 문장에추가한다. 먼저체크박스의 OnClick 이벤트핸들러를아래와같이작성해서, 이미지컴포넌트의 Stretch 속성에반영할수있도록하자. procedure TForm1.CheckBox1Click(Sender: TObject);
Image1.Stretch := CheckBox1.Checked; 그리고, Open 버튼의 OnClick 이벤트핸들러를작성하자. procedure TForm1.Button1Click(Sender: TObject); if OpenPictureDialog1.Execute then Image1.Picture.LoadFromFile(OpenPictureDialog1.FileName); 이제실제로 Save 버튼을클릭하면그림이 DocFile 로저장되도록해야한다. 이렇게하려면 IStorage 에 IStream 스트림을하나생성하고이스트림에내용을기록해야한다. 스트림을생성하는코드는 IStorage 인터페이스의 CreateStream 메소드를사용한다. 이메소드의파라미터로첫번째파라미터에스트림의이름과두번째에 STGM 플래그를설정하고, 마지막파라미터로생성된 IStream 인터페이스가저장될변수를지정한다. 즉, 다음과같은코드를사용한다. Hr := Root.CreateStream( ExamStream, STGM_CREATE or STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE), 0, 0, Stream); 그러면, 이렇게 Stream 이라는변수에 IStream 인터페이스를담아오게되면실제데이터를여기에저장해야한다. 저장하는방법은크게두가지가있는데, 첫번째방법은 IStream 의메소드를직접이용하는것이고, 두번째방법은 AxCtrls.pas 유닛에서제공하는 TOleStream 클래스를이용하는것이다. 델파이 3 에서는 TOleStream 클래스를이용해서이작업을아주쉽게할수가있다. 사용법은아래와같이아주간단하다. OleStream := TOleStream.Create(Stream); Image1.Picture.SaveToStream(OleStream); OleStream.Free; 이제 Save 버튼의 OnClick 이벤트핸들러를제작해보자. procedure TForm1.Button2Click(Sender: TObject);
Hr: HResult; Stream: IStream; OleStream: TOleStream; Root: IStorage; Hr := StgCreateDocFile( 'c:\temp\example1.ole', STGM_CREATE or STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, 0, Root); if (not SUCCEEDED(Hr)) then Exit; Hr := Root.CreateStream('ExampleStream', STGM_CREATE or STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, 0, 0, Stream); if (not SUCCEEDED(Hr)) then Exit; OleStream := TOleStream.Create(Stream); Image1.Picture.Graphic.SaveToStream(OleStream); OleStream.Free; 이제는저장된이미지를스트림을통해서읽어올차례이다. 읽을때에도쓸때와마찬가지로 IStream 의메소드를직접이용하는방법과 TOleStream 클래스의메소드를사용하는방법이있다. 먼저 IStream 의 Read 메소드를사용하는방법에대해알아보자. 이메소드는파라미터를 3 개사용한다. 첫번째파라미터에는데이터를저장할버퍼를, 두번째파라미터에는읽어올데이터의크기 ( 바이트 ), 세번째파라미터에는실제로읽어들인데이터의크기가넘어온다. 그러므로, 데이터를읽어들이기에앞서읽어올데이터의크기를알아야한다. 보통스트림에지금과같이하나의데이터를저장한경우에는스트림의크기가읽어올데이터의크기가된다. 그러면스트림의크기를알아볼수있는함수에대해서알아보자 function GetStreamSize(Stream: IStream): LongInt; Hr: HResult; StatStg: TStatStg; Hr := Stream.Stat(StatStg, STATFLAG_NONAME); if (not SUCCEEDED(Hr)) then Result := -1; Exit;
Result := Round(StatStg.cbSize); IStream 인터페이스의 Stat 메소드를이용하면현재의스트림에대한정보를 TStatStg 클래스에담아준다. 위에서 Stat 메소드에대한파라미터로사용한 STATFLAG_NONAME 플래그는스트림의이름은담아오지말라고지정한것인데, 사실이경우에는이름을사용할이유가없기때문에이플래그를지정함으로써쓸데없는메모리의낭비를막을수가있다. TStatStg 클래스의 cbsize 멤버에스트림의크기가저장되어있으므로이를정수형으로바꾸어그값을반환한다. 그리고나서 IStream 인터페이스를담고있는 Stream 이라는변수가있다고하면아래와같이사용하면된다. Stream.Read(pBuffer, GetStreamSize(Stream), ReadBytes); 그러나, 보통의경우에는 TOleStream 을이용하는것이훨씬편리하다. 사용법도데이터를쓸데와거의유사하므로, 아래의코드를살펴보면금방이해할수있을것이다. 그러면 Load 버튼의 OnClick 이벤트핸들러를다음과같이작성하자. procedure TForm1.Button3Click(Sender: TObject); Hr: HResult; Stream: IStream; OleStream: TOleStream; Root: IStorage; Hr := StgOpenStorage('c:\Temp\Example1.ole', nil, STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, nil, 0, Root); if (not SUCCEEDED(Hr)) then Exit; Hr := Root.OpenStream('ExampleStream', nil, STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, 0, Stream); if (not SUCCEEDED(Hr)) then Exit; OleStream := TOleStream.Create(Stream); if (OleStream.Size > 0) then Image1.Picture.Graphic.LoadFromStream(OleStream); OleStream.Free;
자이제첫번째구조화저장기법을이용한어플리케이션이완성되었다. 아직기능이많은것은아니지만기본적인테크닉을익히는데에는유용했을것으로생각한다. 그럼이제더기능이많은두번째어플리케이션을제작해보도록하자. 파일뷰어의제작 이번에는 DocFile 의내부를들여다볼수있는뷰어를제작해보자. 이를구현하기위해서 IStorage 인터페이스의 EnumElements 메소드를사용하게되는데이메소드의속도가다소느린것이흠이다. EnumElements 메소드의선언부는다음과같다. function EnumElements(reserved1: Longint; reserved2: Pointer; reserved3: Longint; out enm: IEnumStatStg): HResult; stdcall; 즉, 마지막파라미터에적절한 IEnumStatStg 인터페이스형의변수를넣어주면여기에정보를담아서오게된다. 이렇게일단 IEnumStatStg 인터페이스를받아오면이인터페이스의 Next 메소드를사용해서각각의하부요소들을얻을수있게된다. Next 메소드는아래와같이선언되어있다. function Next(celt: Longint; out elt; pceltfetched: PLongint): HResult; stdcall; 첫번째파라미터에는받아올아이템의수, 두번째파라미터에는실제로받아오게될 TStatStg 클래스형의변수를지정하고, 세번째파라미터에는실제로넘어온아이템의수가반환된다. 이함수들을이용해서워드나엑셀등의내부적인저장이어떤식으로되어있는지들여다볼수있는간단한뷰어를제작해보자.
먼저앞의그림과같이새로운어플리케이션을시작하고폼에트리뷰컨트롤하나와 TOpenDialog 대화상자하나, 그리고버튼을하나올려놓자. 버튼의캡션을 Open 으로설정한다. 그리고, uses 절에 Activex.pas, ComObj.pas 유닛을추가하고, 버튼의 OnClick 이벤트핸들러를다음과같이작성한다. 일단파일이름을유니코드형식으로해야하기때문에, WideString 형식으로선언한변수에파일이름을집어넣고, 이를 PWideChar 로형변환해서사용한다. procedure TForm1.Button1Click(Sender: TObject); ws: WideString; Hr: HResult; Root: IStorage; if not OpenDialog1.Execute then Exit; TreeView1.Items.Clear; ws := OpenDialog1.FileName; 이제는이파일에실제로 DocFile 형식인지검사해서그렇다면그파일의정보를나타내야한다. 이를위해서 StgIsStorageFile 이라는함수를사용하는데, 이함수의파라미터로유니코드형식의파일이름을넘겨주게되고만약 DocFile 형식이라면 S_OK 가반환된다.
if (StgIsStorageFile(PWideChar(ws)) <> S_OK) then ShowMessage('DocFile 형식이아닙니다.'); Exit; 지정된파일이 DocFile 형식이므로 StgOpenStorage 함수를사용해서 DocFile 을열고루 트를앞에서선언한 Root 라는 IStorage 형의변수에담아온다. Hr := StgOpenStorage(PWideChar(ws), nil, STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, nil, 0, Root); if not SUCCEEDED(Hr) then ShowMessage( 파일을열수없습니다! ); Exit; 성공적으로파일을열고, Root 를받아왔으면파일의이름을트리뷰컴포넌트의루트로추 가한다. TreeView1.Items.Add(nil, ws); 이제 Enumeration 을이용해서트리뷰에하부요소의이름들을추가해나가자. DocFile 의구조는 Storage 와 Stream 으로이루어진여러단계의디렉토리형태를가지고있기때문에이를모두탐색할수있도록하려면재귀적호출을할수있어야하므로독자적인프로시저를하나정의해야한다. 이프로시저를 Enumeration 이라고정의하고, 파라미터로 IStorage 인터페이스와트리뷰의해당아이템을지정하도록하자. 그러면아래와같이선언될것이다. procedure TForm1.Enumeration(Storage: IStorage; ANode: TTreeNode); 일단 Button1 의 OnClick 이벤트핸들러에서는 Root 와트리뷰의첫번째노드를파라미 터로해서이프로시저를호출하고, 트리뷰를펼쳐보이면모든작업이끝난다. Enumeration(Root, TreeView1.Items[0]); TreeView1.FullExpand;
남은작업은 Enumeration 프로시저를구현하는것이다. 일단다음과같이프로시저와사 용할변수를선언하고, IEnumStatStg 인터페이스를 Enum 변수에담아온다. procedure TForm1.Enumeration(Storage: IStorage; ANode: TTreeNode); Hr: HResult; Enum: IEnumStatStg; SubNode: TTreeNode; StatStg: TStatStg; SubStor: IStorage; HrSubStor: HResult; NumFetched: integer; Hr := Storage.EnumElements(0, nil, 0, Enum); OleCheck(Hr); OleCheck 프로시저는 ComObj.pas 유닛에선언되어있는데결과코드가에러에해당되면 EOleSysError 예외를발생시킨다. EnumElements 메소드를이용해서 IEnumStatStg 인터페이스를 Enum 변수에담아왔으면이인터페이스의 Next 메소드를사용해서 TStatStg 클래스형으로선언된 StatStg 변수에정보를담아오도록하자. 이때 Next 메소드의마지막메소드에는실제로넘어온아이템의수가정수의포인터형으로반환되므로다음과같이사용한다. repeat Hr := Enum.Next(1, StatStg, @NumFetched); if Hr <> S_OK then continue; 이제는 StatStg 변수의 dwtype 정보에따라서다르게대응해야한다. 하부요소가 Storage 라면일단 Storage 의이름이담겨있는 pwcsname 필드를이용해서트리뷰컴포넌트에노드를하나추가한다. 그리고나서, Storage 를다시열어서그 Storage 의 SubStorage 를얻고, 이 SubStorage 에해당되는아이템과추가할트리노드를가지고 Enumeration 프로시저를재귀호출한다. case StatStg.dwType of
STGTY_STORAGE: SubNode := TreeView1.Items.AddChild(ANode, StatStg.pwcsName); HrSubStor := Storage.OpenStorage(StatStg.pwcsName, nil, STGM_READWRITE or STGM_DIRECT or STGM_SHARE_EXCLUSIVE, nil, 0, SubStor); if SUCCEEDED(HrSubStor) then Enumeration(SubStor, SubNode); 하부요소가 Stream 이라면스트림의이름을트리뷰에추가하기만하면된다. STGTY_STREAM: TreeView1.Items.AddChild(ANode, StatStg.pwcsName); 이를계속반복하다가마지막에가면 S_OK 가반환되지않으므로, 이때루프를종료하면 된다. until (Hr <> S_OK); 이것으로두번째어플리케이션이완성되었다. 이제실제로이를컴파일해서실행하고, MS 오피스파일등을열어서하나의파일에어떤형식으로내용들이구조적으로저장되어있는지살펴보도록하자. 아래의그림은오피스의 CommonDB.xls 파일을열어본것이다. 이를보면실제로저장되는내용이하나의파일에여러개의 Stream 과 Storage 로이루어져있다는것을알수있다.
정리 (Summary) 이번장에서는새로운파일저장방식으로사용되고있는구조화저장소기법에대해서알아보았다. 이와같은구조화저장소기법을이용해서파일을저장하면파일하나에여러내용을분류하여관리할수있다. 구조화저장소기법을이용한파일저장방식역시이제는표준으로정착되어가고있다. 그러므로 Stream 과 Storage 를이용하는방법에대해서는잘익혀두는것이도움이될것이다.