Ada 제네릭 프로그래밍 ── 타입으로 계약을 작성하고 재사용을 제로 코스트로 실현하기
· 小村 豪 · Ada, 프로그래밍 언어, 제네릭, 타입 시스템, 정적 타이핑, 계약 모델, 제로 코스트 추상화, GNAT, Alire, 고신뢰성, 코드 재사용
1. 들어가며 ── ‘무엇이든 받는다’가 아니라 ‘무엇을 약속하는가’
정적 타입 언어로 재사용 가능한 코드를 작성하려 하면, 곧 같은 고민에 부딪힙니다. 정수용으로 작성한 스택을 문자열에도 쓰고 싶습니다. 부동소수점 배열에도 같은 통계 처리를 적용하고 싶습니다. 오름차순 정렬 로직을 내림차순 정렬에도 쓰고 싶습니다. 그런데 타입마다 같은 코드를 복사해 나가면 수정 누락이 생깁니다. 반대로 void*나 캐스팅으로 무엇이든 받는 설계를 하면 타입 안전성이 무너집니다.
Ada의 답은 제네릭(generic units)입니다.
Ada의 제네릭은 단순한 텍스트 치환이 아닙니다. 타입, 값, 서브프로그램, 그리고 패키지 자체를 가인수로 받아, 인스턴스화 시점에 정적으로 타입 검사가 이루어집니다. 즉, 실행 시점에 ‘이 타입으로 정말 괜찮은가’를 조사하는 것이 아니라, 컴파일 시점에 ‘이 부품이 이 계약을 만족하는가’를 확정하는 메커니즘입니다.
flowchart LR
A[재사용하고 싶은 처리] --> B{어떻게 재사용할 것인가}
B --> C[복사와 붙여넣기]
B --> D[void* / Object / 캐스팅]
B --> E[Ada 제네릭]
C --> C1[수정 누락이 생기기 쉽다]
D --> D1[실행 시 오류나 타입 붕괴가 생기기 쉽다]
E --> E1[타입 안전]
E --> E2[컴파일 시점 검사]
E --> E3[실행 시 불필요한 디스패치 없음]
이 글에서는 Ada의 제네릭 프로그래밍을 다음 흐름으로 정리합니다.
- 총칭 서브프로그램
- 총칭 패키지
- 타입 파라미터, 값 파라미터, 서브프로그램 파라미터
private,range <>,digits <>등의 타입 카테고리- 정렬, 스택, 통계 처리,
Count_If, 키-값 저장소의 구현 예제 - 정식 패키지 파라미터를 이용한 고차 제네릭
- Ada의 contract model과 실무 설계 사고방식
이 주제는 이 블로그의 연재인 「Ada 언어의 매력」「SPARK를 이용한 형식 검증 입문」「안전한 동시성 처리」「리얼타임 시스템」에 이어지는 위치에 있습니다. Ada의 ‘타입으로 설계를 말한다’는 사고방식을, 제네릭이라는 관점에서 파고들어 봅니다.
2. 이 글의 지도
먼저 전체 그림을 다이어그램으로 파악합니다. Ada의 제네릭을 ‘타입을 인수로 받는 기능’으로만 이해하면 상당히 좁은 시각이 됩니다. 실제로는 재사용하고 싶은 단위에 따라 서브프로그램, 패키지, 서브프로그램 파라미터, 값 파라미터, 정식 패키지 파라미터를 조합합니다.
mindmap
root((Ada Generics))
Generic Subprogram
Swap
Count_If
Sort
Generic Package
Stack
Statistics
KV Store
Formal Parameters
Type
private
limited private
range box
mod box
digits box
delta box
discrete box
Object
Max_Size
Threshold
Subprogram
Less function
Equals function
Predicate
Package
with package P is new Generic
Design Ideas
Contract Model
Static Checking
Zero-Cost Abstraction
Separate Specification and Body
이 글을 읽는 방법은 단순합니다. 전반부에서는 문법을 따라가고, 후반부에서는 설계 판단을 다룹니다. Ada를 처음 접하는 분은 처음부터 세세한 문법을 외우려 하지 말고, ‘무엇을 가인수로 삼고 있는가’, ‘그 가인수에 어떤 연산을 허용하고 있는가’에 주목해 주십시오.
3. 실행 환경과 컴파일 방법
이 글의 코드는 GNAT 15.x 이상을 전제로 합니다. GNAT은 Ada를 대표하는 컴파일러로, Alire를 통해 설치할 수 있습니다. Alire는 Ada / SPARK의 패키지 매니저이며, 툴체인 관리나 빌드에도 사용할 수 있습니다.
gnat --version
# GNAT 15.2.1
GNAT은 Alire(Ada의 패키지 매니저)에서 alr install gnat_native gprbuild로 설치하고, PATH를 설정해 주십시오.
이 글에서 다루는 예제는 저장소 내에 다음과 같이 배치하는 것을 전제로 합니다.
flowchart TB
R[ada-generic-programming/] --> S[src/]
S --> N[snippets/]
N --> A[01_swap.ada]
N --> B[02_stack.ada]
N --> C[03_sort.ada]
N --> D[04_statistics.ada]
N --> E[05_filter.ada]
N --> F[06_kv_store.ada]
R --> README[README.md]
여러 컴파일 단위를 하나의 파일로 모은 예제는 gnatchop으로 분할한 다음 gnatmake합니다.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata는 어서션을 활성화하는 옵션입니다. 제네릭 자체를 사용하기 위해 필수는 아니지만, 학습용 예제에서는 계약이나 경계 조건을 확인하기 쉬워집니다.
sequenceDiagram
participant Dev as 개발자
participant Chop as gnatchop
participant Build as gnatmake
participant Exe as 실행 파일
Dev->>Chop: 하나의 .ada 파일을 전달
Chop-->>Dev: .ads / .adb / main으로 분할
Dev->>Build: gnatmake -gnata main
Build-->>Dev: 바인드와 링크까지 실행
Dev->>Exe: ./main
Exe-->>Dev: 실행 결과
4. Ada 제네릭의 기본 모델
Ada의 제네릭은 대략 다음 3단계로 생각하면 이해하기 쉽습니다.
- 총칭 단위를 작성한다
generic부분에 가인수를 작성한다- 사용하는 쪽에서
new로 인스턴스화한다
flowchart LR
G[generic 선언] --> F[가인수]
F --> B[총칭 본체]
B --> I[new에 의한 인스턴스화]
I --> U[일반 서브프로그램 또는 패키지로 사용]
F --> F1[타입]
F --> F2[값]
F --> F3[서브프로그램]
F --> F4[패키지]
예를 들어 두 값을 교환하는 처리를 제네릭으로 만들면, 타입만을 가인수로 삼을 수 있습니다.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
이 시점에서는 Generic_Swap을 아직 호출할 수 없습니다. 이것은 ‘임의의 Element 타입에 대해 사용할 수 있는 교환 처리 템플릿’입니다. 구체적인 타입을 지정해야 비로소 일반적인 프로시저가 됩니다.
procedure Swap_Integer is new Generic_Swap (Integer);
다이어그램으로 나타내면 다음과 같은 관계입니다.
flowchart TB
Template[Generic_Swap<br/>type Element is private] -->|Integer를 전달| SwapInt[Swap_Integer]
Template -->|Character를 전달| SwapChar[Swap_Character]
Template -->|My_Record를 전달| SwapRecord[Swap_My_Record]
SwapInt --> ICall[Integer 변수를 교환]
SwapChar --> CCall[Character 변수를 교환]
SwapRecord --> RCall[My_Record 변수를 교환]
중요한 것은 템플릿 본체가 Element에 대해 사용할 수 있는 연산만으로 작성되어 있다는 점입니다. type Element is private;로 선언한 경우, 대입이나 동등 비교 같은 기본적인 연산은 사용할 수 있지만, 대소 비교나 산술 연산은 사용할 수 없습니다. 즉, 제네릭 선언 자체가 ‘이 부품이 무엇을 전제할 수 있는가’를 나타냅니다.
5. 가인수의 종류 ── Ada 제네릭의 어휘
Ada의 제네릭이 받을 수 있는 것은 타입만이 아닙니다. 이 점이 C#이나 Java의 일반적인 제네릭과 비교했을 때 크게 다른 부분입니다.
flowchart TB
P[generic formal parameters] --> T[타입 파라미터]
P --> O[오브젝트 / 값 파라미터]
P --> S[서브프로그램 파라미터]
P --> PKG[패키지 파라미터]
T --> T1[type Element is private]
T --> T2[type Index is box]
T --> T3[type Real is digits box]
O --> O1[Max_Size : Positive]
O --> O2[Default_Value : Element]
S --> S1[with function Less...]
S --> S2[with procedure Put ...]
PKG --> P1[with package P is new ...]
대표적인 가인수를 표로 정리하면 다음과 같습니다.
| 종류 | 예 | 의미 |
|---|---|---|
| 타입 파라미터 | type Element is private; |
임의의 definite하고 non-limited한 타입을 받는 기본형 |
| limited 타입 파라미터 | type Element is limited private; |
복사할 수 없는 타입도 받는다 |
| 이산형(discrete type) | type Index is (<>); |
정수형이나 열거형 등, 배열 첨자로 쓸 수 있는 타입 |
| 부호 있는 정수형 | type Count is range <>; |
+, -, 대소 비교 등 정수 연산을 전제할 수 있다 |
| 모듈러 정수형 | type Word is mod <>; |
비트 연산이나 나머지 연산 성격의 정수를 다룬다 |
| 부동소수점형 | type Real is digits <>; |
Float, Long_Float, 사용자 정의 부동소수점형 등 |
| 고정소수점형 | type Money is delta <>; |
고정소수점 연산을 다룬다 |
| 값 파라미터 | Max_Size : Positive; |
크기나 임계값 등을 인스턴스마다 고정한다 |
| 서브프로그램 | with function Predicate (...) return Boolean; |
비교 함수나 술어처럼 동작을 주입한다 |
| 패키지 | with package P is new Some_Generic (<>); |
이미 생성된 제네릭 패키지를 부품으로 받는다 |
이러한 어휘를 가지고 있기 때문에, Ada에서는 ‘무엇이든 받지만 내부에서 위험한 일을 한다’가 아니라 ‘이 연산이 가능한 타입만 받는다’는 방식의 작성이 자연스럽게 가능합니다.
6. 총칭 서브프로그램 ── Generic_Swap으로 최소 구성 이해하기
첫 번째 예로, 임의의 타입인 두 변수를 교환하는 Generic_Swap을 살펴봅니다.
6.1 명세부
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
generic에 이어지는 부분이 가인수입니다. 여기서는 Element라는 타입을 받습니다. is private는 제네릭 본체 입장에서 그 타입의 내부 표현을 알지 못한다는 의미입니다.
이 선언으로 알 수 있는 것은 다음 두 가지입니다.
Generic_Swap은 임의의Element타입에 대해 사용할 수 있다- 본체는
Element의 내부 구조나 대소 비교에는 의존하지 않는다
6.2 본체
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
이 본체에서는 Element에 대해 대입만을 사용하고 있습니다. A < B도, A + B도 사용하지 않습니다. 따라서 Integer, Character, 레코드형, 열거형 등 대입 가능한 타입이라면 자연스럽게 사용할 수 있습니다.
flowchart LR
subgraph Before[호출 전]
A1[A = 10]
B1[B = 20]
end
A1 --> T[Temp = A]
B1 --> A2[A = B]
T --> B2[B = Temp]
subgraph After[호출 후]
A2[A = 20]
B2[B = 10]
end
6.3 인스턴스화
사용하는 쪽에서는 new를 사용합니다.
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
이제 Swap_Int와 Swap_Char는 일반적인 프로시저로 호출할 수 있습니다.
with Ada.Text_IO; use Ada.Text_IO;
procedure Swap_Demo is
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
procedure Generic_Swap (A, B : in out Element) is
Temp : constant Element := A;
begin
A := B;
B := Temp;
end Generic_Swap;
procedure Swap_Int is new Generic_Swap (Integer);
X : Integer := 10;
Y : Integer := 20;
begin
Put_Line ("Before: X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
Swap_Int (X, Y);
Put_Line ("After : X=" & Integer'Image (X) & ", Y=" & Integer'Image (Y));
end Swap_Demo;
실행 이미지는 다음과 같습니다.
Before: X= 10, Y= 20
After : X= 20, Y= 10
여기서 Swap_Int (X, Y);에 Float 변수를 넘길 수는 없습니다. Swap_Int는 Integer용으로 인스턴스화된 일반 프로시저이기 때문입니다. 제네릭을 ‘무엇이든 들어가는 구멍’이 아니라 ‘타입마다 안전한 구체물을 만드는 메커니즘’으로 파악하면 이해하기 쉬워집니다.
7. 총칭 패키지 ── 타입과 값을 파라미터로 삼기
서브프로그램 하나가 아니라 여러 연산과 내부 상태를 한데 묶어 재사용하고 싶을 때는 총칭 패키지를 사용합니다. 대표적인 예가 스택입니다.
스택은 요소 타입과 최대 크기만 바꾸면 기본 로직은 동일합니다.
flowchart TB
G[Generic_Stack] --> P1[Element_Type]
G --> P2[Max_Size]
G --> Ops[Push / Pop / Size / Is_Empty / Is_Full]
G --> I1[Int_Stack<br/>Element=Integer<br/>Max_Size=5]
G --> I2[Float_Stack<br/>Element=Float<br/>Max_Size=3]
G --> I3[String_Stack<br/>Element=Unbounded_String<br/>Max_Size=20]
7.1 명세부
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
여기서는 두 종류의 가인수를 사용하고 있습니다.
Element_Type은 타입 파라미터Max_Size는 값 파라미터
Max_Size는 Positive이므로 0 이하의 크기로는 인스턴스화할 수 없습니다. 이처럼 값 파라미터에도 타입에 의한 제약을 부여할 수 있습니다.
7.2 본체
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is
begin
return Top = 0;
end Is_Empty;
function Is_Full return Boolean is
begin
return Top = Max_Size;
end Is_Full;
function Size return Natural is
begin
return Top;
end Size;
end Generic_Stack;
이 패키지 본체에서 중요한 점은, Data와 Top이 인스턴스마다 별도로 만들어진다는 것입니다.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
이 둘은 같은 템플릿에서 만들어지지만 내부 상태는 공유하지 않습니다.
flowchart LR
Template[Generic_Stack] --> IntStack[Int_Stack]
Template --> FloatStack[Float_Stack]
subgraph I[Int_Stack의 상태]
ITop[Top]
IData[Data : Integer 배열]
end
subgraph F[Float_Stack의 상태]
FTop[Top]
FData[Data : Float 배열]
end
IntStack --> I
FloatStack --> F
7.3 스택의 상태 전이
스택은 상태 기계로 보면 이해하기 쉽습니다.
stateDiagram-v2
[*] --> Empty
Empty --> NonEmpty: Push
NonEmpty --> NonEmpty: Push / Pop
NonEmpty --> Empty: Pop으로 마지막 요소를 꺼냄
NonEmpty --> Full: Push로 Max_Size에 도달
Full --> NonEmpty: Pop
Full --> Overflow: Push
Empty --> Underflow: Pop
7.4 사용 예제
with Ada.Text_IO; use Ada.Text_IO;
procedure Stack_Demo is
generic
type Element_Type is private;
Max_Size : Positive;
package Generic_Stack is
procedure Push (Item : Element_Type);
function Pop return Element_Type;
function Is_Empty return Boolean;
function Is_Full return Boolean;
function Size return Natural;
Stack_Overflow : exception;
Stack_Underflow : exception;
end Generic_Stack;
package body Generic_Stack is
subtype Index_Type is Positive range 1 .. Max_Size;
type Storage_Type is array (Index_Type) of Element_Type;
Data : Storage_Type;
Top : Natural := 0;
procedure Push (Item : Element_Type) is
begin
if Top = Max_Size then
raise Stack_Overflow;
end if;
Top := Top + 1;
Data (Top) := Item;
end Push;
function Pop return Element_Type is
Result : Element_Type;
begin
if Top = 0 then
raise Stack_Underflow;
end if;
Result := Data (Top);
Top := Top - 1;
return Result;
end Pop;
function Is_Empty return Boolean is (Top = 0);
function Is_Full return Boolean is (Top = Max_Size);
function Size return Natural is (Top);
end Generic_Stack;
package Int_Stack is new Generic_Stack (Integer, 5);
begin
Int_Stack.Push (10);
Int_Stack.Push (20);
Int_Stack.Push (30);
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Pop =" & Integer'Image (Int_Stack.Pop));
Put_Line ("Size=" & Natural'Image (Int_Stack.Size));
end Stack_Demo;
총칭 패키지는 실무에서 ‘작은 컨테이너’, ‘고정 길이 버퍼’, ‘링 버퍼’, ‘로그용 큐’, ‘하드웨어 추상화 계층’ 등에서 자주 효과를 발휘합니다. 특히 Ada에서는 크기를 실행 시에 유동적으로 다루기보다, 타입이나 값 파라미터로서 정적으로 고정하는 설계가 고신뢰성 시스템과 잘 맞아떨어집니다.
8. 가인수 서브프로그램 파라미터 ── 동작을 주입하기
타입만 받아서는 아직 표현할 수 없는 것이 있습니다. 예를 들어 정렬에서는 요소 타입뿐 아니라 ‘어느 쪽을 먼저 배치할 것인가’라는 비교 로직이 필요합니다.
Ada에서는 이 비교 함수를 제네릭의 가인수로 만들 수 있습니다.
flowchart LR
A[Generic_Insertion_Sort] --> T[Item_Type]
A --> I[Index]
A --> ARR[Item_Array]
A --> CMP[비교 함수]
CMP --> ASC[표준 비교를 사용]
CMP --> DESC[Greater를 전달해 내림차순]
CMP --> CUSTOM[독자적인 순서를 전달]
8.1 명세부
generic
type Item_Type is private;
type Index is (<>);
type Item_Array is array (Index range <>) of Item_Type;
with function "<" (Left, Right : Item_Type) return Boolean is <>;
procedure Generic_Insertion_Sort (Items : in out Item_Array);
여기에는 4개의 가인수가 있습니다.
Item_Type: 배열 요소의 타입Index: 배열 첨자의 타입Item_Array: 실제 배열형"<": 비교 함수
type Index is (<>);는 이산형을 받습니다. 정수형뿐 아니라 열거형도 받을 수 있습니다. 배열 첨자로 Positive만 쓰는 것이 아니라 Day 같은 열거형도 쓸 수 있다는 점이 Ada답습니다.
with function "<" ... is <>;의 is <>는 실인수를 생략했을 경우, 가시적인 표준 연산자나 적합한 함수를 사용한다는 의미입니다. 즉, Integer처럼 이미 <가 존재하는 타입이라면 비교 함수를 명시하지 않아도 사용할 수 있습니다.
8.2 본체
procedure Generic_Insertion_Sort (Items : in out Item_Array) is
J : Index;
Key : Item_Type;
begin
if Items'Length <= 1 then
return;
end if;
for I in Index'Succ (Items'First) .. Items'Last loop
Key := Items (I);
J := I;
while J > Items'First and then Key < Items (Index'Pred (J)) loop
Items (J) := Items (Index'Pred (J));
J := Index'Pred (J);
end loop;
Items (J) := Key;
end loop;
end Generic_Insertion_Sort;
삽입 정렬은 큰 배열에는 적합하지 않지만, 제네릭을 설명하기에는 적합합니다. 비교 함수만 교체하면 같은 루프 구조를 오름차순에도 내림차순에도 사용할 수 있기 때문입니다.
flowchart TB
Start[정렬되지 않은 배열] --> Pick[왼쪽부터 순서대로 Key를 꺼낸다]
Pick --> Compare{Key가 바로 앞 요소보다 앞서는가?}
Compare -->|Yes| Shift[바로 앞 요소를 오른쪽으로 밀어낸다]
Shift --> Compare
Compare -->|No| Insert[Key를 삽입]
Insert --> Done{끝까지 처리했는가?}
Done -->|No| Pick
Done -->|Yes| End[정렬된 배열]
8.3 오름차순과 내림차순을 같은 본체에서 만들기
type Int_Array is array (Positive range <>) of Integer;
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
Sort_Asc는 표준 <를 사용합니다. 반면 Sort_Desc는 "<" => Greater로 비교 함수를 교체하고 있습니다.
flowchart LR
Data[99, 3, 47, 12] --> A[Sort_Asc<br/>표준 비교]
Data --> D[Sort_Desc<br/>Greater를 비교 함수로 전달]
A --> AO[3, 12, 47, 99]
D --> DO[99, 47, 12, 3]
이 구조는 C++에서 비교 함수 객체를 템플릿 인수로 넘기는 설계나, Rust에서 트레이트 바운드를 사용해 순서를 요구하는 설계와 유사합니다. 다만 Ada에서는 가인수 서브프로그램 파라미터로서 ‘이런 형태의 함수를 전달한다’고 명시합니다.
9. 타입 카테고리 ── private보다 구체적인 계약 작성하기
type T is private;는 편리하지만 무엇이든 할 수 있는 것은 아닙니다. private한 타입에 대해서는 사칙연산이나 대소 비교를 당연하게 사용할 수 없습니다. 그래서 Ada에서는 가인수 타입 파라미터에 카테고리를 지정할 수 있습니다.
flowchart TB
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: 이산형]
FormalType --> Signed[range box: 부호 있는 정수]
FormalType --> Modular[mod box: 모듈러 정수]
FormalType --> Float[digits box: 부동소수점]
FormalType --> Fixed[delta box: 고정소수점]
FormalType --> Access[access 타입]
Discrete --> Enum[열거형]
Discrete --> Int[정수형]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[사용자 정의 부동소수점형]
9.1 카테고리를 지정하면 무엇이 좋은가
예를 들어 평균이나 분산을 계산하려면 가산, 감산, 곱셈, 나눗셈이 필요합니다. private 타입으로는 이러한 연산을 전제할 수 없습니다. 그래서 부동소수점형으로 한정합니다.
generic
type Real is digits <>;
type Real_Array is array (Positive range <>) of Real;
package Generic_Statistics is
function Mean (Values : Real_Array) return Real;
function Variance (Values : Real_Array) return Real;
end Generic_Statistics;
type Real is digits <>;에 의해 Real이 부동소수점형임을 알 수 있습니다. 따라서 제네릭 본체에서는 +, -, *, / 등을 사용할 수 있습니다.
9.2 본체
package body Generic_Statistics is
function Mean (Values : Real_Array) return Real is
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
Sum := Sum + V;
end loop;
return Sum / Real (Values'Length);
end Mean;
function Variance (Values : Real_Array) return Real is
M : constant Real := Mean (Values);
Sum : Real := 0.0;
begin
if Values'Length = 0 then
return 0.0;
end if;
for V of Values loop
declare
D : constant Real := V - M;
begin
Sum := Sum + D * D;
end;
end loop;
return Sum / Real (Values'Length);
end Variance;
end Generic_Statistics;
9.3 Float와 Long_Float로 사용하기
type Float_Array is array (Positive range <>) of Float;
type Long_Array is array (Positive range <>) of Long_Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
package Long_Stats is new Generic_Statistics (Long_Float, Long_Array);
같은 통계 처리를 정밀도가 다른 부동소수점형에 대해 재사용할 수 있습니다.
flowchart LR
Stats[Generic_Statistics<br/>Real is digits box] --> FS[Float_Stats]
Stats --> LS[Long_Stats]
Stats --> MS[My_Real_Stats]
FS --> FCalc[Float로 Mean / Variance]
LS --> LCalc[Long_Float로 Mean / Variance]
MS --> MCalc[사용자 정의 Real로 Mean / Variance]
9.4 카테고리 지정은 ‘타입 수준의 명세서’
카테고리 지정은 단순히 컴파일러를 조용히 시키기 위한 문법이 아닙니다. 읽는 사람에게도 ‘이 부품이 무엇을 요구하는가’를 전달하는 명세서가 됩니다.
| 작성하려는 처리 | 적합한 가인수 타입 | 이유 |
|---|---|---|
| 교환, 저장, 취출 | private |
대입만 가능하면 된다 |
| 복사할 수 없는 리소스 관리 | limited private |
대입을 전제하지 않는다 |
| 배열 첨자, 열거 상태의 순회 | (<>) |
First, Last, Succ, Pred를 사용할 수 있다 |
| 정수의 합계, 카운터 | range <> |
정수 연산을 전제할 수 있다 |
| 비트마스크, 순환 카운터 | mod <> |
모듈러 연산을 전제할 수 있다 |
| 평균, 분산, 수치 계산 | digits <> |
부동소수점 연산을 전제할 수 있다 |
| 금액, 제어량 등의 고정 정밀도 | delta <> |
고정소수점 연산을 전제할 수 있다 |
10. 술어 주입 ── Count_If를 Ada답게 작성하기
가인수 서브프로그램 파라미터는 비교 함수뿐 아니라 술어(predicate)에도 사용할 수 있습니다. 술어란 값을 받아 Boolean을 반환하는 함수입니다.
C#의 Func<T, bool>, Java의 Predicate<T>, C++의 람다나 함수 객체와 비슷한 역할을, Ada에서는 제네릭의 가인수 서브프로그램으로 표현할 수 있습니다.
10.1 명세부
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
with function Predicate (Item : Element) return Boolean;
function Generic_Count_If (Arr : Array_Type) return Natural;
여기서는 Predicate에 is <>를 붙이지 않았습니다. 표준으로 가시적인 술어 함수가 있는 것이 아니므로, 사용하는 쪽에서 반드시 전달하도록 하는 설계입니다.
10.2 본체
function Generic_Count_If (Arr : Array_Type) return Natural is
Count : Natural := 0;
begin
for Item of Arr loop
if Predicate (Item) then
Count := Count + 1;
end if;
end loop;
return Count;
end Generic_Count_If;
처리 흐름은 단순합니다.
flowchart LR
Arr[배열] --> Loop[각 요소를 순회]
Loop --> P{"Predicate(Item)?"}
P -->|True| Inc[Count를 증가]
P -->|False| Skip[아무것도 하지 않음]
Inc --> Next[다음 요소로]
Skip --> Next
Next --> Result[Count를 반환]
10.3 짝수 개수와 임계값 개수
type Int_Array is array (Positive range <>) of Integer;
function Is_Even (N : Integer) return Boolean is
(N mod 2 = 0);
function Is_Large (N : Integer) return Boolean is
(N > 50);
function Count_Even is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Even);
function Count_Large is new Generic_Count_If
(Element => Integer,
Index => Positive,
Array_Type => Int_Array,
Predicate => Is_Large);
같은 순회 로직에서 조건만 다른 두 함수를 만들 수 있습니다.
flowchart TB
G[Generic_Count_If] --> E[Count_Even<br/>Predicate = Is_Even]
G --> L[Count_Large<br/>Predicate = Is_Large]
Data[12, 7, 88, 3, 56, 91, 44, 19, 62] --> E
Data --> L
E --> ER[짝수의 개수]
L --> LR[50보다 큰 개수]
이 예제에서는 배열 순회, 카운터 관리, 결과 반환이 모두 공통입니다. 반면 ‘무엇을 셀 것인가’만을 함수로서 주입하고 있습니다. 이것이 Ada에서 말하는 고차적 설계의 기본형입니다.
11. 여러 파라미터의 합성 ── 범용 키-값 저장소
실제 부품에서는 타입 파라미터 하나만으로 끝나는 경우가 많지 않습니다. 키와 값의 타입, 키의 비교 방법, 최대 건수 등 여러 조건을 조합해야 합니다.
여기서는 고정 길이의 간단한 키-값 저장소를 예로 듭니다.
flowchart TB
KV[Generic_KV_Store] --> K[Key_Type]
KV --> V[Value_Type]
KV --> EQ[키 일치 판정 함수]
KV --> M[Max_Entries]
KV --> Ops[Put / Get / Contains]
Ops --> Use1[설정값 저장소]
Ops --> Use2[소규모 캐시]
Ops --> Use3[임베디드용 고정 길이 딕셔너리]
11.1 명세부
generic
type Key_Type is private;
type Value_Type is private;
with function "=" (Left, Right : Key_Type) return Boolean is <>;
Max_Entries : Positive := 50;
package Generic_KV_Store is
procedure Put (Key : Key_Type; Val : Value_Type);
function Get (Key : Key_Type) return Value_Type;
function Contains (Key : Key_Type) return Boolean;
Key_Not_Found : exception;
Store_Full : exception;
end Generic_KV_Store;
이 패키지에는 4개의 가인수가 있습니다.
| 파라미터 | 종류 | 역할 |
|---|---|---|
Key_Type |
타입 | 키의 타입 |
Value_Type |
타입 | 값의 타입 |
"=" |
서브프로그램 | 키의 일치 판정 |
Max_Entries |
값 | 최대 엔트리 수 |
Max_Entries에는 := 50으로 기본값을 부여하고 있습니다. 따라서 특별히 지정하지 않으면 50건짜리 저장소가 됩니다.
11.2 본체
package body Generic_KV_Store is
subtype Index_Type is Positive range 1 .. Max_Entries;
type Key_Array is array (Index_Type) of Key_Type;
type Value_Array is array (Index_Type) of Value_Type;
type Used_Array is array (Index_Type) of Boolean;
Keys : Key_Array;
Values : Value_Array;
Used : Used_Array := (others => False);
function Find_Index (Key : Key_Type) return Natural is
begin
for I in Index_Type loop
if Used (I) and then Keys (I) = Key then
return I;
end if;
end loop;
return 0;
end Find_Index;
function Find_Free return Natural is
begin
for I in Index_Type loop
if not Used (I) then
return I;
end if;
end loop;
return 0;
end Find_Free;
procedure Put (Key : Key_Type; Val : Value_Type) is
Pos : Natural := Find_Index (Key);
begin
if Pos = 0 then
Pos := Find_Free;
if Pos = 0 then
raise Store_Full;
end if;
Used (Pos) := True;
Keys (Pos) := Key;
end if;
Values (Pos) := Val;
end Put;
function Get (Key : Key_Type) return Value_Type is
Pos : constant Natural := Find_Index (Key);
begin
if Pos = 0 then
raise Key_Not_Found;
end if;
return Values (Pos);
end Get;
function Contains (Key : Key_Type) return Boolean is
begin
return Find_Index (Key) /= 0;
end Contains;
end Generic_KV_Store;
이 구현은 선형 탐색이므로 대량 데이터에는 적합하지 않습니다. 하지만 고정 길이・소규모・동적 메모리 할당 없음이라는 특성이 중요한 상황에서는 사용하기 쉬운 형태입니다.
sequenceDiagram
participant App as 호출 측
participant Store as Generic_KV_Store 인스턴스
participant Data as Keys/Values/Used
App->>Store: Put(Key, Value)
Store->>Data: Find_Index(Key)
alt 기존 키 있음
Store->>Data: Values(Pos) := Value
else 신규 키
Store->>Data: Find_Free
Store->>Data: Keys(Pos) := Key
Store->>Data: Values(Pos) := Value
Store->>Data: Used(Pos) := True
end
App->>Store: Get(Key)
Store->>Data: Find_Index(Key)
Data-->>Store: Pos
Store-->>App: Values(Pos)
11.3 인스턴스화 예제
with Ada.Strings.Unbounded;
use Ada.Strings.Unbounded;
procedure KV_Demo is
package Int_String_Store is new Generic_KV_Store
(Key_Type => Integer,
Value_Type => Unbounded_String,
Max_Entries => 10);
begin
Int_String_Store.Put (1, To_Unbounded_String ("Ada"));
Int_String_Store.Put (2, To_Unbounded_String ("SPARK"));
if Int_String_Store.Contains (1) then
-- Get (1)로 값을 가져올 수 있다
null;
end if;
end KV_Demo;
"="은 생략했습니다. Integer에는 표준 동등 연산자가 있으며, is <>에 의해 그것이 사용되기 때문입니다.
만약 키가 대소문자를 구분하지 않는 문자열 등이라면, 독자적인 동등 함수를 전달할 수 있습니다.
function Same_Key (Left, Right : Unbounded_String) return Boolean is
(To_Lower (To_String (Left)) = To_Lower (To_String (Right)));
package String_Key_Store is new Generic_KV_Store
(Key_Type => Unbounded_String,
Value_Type => Integer,
"=" => Same_Key,
Max_Entries => 100);
12. 정식 패키지 파라미터 ── 제네릭을 한층 더 부품화하기
Ada의 제네릭에서는 패키지 자체를 가인수로 만들 수 있습니다. 이를 이용하면 ‘어떤 제네릭 패키지에서 만들어진 인스턴스’를 다른 제네릭의 입력으로 다룰 수 있습니다.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 스택을 받는 로거
예를 들어, 앞서 본 Generic_Stack의 인스턴스를 받아 그 크기를 출력하는 로거를 만든다고 합시다.
generic
with package Stack is new Generic_Stack (<>);
package Generic_Stack_Logger is
procedure Print_Size;
end Generic_Stack_Logger;
본체는 다음과 같습니다.
with Ada.Text_IO; use Ada.Text_IO;
package body Generic_Stack_Logger is
procedure Print_Size is
begin
Put_Line ("Stack size =" & Natural'Image (Stack.Size));
end Print_Size;
end Generic_Stack_Logger;
사용하는 쪽에서는 먼저 스택을 만들고, 그 스택을 로거에 전달합니다.
package Int_Stack is new Generic_Stack
(Element_Type => Integer,
Max_Size => 10);
package Int_Stack_Logger is new Generic_Stack_Logger
(Stack => Int_Stack);
이러한 설계를 통해 제네릭 부품들을 서로 조합할 수 있습니다.
flowchart TB
subgraph Layer1[1단계]
T1[Generic_Stack] --> I1[Int_Stack]
end
subgraph Layer2[2단계]
T2[Generic_Stack_Logger] --> I2[Int_Stack_Logger]
end
I1 --> T2
I2 --> API[Print_Size]
C++의 템플릿 템플릿 파라미터와 비슷한 용도이지만, Ada에서는 ‘이 제네릭 패키지의 인스턴스를 받는다’고 명시할 수 있습니다. 대규모 Ada 코드에서는 컨테이너, 알고리즘, 로그, 검사, 테스트 보조 등을 나누어 조합할 때 편리합니다.
13. Contract Model ── Ada 제네릭에서 가장 중요한 사고방식
Ada의 제네릭을 이해하는 데 있어 중요한 것이 contract model입니다.
제네릭 본체는 가인수가 약속한 연산만을 사용해 작성해야 합니다. 예를 들어 type Element is private;만 선언했는데 Element에 대해 <를 사용할 수는 없습니다. <를 사용하고 싶다면 가인수 서브프로그램으로 명시하거나, 타입 카테고리를 더 구체적으로 만들어야 합니다.
flowchart TB
Spec[generic formal part<br/>계약] --> Body[generic body<br/>계약 범위 내에서 구현]
Body --> Check1[본체 단독으로 타입 검사]
Spec --> Inst[인스턴스화]
Actual[actual parameters<br/>실제 타입・함수・값] --> Inst
Inst --> Check2[실인수가 계약을 만족하는지 검사]
Check2 --> Instance[일반 패키지/서브프로그램]
이러한 설계에 의해 제네릭의 이용자뿐 아니라 제네릭을 작성하는 쪽도 보호받습니다.
13.1 C++ 템플릿과 다르게 보이는 점
C++ 템플릿은 강력하지만, 역사적으로는 ‘템플릿 본체를 인스턴스화해야 비로소 오류가 드러난다’는 성질이 있었습니다. C++20의 concepts로 개선되었지만, Ada의 제네릭은 처음부터 계약을 명시하는 모델입니다.
flowchart LR
subgraph Ada[Ada]
A1[formal part에 계약을 작성] --> A2[body는 계약 내에서 검사]
A2 --> A3[instantiation에서 actual을 검사]
end
subgraph CPP[C++ templates]
C1[template body를 작성] --> C2[instantiation 시에 요구식이 구체화]
C2 --> C3[concepts로 제약을 명시 가능]
end
Java나 C#의 제네릭은 참조형과 제약, 타입 소거, 실행 시 표현과의 관계가 설계의 중심이 됩니다. 반면 Ada의 제네릭은 컴파일 시점에 구체적인 인스턴스를 만드는 사고방식에 가깝습니다.
| 관점 | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| 계약 작성 방식 | formal part에 타입・값・함수・패키지를 작성 | templates / concepts | 타입 파라미터와 bounds | trait bounds |
| 본체 검사 | 가인수의 계약 내에서 검사 | 인스턴스화 시의 구체화가 중심 | bounds 내에서 검사 | trait bounds 내에서 검사 |
| 실행 시 비용 | 정적 해결이 기본 | 정적 생성이 기본 | 타입 소거의 영향을 받음 | 단상화가 기본 |
| 값 파라미터 | 있음 | 있음 | 제한적 | const generics |
| 서브프로그램을 가인수로 삼기 | 있음 | 함수 객체 등으로 표현 | 람다/함수형 인터페이스 | 클로저/함수/trait |
| 패키지를 가인수로 삼기 | 있음 | 템플릿 템플릿 등 | 없음 | 모듈 구조와는 별개 |
세부적인 언어 기능은 각기 다르지만, Ada의 특징은 ‘계약을 문법으로서 먼저 작성한다’는 점입니다.
14. 실무에서의 설계 판단 ── 무엇을 제네릭으로 만들어야 하는가
제네릭은 편리하지만, 무엇이든 제네릭으로 만들면 되는 것은 아닙니다. 실무에서는 다음과 같이 판단하면 실패하기 어려워집니다.
flowchart TB
Start[재사용하고 싶은 처리가 있다] --> Q1{타입만 다른가?}
Q1 -->|Yes| GType[타입 파라미터를 검토]
Q1 -->|No| Q2{크기나 임계값도 다른가?}
Q2 -->|Yes| GObject[값 파라미터를 추가]
Q2 -->|No| Q3{비교나 판정의 동작이 다른가?}
Q3 -->|Yes| GSubp[가인수 서브프로그램을 추가]
Q3 -->|No| Q4{내부 상태나 API를 묶고 싶은가?}
Q4 -->|Yes| GPackage[총칭 패키지]
Q4 -->|No| Normal[일반 서브프로그램으로 충분]
14.1 총칭 서브프로그램이 적합한 상황
총칭 서브프로그램은 상태를 갖지 않는 알고리즘에 적합합니다.
SwapSortCount_IfFindMap성격의 변환Min/Max
알고리즘 본체가 짧고 입력과 출력이 명확한 경우에는, 패키지보다 서브프로그램 쪽이 읽기 쉬워집니다.
14.2 총칭 패키지가 적합한 상황
총칭 패키지는 타입과 함께 여러 연산이나 내부 상태를 가지고 싶은 경우에 적합합니다.
- 고정 길이 스택
- 링 버퍼
- 소규모 딕셔너리
- 통계 처리 세트
- 디바이스별 I/O 추상화
- 단위계가 있는 수치형의 연산 세트
특히 Ada에서는 패키지 명세부가 공개 API, 패키지 본체가 구현이라는 형태로 분리되어 있기 때문에, 제네릭 패키지는 ‘타입 안전한 모듈 템플릿’으로 사용할 수 있습니다.
flowchart LR
Spec[package spec<br/>공개 API] --> User[이용하는 쪽]
Body[package body<br/>내부 구현] -.은폐.-> User
Formal[generic formal part<br/>타입・값・함수의 계약] --> Spec
Formal --> Body
14.3 가인수는 적게 시작하기
가인수를 지나치게 늘리면 인스턴스화를 읽기 어려워집니다. 처음에는 최소한으로 시작하고, 교체하고 싶은 이유가 생기면 늘리는 것이 안전합니다.
-- 읽기 어려워지기 쉬운 예
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- 이름 붙은 연관으로 의도를 남긴다
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
Ada에서는 인스턴스화 시에 이름 붙은 연관을 사용할 수 있습니다. 제네릭은 설계상의 중요한 점이 인스턴스화에 나타나기 때문에, 실무 코드에서는 이름을 붙여 작성하는 쪽이 유지보수하기 쉬운 경우가 많습니다.
15. 자주 발생하는 실수
Ada의 제네릭은 강력하지만, 처음에 실수하기 쉬운 지점이 있습니다.
15.1 private 타입으로는 대소 비교를 할 수 없다
다음과 같은 본체는 작성할 수 없습니다.
generic
type Element is private;
function Bad_Min (A, B : Element) return Element;
function Bad_Min (A, B : Element) return Element is
begin
if A < B then -- 여기서 오류
return A;
else
return B;
end if;
end Bad_Min;
Element는 private로만 선언되어 있기 때문에 <를 사용할 수 있다고 단정할 수 없습니다. 비교하고 싶다면 다음과 같이 계약에 추가합니다.
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
flowchart LR
Need[본체에서 비교를 사용하고 싶다] --> Contract[formal part에 비교 함수를 작성]
Contract --> OK[인스턴스화 시에 비교 가능성을 확인]
Need --> NoContract[private뿐]
NoContract --> Error[제네릭 본체에서 컴파일 오류]
15.2 is <>는 ‘무엇이든 자동 추론’이 아니다
is <>는 편리하지만 마법은 아닙니다. 인스턴스화 지점에서 적합한 연산자나 서브프로그램이 가시적이어야 합니다. 독자적인 비교 함수를 다른 패키지에 둔 경우에는 적절하게 with나 use를 하거나, 이름을 붙여 명시적으로 전달하는 것이 안전합니다.
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 인스턴스마다 예외도 별개가 된다
제네릭 패키지의 명세부에 예외를 선언하면, 인스턴스마다 별개의 예외가 됩니다.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
이 경우 Int_Stack.Stack_Overflow와 Float_Stack.Stack_Overflow는 서로 다른 예외로 취급됩니다. 공통 예외로 다루고 싶다면 제네릭 바깥쪽에 예외를 정의하는 설계도 검토합니다.
flowchart TB
Generic[Generic_Stack<br/>Stack_Overflow 선언] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.다른 예외.-> F
15.4 코드 크기가 늘어날 수 있다
제네릭은 실행 시의 불필요한 간접 참조를 피하기 쉬운 한편, 타입마다 인스턴스를 만들기 때문에 인스턴스 수가 많으면 코드 크기가 늘어날 가능성이 있습니다.
이는 C++의 템플릿이나 Rust의 단상화에서도 볼 수 있는 트레이드오프입니다. 고신뢰성・임베디드・리얼타임 성향의 개발에서는 실행 시의 불확실성을 줄이는 대신 빌드 시 생성물 크기를 관리한다는 사고방식이 됩니다.
flowchart LR
Generic[하나의 generic 본체] --> I1[Integer 버전]
Generic --> I2[Float 버전]
Generic --> I3[Long_Float 버전]
Generic --> I4[My_Type 버전]
I1 --> Code[생성된 코드]
I2 --> Code
I3 --> Code
I4 --> Code
Code --> Pros[실행 시 타입 판정이나 박싱을 피하기 쉬움]
Code --> Cons[인스턴스가 많으면 크기 증가에 주의]
15.5 limited private를 사용해야 하는 상황
type Element is private;는 대입을 전제합니다. 파일 핸들, 잠금, 디바이스 핸들처럼 복사되어서는 안 되는 것을 다루는 경우에는 limited private를 검토합니다.
generic
type Resource is limited private;
with procedure Close (R : in out Resource);
procedure Generic_Use_And_Close (R : in out Resource);
복사할 수 없는 타입을 다루는 설계에서는, 값을 저장하는 컨테이너보다 절차를 적용하는 알고리즘이나 참조를 명시하는 설계 쪽이 더 안전합니다.
16. 작은 설계 패턴 모음
여기서부터는 실무에서 자주 사용하는 형태를 짧게 정리합니다.
16.1 비교 가능한 값에만 Min을 제공하기
generic
type Element is private;
with function "<" (Left, Right : Element) return Boolean is <>;
function Generic_Min (A, B : Element) return Element;
function Generic_Min (A, B : Element) return Element is
begin
if A < B then
return A;
else
return B;
end if;
end Generic_Min;
flowchart LR
T[Element] --> C[비교 함수가 필요]
C --> M[Generic_Min]
M --> R[더 작은 쪽을 반환]
16.2 임계값을 값 파라미터로 삼기
generic
type Count_Type is range <>;
Threshold : Count_Type;
function Generic_Is_Over (Value : Count_Type) return Boolean;
function Generic_Is_Over (Value : Count_Type) return Boolean is
begin
return Value > Threshold;
end Generic_Is_Over;
값 파라미터는 실행 시의 설정값이 아니라, 인스턴스의 성질로서 고정하고 싶은 값에 적합합니다.
16.3 출력 수단을 주입하기
generic
type Element is private;
with procedure Put (Item : Element);
procedure Generic_Print_Twice (Item : Element);
procedure Generic_Print_Twice (Item : Element) is
begin
Put (Item);
Put (Item);
end Generic_Print_Twice;
이런 형태로 만들면 표준 출력, 로그, 테스트용 버퍼 등 출력 대상을 교체할 수 있습니다.
flowchart TB
Print[Generic_Print_Twice] --> Put[Put을 가인수 서브프로그램으로 받는다]
Put --> Console[콘솔 출력]
Put --> Log[로그 출력]
Put --> Test[테스트용 버퍼]
16.4 배열의 첨자형을 고정하지 않기
Ada에서는 배열의 첨자형도 중요한 타입 정보입니다. Positive로 고정하지 않고, 필요에 따라 첨자형도 가인수로 삼으면 재사용성이 높아집니다.
generic
type Element is private;
type Index is (<>);
type Array_Type is array (Index range <>) of Element;
procedure Generic_Clear (Arr : in out Array_Type; Value : Element);
이런 설계라면 Positive 첨자 배열뿐 아니라 열거형 첨자 배열에도 대응할 수 있습니다.
flowchart LR
Index[Index is discrete] --> Positive[Positive range]
Index --> Day[Day 열거형]
Index --> State[State 열거형]
Index --> Slot[독자적인 정수형]
17. Ada다운 API로 만들기 위한 체크리스트
제네릭을 작성할 때는 마지막으로 다음 관점에서 다시 살펴보면 읽기 쉬워집니다.
flowchart TB
C[Generic 설계 체크] --> C1[가인수는 최소한인가]
C --> C2[필요한 연산을 formal part에 명시했는가]
C --> C3[private / range / digits 등의 카테고리는 적절한가]
C --> C4[이름 붙은 연관으로 읽기 쉽게 인스턴스화할 수 있는가]
C --> C5[인스턴스별 상태나 예외를 의식하고 있는가]
C --> C6[코드 크기 증가를 허용할 수 있는가]
C --> C7[테스트용 인스턴스를 준비했는가]
글로 정리하면 다음과 같습니다.
- 본체에서 사용하는 연산은 반드시 가인수의 계약으로 보이는 형태로 만든다.
private로 충분하다면private로 한다. 산술이 필요하면range <>나digits <>를 사용한다.- 비교, 해시, 출력, 변환 등 타입마다 달라지는 동작은 가인수 서브프로그램으로 만든다.
- 크기나 임계값이 인스턴스의 성질이라면 값 파라미터로 만든다.
- 상태를 가진다면 총칭 패키지, 상태를 갖지 않는다면 총칭 서브프로그램을 먼저 고려한다.
- 인스턴스화에서는 인수가 많을수록 이름 붙은 연관을 사용한다.
- 예외나 내부 상태는 인스턴스마다 독립한다는 것을 전제로 설계한다.
18. 예제 전체 구성 예시
글 속의 예제를 파일로 나눈다면, 다음과 같은 구성으로 만들면 읽기 쉬워집니다.
flowchart TB
Root[ada-generic-programming] --> Src[src]
Src --> G[generics]
Src --> D[demos]
G --> SwapSpec[generic_swap.ads]
G --> SwapBody[generic_swap.adb]
G --> StackSpec[generic_stack.ads]
G --> StackBody[generic_stack.adb]
G --> SortSpec[generic_insertion_sort.ads]
G --> SortBody[generic_insertion_sort.adb]
G --> StatsSpec[generic_statistics.ads]
G --> StatsBody[generic_statistics.adb]
G --> CountSpec[generic_count_if.ads]
G --> CountBody[generic_count_if.adb]
G --> KVSpec[generic_kv_store.ads]
G --> KVBody[generic_kv_store.adb]
D --> SwapDemo[swap_demo.adb]
D --> StackDemo[stack_demo.adb]
D --> SortDemo[sort_demo.adb]
D --> StatsDemo[statistics_demo.adb]
D --> CountDemo[count_if_demo.adb]
D --> KVDemo[kv_demo.adb]
작은 기사용 예제라면 하나의 파일로 모아 gnatchop하는 것도 편리하지만, 실무나 장기 유지보수를 고려한다면 명세부 .ads와 본체 .adb를 나누는 쪽이 Ada다운 구성이 됩니다.
19. 정리 ── 타입으로 재사용의 경계를 정하기
Ada의 제네릭 프로그래밍은 ‘타입에 의존하지 않는 코드를 작성하는 기능’만이 아닙니다. 오히려 본질은 재사용 가능한 부품이 무엇을 요구하는지를 타입과 서브프로그램과 값의 계약으로 명시하는 것에 있습니다.
flowchart LR
Contract[계약을 작성] --> Generic[제네릭 본체를 작성]
Generic --> Instance[타입・값・함수를 전달해 인스턴스화]
Instance --> Safe[타입 안전하게 사용]
Safe --> Reuse[복사 없이 재사용]
이 글에서 살펴본 것처럼, Ada의 제네릭에서는 다음과 같은 것들을 가인수로 만들 수 있습니다.
- 타입
- 값
- 서브프로그램
- 패키지
나아가 타입에 대해서는 private, limited private, range <>, mod <>, digits <>, delta <>, (<>) 등 상당히 세밀한 카테고리를 지정할 수 있습니다. 이를 통해 제네릭 본체는 ‘가능한지 알 수 없는 연산’에 의존하지 않고, 계약에 적힌 연산만으로 안전하게 구현할 수 있습니다.
C나 오래된 C++ 자산에서는 재사용을 위해 매크로, void*, 함수 포인터, 손으로 작성한 타입 분기가 사용되는 경우가 있습니다. Ada의 제네릭은 그러한 용도의 많은 부분을 타입 안전하고 읽기 쉬운 형태로 대체할 수 있습니다. 특히 장기 유지보수, 임베디드, 리얼타임, 고신뢰성 소프트웨어에서는 이러한 ‘컴파일 시점에 경계를 정하는 설계’가 큰 가치를 지닙니다.
20. 관련 상담 영역
합동회사 코무라소프트에서는 Windows 애플리케이션 개발, 기존 자산의 조사・개수, COM / ActiveX / 32bit / 64bit 경계 정리, 기술 상담・설계 리뷰 등을 다루고 있습니다. Ada와 같은 정적 타이핑・고신뢰성 지향 설계뿐 아니라, 기존의 C/C++, C#, VB6, MFC, COM 자산을 어떻게 정리해 연명・이행할 것인가도 실무상 가까운 주제입니다.
참고 자료
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Ada 언어의 매력 —— 타입으로 설계를 말하고, 수십 년간 동작하는 소프트웨어를 지탱하는 언어
Ada 언어의 매력을 소개합니다. 강한 타입, 범위 제약, 패키지를 통한 사양과 구현의 분리, 계약에 의한 설계, 언어에 내장된 태스크, SPARK를 통한 정형 검증, GNAT와 Alire를 이용한 개발 환경까지, 고신뢰 소프트웨어를 지탱하는 설...
Ada를 활용한 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어의 실전
Ada의 Annex D(실시간 시스템)가 제공하는 실시간 기능──태스크 우선순위, Ceiling_Locking 프로토콜, delay until에 의한 주기 실행, Ravenscar 프로파일, 타이밍 이벤트, 보호 큐, 태스크별 실행 시간 계측, ...
Ada의 안전한 동시성 처리 ── 태스크와 보호 객체 실전 가이드
Ada의 언어 내장 동시성 처리인 태스크와 보호 객체를 다루는 실전 입문 글입니다. 랑데부(entry/accept), 선택적 accept, 생산자·소비자, 보호 객체를 통한 배타 제어, 배리어가 있는 보호 엔트리, 타임아웃이 있는 호출, 태스크 ...
SPARK를 이용한 형식 검증 입문 —— Ada의 계약에서 수학적 증명으로
Ada의 서브셋 언어인 SPARK를 이용한 형식 검증의 실전 입문 글입니다. 계약(Pre/Post)에서 증명으로 단계를 밟아가는 방법, GNATprove 사용법, 루프 불변조건, 데이터 흐름 계약, 증명 레벨, 그리고 실제 프로젝트 적용 방법까지...
PerfView와 dotnet-trace로 “느림”의 원인을 찾는다 ── .NET 성능 조사 실무 입문
업무 앱이 “느리다”, “CPU를 다 써버린다”, “가끔 멈춘다”고 할 때, 어떤 도구로 무엇을 봐야 할까요. PerfView와 dotnet-trace의 역할 분담, CPU 샘플링을 읽는 방법(inclusive/exclusive), ThreadT...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Ada의 제네릭이란 무엇인가요?
- 타입, 값, 서브프로그램, 그리고 패키지 자체를 가인수(형식 파라미터)로 받아들여, new에 의한 인스턴스화 시점에 정적으로 타입 검사되는 재사용 메커니즘입니다. 단순한 텍스트 치환이 아니라, 컴파일 시점에 '이 부품이 이 계약을 만족하는가'를 확정합니다. 총칭 서브프로그램은 선언만으로는 호출할 수 없으며, 구체적인 타입을 지정해 인스턴스화해야 비로소 일반적인 프로시저나 함수로 사용할 수 있습니다.
- Ada의 제네릭은 C++의 템플릿과 무엇이 다른가요?
- Ada는 처음부터 계약을 명시하는 contract model을 채택하고 있어, 제네릭 본체는 가인수가 약속한 연산만을 사용해 작성되며 본체 단독으로 타입 검사가 이루어집니다. C++ 템플릿은 역사적으로 인스턴스화해야 비로소 오류가 드러나는 성질이 있었고, C++20의 concepts로 개선되었습니다. 또한 Ada에서는 값 파라미터, 서브프로그램 파라미터뿐 아니라 패키지 자체도 가인수로 받을 수 있어, 제네릭 부품들을 서로 조합할 수 있습니다.
- Ada의 가인수 타입 파라미터에서 타입 카테고리를 지정하는 이유는 무엇인가요?
- 본체에서 사용할 수 있는 연산을 계약으로 명시하기 위해서입니다. type T is private로는 대입이나 동등 비교 같은 기본 연산만 전제할 수 있고, 대소 비교나 산술 연산은 사용할 수 없습니다. 정수 연산이 필요하면 range <>, 부동소수점 연산이 필요하면 digits <>, 비트 연산이 필요하면 mod <>처럼 카테고리를 지정합니다. 카테고리 지정은 읽는 사람에게 '이 부품이 무엇을 요구하는가'를 전달하는 타입 수준의 명세서로도 기능합니다.
- Ada의 제네릭에서 주의해야 할 점이 있나요?
- 타입마다 인스턴스를 만들기 때문에 인스턴스 수가 많으면 코드 크기가 늘어날 수 있습니다. 이는 C++ 템플릿이나 Rust의 단상화(monomorphization)에서도 볼 수 있는 트레이드오프입니다. 또한 제네릭 패키지의 명세부에 선언한 예외는 인스턴스마다 별개의 예외가 됩니다. 가인수는 처음에는 최소한으로 하고, 교체하고 싶은 이유가 생기면 늘리는 것, 그리고 인수가 많은 인스턴스화에서는 이름 붙은 연관(named association)을 사용하는 것이 실무상의 지침입니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크