표준 라이브러리: 파일과 스트림

표준 라이브러리: 파일과 스트림 (Standard library: Files and streams)

Ada는 파일 입출력(I/O)에 대해 서로 다른 접근 방식을 제공해요:

  • 텍스트 I/O(Text I/O) — 텍스트 형식의 파일 I/O를 지원하며, 콘솔에 정보 표시를 포함해요
  • 순차 I/O(Sequential I/O) — 특정 데이터 타입에 대해 순차적 방식으로 작성된 이진 형식의 파일 I/O를 지원해요
  • 직접 I/O(Direct I/O) — 특정 데이터 타입에 대한 이진 형식의 파일 I/O를 지원하지만, 파일의 어떤 위치에도 접근할 수 있어요
  • 스트림 I/O(Stream I/O) — 이진 형식의 파일을 사용해 여러 데이터 타입(무한 타입의 객체 포함)의 정보 I/O를 지원해요

다음 표는 방금 본 기능들의 요약을 제시해요:

파일 I/O 옵션 형식 임의 접근 데이터 타입
Text I/O text string type
Sequential I/O binary single type
Direct I/O binary single type
Stream I/O binary multiple types

다음 절들에서 이 I/O 접근 방식들에 대한 자세한 내용을 논의할게요.

출처: 표준 라이브러리: 파일과 스트림 문서

본문

텍스트 I/O (Text I/O)

이 과정의 대부분에서 Put_Line 프로시저를 사용해 콘솔에 정보를 표시했어요. 하지만 이 프로시저는 File_Type 매개변수도 받아들여요. 예를 들어 이 매개변수를 명시적으로 설정해 표준 출력과 표준 오류 사이를 선택할 수 있어요:

with Ada.Text_IO; use Ada.Text_IO;

procedure Show_Std_Text_Out is
begin
   Put_Line (Standard_Output, "Hello World #1");
   Put_Line (Standard_Error,  "Hello World #2");
end Show_Std_Text_Out;

이 매개변수를 사용해 어떤 텍스트 파일에도 정보를 쓸 수도 있어요. 쓰기를 위한 새 파일을 만들려면 Create 프로시저를 사용해, 나중에 Put_Line에(예: Standard_Output 대신) 전달할 수 있는 File_Type 요소를 초기화해요. 정보 쓰기를 마친 후 Close 프로시저를 호출해 파일을 닫을 수 있어요.

텍스트 파일에서 정보를 읽으려면 유사한 방법을 사용해요. 하지만 파일을 열 때 출력 파일(Out_File) 대신 입력 파일(In_File)임을 지정해야 해요. 또한 Put_Line 프로시저를 호출하는 대신 Get_Line 함수를 호출해 파일에서 정보를 읽어요.

새 텍스트 파일에 정보를 쓴 다음 같은 파일에서 다시 읽는 예시를 볼게요:

with Ada.Text_IO; use Ada.Text_IO;

procedure Show_Simple_Text_File_IO is
   F         : File_Type;
   File_Name : constant String := "simple.txt";
begin
   Create (F, Out_File, File_Name);
   Put_Line (F, "Hello World #1");
   Put_Line (F, "Hello World #2");
   Put_Line (F, "Hello World #3");
   Close (F);

   Open (F, In_File, File_Name);
   while not End_Of_File (F) loop
      Put_Line (Get_Line (F));
   end loop;
   Close (F);
end Show_Simple_Text_File_IO;

CreateClose 프로시저 외에도 표준 라이브러리는 Reset 프로시저를 포함하는데, 이름이 시사하듯 파일의 모든 정보를 재설정(지움)해요. 예를 들어:

with Ada.Text_IO; use Ada.Text_IO;

procedure Show_Text_File_Reset is
   F         : File_Type;
   File_Name : constant String := "simple.txt";
begin
   Create (F, Out_File, File_Name);
   Put_Line (F, "Hello World #1");
   Reset (F);
   Put_Line (F, "Hello World #2");
   Close (F);

   Open (F, In_File, File_Name);
   while not End_Of_File (F) loop
      Put_Line (Get_Line (F));
   end loop;
   Close (F);
end Show_Text_File_Reset;

이 프로그램을 실행하면 파일에 첫 문자열("Hello World #1")을 썼음에도 Reset 호출 때문에 지워진 것을 알 수 있어요.

