모듈형 프로그래밍

모듈형 프로그래밍 (Modular programming)

지금까지 우리의 예시는 단순한 독립형 서브프로그램이었어요. Ada는 선언부에 임의의 선언을 허용해서 이런 점에서 도움이 되지만, 실제 애플리케이션에는 이 방식이 확장되지 않는다는 걸 쉽게 알 수 있어요. 프로그램을 모듈형이고 구별되는 단위로 구조화할 더 나은 방법이 필요해요. Ada는 프로그램을 여러 패키지와 하위 패키지로 나누도록 장려하며, 완벽하게 조직된 코드베이스를 추구하는 프로그래머에게 많은 도구를 제공해요.

출처: 모듈형 프로그래밍 문서

본문

패키지 (Packages)

Ada의 패키지 선언 예시를 볼게요:

package Week is

   Mon : constant String := "Monday";
   Tue : constant String := "Tuesday";
   Wed : constant String := "Wednesday";
   Thu : constant String := "Thursday";
   Fri : constant String := "Friday";
   Sat : constant String := "Saturday";
   Sun : constant String := "Sunday";

end Week;

그리고 사용법은 이렇습니다:

with Ada.Text_IO; use Ada.Text_IO;
with Week;
--  References the Week package, and
--  adds a dependency from Main to Week

procedure Main is
begin
   Put_Line ("First day of the week is "
             & Week.Mon);
end Main;

패키지는 코드를 모듈화해서 프로그램을 의미 있는 단위로 나눌 수 있게 해줘요. 게다가 패키지의 선언과 본문(아래에서 볼 것)을 분리하면 컴파일 시간을 줄일 수 있어요.

with 절은 의존성을 나타내지만, 위 예시에서 Week 패키지의 개체를 참조할 때 여전히 패키지 이름으로 접두사를 붙여야 해요. (use Week 절을 넣으면 그런 접두사가 필요 없어져요.)

패키지에서 개체에 접근하는 데는 점 표기법 A.B를 사용하는데, 이는 레코드 필드에 접근할 때 쓰는 것과 동일한 표기법이에요.

with 절은 컴파일 유닛의 전주부(prelude, 즉 procedure 같은 유닛의 시작을 표시하는 예약어 앞)에만 나타날 수 있어요. 다른 곳에는 허용되지 않아요. 이 규칙은 방법론적인 이유로만 필요해요. 코드를 읽는 사람이 코드가 의존하는 유닛을 즉시 볼 수 있어야 하기 때문이죠.

C/C++의 헤더 파일과 달리, 패키지는 언어 수준의 메커니즘이에요. C 전처리기의 기능인 #include된 헤더 파일과는 근본적으로 달라요. with 구조는 텍스트 포함 메커니즘이 아니라 의미 포함(semantic inclusion) 메커니즘이에요. 그래서 패키지를 with 할 때 컴파일러에게 "이 의미 유닛에 의존한다"고 말하는 것이지 "이 텍스트 묶음을 여기에 포함하라"는 게 아니에요.

패키지의 효과는 어디서 with 되었는지에 따라 달라지지 않아요. #include가 나타나는 문맥에 따라 포함된 텍스트의 의미가 달라지는 C/C++와 대조적이에요. 이는 컴파일/재컴파일을 더 효율적으로 만들고, IDE 같은 도구가 프로그램의 의미에 대한 정확한 정보를 가질 수 있게 해줘요. 결과적으로 더 나은 도구와 더 분석 가능한 코드(사람이 분석해도 마찬가지)를 만들 수 있어요.

#include와 비교했을 때 Ada with 절의 중요한 이점은 상태가 없다(stateless)는 점이에요. withuse 절의 순서는 중요하지 않고, 부수 효과 없이 바꿀 수 있어요.

GNAT 툴체인에서: Ada 언어 표준은 소스 파일과 패키지의 특정 관계를 강제하지 않아요. 예를 들어 이론상 모든 코드를 한 파일에 넣거나 자신만의 파일 명명 규칙을 쓸 수 있어요. 다만 실제로는 구현체가 특정 규칙을 가져요. GNAT에서는 각 최상위 컴파일 유닛이 별도 파일에 들어가야 해요. 위 예시에서 Week 패키지는 .ads 파일(Ada specification), Main 프로시저는 .adb 파일(Ada body)에 들어가요.

