Ada의 generic programming ── 타입으로 계약을 쓰고, 재사용을 제로 비용으로 구현한다
· 업데이트: · Go Komura · Ada, ProgrammingLanguage, Generics, TypeSystem, StaticTyping, ContractModel, ZeroCostAbstraction, GNAT, Alire, 고신뢰성, 코드 재사용
수정 이력(7건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 관련 기사 링크를 한국어 permalink에 맞추는 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- permalink·저자 표기·지식 맵 래퍼·깨진 내부 링크 등 CI가 지적한 표시용 수정을 반영했습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 서두에 "이 글의 지식 맵" 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하면 됩니다.
- 대상 독자와 목적별 읽기 가이드를 서두에 추가하고, generic 단위 관련 용어 사전을 넣었습니다. 더불어 각 장의 코드 예에 실행 결과를 붙이고, 키와 값 데모에서 분기의 본문이 비어 있던 부분을 보완했습니다.
- 그림에서 Ada의 `<>`를 `box`로 표기하는 이유(Mermaid가 `<>`를 그리지 못하기 때문)를 각 그림 바로 뒤에 주석으로 달았습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.21635334)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「Ada의 generic programming ── 타입으로 계약을 쓰고, 재사용을 제로 비용으로 구현한다」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/ada-generic-programming/
- DOI(등록된 아카이브)
- 10.5281/zenodo.21635334
- DOI(마지막 등록 버전)
- 10.5281/zenodo.21635335
1. 들어가며 ── “무엇이든 받는다”가 아니라 “무엇을 약속하는가”
정적 타입 언어에서 재사용 가능한 코드를 쓰려고 하면, 곧바로 같은 고민에 부딪힙니다. 정수용으로 쓴 스택을 문자열에도 쓰고 싶다. 부동소수점 배열에도 같은 통계 처리를 쓰고 싶다. 오름차순 정렬 로직을 내림차순 정렬에도 쓰고 싶다. 그런데 타입마다 같은 코드를 복사해 가면 수정 누락이 생깁니다. 반대로 void*나 캐스트로 무엇이든 받는 설계로 가면 타입 안전성이 무너집니다.
Ada의 답은 generic(generic units) 입니다.
Ada의 generic은 단순한 텍스트 치환이 아닙니다. 타입, 값, subprogram, 나아가 package 자체를 formal parameter로 받아, instantiation 시점에 정적으로 타입 검사됩니다. 즉 실행 시점에 “이 타입으로 정말 괜찮은가”를 조사하는 것이 아니라, 컴파일 시점에 “이 부품이 이 계약을 충족하는가”를 확정하는 메커니즘입니다.
flowchart LR
A[재사용하고 싶은 처리] --> B{어떻게 재사용할 것인가}
B --> C[복사와 붙여넣기]
B --> D[void* / Object / 캐스트]
B --> E[Ada generic]
C --> C1[수정 누락이 일어나기 쉽다]
D --> D1[런타임 오류나 타입 붕괴가 일어나기 쉽다]
E --> E1[타입 안전]
E --> E2[컴파일 시점 검사]
E --> E3[런타임의 여분 dispatch 없음]
이 글에서는 Ada의 generic programming을 다음 흐름으로 정리합니다.
- generic subprogram
- generic package
- 타입 parameter, 값 parameter, subprogram parameter
private,range <>,digits <>등의 타입 카테고리- 정렬, 스택, 통계 처리,
Count_If, 키-값 저장소의 구현 예 - formal package parameter에 의한 고차 generic
- Ada의 contract model과 실무 설계의 생각
1.1 대상 독자와, 이 글에서 가져갈 수 있는 것
이 글은 다음과 같은 분을 대상으로 합니다.
- C++ template, C#이나 Java의 generics, Rust generics 중 하나를 써 본 경험이 있는 분
- Ada 문법에는 익숙하지 않지만, “타입으로 계약을 쓴다”는 설계 생각에 관심이 있는 분
- 장기 유지보수, 임베디드, 고신뢰성 분야에서 재사용 부품의 설계 방침을 검토하는 분
Ada 구문으로는 package(specification .ads와 body .adb의 분리)와 in / out / in out parameter mode를 알고 있으면 읽어 나갈 수 있습니다. 이 두 가지가 불안하면 먼저 “Ada 언어의 매력“을 보면 이해가 빨라집니다.
가져갈 것은 Ada 구문 그 자체보다, 재사용 가능한 부품이 무엇을 요구하는지를, 구현보다 먼저 타입으로 적어 낸다는 설계 습관입니다. 이 생각은 C#의 interface 제약이나 C++20 concepts를 어디까지 쓸지를 정할 때도 그대로 응용할 수 있습니다.
1.2 읽기 가이드 ── 전부를 읽을 필요는 없습니다
이 글은 20장입니다. 통독하지 않아도, 목적에 따라 다음과 같이 골라 읽을 수 있습니다.
| 목적 | 읽을 장 |
|---|---|
| 생각만 짧은 시간에 잡고 싶다 | 4장(기본 모델)→ 6장(최소 generic subprogram)→ 13장(contract model) |
| 직접 부품을 쓰고 싶다 | 4장 → 6장 → 7장(generic package)→ 8장(동작 주입)→ 9장(타입 카테고리) |
| 설계 판단의 지침만 필요하다 | 13장 → 14장(무엇을 generic으로 할 것인가)→ 15장(막히는 지점)→ 17장(체크리스트) |
| 손을 움직여 보고 싶다 | 3장에서 환경을 만들고, 6장·7장의 완성된 샘플부터 |
가장 짧은 경로는 4장·6장·13장의 3장입니다. 이 3장에서 formal parameter, instantiation, contract model이라는 핵심이 갖춰집니다. 5장·9장은 formal parameter 목록이므로, 필요할 때 사전처럼 찾아 읽어도 됩니다.
이 주제는 이 블로그 연재 “Ada 언어의 매력”, “SPARK 형식 검증 입문”, “안전한 병행 처리”, “실시간 시스템“에 이어지는 위치입니다. Ada의 “타입으로 설계를 말한다”는 생각을 generic이라는 각도에서 파고듭니다.
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 21건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
2. 이 글의 지도
먼저 전체 그림을 도식으로 잡습니다. Ada의 generic을 “타입을 인수로 받는 기능”으로만 이해하면 꽤 좁은 시야가 됩니다. 실제로는 재사용하고 싶은 단위에 따라 subprogram, package, subprogram parameter, 값 parameter, formal package parameter를 조합합니다.
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
그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.
이 글의 읽기 방법은 단순합니다. 전반에서는 구문을 따라가고, 후반에서는 설계 판단을 다룹니다. Ada를 처음 읽는 분은 처음부터 세세한 구문을 외우려 하지 말고, “무엇을 formal parameter로 두었는가”, “그 formal parameter에 어떤 연산을 허용하는가”에 주목하면 됩니다.
2.1 용어 미니 사전
이후 반복해서 나오는 말을 먼저 한영으로 맞춰 둡니다. Ada의 일본어 문헌에서는 generic을 “総称”으로 옮기는 경우가 많고, 이 글에서도 같은 개념을 가리키지만, 한국어 기술 문맥에 맞춰 “generic subprogram”, “generic package”로 씁니다. 의미는 “generic한” 것과 같습니다.
| 이 글의 표기 | 영어 | 의미 |
|---|---|---|
| generic 단위 | generic unit | generic으로 시작하는 선언. generic subprogram과 generic package를 묶은 이름 |
| formal parameter | generic formal parameter | generic과 선언 본문 사이에 쓰는, 받는 쪽의 인수. 타입·값·subprogram·package 중 하나 |
| formal part | generic formal part | formal parameter를 나열한 부분 그 자체. “계약을 쓰는 장소”로 읽어도 됩니다 |
| actual parameter | generic actual parameter | instantiation 때 실제로 넘기는 타입·값·subprogram·package |
| instantiation | instantiation | new로 generic 단위에서 일반 subprogram이나 package를 만드는 것 |
| box | <> |
Ada에서 <> 기호의 이름. range <>처럼 “구체적인 타입은 instantiation 때 정한다”는 뜻 |
| contract model | contract model | formal parameter에 적은 약속의 범위만으로 본문을 쓰고, 본문만으로 타입 검사하는 Ada 방식(13장) |
본문에서는 Ada 표기 그대로 range <>, digits <>로 쓰지만, 그림 안에서만 range box, digits box로 씁니다. Mermaid는 <>를 포함한 레이블을 그대로 그리지 못하기 때문이며, 의미는 본문과 같습니다. 위 표대로 Ada 자신이 <>를 box라고 부르므로, 그림 표기도 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]
여러 컴파일 단위를 1파일에 모은 샘플은 gnatchop으로 나눈 뒤 gnatmake합니다.
mkdir work
cd work
gnatchop ../src/snippets/01_swap.ada
gnatmake -gnata swap_demo
./swap_demo
-gnata는 assertion을 켜는 옵션입니다. generic 자체를 쓰기 위해 필수는 아니지만, 학습용 샘플에서는 계약이나 경계 조건을 확인하기 쉬워집니다.
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: bind와 link까지 실행
Dev->>Exe: ./main
Exe-->>Dev: 실행 결과
4. Ada generic의 기본 모델
Ada의 generic은 대략 다음 3가지로 생각하면 이해하기 쉽습니다.
- generic 단위를 쓴다
generic부분에 formal parameter를 쓴다- 쓰는 쪽에서
new로 instantiation한다
flowchart LR
G[generic 선언] --> F[formal parameter]
F --> B[generic 본문]
B --> I[new에 의한 instantiation]
I --> U[일반 subprogram 또는 package로 사용]
F --> F1[타입]
F --> F2[값]
F --> F3[subprogram]
F --> F4[package]
예를 들어 두 값을 맞바꾸는 처리를 generic으로 만들면, 타입만 formal parameter로 둘 수 있습니다.
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
이 시점에서는 Generic_Swap을 아직 호출할 수 없습니다. 이것은 “임의의 Element 타입에 대해 쓸 수 있는 맞바꾸기 처리의 template”입니다. 구체적인 타입을 주어야 비로소 일반 procedure가 됩니다.
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 변수를 맞바꾼다]
중요한 것은 template 본문이 Element에 대해 쓸 수 있는 연산만으로 쓰여 있다는 점입니다. type Element is private;로 선언한 경우, 대입이나 동등 비교 같은 기본 연산은 쓸 수 있지만 대소 비교나 산술 연산은 쓸 수 없습니다. 즉 generic 선언 자체가 “이 부품은 무엇을 전제로 해도 되는가”를 나타냅니다.
5. formal parameter의 종류 ── Ada generic의 어휘
Ada generic에서 받을 수 있는 것은 타입만이 아닙니다. 여기가 C#이나 Java의 일반적인 generics와 크게 다른 점입니다.
flowchart TB
P[generic formal parameters] --> T[타입 parameter]
P --> O[객체 / 값 parameter]
P --> S[subprogram parameter]
P --> PKG[package parameter]
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 ...]
그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.
대표적인 formal parameter를 표로 보면 다음과 같습니다.
| 종류 | 예 | 의미 |
|---|---|---|
| 타입 parameter | type Element is private; |
임의의 definite한 non-limited 타입을 받는 기본형 |
| limited 타입 parameter | type Element is limited private; |
복사할 수 없는 타입도 받는다 |
| 이산형 | type Index is (<>); |
정수형이나 열거형 등, 배열 첨자로 쓸 수 있는 타입 |
| 부호 있는 정수형 | type Count is range <>; |
+, -, 대소 비교 등 정수 연산을 전제할 수 있다 |
| modular 정수형 | type Word is mod <>; |
비트 연산이나 나머지 기반 정수를 다룬다 |
| 부동소수점형 | type Real is digits <>; |
Float, Long_Float, 사용자 정의 부동소수점형 등 |
| 고정소수점형 | type Money is delta <>; |
고정소수점 연산을 다룬다 |
| 값 parameter | Max_Size : Positive; |
크기나 임계값 등을 instance마다 고정한다 |
| subprogram | with function Predicate (...) return Boolean; |
비교 함수나 술어 등, 동작을 주입한다 |
| package | with package P is new Some_Generic (<>); |
이미 만든 generic package를 부품으로 받는다 |
이 어휘가 있기 때문에 Ada에서는 “무엇이든 받지만 안에서 위험한 일을 한다”가 아니라 “이 연산을 할 수 있는 타입만 받는다”는 쓰기가 자연스럽게 됩니다.
6. generic subprogram ── Generic_Swap으로 최소 구성을 이해한다
첫 예로, 임의의 타입의 두 변수를 맞바꾸는 Generic_Swap을 봅니다.
6.1 specification
generic
type Element is private;
procedure Generic_Swap (A, B : in out Element);
generic에 이어지는 부분이 formal parameter입니다. 여기서는 Element라는 타입을 받습니다. is private는 generic 본문에서 볼 때 그 타입의 내부 표현을 모른다는 뜻입니다.
이 선언에서 알 수 있는 것은 다음 두 가지입니다.
Generic_Swap은 임의의Element타입에 대해 쓸 수 있다- 본문에서는
Element의 내부 구조나 대소 비교에 의존하지 않는다
6.2 body
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, record 타입, 열거형 등 대입 가능한 타입이면 자연스럽게 쓸 수 있습니다.
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 instantiation
쓰는 쪽에서는 new를 씁니다.
procedure Swap_Int is new Generic_Swap (Integer);
procedure Swap_Char is new Generic_Swap (Character);
이제 Swap_Int와 Swap_Char는 일반 procedure로 호출할 수 있습니다.
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용으로 instantiation된 일반 procedure이기 때문입니다. generic은 “무엇이든 들어가는 구멍”이 아니라 “타입마다 안전한 구체물을 만드는 메커니즘”으로 보면 이해하기 쉽습니다.
7. generic package ── 타입과 값을 parameter로 둔다
subprogram 하나가 아니라, 여러 연산과 내부 상태를 묶어 재사용하고 싶을 때는 generic package를 씁니다. 대표 예는 스택입니다.
스택은 요소 타입과 최대 크기만 바꾸면, 기본 로직은 같습니다.
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 specification
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;
여기서는 두 종류의 formal parameter를 씁니다.
Element_Type은 타입 parameterMax_Size는 값 parameter
Max_Size는 Positive이므로, 0 이하의 크기에서는 instantiation할 수 없습니다. 이렇게 값 parameter에도 타입에 의한 제약을 둘 수 있습니다.
7.2 body
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;
이 package body에서 중요한 것은, Data와 Top이 instance마다 따로 만들어진다는 점입니다.
package Int_Stack is new Generic_Stack (Integer, 5);
package Float_Stack is new Generic_Stack (Float, 3);
이 둘은 같은 template에서 만들어지지만, 내부 상태는 공유하지 않습니다.
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;
3장과 같은 절차로 빌드해 실행합니다.
gnatchop ../src/snippets/02_stack.ada
gnatmake -gnata stack_demo
./stack_demo
Size= 3
Pop = 30
Pop = 20
Size= 1
= 오른쪽에 공백이 하나 들어가는 것은, 정수형의 'Image가 음이 아닌 값 앞에 공백을 한 글자 두는 사양 때문입니다. Push를 3번, Pop을 2번 했으므로 마지막 Size는 1이 됩니다. 스택이 가득 찬 상태(이 예에서는 Max_Size의 5개)에서 추가로 Push를 부르면 Int_Stack.Stack_Overflow가, 빈 상태에서 Pop을 부르면 Int_Stack.Stack_Underflow가 발생합니다.
generic package는 실무에서 “작은 컨테이너”, “고정 길이 버퍼”, “링 버퍼”, “로그용 큐”, “하드웨어 추상화 계층” 등에서 효과가 큽니다. 특히 Ada에서는 크기를 실행 시점에 바꾸는 것보다, 타입이나 값 parameter로 정적으로 고정하는 설계가 고신뢰 시스템과 잘 맞습니다.
8. formal subprogram parameter ── 동작을 주입한다
타입만 받아도 아직 표현하지 못하는 것이 있습니다. 예를 들어 정렬에서는 요소 타입뿐 아니라 “어느 쪽을 먼저 늘어놓을까”라는 비교 로직이 필요합니다.
Ada에서는 이 비교 함수를 generic의 formal parameter로 둘 수 있습니다.
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 specification
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개의 formal parameter가 있습니다.
Item_Type: 배열 요소의 타입Index: 배열 첨자의 타입Item_Array: 실제 배열 타입"<": 비교 함수
type Index is (<>);는 이산형을 받습니다. 정수형뿐 아니라 열거형도 받을 수 있습니다. 배열 첨자에 Positive만 쓰는 것이 아니라 Day 같은 열거형을 쓸 수 있는 점이 Ada다운 부분입니다.
with function "<" ... is <>;의 is <>는, actual parameter를 생략한 경우 보이는 표준 연산자나 적합한 함수를 쓴다는 뜻입니다. 즉 Integer처럼 이미 <가 있는 타입이면 비교 함수를 명시하지 않아도 쓸 수 있습니다.
8.2 body
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;
삽입 정렬은 큰 배열에는 맞지 않지만, generic 설명에는 맞습니다. 비교 함수만 갈아 끼우면 같은 루프 구조를 오름차순에도 내림차순에도 쓸 수 있기 때문입니다.
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++에서 비교 함수 객체를 template 인수로 넘기는 설계나, Rust에서 trait bound로 순서를 요구하는 설계에 가깝습니다. 다만 Ada에서는 formal subprogram parameter로 “이 형태의 함수를 넘긴다”고 명시합니다.
8.4 실행 예
18장 구성에 따라 Generic_Insertion_Sort를 별도 파일(generic_insertion_sort.ads / .adb)에 둔 경우의 호출 측은 다음과 같습니다.
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Insertion_Sort;
procedure Sort_Demo is
type Int_Array is array (Positive range <>) of Integer;
function Greater (Left, Right : Integer) return Boolean is
(Left > Right);
procedure Sort_Asc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array);
procedure Sort_Desc is new Generic_Insertion_Sort
(Item_Type => Integer,
Index => Positive,
Item_Array => Int_Array,
"<" => Greater);
procedure Show (Label : String; Items : Int_Array) is
begin
Put (Label);
for V of Items loop
Put (Integer'Image (V));
end loop;
New_Line;
end Show;
Asc : Int_Array := (99, 3, 47, 12);
Desc : Int_Array := (99, 3, 47, 12);
begin
Sort_Asc (Asc);
Sort_Desc (Desc);
Show ("Asc :", Asc);
Show ("Desc:", Desc);
end Sort_Demo;
gnatchop ../src/snippets/03_sort.ada
gnatmake -gnata sort_demo
./sort_demo
Asc : 3 12 47 99
Desc: 99 47 12 3
같은 Generic_Insertion_Sort 본문에서 만든 두 procedure가, 비교 함수만 갈아 끼워 역순이 된 것을 확인할 수 있습니다. 요소 앞의 공백은 정수형 'Image가 음이 아닌 값 앞에 공백을 한 글자 두기 때문입니다.
9. 타입 카테고리 ── private보다 구체적인 계약을 쓴다
type T is private;는 편리하지만, 무엇이든 할 수 있는 것은 아닙니다. private 타입에 대해서는 사칙연산이나 대소 비교를 당연하게 쓸 수 없습니다. 그래서 Ada에서는 formal 타입 parameter에 카테고리를 지정할 수 있습니다.
flowchart TB
FormalType[Formal Type] --> Private[private]
FormalType --> Limited[limited private]
FormalType --> Discrete[discrete box: 이산형]
FormalType --> Signed[range box: 부호 있는 정수]
FormalType --> Modular[mod box: modular 정수]
FormalType --> Float[digits box: 부동소수점]
FormalType --> Fixed[delta box: 고정소수점]
FormalType --> Access[access 타입]
Discrete --> Enum[열거형]
Discrete --> Int[정수형]
Float --> F1[Float]
Float --> F2[Long_Float]
Float --> F3[사용자 정의 부동소수점형]
그림 안의 box는 Ada의 <>(box compound delimiter)입니다. Mermaid가 <>를 그대로 그리지 못하기 때문에, 그림 안에서만 box로 표기합니다.
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이 부동소수점형임을 알 수 있습니다. 따라서 generic 본문에서는 +, -, *, / 등을 쓸 수 있습니다.
9.2 body
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 실행 예
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Float_Text_IO; use Ada.Float_Text_IO;
with Generic_Statistics;
procedure Statistics_Demo is
type Float_Array is array (Positive range <>) of Float;
package Float_Stats is new Generic_Statistics (Float, Float_Array);
Samples : constant Float_Array := (1.0, 2.0, 3.0, 4.0);
begin
Put ("Mean = ");
Put (Float_Stats.Mean (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
Put ("Variance = ");
Put (Float_Stats.Variance (Samples), Fore => 1, Aft => 3, Exp => 0);
New_Line;
end Statistics_Demo;
gnatchop ../src/snippets/04_statistics.ada
gnatmake -gnata statistics_demo
./statistics_demo
Mean = 2.500
Variance = 1.250
Ada.Float_Text_IO는 Ada.Text_IO.Float_IO를 Float에 대해 인스턴스화한 표준 라이브러리이며, Exp => 0을 지정하면 지수 형식이 아니라 보통의 소수 표기가 됩니다. Aft => 3이 소수부 자릿수입니다. 이 Variance는 표본분산이 아니라 모분산(Values'Length로 나누는 형태)이라는 점에 주의해야 합니다.
9.5 카테고리 지정은 “타입 수준의 명세서”
카테고리 지정은 컴파일러를 달래기 위한 구문만은 아닙니다. 읽는 사람에게도 “이 부품이 무엇을 요구하는가”를 전하는 명세서가 됩니다.
| 쓰고 싶은 처리 | 맞는 formal 타입 | 이유 |
|---|---|---|
| 맞바꾸기, 저장, 꺼내기 | private |
대입만 되면 된다 |
| 복사할 수 없는 리소스 관리 | limited private |
대입을 전제하지 않는다 |
| 배열 첨자, 열거 상태 순회 | (<>) |
First, Last, Succ, Pred를 쓸 수 있다 |
| 정수 합계, 카운터 | range <> |
정수 연산을 전제할 수 있다 |
| 비트 마스크, 순환 카운터 | mod <> |
modular 연산을 전제할 수 있다 |
| 평균, 분산, 수치 계산 | digits <> |
부동소수점 연산을 전제할 수 있다 |
| 금액, 제어량 등의 고정 정밀도 | delta <> |
고정소수점 연산을 전제할 수 있다 |
10. 술어 주입 ── Count_If를 Ada답게 쓴다
formal subprogram parameter는 비교 함수뿐 아니라 술어에도 쓸 수 있습니다. 술어란 값을 받아 Boolean을 반환하는 함수입니다.
C#의 Func<T, bool>, Java의 Predicate<T>, C++의 람다나 함수 객체에 가까운 역할을, Ada에서는 generic의 formal subprogram으로 표현할 수 있습니다.
10.1 specification
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 body
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에서 고차적인 설계의 기본형입니다.
10.4 실행 예
with Ada.Text_IO; use Ada.Text_IO;
with Generic_Count_If;
procedure Count_If_Demo is
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);
Data : constant Int_Array := (12, 7, 88, 3, 56, 91, 44, 19, 62);
begin
Put_Line ("Even =" & Natural'Image (Count_Even (Data)));
Put_Line ("Large =" & Natural'Image (Count_Large (Data)));
end Count_If_Demo;
gnatchop ../src/snippets/05_filter.ada
gnatmake -gnata count_if_demo
./count_if_demo
Even = 5
Large = 4
Data의 9개 요소 중 짝수는 12, 88, 56, 44, 62의 5개, 50보다 큰 것은 88, 56, 91, 62의 4개입니다.
11. 여러 parameter의 합성 ── 범용 키-값 저장소
현실의 부품에서는 타입 parameter 하나만으로 끝나는 경우가 많지 않습니다. 키와 값의 타입, 키 비교 방법, 최대 건수 등 여러 조건을 조합해야 합니다.
여기서는 고정 길이의 간단한 키-값 저장소를 예로 듭니다.
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 specification
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;
이 package에는 4개의 formal parameter가 있습니다.
| parameter | 종류 | 역할 |
|---|---|---|
Key_Type |
타입 | 키의 타입 |
Value_Type |
타입 | 값의 타입 |
"=" |
subprogram | 키의 일치 판정 |
Max_Entries |
값 | 최대 엔트리 수 |
Max_Entries에는 := 50으로 기본값을 줍니다. 따라서 특별히 지정하지 않으면 50건의 저장소가 됩니다.
11.2 body
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 instance
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 instantiation 예
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Generic_KV_Store;
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
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
else
Put_Line ("1 => (not found)");
end if;
-- 기존 키에 대한 Put은 덮어쓰기가 된다
Int_String_Store.Put (1, To_Unbounded_String ("Ada 2022"));
Put_Line ("1 => " & To_String (Int_String_Store.Get (1)));
if Int_String_Store.Contains (9) then
Put_Line ("9 => " & To_String (Int_String_Store.Get (9)));
else
Put_Line ("9 => (not found)");
end if;
end KV_Demo;
gnatchop ../src/snippets/06_kv_store.ada
gnatmake -gnata kv_demo
./kv_demo
1 => Ada
1 => Ada 2022
9 => (not found)
Contains로 존재를 확인하지 않고 Get을 부르면, 키가 없을 때 Key_Not_Found가 발생합니다. 위 예처럼 Contains로 분기하거나, 예외 핸들러를 쓰거나 둘 중 하나를 고르면 됩니다.
"="는 생략했습니다. 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. formal package parameter ── generic을 더 부품화한다
Ada의 generic에서는 package 자체를 formal parameter로 둘 수 있습니다. 이를 쓰면 “어떤 generic package에서 만든 instance”를 다른 generic의 입력으로 다룰 수 있습니다.
flowchart LR
GS[Generic_Stack] --> IS[Int_Stack]
IS --> Logger[Generic_Stack_Logger]
Logger --> Logged[Int_Stack_Logger_Instance]
12.1 스택을 받는 logger
예를 들어 앞의 Generic_Stack instance를 받아, 그 크기를 표시하는 logger를 만든다고 합시다.
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;
쓰는 쪽에서는 먼저 스택을 만들고, 그 스택을 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);
이런 설계로 generic 부품끼리 조합할 수 있습니다.
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++의 template template parameter에 가까운 용도이지만, Ada에서는 “이 generic package의 instance를 받는다”고 명시할 수 있습니다. 대규모 Ada 코드에서는 컨테이너, 알고리즘, 로그, 검사, 테스트 보조 등을 나눠 조합할 때 편리합니다.
13. Contract Model ── Ada generic에서 가장 중요한 생각
Ada generic을 이해하는 데 중요한 것이 contract model입니다.
generic 본문은 formal parameter가 약속한 연산만 사용해 써야 합니다. 예를 들어 type Element is private;만 선언했는데 Element에 대해 <를 쓸 수는 없습니다. <를 쓰고 싶다면 formal subprogram으로 명시하거나, 타입 카테고리를 더 구체적으로 해야 합니다.
flowchart TB
Spec[generic formal part<br/>계약] --> Body[generic body<br/>계약 범위 안에서 구현]
Body --> Check1[본문만으로 타입 검사]
Spec --> Inst[instantiation]
Actual[actual parameters<br/>실타입·실함수·실값] --> Inst
Inst --> Check2[actual parameter가 계약을 충족하는지 검사]
Check2 --> Instance[일반 package/subprogram]
이 설계로 generic의 이용자뿐 아니라, generic을 작성하는 쪽도 지켜집니다.
13.1 C++ template과의 차이
C++ template은 강력하지만, 역사적으로는 “template 본문을 instantiation해야만 오류가 나온다”는 성질이 있었습니다. C++20 concepts로 개선되었지만, Ada generic은 처음부터 계약을 명시하는 모델입니다.
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#의 generics는 참조형이나 제약, type erasure, 런타임 표현과의 관계가 설계의 중심이 됩니다. 한편 Ada generic은 컴파일 시점에 구체적인 instance를 만드는 생각에 가깝습니다.
| 관점 | Ada | C++ | Java | Rust |
|---|---|---|---|---|
| 계약의 쓰기 | formal part에 타입·값·함수·package를 쓴다 | templates / concepts | 타입 parameter와 bounds | trait bounds |
| 본문의 검사 | formal parameter의 계약 안에서 검사 | instantiation 때 구체화가 중심 | bounds 안에서 검사 | trait bounds 안에서 검사 |
| 런타임 비용 | 정적 해결이 기본 | 정적 생성이 기본 | type erasure의 영향을 받는다 | monomorphization이 기본 |
| 값 parameter | 있다 | 있다 | 제한적 | const generics |
| subprogram을 formal parameter로 | 있다 | 함수 객체 등으로 표현 | 람다/함수형 interface | 클로저/함수/trait |
| package를 formal parameter로 | 있다 | template template 등 | 없다 | 모듈 구조와는 별개 |
세세한 언어 기능은 각각 다르지만, Ada의 특징은 “계약을 구문으로 먼저 쓴다”는 점입니다.
14. 실무에서의 설계 판단 ── 무엇을 generic으로 해야 하는가
generic은 편리하지만, 무엇이든 generic으로 만들면 되는 것은 아닙니다. 실무에서는 다음과 같이 판단하면 실패하기 어렵습니다.
flowchart TB
Start[재사용하고 싶은 처리가 있다] --> Q1{타입만 다른가?}
Q1 -->|Yes| GType[타입 parameter를 검토]
Q1 -->|No| Q2{크기나 임계값도 다른가?}
Q2 -->|Yes| GObject[값 parameter를 추가]
Q2 -->|No| Q3{비교나 판정의 동작이 다른가?}
Q3 -->|Yes| GSubp[formal subprogram을 추가]
Q3 -->|No| Q4{내부 상태나 API를 묶고 싶은가?}
Q4 -->|Yes| GPackage[generic package]
Q4 -->|No| Normal[일반 subprogram으로 충분]
14.1 generic subprogram이 맞는 장면
generic subprogram은 상태를 갖지 않는 알고리즘에 맞습니다.
SwapSortCount_IfFindMap적인 변환Min/Max
알고리즘 본문이 짧고, 입력과 출력이 명확한 경우에는 package보다 subprogram이 읽기 쉽습니다.
14.2 generic package가 맞는 장면
generic package는 타입과 함께 여러 연산이나 내부 상태를 갖고 싶을 때 맞습니다.
- 고정 길이 스택
- 링 버퍼
- 소규모 사전
- 통계 처리 세트
- 디바이스별 I/O 추상화
- 단위계가 있는 수치형의 연산 세트
특히 Ada에서는 package specification이 공개 API, package body가 구현이라는 분리이므로, generic package는 “타입 안전한 모듈 template”로 쓸 수 있습니다.
flowchart LR
Spec[package spec<br/>공개 API] --> User[이용 측]
Body[package body<br/>내부 구현] -.은닉.-> User
Formal[generic formal part<br/>타입·값·함수의 계약] --> Spec
Formal --> Body
14.3 formal parameter는 적게 시작한다
formal parameter를 너무 늘리면 instantiation이 읽기 어려워집니다. 처음에는 최소한으로 두고, 갈아 끼울 이유가 생기면 늘리는 편이 안전합니다.
-- 읽기 어려워지기 쉬운 예
package X is new Generic_Foo
(A, B, C, D, E, F, G);
-- named association으로 의도를 남긴다
package X is new Generic_Foo
(Element_Type => Integer,
Index_Type => Positive,
Buffer_Size => 128,
"<" => Less);
Ada에서는 instantiation 때 named association을 쓸 수 있습니다. generic은 설계상 중요한 점이 instantiation에 드러나므로, 실무 코드에서는 이름 붙여 쓰는 편이 유지보수하기 쉬운 경우가 많습니다.
15. 자주 막히는 지점
Ada generic은 강력하지만, 처음에 막히기 쉬운 점이 있습니다.
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[instantiation 때 비교 가능성을 확인]
Need --> NoContract[private만]
NoContract --> Error[generic 본문에서 컴파일 오류]
15.2 is <>는 “무엇이든 자동 추론”이 아니다
is <>는 편리하지만 마법은 아닙니다. instantiation 지점에서 적합한 연산자나 subprogram이 보여야 합니다. 독자 비교 함수를 다른 package에 둔 경우에는 적절히 with나 use를 하거나, 이름을 붙여 명시적으로 넘기는 편이 안전합니다.
procedure Sort_By_Age is new Generic_Insertion_Sort
(Item_Type => Person,
Index => Positive,
Item_Array => Person_Array,
"<" => Younger_Than);
15.3 instance마다 예외도 별개가 된다
generic package의 specification에 예외를 선언하면, instance마다 별개의 예외가 됩니다.
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는 별개의 예외로 다뤄집니다. 공통 예외로 다루고 싶다면, generic 바깥에 예외를 정의하는 설계도 검토합니다.
flowchart TB
Generic[Generic_Stack<br/>Stack_Overflow 선언] --> I[Int_Stack.Stack_Overflow]
Generic --> F[Float_Stack.Stack_Overflow]
I -.별개의 예외.-> F
15.4 코드 크기가 늘어날 수 있다
generic은 런타임의 여분 간접 참조를 피하기 쉬운 한편, 타입마다 instance를 만들기 때문에 instance 수가 많으면 코드 크기가 늘어날 수 있습니다.
이는 C++ template이나 Rust의 monomorphization에서도 보이는 trade-off입니다. 고신뢰·임베디드·실시간 쪽 개발에서는 런타임의 불확실성을 줄이는 대신, 빌드 시점의 생성물 크기를 관리한다는 생각이 됩니다.
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[런타임의 타입 판정이나 boxing을 피하기 쉽다]
Code --> Cons[instance가 많으면 크기 증가에 주의]
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 임계값을 값 parameter로 둔다
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;
값 parameter는 런타임 설정값이 아니라, instance의 성질로 고정하고 싶은 값에 맞습니다.
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을 formal subprogram으로 받는다]
Put --> Console[콘솔 출력]
Put --> Log[로그 출력]
Put --> Test[테스트용 버퍼]
16.4 배열의 첨자 타입을 고정하지 않는다
Ada에서는 배열의 첨자 타입도 중요한 타입 정보입니다. Positive로 고정하지 않고, 필요하면 첨자 타입도 formal parameter로 두면 재사용성이 올라갑니다.
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로 만들기 위한 체크리스트
generic을 쓸 때는 마지막에 다음 관점으로 다시 보면 읽기 쉬워집니다.
flowchart TB
C[Generic 설계 체크] --> C1[formal parameter는 최소한인가]
C --> C2[필요한 연산을 formal part에 명시했는가]
C --> C3[private / range / digits 등의 카테고리는 적절한가]
C --> C4[named association으로 읽기 쉽게 instantiation할 수 있는가]
C --> C5[instance마다의 상태나 예외를 의식하고 있는가]
C --> C6[코드 크기 증가를 허용할 수 있는가]
C --> C7[테스트용 instance를 준비하고 있는가]
글로 정리하면 다음과 같습니다.
- 본문에서 쓰는 연산은 반드시 formal parameter의 계약으로 보이게 한다.
private로 충분하면private로 둔다. 산술이 필요하면range <>나digits <>를 쓴다.- 비교, 해시, 출력, 변환처럼 타입마다 바뀌는 동작은 formal subprogram으로 둔다.
- 크기나 임계값이 instance의 성질이면 값 parameter로 둔다.
- 상태가 있으면 generic package, 상태가 없으면 generic subprogram을 먼저 생각한다.
- instantiation에서는 인수가 많을수록 named association을 쓴다.
- 예외와 내부 상태는 instance마다 독립한다는 전제로 설계한다.
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]
작은 글용 샘플이라면 1파일에 모아 gnatchop하는 것도 편리하지만, 실무나 장기 유지보수를 생각하면 specification .ads와 body .adb를 나누는 편이 Ada다운 구성이 됩니다.
19. 정리 ── 타입으로 재사용의 경계를 정한다
Ada의 generic programming은 “타입에 의존하지 않는 코드를 쓰는 기능”만은 아닙니다. 본질은 오히려 재사용 가능한 부품이 무엇을 요구하는지를, 타입과 subprogram과 값의 계약으로 명시하는 것에 있습니다.
flowchart LR
Contract[계약을 쓴다] --> Generic[generic 본문을 쓴다]
Generic --> Instance[타입·값·함수를 넘겨 instantiation]
Instance --> Safe[타입 안전하게 쓴다]
Safe --> Reuse[복사 없이 재사용]
이 글에서 본 것처럼, Ada generic에서는 다음을 formal parameter로 둘 수 있습니다.
- 타입
- 값
- subprogram
- package
나아가 타입에 대해서는 private, limited private, range <>, mod <>, digits <>, delta <>, (<>) 등 꽤 세세한 카테고리를 지정할 수 있습니다. 이로써 generic 본문은 “할 수 있는지 모르는 연산”에 의존하지 않고, 계약에 적힌 연산만으로 안전하게 구현할 수 있습니다.
C나 오래된 C++ 자산에서는 재사용을 위해 매크로, void*, 함수 포인터, 손으로 쓴 타입 분기가 쓰이기도 합니다. Ada generic은 그런 용도의 상당수를 타입 안전하고 읽기 쉬운 형태로 바꿀 수 있습니다. 특히 장기 유지보수, 임베디드, 실시간, 고신뢰성 소프트웨어에서는 이런 “컴파일 시점에 경계를 정하는 설계”가 큰 가치를 갖습니다.
20. 관련된 상담 영역
合同会社小村ソフト에서는 Windows 앱 개발, 기존 자산의 조사·수정, COM / ActiveX / 32bit / 64bit의 경계 정리, 기술 상담·설계 리뷰 등을 다룹니다. Ada처럼 정적 타이핑·고신뢰성 쪽 설계뿐 아니라, 기존의 C/C++, C#, VB6, MFC, COM 자산을 어떻게 정리해 수명 연장·이전할지도 실무에서 가까운 주제가 됩니다.
참고 링크
- Ada 2022 Language Reference Manual, Section 12: Generic Units ── generic 단위의 규정 그 자체입니다. 12.1이 generic 선언, 12.3이 instantiation, 12.4가 formal object(값 parameter), 12.5가 formal 타입(
private,range <>,digits <>등의 카테고리), 12.6이 formal subprogram, 12.7이 formal package에 해당합니다. 이 글의 각 장에서 막히면 대응하는 절을 찾는 것이 확실합니다. - Ada 2022 Language Reference Manual, 2.2: Lexical Elements, Separators, and Delimiters ──
<>가 복합 구분 기호 “box”로 불린다고 정의된 부분입니다. 그림 속box표기의 근거에 해당합니다. - GNAT User’s Guide for Native Platforms ──
gnatmake,gnatchop사용법과-gnata를 비롯한 컴파일 옵션 목록입니다. 3장 절차에서 막히면 여기를 봅니다. - Alire Documentation ──
alr도입 방법, 툴체인(GNAT / gprbuild) 관리, crate 만드는 법이 실려 있습니다. 환경 구축만이면 이 자료로 충분합니다. - 샘플 코드 (GitHub) ── 이 글의 샘플을 18장 구성으로 파일에 나눈 것입니다.
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Ada 언어의 매력 ── 타입으로 설계를 말하고, 수십 년 동안 동작하는 소프트웨어를 지탱하는 언어
Ada 언어의 매력을 소개합니다. 강한 타입 시스템, 범위 제약, 패키지로 명세와 구현을 분리하는 방식, Design by Contract, 언어에 내장된 task, SPARK에 의한 형식 검증, GNAT과 Alire 개발 환경까지, 고신뢰 소프...
Ada로 하는 실시간 시스템 프로그래밍 ── 우선순위·주기·실행 시간 제어 실전
Ada의 Annex D(실시간 시스템)를 8가지 실전 코드 예로 배웁니다. 태스크 우선순위, Ceiling_Locking, delay until을 이용한 주기 실행, Ravenscar 프로파일, 태스크별 실행 시간 계측까지 단계적으로 정리합니다.
Ada에서의 안전한 동시성 처리 ── task와 protected object 실전 가이드
Ada의 언어 내장 동시성 처리인 task와 protected object 입문 기사입니다. rendezvous(entry/accept), 선택적 accept, protected object를 통한 상호 배제, 타임아웃이 있는 호출, task 우...
SPARK로 하는 형식 검증 입문 ── Ada의 계약에서 수학적 증명으로
Ada의 서브셋 언어 SPARK로 형식 검증을 다루는 실무 입문 기사입니다. 계약(Pre/Post)에서 증명으로 단계를 올리는 방법, GNATprove 사용법, 루프 불변조건, 데이터 플로 계약, 증명 레벨, 그리고 실제 프로젝트에 적용하는 방법...
디스크 사용률 100%는 무엇을 멈추면 해결될까 ── SysMain·Windows Search·Defender 구분법
Windows의 디스크 사용률이 100%가 되는 원인을 처리량·응답 시간·파일로 분리해 봅니다. SysMain의 일시 중지와 복귀, Windows Search 검색 범위의 재검토, Defender를 끄지 않는 조사 방법을 그림으로 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- Ada의 generic이란 무엇입니까?
- 타입, 값, subprogram, package 자체를 formal parameter로 받아, new로 instantiation하는 시점에 정적으로 타입 검사되는 재사용 메커니즘입니다. 단순한 텍스트 치환이 아니라, 컴파일 시점에 "이 부품이 이 계약을 충족하는가"를 확정합니다. generic subprogram은 선언만으로는 호출할 수 없고, 구체적인 타입을 주어 instantiation해야 비로소 일반 procedure나 function으로 쓸 수 있습니다.
- Ada의 generic은 C++ template과 무엇이 다릅니까?
- Ada는 처음부터 계약을 명시하는 contract model을 채택하며, generic 본문은 formal parameter가 약속한 연산만 사용해 작성되고, 본문만으로 타입 검사됩니다. C++ template은 역사적으로 instantiation해야만 오류가 드러나는 성질이 있었고, C++20 concepts로 개선되었습니다. 또한 Ada에서는 값 parameter, subprogram parameter에 더해 package 자체를 formal parameter로 둘 수 있어, generic 부품끼리 조합할 수 있습니다.
- Ada의 formal 타입 parameter에 타입 카테고리를 지정하는 이유는 무엇입니까?
- 본문에서 쓸 수 있는 연산을 계약으로 명시하기 위해서입니다. type T is private에서는 대입이나 동등 비교 같은 기본 연산만 전제할 수 있고, 대소 비교나 산술 연산은 쓸 수 없습니다. 정수 연산이 필요하면 range <>, 부동소수점 연산이 필요하면 digits <>, 비트 연산이면 mod <>처럼 카테고리를 지정합니다. 카테고리 지정은 읽는 사람에게 "이 부품이 무엇을 요구하는가"를 전하는 타입 수준의 명세서로도 기능합니다.
- Ada generic에서 주의할 점은 있습니까?
- 타입마다 instance를 만들기 때문에, instance 수가 많으면 코드 크기가 늘어날 수 있습니다. 이는 C++ template이나 Rust의 monomorphization에서도 보이는 trade-off입니다. 또한 generic package의 specification에 선언한 예외는 instance마다 별개의 예외가 됩니다. formal parameter는 처음에는 최소한으로 두고, 교체할 이유가 생기면 늘리며, 인수가 많은 instantiation에서는 named association을 쓰는 것이 실무 지침입니다.