읽기나 쓰기를 위해 파일을 여는 것 외에도 기존 파일을 열어 추가(append)할 수도 있어요. Open 프로시저를 Append_File 옵션으로 호출해 이걸 해요.

Open 프로시저를 호출할 때 지정된 파일이 없으면 예외가 발생해요. 따라서 그 문맥에서 예외를 처리해야 해요. 다음 예시는 파일을 삭제한 다음 같은 파일을 읽기 위해 열려고 시도해요:

with Ada.Text_IO; use Ada.Text_IO;

procedure Show_Text_File_Input_Except is
   F         : File_Type;
   File_Name : constant String := "simple.txt";
begin
   --  출력 파일을 열고 삭제
   Create (F, Out_File, File_Name);
   Delete (F);

   --  삭제된 파일을 열려고 시도
   Open (F, In_File, File_Name);
   Close (F);
exception
   when Name_Error =>
      Put_Line ("File does not exist");
   when others =>
      Put_Line
        ("Error while processing input file");
end Show_Text_File_Input_Except;

이 예시에서 Create 호출로 파일을 만들고 Delete 호출로 삭제해요. Delete 호출 후에는 더 이상 File_Type 요소를 사용할 수 없어요. 파일을 삭제한 후 존재하지 않는 파일을 열려고 시도해 Name_Error 예외가 발생해요.

순차 I/O (Sequential I/O)

이전 절에서 텍스트 파일 I/O에 대한 자세한 내용을 제시했어요. 여기서는 이진 형식으로 파일 I/O를 하는 방법을 논의할게요. 처음 탐구할 패키지는 Ada.Sequential_IO 패키지예요. 이 패키지는 제네릭이므로 파일 I/O에 사용할 데이터 타입으로 인스턴스화해야 해요. 그렇게 한 후 이전 절에서 본 것과 같은 프로시저들(Create, Open, Close, Reset, Delete)을 사용할 수 있어요. 하지만 Get_LinePut_Line 프로시저를 호출하는 대신 ReadWrite 프로시저를 호출할게요.

다음 예시에서 부동 소수점 타입에 대해 Ada.Sequential_IO 패키지를 인스턴스화해요:

with Ada.Text_IO;
with Ada.Sequential_IO;

procedure Show_Seq_Float_IO is
   package Float_IO is
     new Ada.Sequential_IO (Float);
   use Float_IO;

   F         : Float_IO.File_Type;
   File_Name : constant String :=
                 "float_file.bin";