패키지 사용하기 (Using a package)

with 절이 다른 패키지에 대한 의존성을 나타낸다는 걸 위에서 보았어요. 하지만 Week 패키지에서 온 개체를 참조할 때마다 패키지 전체 이름으로 접두사를 붙여야 했어요. use 절을 사용하면 현재 스코프에서 패키지의 모든 개체를 직접 볼 수 있게 만들 수 있어요. 사실 우리는 이 튜토리얼 시작부터 거의 use 절을 사용해 왔어요.

with Ada.Text_IO; use Ada.Text_IO;
--                ^ Make every entity of the
--                  Ada.Text_IO package
--                  directly visible.
with Week;

procedure Main is
   use Week;
   --  Make every entity of the Week
   --  package directly visible.
begin
   Put_Line ("First day of the week is " & Mon);
end Main;
  • Put_LineAda.Text_IO 패키지에서 온 서브프로그램이에요. Main 유닛의 맨 위에서 패키지를 use 했기 때문에 직접 참조할 수 있어요.
  • with 절과 달리 use 절은 전주부나 어떤 선언 영역에도 둘 수 있어요. 후자의 경우 use 절은 그것을 포함하는 어휘 스코프에서 효과를 가지게 돼요.

패키지 본문 (Package body)

위의 단순한 예시에서 Week 패키지는 선언만 있고 본문이 없어요. 이건 실수가 아니에요. 위에 나온 패키지 선언(specification)에서는 본문을 선언할 수 없고, 그건 패키지 본문에 있어야 하기 때문이에요.

package Operations is

   --  Declaration
   function Increment_By
     (I    : Integer;
      Incr : Integer := 0) return Integer;

   function Get_Increment_Value return Integer;

end Operations;

package body Operations is

   Last_Increment : Integer := 1;

   function Increment_By
     (I    : Integer;
      Incr : Integer := 0) return Integer is
   begin
      if Incr /= 0 then
         Last_Increment := Incr;
      end if;

      return I + Last_Increment;
   end Increment_By;

   function Get_Increment_Value return Integer is
   begin
      return Last_Increment;
   end Get_Increment_Value;

end Operations;

여기서 Increment_By 함수의 본문은 패키지 본문에 선언돼야 함을 볼 수 있어요. 덕분에 Last_Increment 변수를 본문에 두어 Operations 패키지 사용자에게 접근 불가능하게 만들 수 있고, 이것이 첫 번째 형태의 캡슐화를 제공해요. 본문에 선언된 개체는 본문에서만 보이기 때문이에요.

이 예시는 Last_Increment가 간접적으로 어떻게 사용되는지 보여줘요:

with Ada.Text_IO; use Ada.Text_IO;
with Operations;