begin
   Create (F, Out_File, File_Name);
   Write (F,  1.5);
   Write (F,  2.4);
   Write (F,  6.7);
   Close (F);

   declare
      Value : Float;
   begin
      Open (F, In_File, File_Name);
      while not End_Of_File (F) loop
         Read (F, Value);
         Ada.Text_IO.Put_Line
           (Float'Image (Value));
      end loop;
      Close (F);
   end;
end Show_Seq_Float_IO;

복잡한 정보를 읽고 쓰는 데도 같은 접근 방식을 사용해요. 다음 예시는 Boolean과 부동 소수점 값을 포함하는 레코드를 사용해요:

with Ada.Text_IO;
with Ada.Sequential_IO;

procedure Show_Seq_Rec_IO is
   type Num_Info is record
      Valid : Boolean := False;
      Value : Float;
   end record;

   procedure Put_Line (N : Num_Info) is
   begin
      if N.Valid then
         Ada.Text_IO.Put_Line
           ("(ok,     "
            & Float'Image (N.Value) & ")");
      else
         Ada.Text_IO.Put_Line
           ("(not ok,  -----------)");
      end if;
   end Put_Line;

   package Num_Info_IO is new
     Ada.Sequential_IO (Num_Info);
   use Num_Info_IO;

   F         : Num_Info_IO.File_Type;
   File_Name : constant String :=
                 "float_file.bin";
begin
   Create (F, Out_File, File_Name);
   Write (F,  (True,  1.5));
   Write (F,  (False, 2.4));
   Write (F,  (True,  6.7));
   Close (F);

   declare
      Value : Num_Info;
   begin
      Open (F, In_File, File_Name);
      while not End_Of_File (F) loop
         Read (F, Value);
         Put_Line (Value);
      end loop;
      Close (F);
   end;
end Show_Seq_Rec_IO;

예시에서 보듯 부동 소수점 타입에 사용한 것과 같은 접근 방식을 이 레코드의 파일 I/O에 사용할 수 있어요. 레코드 타입에 대해 Ada.Sequential_IO 패키지를 인스턴스화하면 파일 I/O 연산이 같은 방식으로 수행돼요.

직접 I/O (Direct I/O)

직접 I/O는 Ada.Direct_IO 패키지에 있어요. 이 메커니즘은 방금 제시한 순차 I/O와 유사하지만 파일의 어떤 위치에도 접근할 수 있게 해줘요. 패키지 인스턴스화와 대부분의 연산은 순차 I/O와 매우 유사해요. 이전 절에서 제시한 Show_Seq_Float_IO 애플리케이션을 Ada.Direct_IO 패키지를 사용하도록 다시 작성하려면 Ada.Sequential_IO 패키지의 인스턴스를 Ada.Direct_IO 패키지로 바꾸기만 하면 돼요. 다음이 새 소스 코드예요:

with Ada.Text_IO;
with Ada.Direct_IO;

procedure Show_Dir_Float_IO is
   package Float_IO is new Ada.Direct_IO (Float);
   use Float_IO;

   F         : Float_IO.File_Type;
   File_Name : constant String :=
                 "float_file.bin";
begin
   Create (F, Out_File, File_Name);
   Write (F,  1.5);
   Write (F,  2.4);
   Write (F,  6.7);
   Close (F);

   declare
      Value : Float;
   begin
      Open (F, In_File, File_Name);
      while not End_Of_File (F) loop
         Read (F, Value);
         Ada.Text_IO.Put_Line
           (Float'Image (Value));
      end loop;
      Close (F);
   end;
end Show_Dir_Float_IO;

순차 I/O와 달리 직접 I/O는 파일의 어떤 위치에도 접근할 수 있어요. 하지만 파일에 정보를 추가하는 옵션은 제공하지 않아요. 대신 같은 File_Type 요소를 통해 파일에 읽기와 쓰기를 모두 허용하는 Inout_File 모드를 제공해요.

파일의 어떤 위치에든 접근하려면 Set_Index 프로시저를 호출해 새 위치/인덱스를 설정해요. Index 함수를 사용해 현재 인덱스를 검색할 수 있어요. 예시를 볼게요:

with Ada.Text_IO;
with Ada.Direct_IO;

procedure Show_Dir_Float_In_Out_File is
   package Float_IO is new Ada.Direct_IO (Float);
   use Float_IO;

   F         : Float_IO.File_Type;
   File_Name : constant String :=
                 "float_file.bin";
begin
   --  입력/출력을 위해 파일 열기
   Create (F, Inout_File, File_Name);
   Write (F,  1.5);
   Write (F,  2.4);
   Write (F,  6.7);

   --  인덱스를 이전 위치로 설정하고
   --  값 덮어쓰기
   Set_Index (F, Index (F) - 1);
   Write (F,  7.7);

   declare
      Value : Float;
   begin
      --  인덱스를 파일 시작으로 설정
      Set_Index (F, 1);

      while not End_Of_File (F) loop
         Read (F, Value);
         Ada.Text_IO.Put_Line
           (Float'Image (Value));
      end loop;
      Close (F);
   end;
end Show_Dir_Float_In_Out_File;

이 예시를 실행하면 파일에 이전에 쓴 6.7 대신 7.7이 들어 있는 것을 볼 수 있어요. 다시 쓰기 전에 인덱스를 이전 위치로 변경해 값을 덮어썼죠.

이 예시에서 Inout_File 모드를 사용했어요. 이 모드를 사용해, 파일을 닫고 읽기 위해 다시 열지 않고도 파일에서 읽기 전에 인덱스를 초기 위치로 되돌렸죠(Set_Index (F, 1)).

스트림 I/O (Stream I/O)

이진 형식의 파일 I/O에 대한 이전 접근 방식들(순차 및 직접 I/O)은 모두 단일 데이터 타입(인스턴스화한 타입)에 특화돼 있어요. 이 접근 방식을 사용해 단일 데이터 타입의 객체를 쓸 수 있는데, 그 객체는 (잠재적으로 많은 필드가 있는) 배열이나 레코드일 수 있어요. 하지만 서로 다른 데이터 타입을 포함하는 파일을 만들고 처리해야 하거나, 무한 타입의 객체가 필요하다면 이 접근 방식으로는 충분하지 않아요. 대신 스트림 I/O를 사용해야 해요.

스트림 I/O는 이전 접근 방식들과 몇 가지 유사점을 공유해요. 여전히 Create, Open, Close 프로시저를 사용해요. 하지만 File_Type 요소를 통해 파일에 직접 접근하는 대신 Stream_Access 요소를 사용해요. 정보를 읽고 쓰려면 읽거나 쓰는 데이터 타입의 'Read 또는 'Write 애트리뷰트를 사용해요.

이전 절의 Show_Dir_Float_IO 프로시저를 직접 I/O 대신 스트림 I/O를 사용하는 버전을 볼게요:

with Ada.Text_IO;

with Ada.Streams.Stream_IO;
use  Ada.Streams.Stream_IO;

procedure Show_Float_Stream is
   F         : File_Type;
   S         : Stream_Access;
   File_Name : constant String :=
                 "float_file.bin";
begin
   Create (F, Out_File, File_Name);
   S := Stream (F);

   Float'Write (S, 1.5);
   Float'Write (S, 2.4);
   Float'Write (S, 6.7);

   Close (F);

   declare
      Value : Float;
   begin
      Open (F, In_File, File_Name);
      S := Stream (F);

      while not End_Of_File (F) loop
         Float'Read (S, Value);
         Ada.Text_IO.Put_Line
           (Float'Image (Value));
      end loop;
      Close (F);
   end;
end Show_Float_Stream;

Create 호출 후 Stream 함수를 호출해 해당 Stream_Access 요소를 검색해요. 그런 다음 이 스트림을 사용해 Float 타입의 'Write 애트리뷰트를 통해 파일에 정보를 써요. 파일을 닫고 읽기 위해 다시 연 후 다시 해당 Stream_Access 요소를 검색하고 Float 타입의 'Read 애트리뷰트를 통해 파일에서 정보를 읽어요.

스트림을 사용해 같은 파일 안에 서로 다른 데이터 타입을 포함하는 파일을 만들고 처리할 수 있어요. 문자열 같은 무한 데이터 타입도 읽고 쓸 수 있어요. 하지만 무한 데이터 타입을 사용할 때는 그 무한 데이터 타입의 'Input'Output 애트리뷰트를 호출해야 해요: 이 애트리뷰트들은 객체의 실제 데이터 외에 경계(bounds)나 판별자(discriminant)에 대한 정보도 써요.

다음 예시는 서로 다른 길이의 문자열과 부동 소수점 값을 혼합한 파일 I/O를 보여줘요:

with Ada.Text_IO;

with Ada.Streams.Stream_IO;
use  Ada.Streams.Stream_IO;

procedure Show_String_Stream is
   F         : File_Type;
   S         : Stream_Access;
   File_Name : constant String :=
                 "float_file.bin";

   procedure Output (S  : Stream_Access;
                     FV : Float;
                     SV : String) is
   begin
      String'Output (S, SV);
      Float'Output (S,  FV);
   end Output;

   procedure Input_Display (S : Stream_Access) is
      SV : String := String'Input (S);
      FV : Float  := Float'Input (S);
   begin
      Ada.Text_IO.Put_Line (Float'Image (FV)
                            & " --- " & SV);
   end Input_Display;

begin
   Create (F, Out_File, File_Name);
   S := Stream (F);

   Output (S, 1.5, "Hi!!");
   Output (S, 2.4, "Hello world!");
   Output (S, 6.7, "Something longer here...");

   Close (F);

   Open (F, In_File, File_Name);
   S := Stream (F);

   while not End_Of_File (F) loop
      Input_Display (S);
   end loop;
   Close (F);

end Show_String_Stream;

스트림 I/O를 사용할 때 쓰는 데이터의 타입을 나타내는 정보는 파일에 기록되지 않아요. 파일이 서로 다른 타입의 데이터를 포함한다면, 읽을 때 파일에서 참조하는 타입을 쓸 때와 같은 순서로 참조해야 해요. 그렇지 않으면 얻는 정보가 손상돼요. 불행히도 강한 데이터 타입(strong data typing)은 이 경우 도움이 되지 않아요. 파일 I/O를 위한 간단한 프로시저(위 예시처럼)를 작성하면 파일 형식이 일관되도록 보장하는 데 도움이 될 수 있어요.

직접 I/O처럼 스트림 I/O 지원도 파일의 어떤 위치에도 접근할 수 있게 해줘요. 하지만 그렇게 할 때는 새 인덱스의 위치가 기대하는 데이터 타입과 일관적인지 매우 주의해야 해요.

더 알아보기 (Learn more)