procedure Main is
   use Operations;

   I : Integer := 0;
   R : Integer;

   procedure Display_Update_Values is
      Incr : constant Integer :=
               Get_Increment_Value;
   begin
      Put_Line (Integer'Image (I)
                & " incremented by "
                & Integer'Image (Incr)
                & " is "
                & Integer'Image (R));
      I := R;
   end Display_Update_Values;
begin
   R := Increment_By (I);
   Display_Update_Values;
   R := Increment_By (I);
   Display_Update_Values;

   R := Increment_By (I, 5);
   Display_Update_Values;
   R := Increment_By (I);
   Display_Update_Values;

   R := Increment_By (I, 10);
   Display_Update_Values;
   R := Increment_By (I);
   Display_Update_Values;
end Main;

자식 패키지 (Child packages)

패키지는 계층 구조를 만드는 데 사용될 수 있어요. 이는 부모 패키지의 기능을 확장하는 자식 패키지를 사용해 달성해요. 지금까지 우리가 사용해 온 자식 패키지의 예시가 Ada.Text_IO 패키지예요. 여기서 부모 패키지는 Ada, 자식 패키지는 Text_IO예요. 앞선 예시들에서 우리는 Text_IO 자식 패키지의 Put_Line 프로시저를 사용해 왔어요.

중요: Ada는 중첩 패키지도 지원해요. 하지만 사용하기 더 복잡할 수 있어서 자식 패키지를 사용하는 것을 권장해요. 중첩 패키지는 고급 과정에서 다룰 거예요.

아까의 Week 패키지로 자식 패키지 이야기를 시작해 볼게요:

package Week is

   Mon : constant String := "Monday";
   Tue : constant String := "Tuesday";
   Wed : constant String := "Wednesday";
   Thu : constant String := "Thursday";
   Fri : constant String := "Friday";
   Sat : constant String := "Saturday";
   Sun : constant String := "Sunday";

end Week;

Week용 자식 패키지를 만들고 싶다면 이렇게 쓸 수 있어요:

package Week.Child is

   function Get_First_Of_Week return String;

end Week.Child;

여기서 Week는 부모 패키지이고 Child는 자식 패키지예요. Week.Child의 해당 패키지 본문:

package body Week.Child is

   function Get_First_Of_Week return String is
   begin
      return Mon;
   end Get_First_Of_Week;

end Week.Child;

Get_First_Of_Week 함수를 구현할 때 Mon 문자열을 직접 사용할 수 있어요. 부모 패키지 Week에 선언됐어도 말이죠. 여기서 with Week를 쓰지 않았는데, 그 이유는 Week 패키지의 선언에 있는 모든 요소(Mon, Tue 등)가 자식 패키지 Week.Child에서 보이기 때문이에요.

이제 Week.Child 패키지 구현을 마쳤으니, with Week.Child라고 쓰기만 하면 이 자식 패키지의 요소를 서브프로그램에서 사용할 수 있어요. 마찬가지로 이 요소들을 직접 사용하고 싶다면 use Week.Child를 추가로 써요. 예를 들어:

with Ada.Text_IO; use Ada.Text_IO;
with Week.Child;  use Week.Child;

procedure Main is
begin
   Put_Line ("First day of the week is "
             & Get_First_Of_Week);
end Main;

자식 패키지의 자식 (Child of a child package)

지금까지 2단계 패키지 계층을 봤어요. 하지만 만들 수 있는 계층은 그게 전부가 아니에요. 예를 들어 Week.Child.Grandchild 패키지를 선언해서 이전 예시의 계층을 확장할 수 있어요. 이 경우 Week.ChildGrandchild 패키지의 부모가 돼요. 구현을 볼게요:

package Week.Child.Grandchild is

   function Get_Second_Of_Week return String;

end Week.Child.Grandchild;

package body Week.Child.Grandchild is

   function Get_Second_Of_Week return String is
   begin
      return Tue;
   end Get_Second_Of_Week;

end Week.Child.Grandchild;

이 새로운 Grandchild 패키지를 테스트 애플리케이션에서 이전과 같은 방식으로 사용할 수 있어요. 이전 테스트 앱을 재사용하고 withuse, 함수 호출만 고치면 돼요. 갱신된 코드:

with Ada.Text_IO; use Ada.Text_IO;

with Week.Child.Grandchild;
use  Week.Child.Grandchild;

procedure Main is
begin
   Put_Line ("Second day of the week is "
             & Get_Second_Of_Week);
end Main;

다시 말하지만 패키지 계층은 여기서 멈추지 않아요. Week.Child.Grandchild.Grand_grandchild 패키지를 구현해서 이전 예시의 계층을 계속 확장할 수 있어요.

여러 자식 (Multiple children)

지금까지 부모 패키지의 단일 자식 패키지를 봤어요. 하지만 부모 패키지는 여러 자식을 가질 수도 있어요. 위 예시를 확장해서 Week.Child_2 패키지를 구현할 수 있어요. 예를 들어:

package Week.Child_2 is

   function Get_Last_Of_Week return String;

end Week.Child_2;

여기서 Week는 여전히 Child 패키지의 부모이지만 Child_2 패키지의 부모이기도 해요. 마찬가지로 Child_2는 당연히 Week의 자식 패키지 중 하나예요. Week.Child_2의 해당 패키지 본문:

package body Week.Child_2 is

   function Get_Last_Of_Week return String is
   begin
      return Sun;
   end Get_Last_Of_Week;

end Week.Child_2;

테스트 애플리케이션에서 두 자식을 모두 참조할 수 있어요:

with Ada.Text_IO;  use Ada.Text_IO;
with Week.Child;   use Week.Child;
with Week.Child_2; use Week.Child_2;

procedure Main is
begin
   Put_Line ("First day of the week is "
             & Get_First_Of_Week);
   Put_Line ("Last day of the week is "
             & Get_Last_Of_Week);
end Main;

가시성 (Visibility)

이전 절에서 부모 패키지 선언에 선언된 요소는 자식 패키지에서 보인다는 것을 봤어요. 하지만 부모 패키지 본문에 선언된 요소는 그렇지 않아요.

Book 패키지와 그 자식 Additional_Operations를 생각해 볼게요:

package Book is

   Title : constant String :=
     "Visible for my children";

   function Get_Title return String;

   function Get_Author return String;

end Book;

package Book.Additional_Operations is

   function Get_Extended_Title return String;

   function Get_Extended_Author return String;

end Book.Additional_Operations;

두 패키지의 본문:

package body Book is

   Author : constant String :=
     "Author not visible for my children";

   function Get_Title return String is
   begin
      return Title;
   end Get_Title;

   function Get_Author return String is
   begin
      return Author;
   end Get_Author;

end Book;

package body Book.Additional_Operations is

   function Get_Extended_Title return String is
   begin
      return "Book Title: " & Title;
   end Get_Extended_Title;

   function Get_Extended_Author return String is
   begin
      --  "Author" string declared in the body
      --  of the Book package is not visible
      --  here. Therefore, we cannot write:
      --
      --  return "Book Author: " & Author;

      return "Book Author: Unknown";
   end Get_Extended_Author;

end Book.Additional_Operations;

Get_Extended_Title 구현에서 부모 패키지 BookTitle 상수를 사용해요. 하지만 Get_Extended_Author 함수의 주석에 표시된 대로, Book 패키지 본문에 선언한 Author 문자열은 Book.Additional_Operations 패키지에서 보이지 않아요. 그래서 Get_Extended_Author 함수를 구현하는 데 사용할 수 없어요.

다만 BookGet_Author 함수를 Get_Extended_Author 구현에서 사용해 이 문자열을 얻을 수 있어요. 마찬가지로 이 전략을 Get_Extended_Title 함수 구현에도 쓸 수 있어요. 적응된 코드:

package body Book.Additional_Operations is

   function Get_Extended_Title return String is
   begin
      return "Book Title: " & Get_Title;
   end Get_Extended_Title;

   function Get_Extended_Author return String is
   begin
      return "Book Author: " & Get_Author;
   end Get_Extended_Author;

end Book.Additional_Operations;

위 패키지들을 위한 간단한 테스트 애플리케이션:

with Ada.Text_IO; use Ada.Text_IO;

with Book.Additional_Operations;
use  Book.Additional_Operations;

procedure Main is
begin
   Put_Line (Get_Extended_Title);
   Put_Line (Get_Extended_Author);
end Main;

패키지의 본문에 요소를 선언함으로써 Ada에서 캡슐화를 구현할 수 있어요. 그 요소는 패키지 본문에서만 보이고 그 외 어디서도 보이지 않아요. 하지만 이것이 Ada에서 캡슐화를 이루는 유일한 방법은 아니에요. 다른 접근법은 Privacy 장에서 논의할 거예요.

이름 변경 (Renaming)

앞서 서브프로그램은 이름을 바꿀 수 있다고 언급했어요. 패키지도 이름을 바꿀 수 있어요. 역시 renames 키워드를 사용해요. 다음 예시는 Ada.Text_IO 패키지를 TIO로 바꿔요:

with Ada.Text_IO;

procedure Main is
   package TIO renames Ada.Text_IO;
begin
   TIO.Put_Line ("Hello");
end Main;

더 짧은 패키지 이름을 사용해서 코드의 가독성을 높이는 데 이름 변경을 쓸 수 있어요. 위 예시에서 더 긴 버전(Ada.Text_IO.Put_Line) 대신 TIO.Put_Line을 써요. 이 접근법은 특히 패키지를 use하지 않고 코드가 지나치게 장황해지는 걸 피하고 싶을 때 유용해요.

패키지 안의 서브프로그램과 객체도 이름을 바꿀 수 있어요. 예를 들어 위 예시에서 Put_Line 프로시저만 바꿀 수도 있어요:

with Ada.Text_IO;

procedure Main is
   procedure Say (Something : String)
     renames Ada.Text_IO.Put_Line;
begin
   Say ("Hello");
end Main;

이 예시에서는 Put_Line 프로시저를 Say로 바꿨어요.

더 알아보기 (Learn more)