Ada 프로그래밍 (초안)


최종 수정일:

김호동 <hodong@nimfsoft.art>

머리말

이 책은 기존 프로그래밍 경험이 있는 개발자를 대상으로 하는 Ada 프로그래밍 언어 학습서입니다. 모든 기술적인 내용은 Ada 2022 레퍼런스 매뉴얼을 기반으로 합니다.

Ada는 신뢰성, 유지보수성, 효율성에 중점을 두고 설계되어, 소프트웨어의 오류가 치명적인 결과로 이어질 수 있는 고신뢰성 시스템 개발에 사용되어 왔습니다. 이 책은 Ada의 기본 문법과 주요 특징을 공부하고, 그 외 C/C++ 연동 등의 실용적인 주제를 학습하는 것을 목표로 합니다.

이 책의 모든 예시 소스코드는 부록에 수록된 Clair 코딩 스타일 가이드를 따릅니다. 이 가이드는 타 언어 사용자들이 Ada에 쉽게 적응할 수 있도록, 다른 주요 프로그래밍 언어의 일반적인 코딩 스타일을 Ada의 특성에 맞게 조정한 것입니다.


크리에이티브 커먼즈 라이선스 이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 국제 라이선스에 따라 이용할 수 있습니다.


목차


1. Ada 언어 소개

Ada는 신뢰성(reliability), 유지보수성(maintainability), 효율성(efficiency)을 목표로 미국 국방부(U.S. Department of Defense)의 주도로 개발된 언어입니다. Ada는 대규모의 복잡한 소프트웨어를 장기간 안정적으로 운영하고 개선할 수 있도록 설계되었습니다. 그 결과, 항공우주, 국방, 철도, 의료 등 고도의 안전성이 요구되는 시스템 개발에 사용되어 왔습니다. 본 장에서는 Ada의 역사와 발전 과정, 언어의 기반이 되는 설계 철학, 그리고 주요 특징을 순서대로 설명합니다.

1.1 Ada의 역사1

Ada 프로그래밍 언어는 미국 국방부(Department of Defense, DoD)의 주도로 개발되었습니다. 1970년대 중반, 국방부에서는 450개가 넘는 서로 다른 프로그래밍 언어가 사용되면서 소프트웨어 프로젝트의 비용 초과 및 관리 문제가 발생하고 있었습니다. 이러한 문제를 해결하기 위해 국방부는 임베디드 컴퓨터 시스템에 적합한 단일 표준 프로그래밍 언어를 개발하는 프로젝트를 시작했습니다.

이 프로젝트는 고급 언어 실무 그룹(High Order Language Working Group, HOLWG)이라는 이름으로 알려졌으며, 언어에 대한 요구사항을 정의하는 일련의 문서를 작성했습니다. 이 요구사항은 신뢰성, 유지보수성, 효율성을 핵심 목표로 삼았습니다. 이에 따라 1977년부터 국제 공모가 시작되어 여러 설계안이 제출되었고, 최종적으로 프랑스 CII Honeywell Bull 팀의 Jean Ichbiah 박사가 이끄는 팀이 제출한 설계안이 채택되었습니다. 이 설계안을 바탕으로 한 새로운 언어는 ‘Ada’ 로 명명되었으며, 관련 예비 사양인 ‘Preliminary Ada Reference Manual’이 1979년 6월 ACM SIGPLAN 고지(Notices)를 통해 공개되었습니다. 이 이름은 세계 최초의 프로그래머로 알려진 에이다 러브레이스(Ada Lovelace)를 기리기 위한 것입니다. 공개 검토를 거쳐 완성된 Ada 언어의 첫 버전은 1980년에 미국 국방부 표준으로 채택되었으며, 표준 번호는 에이다 러브레이스의 출생 연도를 기념하여 MIL-STD-1815로 부여되었습니다. 이후 Ada는 지속적으로 발전하여 새로운 기능과 개선 사항을 포함하는 개정판이 발표되었습니다.

  • Ada 83: 언어의 첫 표준안은 1980년에 미국 국방부 표준(MIL-STD-1815)으로 발표되었으나, 여기에는 여러 오류와 불일치가 존재했습니다. 이를 수정한 개정판이 1983년에 ANSI/MIL-STD-1815A로 승인되었으며, 이것이 바로 첫 공식 표준인 ‘Ada 83’입니다. 이 표준은 이후 ISO/IEC 8652:1987로 국제 표준이 되었습니다. Ada 83은 강타입 검사, 예외 처리, 동시성 지원과 같은 기능을 통해 대규모의 안정적인 소프트웨어 시스템 개발을 지원했습니다.

  • Ada 95: 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 지원이 추가되고 동시성 기능이 향상되었으며, ISO/IEC 8652:1995 국제 표준으로 승인되었습니다. 이는 ISO 표준으로 채택된 첫 번째 객체 지향 언어였습니다.

  • Ada 2005: 인터페이스, 향상된 라이브러리 등의 기능이 추가되어 재사용성과 유연성이 강화되었고, ISO/IEC 8652:1995/Amd 1:2007 국제 표준으로 승인되었습니다.

  • Ada 2012: 계약 기반 프로그래밍(Contract-Based Programming)을 언어 구문에 공식적으로 통합하고 함수형 프로그래밍(Functional Programming) 스타일의 구문을 도입했으며, ISO/IEC 8652:2012 국제 표준으로 승인되었습니다.

  • Ada 2022: 병렬 실행(parallel execution)에 대한 지원이 향상되었고, 서브프로그램 인터페이스 명세와 계약 기반 프로그래밍 기능이 강화되었습니다. 또한, 델타 애그리게이트(delta aggregate) 및 선언 표현식(declare expression)과 같은 새로운 구문이 추가되었으며, ISO/IEC 8652:2023 국제 표준으로 승인되었습니다.

1.2 Ada의 설계 목표2

Ada는 세 가지 최우선 목표를 가지고 설계되었습니다: 프로그램의 신뢰성과 유지보수, 인간 활동으로서의 프로그래밍, 그리고 효율성입니다. 이러한 목표들은 언어의 기능에 반영되어 있습니다.

신뢰성 및 유지보수(reliability and maintenance)

신뢰성을 증진하고 유지보수를 단순화하기 위해, Ada는 코드 작성의 용이성보다 프로그램의 가독성에 중점을 두었습니다. 모든 변수는 타입을 명시적으로 선언해야 하며, 컴파일러는 이 타입 규칙을 엄격하게 검사하여 런타임에 발생할 수 있는 오류를 사전에 방지합니다. 또한, 분리 컴파일(separate compilation)을 지원하여 대규모 프로그램을 여러 개의 개별적인 단위로 개발하고 관리할 수 있게 함으로써, 프로그램의 개발과 유지보수를 용이하게 합니다.

인간 활동으로서의 프로그래밍(programming as a human activity)

Ada는 프로그래머라는 ‘사람’을 고려하여 설계되었습니다. 언어는 비교적 적은 수의 기본 개념들을 일관되고 체계적인 방식으로 통합하여, 과도한 복잡성을 피하고자 했습니다. 특히 언어의 여러 기능들은 프로그래머가 직관적으로 기대하는 방식에 부합하도록 설계되었습니다. 이는 독립적으로 개발된 소프트웨어 컴포넌트를 쉽게 조립하여 하나의 프로그램을 완성할 수 있도록 하는 패키지(package), private 타입, 제네릭(generic) 등의 개념으로 구체화되었습니다.

효율성(efficiency)

프로그래밍 언어의 비효율성은 시스템 성능을 저하시킬 수 있습니다. Ada 언어의 모든 구성 요소는 컴파일러 구현 기술의 현재 수준을 고려하여 설계되었습니다. 구현이 불명확하거나 과도한 기계 자원을 요구하는 구성 요소에 대한 제안은 배제되었으며 멀티코어 아키텍처를 안전하고 효율적으로 활용할 수 있도록 병렬 구문이 도입되었습니다.

1.3 Ada의 주요 특징

Ada는 신뢰성 및 유지보수, 인간 활동으로서의 프로그래밍, 효율성을 목표로 설계된 프로그래밍 언어입니다. 이러한 설계 철학은 언어의 여러 가지 특징에 반영되어 있습니다. 본 절에서는 Ada의 주요 특징들을 소개합니다.

신뢰성과 안전성 (reliability and safety)

소프트웨어 시스템에서 신뢰성(reliability)은 명시된 기간 동안 의도된 기능을 오류 없이 수행하는 확률을 나타내며, 안전성(safety)은 오작동이 치명적인 사고로 이어지지 않도록 하는 시스템의 속성을 의미합니다. Ada는 컴파일 시점에 수행되는 정적 검사(static check)와 실행 시점에 이루어지는 런타임 검사(runtime check)를 언어 차원에서 지원합니다.

정적 검사는 프로그램 실행 전 소스 코드를 분석하여 오류를 탐지 및 방지하는 과정입니다. Ada 컴파일러는 타입 불일치, 서브프로그램 호출 시의 매개변수 오류, 패키지의 비공개(private) 영역 접근 위반, 초기화되지 않은 변수의 사용 가능성 등 다양한 종류의 결함을 컴파일 단계에서 검출합니다.

이러한 정적 분석의 기반은 Ada의 강타입 시스템(strong typing)입니다. 이 시스템은 서로 다른 데이터 타입 간의 연산을 허용하지 않습니다. 예를 들어, Meters로 정의된 거리 타입과 Kilograms로 정의된 무게 타입 변수 간의 산술 연산은 컴파일 시점에 오류로 처리되어 데이터의 의미상 오류를 방지합니다. 이러한 정적 검증은 개발 초기 단계에서 논리적 오류를 식별하여 소프트웨어의 신뢰도를 높이는 데 기여합니다.

정적 분석만으로 탐지하기 어려운 동적 오류에 대응하기 위해, Ada는 프로그램 실행 중에 이루어지는 런타임 검사(runtime check)를 통해 예외적 상황에 대응합니다. 언어 표준에 명시된 런타임 검사는 배열 인덱스 범위 초과나 정수 오버플로와 같은 메모리 및 데이터 무결성 저해 동작, 초기화되지 않은 변수를 참조하는 프로그램 논리 오류, 그리고 종료된 태스크에 접근하려는 동시성 오류 등을 감지합니다. 런타임 검사가 실패하면, 시스템은 미정의 상태(undefined state)가 되는 대신 예외(exception)를 발생시킵니다. 개발자는 예외 처리 구문을 통해 이러한 상황에 대응할 수 있습니다.

또한, Ada는 선언적 명세를 통해 서브프로그램의 동작을 정의하는 계약 기반 프로그래밍(Programming by Contract)을 지원합니다. 개발자는 서브프로그램 실행 전에 만족해야 할 사전조건(precondition)과 실행 후에 보장해야 할 사후조건(postcondition)을 명시할 수 있습니다. 이러한 계약은 실행 중에 검증되어 프로그램의 논리적 정확성을 확인하는 데 사용됩니다.

Ada의 런타임 검사보다 더 높은 수준의 수학적 정확성이 요구될 경우 정형 검증(formal verification)을 사용할 수 있습니다. 그 예시인 SPARK는 Ada의 검증 가능한 부분집합(verifiable subset)으로서, 코드에 명시된 계약이 모든 실행 경로에서 만족됨을 정적 분석을 통해 증명합니다.

동시성 (concurrency)

Ada 프로그램의 실행은 하나 이상의 태스크(task) 실행으로 구성됩니다. 각 태스크는 독립적으로 동시에 실행되는 분리 가능한 활동(separable activity)을 나타냅니다. 태스크는 엔트리 호출(entry call)을 통해 다른 태스크와 동기적으로 통신할 수 있습니다.

여러 태스크가 공유 데이터에 접근할 때 발생하는 데이터 경쟁(data race)을 방지하기 위해, Ada는 보호된 객체(protected object)를 제공합니다. 보호된 객체는 보호된 연산(protected operation)을 통해서만 데이터 접근을 허용합니다. 보호된 연산에는 배타적인 읽기-쓰기를 위한 보호된 프로시저(protected procedure), 동시적인 읽기 전용 접근을 위한 보호된 함수(protected function), 그리고 특정 조건이 만족될 때까지 태스크를 대기시키는 보호된 엔트리(protected entry)가 있습니다.

이러한 태스크 기반 모델 외에도, Ada는 멀티코어 아키텍처를 활용하기 위한 병렬 구문(parallel construct)을 지원합니다. 예를 들어, 병렬 for 루프(parallel for loop)는 반복 가능한 대상의 각 항목에 대한 동일한 작업을 여러 코어에 분배하는 기능을 제공합니다. 이러한 기능은 Ada의 적법성 규칙(legality rules)에 따라 컴파일 시점에 데이터 경쟁과 같은 오류를 방지하며, 다중 코어를 사용한 병렬 실행을 가능하게 합니다.

모듈성과 데이터 추상화

Ada는 패키지(package)를 통해 소프트웨어를 여러 논리적 단위로 구성하고 관리하는 기능을 지원합니다. 패키지는 관련된 타입, 변수, 서브프로그램 등을 하나의 그룹으로 묶는 역할을 합니다. 각 패키지는 인터페이스를 정의하는 명세(specification)와 실제 구현을 포함하는 본체(body)로 분리됩니다. 패키지 명세는 외부에서 사용 가능한 가시부(visible part)private 예약어 뒤에 오는 전용부(private part)를 포함할 수 있습니다. 이를 통해 데이터의 내부 표현을 숨기고 외부에는 필요한 연산만 노출하는 데이터 추상화가 가능합니다.

객체 지향 프로그래밍 (Object-Oriented Programming)

Ada는 패키지를 통한 데이터 추상화 기능을 기반으로, Ada 95부터 객체 지향 프로그래밍을 지원합니다. 객체 지향 프로그래밍의 주요 원칙은 캡슐화, 상속, 다형성입니다.

캡슐화란 데이터와 그 데이터에 작용하는 메서드를 함께 묶고 객체의 일부 구성 요소에 대한 직접적인 접근을 제한하기 위한 언어적 메커니즘입니다.3 Ada에서는 패키지를 사용하여 데이터와 관련 서브프로그램을 하나의 단위로 묶고, private 타입 또는 private 확장을 통해 내부 구현을 외부로부터 분리할 수 있습니다.

상속(inheritance)은 하나의 객체나 클래스가 다른 객체나 클래스를 기반으로 하여, 유사한 구현을 유지하는 메커니즘입니다.4 Ada에서는 태그된 타입(tagged type)을 사용하여 기존 타입의 데이터와 기능을 상속받는 새로운 타입을 파생시키고, 타입 간의 계층 구조를 표현할 수 있습니다.

다형성(polymorphism)은 서로 다른 데이터 타입의 개체(entities)들에게 하나의 공통 인터페이스를 제공하는 것입니다.5 Ada에서는 클래스-범위(class-wide) 타입과 태그(tag)를 사용하여, 해당 타입에 맞는 실제 서브프로그램을 동적으로 결정할 수 있습니다.

함수형 프로그래밍 지원 (support for functional programming)

Ada는 순수 함수형 언어는 아니지만, Ada 2012와 2022 표준을 통해 함수형 프로그래밍 스타일의 기능들이 도입 및 확장되었습니다.

Ada는 로직을 문장(statement)이 아닌 값을 반환하는 표현식(expression) 중심으로 구성하는 것을 지원하며, 여기에는 단일 표현식으로 함수를 정의하는 표현식 함수(expression function), 조건에 따라 값을 결정하는 조건 표현식(conditional expression) 및 case 표현식, 표현식 내에서 임시 상수를 선언하는 선언식 표현(declare expression), 그리고 컨테이너의 모든 원소를 하나의 값으로 집계하는 감축 표현식(reduction expression)이 포함됩니다.

이러한 표현식 중심의 접근은 데이터 처리에도 적용됩니다. 수량자 표현식(quantified expression)은 루프 없이 컨테이너 원소의 속성을 검사하며, 일반화된 반복자(generalized iterator)이터레이터 필터(iterator filter)는 데이터의 순회 및 필터링을 지원합니다. 또한 델타 애그리게이트(delta aggregate)는 기존 객체를 수정하지 않고 일부 값만 변경된 새로운 객체를 생성하여 불변성(immutability)을 지원합니다. 이러한 표현식 중심의 접근은 프로그램 실행 중 발생하는 상태 변화와 부작용(side effect)을 줄이는 효과가 있습니다.

효율성 (efficiency)

Ada 언어의 설계 목표 중 효율성은 컴파일러 구현언어의 실행 효율이라는 두 가지 관점에 반영되어 있습니다.

Ada의 문법은 모호함이 적어 컴파일러의 정적 분석을 용이하게 합니다. 예를 들어, 모든 선언은 사용되기 전에 명시적으로 이루어져야 하며, 패키지의 명세부(specification)구현부(body)가 명확히 분리됩니다. 이는 컴파일러가 복잡한 추론 없이 코드를 해석하고 모듈 단위로 컴파일할 수 있게 하여, 컴파일러 구현의 복잡성을 낮추고 자원이 제한된 환경에서도 Ada를 사용할 수 있는 기반이 됩니다.

또한, Ada 언어의 구성 요소들은 저장 공간(storage)과 실행 시간(execution time)을 효율적으로 사용하도록 설계되었습니다. 예를 들어, 제네릭(generic)이나 인라인(inlined) 서브프로그램과 같은 추상화 메커니즘은 대부분 컴파일 시점에 처리되어, 저수준 코드로 작성된 것과 유사한 수준의 실행 효율을 보입니다. 더 나아가, 프로그래머는 표현 절(representation clause)을 통해 데이터 구조의 메모리 배치를 비트 단위까지 직접 제어하거나, 프라그마(pragma)를 통해 컴파일러의 최적화 방식을 지정하는 등 정밀한 자원 제어가 가능합니다.

가독성과 명확성 (readability and clarity)

Ada의 구문은 프로그램의 논리적 구조를 명시적으로 나타내도록 설계되었습니다. 코드의 구조적 명확성은 소프트웨어 유지보수성에 영향을 미치는 요소입니다.

Ada는 블록과 제어 구문의 범위를 명시적인 키워드로 한정합니다. 예를 들어, 서브프로그램이나 패키지는 beginend로, 조건문은 ifend if;로, 반복문은 loop와 **end loop;로 각각의 범위를 지정합니다.

각 제어 구조가 고유한 종결 예약어를 사용하므로, 코드 블록의 범위가 구문적으로 결정됩니다. 이는 프로그램의 제어 흐름과 중첩 구조에 대한 잠재적 모호성을 제거합니다.

2. 개발 환경 구성

프로그래밍을 시작하기 위해 우선적으로 필요한 것은 코드를 컴퓨터가 이해할 수 있는 언어로 번역해 줄 컴파일러를 설치하는 것입니다. 본 장에서는 FreeBSD 환경에 초점을 맞추어, Ada 언어의 표준 컴파일러인 GNAT(GNU Ada Translator)를 설치하는 방법을 설명합니다.

설치가 완료되면, 간단한 “Hello, World!” 프로그램을 직접 작성하고 명령줄(command line)에서 컴파일하여 실행하는 과정을 통해 개발 환경 구성이 올바르게 완료되었는지 검증할 것입니다.

2.1 GNAT 툴체인

GNU NYU Ada Translator (GNAT)는 널리 사용되는 Ada용 컴파일러 및 툴체인입니다. 무료이며 오픈 소스이고, GNU 컴파일러 모음(GCC)의 일부입니다. GNAT 프로젝트는 1992년 미국 공군이 뉴욕 대학교(NYU)에 계약을 의뢰하여 시작되었습니다. 목표는 다가오는 Ada 9X 표준화 과정(Ada 95가 됨)을 지원하기 위해 고품질의 무료 Ada 컴파일러를 만드는 것이었습니다. 결과물인 컴파일러의 저작권은 자유 소프트웨어 재단(Free Software Foundation)에 양도되어 지속적인 가용성을 보장했습니다.

GNAT의 원저자들은 나중에 AdaCore를 설립하여 GNAT 기술에 대한 상용 등급의 지원, 도구 및 지속적인 개발을 제공했습니다. AdaCore는 GNAT의 무료 커뮤니티 에디션과 상업적으로 지원되는 버전인 GNAT Pro를 모두 제공합니다.

GNAT 툴체인의 주요 구성 요소는 다음과 같습니다:

  • GNAT Compiler (gcc): Ada 소스 코드(.ads, .adb 파일)를 목적 파일(.o 파일)로 변환하는 컴파일러입니다.

  • GNAT Binder (gnatbind): GNAT 바인더는 컴파일러가 생성한 Ada 라이브러리 정보(.ali) 파일을 처리하여, 각기 독립적으로 컴파일된 프로그램 구성 요소 간의 일관성을 확인하고 상호 의존성을 해결하는 도구입니다. 이 과정을 통해 서브프로그램이나 패키지와 같이 개별적으로 컴파일된 코드들을 하나의 실행 가능한 프로그램으로 연결합니다.

  • GNAT Linker (gnatlink): 목적 파일들을 Ada 런타임 라이브러리와 연결하여 실행 파일을 만드는 최종 도구입니다.

  • GNAT Make (gnatmake): 컴파일-바인드-링크 과정을 자동화하는 유틸리티입니다. with 절을 통해 Ada 소스 파일의 의존성을 분석하고 필요한 파일만 올바른 순서로 자동으로 컴파일합니다.

  • GNAT Project Manager (gprbuild): 프로젝트 파일(.gpr)을 사용하여 여러 소스 디렉토리, 다른 언어, 다양한 컴파일러 옵션을 가진 복잡한 프로젝트를 관리하는 더 진보된 빌드 도구입니다.

GNAT 커뮤니티 에디션의 설치 지침은 AdaCore 웹사이트에서 찾을 수 있습니다. 리눅스 시스템에서는 배포판의 패키지 관리자를 통해 GNAT을 직접 설치할 수 있습니다(예: 데비안 기반 시스템에서는 sudo apt install gnat, 페도라 기반 시스템에서는 sudo dnf install gcc-gnat).

2.2 통합 개발 환경(IDE)

Ada 코드는 어떤 텍스트 편집기에서든 작성할 수 있지만, 언어 인식 기능이 있는 IDE를 사용하면 생산성이 크게 향상됩니다. 주요 옵션으로는 GNAT Studio와 Visual Studio Code가 있습니다.

GNAT Studio

GNAT Studio는 AdaCore에서 개발한 Ada와 SPARK 전용의 경량 IDE입니다. Ada 코드의 편집, 빌드, 디버깅 및 형식적 검증을 위한 긴밀하게 통합된 환경을 제공합니다. 주요 기능은 다음과 같습니다:

  • 프로젝트 관리: GNAT 프로젝트 파일(.gpr)을 생성하고 관리하기 위한 마법사.

  • 소스 코드 편집기: 구문 강조, 코드 완성 및 소스 탐색 기능 제공.

  • 빌드 통합: 프로젝트를 빌드, 정리, 실행하기 위한 원클릭 동작.

  • Ada 인식 디버거: 태스크 상태, 보호 객체, 복잡한 레코드 형식과 같은 Ada 고유의 구조를 이해하는 GDB용 그래픽 프론트엔드.

GNAT Studio에서의 일반적인 작업 흐름은 새 프로젝트를 만들고, 소스 파일을 추가하고, ‘Build -> Project -> Build All’ 메뉴를 사용하여 컴파일한 다음, ‘Debug -> Run’ 메뉴를 사용하여 애플리케이션을 실행하거나 디버깅하는 것을 포함합니다.

Visual Studio Code와 AdaCore 확장 프로그램

Visual Studio Code(VS Code)는 AdaCore의 공식 확장 프로그램을 사용하여 Ada 개발을 위해 구성할 수 있는 인기 있는 범용 코드 편집기입니다. AdaCore.ada 확장 프로그램은 Ada 언어 서버를 통합하여 풍부한 언어 지원을 제공합니다.

VS Code에서 Ada 개발 환경을 설정하려면:

  1. Visual Studio Code를 설치합니다.
  2. GNAT 툴체인을 설치하고 시스템의 PATH에 있는지 확인합니다.
  3. VS Code 마켓플레이스에서 AdaCore.ada를 검색하여 ‘Ada & SPARK’ 확장 프로그램을 설치합니다.

확장 프로그램은 다음 기능을 제공합니다:

  • 구문 강조, 코드 탐색 및 자동 완성.

  • VS Code 작업을 통한 프로젝트 빌드를 위한 gprbuild와의 통합. 기본 빌드 작업은 Ctrl + Shift + B 단축키로 실행할 수 있습니다.

  • 사용자가 중단점을 설정하고, 코드를 단계별로 실행하고, 변수를 검사할 수 있는 디버깅 지원.

VS Code에서의 일반적인 작업 흐름은 작업 공간 폴더를 만들고, GNAT 프로젝트 파일(.gpr)을 추가하고, 소스 코드를 작성하고, 내장된 작업을 사용하여 프로그램을 컴파일하고 디버깅하는 것을 포함합니다.

2.3 기본적인 프로그램: “Hello, World!”

어떤 새로운 언어에서든 전통적인 첫 번째 프로그램은 “Hello, World!”입니다. Ada에서는 이 간단한 프로그램조차도 언어의 구조와 철학에 대한 여러 핵심 개념을 소개합니다. 다음 예제는 이 학습서의 모든 코드에 사용될 Clair 코딩 스타일 가이드에 따라 작성되었습니다.

hello.adb라는 이름의 파일을 만듭니다. 파일 이름을 주 프로시저 이름과 일치시키는 관례는 컴파일러 경고를 피하는 데 중요합니다.

with Ada.Text_IO;

procedure hello is
begin
  Ada.Text_IO.put_line ("Hello, World!");
end hello;

명령줄에서 이 프로그램을 컴파일하고 실행하려면 gnatmake를 사용합니다:

$ gnatmake hello.adb
$./hello
Hello, World!

gnatmake 명령어는 최종 실행 파일 hello를 생성하기 위해 컴파일(gcc -c hello.adb), 바인딩(gnatbind hello.ali), 링킹(gnatlink hello.ali)의 세 단계를 자동화합니다.

이 간단한 프로그램을 분해해 보겠습니다:

  • with Ada.Text_IO;: 이것은 with 절입니다. 라이브러리 패키지 Ada.Text_IO에 대한 의존성을 선언합니다. 이로써 put_line 프로시저와 같은 해당 패키지 내의 공개 선언들을 hello 프로시저에서 사용할 수 있게 됩니다. 이것이 Ada에서 모듈성을 위한 기본적인 메커니즘입니다. 첫 번째 프로그램부터 Ada는 의존성을 명시적으로 선언해야 하는 구조화된 모듈식 접근 방식을 강제합니다. 이는 전역적이고 묵시적으로 사용 가능한 I/O 함수를 가질 수 있는 언어들과 대조됩니다.

  • procedure hello is ... end hello;: 이것은 주 프로그램 단위를 정의합니다. Ada에서 프로그램의 진입점은 매개변수 없는 프로시저입니다. 구조는 다음과 같습니다:

    • procedure 키워드와 이름(hello).

    • is 키워드, 이는 프로시저의 헤더와 선언부를 분리합니다. isbegin 사이의 공간은 지역 변수, 형식 또는 중첩된 서브프로그램이 선언될 위치입니다.

    • begin 키워드, 이는 실행 가능한 문장의 시작을 표시합니다.

    • end hello; 문장, 이는 프로시저의 끝을 표시합니다. 프로시저 이름(hello)을 명시적으로 반복하는 것은 가독성을 향상시키고 컴파일러가 블록이 올바르게 일치하는지 확인하는 데 도움이 되는 스타일 관례입니다.

  • Ada.Text_IO.put_line ("Hello, World!");: 이것은 서브프로그램 호출입니다.

    • Ada.Text_IO.put_line: 점 표기법을 사용하여 Ada.Text_IO 패키지 내에 위치한 put_line 프로시저를 호출합니다. 이 명시적 한정은 네임스페이스 오염을 피하고 서브프로그램이 어디에 정의되어 있는지 명확하게 하는 모범 사례입니다.

    • ` (…) `: Clair 스타일 가이드에서 요구하는 대로 서브프로그램 이름과 여는 괄호 사이에 공백을 사용하여 서브프로그램 호출을 다른 구조와 시각적으로 구별합니다.

    • "Hello, World!": 이것은 문자열 리터럴이며, put_line 프로시저에 전달되는 인수입니다.

    • ;: 세미콜론은 Ada에서 문장 종결자입니다.

이 첫 번째 프로그램의 구조는 Ada 설계의 핵심적인 측면을 드러냅니다: 툴체인은 지능적이고 언어를 인식합니다. gnatmake가 호출될 때, 단순히 주어진 파일을 컴파일하는 것이 아닙니다. Ada 소스를 파싱하고, with Ada.Text_IO; 절을 읽고, 미리 컴파일된 Ada.Text_IO 라이브러리 단위에 대해 링크해야 함을 이해합니다. 언어의 모듈성 기능과 빌드 도구 간의 이러한 긴밀한 통합은 대규모의 다중 파일 시스템 관리를 다루기 쉽게 만들어, 언어의 원래 설계 목표 중 하나를 충족시킵니다.

3. 어휘 구조 (lexical structure)

어휘 구조(lexical structure)는 프로그래밍 언어의 텍스트를 구성하는 규칙과 단위들을 정의합니다. Ada 프로그램의 텍스트는 문자들로 구성되며, 컴파일러는 이 텍스트를 어휘 요소(lexical element)지시문(directive)의 연속으로 인식합니다.

어휘 요소(lexical element)란 프로그래밍 언어를 구성하는 문법적으로 의미 있는 최소 단위입니다. Ada 프로그램의 텍스트는 문자들로 구성된 어휘 요소의 연속입니다.

Ada 프로그램 텍스트에 사용될 수 있는 문자 집합은 일부 특수 목적의 코드 포인트를 제외하고, 유니코드(ISO/IEC 10646:2020)에 정의된 사실상 거의 모든 문자를 포함합니다. 따라서 식별자나 문자열 리터럴 등에 한국어, 중국어, 일본어 등 다양한 언어의 문자를 사용할 수 있습니다.

프로그램의 의미는 주석을 제외한 어휘 요소들의 특정 순서에 의해서만 결정됩니다. 즉, 컴파일러는 소스 코드를 공백이나 줄 바꿈과 같은 형식이 아닌 어휘 요소의 흐름으로 인식합니다.

Ada에서는 식별자(identifier), 예약어(reserved word), 숫자 리터럴(numeric literal), 문자 리터럴(character literal), 문자열 리터럴(string literal), 구분자(delimiter), 주석(comment)이 어휘 요소에 해당합니다.

이 장에서는 Ada 프로그램을 구성하는 이러한 기본적인 문법 단위인 어휘 요소에 대해 학습하겠습니다.

3.1 식별자 (identifier)

식별자(identifier)는 변수, 상수, 타입, 프로시저 등 프로그램의 다양한 구성 요소에 이름을 부여하기 위해 사용하는 기호적 명칭입니다.

Ada 식별자의 작성 규칙은 다음과 같습니다.

  1. 식별자는 반드시 문자 또는 숫자-문자(예: 로마 숫자)로 시작해야 합니다. 숫자-문자(number letter)란 일반적인 10진수 숫자(0~9)와 달리 문자처럼 취급되는 유니코드 문자(, , 등)를 의미합니다. 따라서 Ⅳ_Generation은 유효한 식별자이지만, 4th_Generation은 숫자로 시작하므로 허용되지 않습니다.
  2. 두 번째 문자부터는 문자, 10진수 숫자, 또는 밑줄(_)을 포함할 수 있습니다.
  3. 연속된 밑줄(__)이나 식별자의 끝에 오는 밑줄은 허용되지 않습니다.
  4. Ada 식별자는 대소문자를 구분하지 않으며, 대소문자를 무시하고 비교했을 때 동일한 문자열이면 같은 식별자로 간주됩니다. 예를 들어, Count, count, COUNT는 모두 동일한 식별자입니다.
  5. 식별자로 if, for, begin 등의 예약어(reserved word)를 사용할 수 없습니다.

올바른 식별자 예시

  • Temperature
  • page_count
  • x1
  • get_symbol
  • is_device_ready

Ada는 유니코드를 지원하므로 다음과 같이 한글, 한자, 일본어 등을 사용한 식별자도 유효합니다.

-- 유니코드 식별자 선언 예시
declare
  온도       : Float;           -- 한글
  国家       : String(1 .. 2);  -- 중국어
  みず       : Integer;         -- 일본어 (히라가나)
  café       : String(1 .. 10); -- 프랑스어 (é 포함)
  año        : Positive;        -- 스페인어 (ñ 포함)
  grünerWert : Float;           -- 독일어 (ü 포함)
begin
  null;
end;

잘못된 식별자 예시

  • 1st_reading (숫자로 시작)
  • Page__count (밑줄 연속 사용)
  • End_Of_File_ (밑줄로 끝남)
  • end (예약어 사용)

3.2 예약어 (reserved words)

예약어(reserved word)는 프로그래밍 언어의 문법 구조, 특정한 의미 또는 기능을 위해 미리 약속된 단어들입니다. 따라서 프로그래머는 예약어를 변수나 함수의 이름과 같은 식별자로 사용할 수 없습니다. Ada 언어에서 예약어는 대소문자를 구분하지 않으며, IF, If, if, iF는 모두 동일하게 인식됩니다.

Ada 언어의 예약어 목록은 다음과 같습니다.

abort       delay       in          parallel    some
abs         delta       interface   pragma      subtype
abstract    digits      is          private     synchronized
accept      do          limited     procedure   tagged
access      else        loop        protected   task
aliased     elsif       mod         raise       terminate
all         end         new         range       then
and         entry       not         record      type
array       exception   null        rem         until
at          exit        of          renames     use
begin       for         or          requeue     when
body        function    others      return      while
case        generic     out         reverse     with
constant    goto        overriding  select      xor
declare     if          package     separate

3.3 리터럴 (literal)

리터럴(literal)은 변수와 달리, 소스 코드에 적힌 그대로의 고정된 값을 의미합니다.

예를 들어, 코드에 10이라고 쓰면 이는 ‘10이라는 숫자 값’ 그 자체를 뜻합니다. 어떤 값을 담는 상자(변수)가 아니라, 값 자체가 코드에 직접 나타나는 것입니다. 프로그램에서 변수를 초기화하거나, 연산을 수행하고, 특정 조건을 검사하는 등 구체적인 데이터가 필요한 모든 곳에 리터럴이 사용됩니다.

Ada는 데이터의 종류에 따라 세 가지 기본적인 리터럴을 정의하며, 각 리터럴은 명확히 구분되는 표기법을 사용합니다.

  • 숫자 리터럴 (numeric literal): 123, 3.14159와 같이 정수나 실수를 표현합니다.
  • 문자 리터럴 (character literal): 'A', '%'와 같이 하나의 문자를 표현합니다.
  • 문자열 리터럴 (string literal): "Hello, Ada!"와 같이 여러 문자의 연속을 표현합니다.

이번 절에서는 이 세 가지 리터럴의 작성법과 규칙을 차례로 살펴보겠습니다. 가장 먼저 숫자 리터럴부터 시작하겠습니다.

3.3.1 숫자 리터럴 (numeric literal)

숫자 리터럴(numeric literal)은 코드에 직접 작성하는 숫자 값입니다.

Ada는 정수 리터럴(integer literal)과 실수 리터럴(real literal) 두 종류의 숫자 리터럴을 제공합니다. 소수점이 없는 숫자는 정수 리터럴이고, 소수점이 있는 숫자는 실수 리터럴입니다.

가독성을 높이기 위해 숫자 사이에 밑줄(_)을 자유롭게 사용할 수 있으며, 이 밑줄은 값에 영향을 주지 않습니다.

숫자 리터럴은 10진법으로 표현할 수도 있고, 밑(base)을 지정하는 기수법으로 표현할 수도 있습니다. Ada에서 숫자 리터럴은 처음에는 Integer와 같은 구체적인 타입에 바로 속하지 않고, 범용(universal) 타입으로 취급됩니다. 이러한 범용 타입의 동작 방식과 활용은 4.1.3절에서 더 자세히 살펴보겠습니다.[^ref]

10진 리터럴 (decimal literal)

우리가 일상적으로 사용하는 밑이 10인 숫자 표기법입니다. E 또는 e를 사용하여 10의 거듭제곱을 나타내는 지수(exponent)를 표기할 수 있습니다.

-- 10진 정수 리터럴
123
1_000_000
5e2    -- 5 * (10 ** 2) = 500

-- 10진 실수 리터럴
0.0
12.34
3.14159_26
1.2e-3 -- 1.2 * (10 ** -3) = 0.0012

기수 리터럴 (based literal)

기수(base 또는 radix)란, 특정 숫자 시스템에서 숫자를 표현하는 데 사용되는 고유한 숫자의 개수를 의미합니다. 우리가 일상에서 사용하는 10진법은 0부터 9까지 10개의 숫자를 사용하므로 기수가 10입니다.

컴퓨터 과학에서는 컴퓨터가 데이터를 처리하는 방식과 밀접한 2진법(0, 1)과 16진법(0-9, A-F)도 자주 사용합니다. Ada는 이처럼 다양한 기수법을 코드에서 직접 표현하는 기능을 제공합니다.

기수 리터럴은 밑(base)을 2에서 16 사이에서 명시적으로 지정하는 표기법으로, 밑#숫자# 형식을 사용합니다. 지수 e는 밑(base)의 거듭제곱을 의미합니다.

16진법에서는 a부터 f까지의 문자를 10부터 15를 나타내는 숫자로 사용하며, 대소문자를 구분하지 않습니다.

-- 정수 255를 다양한 기수법으로 표현
2#1111_1111#   -- 2진법
16#FF#         -- 16진법 (대문자 사용)
16#ff#         -- 16진법 (소문자 사용)

-- 정수 224를 16진법 지수 표기법으로 표현
16#e#e1        -- 14 * (16 ** 1) = 224

-- 실수 4095.0을 2진법 지수 표기법으로 표현
2#1.1111_1111_111#e11   -- (1 + 2047/2048) * (2 ** 11) = 4095.0

지수(exponent) 표기법이란?

지수 표기법(E 또는 e)은 ‘거듭제곱’을 의미하며, 매우 크거나 작은 수를 간결하게 표현하는 과학적 표기법(scientific notation)입니다.

e 앞의 숫자에 e 뒤의 숫자만큼 해당 리터럴의 밑(base)을 거듭제곱한 값을 곱하는 것을 의미합니다.

10진 리터럴의 경우 밑은 10입니다.

  • 5.12e25.12 * (10 ** 2)와 같으며, 512.0이 됩니다.

기수 리터럴의 경우 명시된 밑(예: 2 또는 16)을 따릅니다.

  • 16#a.0#e210.0 * (16 ** 2)과 같으며, 2560.0이 됩니다.

3.3.2 문자 리터럴 (character literal)

문자 리터럴(character literal)은 소스 코드에서 단 하나의 문자를 직접 표현하는 값입니다. Ada에서는 하나의 그래픽 문자(graphic character)를 아포스트로피(')로 감싸서 문자 리터럴을 작성합니다.

그래픽 문자

문자 리터럴에 사용되는 그래픽 문자(graphic character)란, 화면에 표시되는 문자(글자, 숫자, 공백, 특수 기호 등)를 의미합니다. Ada 언어 표준에 따르면 공백 문자(' ')도 그래픽 문자에 포함됩니다. 그러나, 줄 바꿈이나 탭처럼 화면에 직접 표시되지 않는 문자는 ' ' 안에 넣어 문자 리터럴로 표현할 수 없습니다. 이러한 문자를 다루는 방법은 4장에서 자세히 학습합니다.[^ref]

예시

이제 그래픽 문자를 이용한 문자 리터럴의 예시를 살펴보겠습니다.

'A'  -- 문자 A
'*'  -- 별표 문자
' '  -- 공백 문자
'''  -- 아포스트로피 문자 자신

Ada는 유니코드를 지원하므로, 다양한 언어의 문자나 기호 또한 문자 리터럴로 사용할 수 있습니다.

'Ω'  -- 그리스어 오메가 대문자
'가'  -- 한글
'∞'  -- 무한대 기호
'א'  -- 히브리어 알레프

3.3.3 문자열 리터럴 (string literal)

문자열 리터럴(string literal)은 소스 코드에서 0개 이상의 문자 순서(sequence)를 직접 표현하는 값입니다. Ada에서는 문자열을 큰따옴표(")로 감싸서 작성합니다.

기본 작성법

문자열 리터럴은 큰따옴표 안에 표현하고자 하는 그래픽 문자들을 순서대로 나열하여 만듭니다.

예시

"This is a string."
"Hello, Ada!"
""   -- 아무 문자도 포함하지 않은 빈 문자열(null string)
"A"  -- 문자 하나만으로 구성된 문자열

문자열 리터럴 안에 큰따옴표 문자 자체를 포함해야 할 경우, 큰따옴표를 두 번 연속해서("") 사용해야 합니다.

"He said, ""Hello!"""  -- "He said, "Hello!"" 라는 문자열을 의미

유니코드 지원

문자 리터럴과 마찬가지로, 문자열 리터럴 안에도 다양한 유니코드 문자를 포함할 수 있습니다.

예시

"프로그래밍 언어 Ada"
"默认值"  -- 중국어 간체
"PI (π) is a mathematical constant."

3.4 구분자 (delimiter)

구분자(delimiter)는 언어의 문법 구조를 형성하는 기호입니다.

Ada의 구분자는 하나의 문자로 이루어진 단일 구분자와 두 개의 문자가 결합된 복합 구분자로 나뉩니다.

단일 구분자 (single delimiter)

다음은 하나의 문자로 구성된 단일 구분자들의 목록입니다.

&    '    (    )    *    +    ,    -    .    /
:    ;    <    =    >    @    [    ]    |

이 기호들은 각각 연산, 구문의 시작과 끝, 항목 나열, 문장(statement)의 종료 등 다양한 문법적 역할을 수행합니다.

복합 구분자 (compound delimiter)

두 개의 특수 문자가 결합하여 하나의 구분자를 형성합니다.

=>    ..    **    :=    /=    >=    <=    <<    >>    <>

복합 구분자 예시와 의미

  • => (arrow): 매개변수나 선택지를 연결할 때 사용합니다.
  • .. (double dot): 범위를 지정할 때 사용합니다. (예: 1 .. 10)
  • ** (double star): 거듭제곱 연산을 의미합니다.
  • := (assignment): 변수에 값을 할당할 때 사용합니다.
  • /= (inequality): “같지 않다”를 의미하는 비교 연산자입니다.

분리자

공백(space), 탭(tab), 줄 바꿈(new line)과 같은 문자들은 그 자체로 구분자는 아니지만, 인접한 어휘 요소들을 분리하는 분리자(separator)로 사용됩니다.

분리자는 두 어휘 요소(예: 예약어와 식별자)가 하나의 요소로 잘못 결합되는 것을 방지하기 위해 반드시 필요합니다.

-- 정상: 'if'(예약어)와 'x'(식별자)가 공백(분리자)으로 분리됨
if x then
  ...
end if;

-- 컴파일 오류: 'ifx'라는 하나의 식별자로 인식됨
ifx then
  ...
end if;

반면, 구분자(delimiter) 자체가 이미 어휘 요소의 경계를 명확히 하는 경우에는 분리자(공백)가 문법적으로 필수는 아닙니다.

x:=1;    -- 문법적으로 올바른 코드입니다.

x := 1;  -- 분리자를 선택적으로 추가한 코드입니다.

3.5 주석 (comment)

주석(comment)은 소스 코드에 설명이나 메모를 작성하는 기능입니다. 주석으로 작성된 내용은 프로그램의 실행에 영향을 주지 않으며, 컴파일러는 이 부분을 무시합니다. 주석의 주된 목적은 코드를 읽는 사람의 이해를 돕는 데 있습니다.

Ada에서 주석은 두 개의 하이픈(--)으로 시작하며, 그 지점부터 해당 줄의 끝까지 모든 내용이 주석으로 처리됩니다.

사용 예시

주석은 한 줄 전체를 차지할 수도 있고, 코드 뒤에 위치할 수도 있습니다.

-- 이 줄 전체는 주석입니다.
-- 변수 x에 초기값을 할당합니다.
x : Integer := 1;  -- 변수 선언과 동시에 값을 1로 설정

-- 여러 줄에 걸쳐 주석을 작성할 경우,
-- 각 줄을 '--'로 시작합니다.

빈 줄 사이에 주석을 사용하여 코드의 논리적 구역을 구분하기도 합니다.

-- 변수 초기화
x : Integer := 0;

-- 반복문 시작
for i in 1 .. 10 loop
  x := x + i;
end loop;

주석의 역할

주석은 다음과 같은 역할을 수행합니다.

  • 코드 의도 설명: 코드의 동작 방식(‘무엇을’)과 더불어, 설계 배경(‘왜’)과 같은 개발자의 의도를 설명합니다.
  • 로직 해설: 특정 알고리즘이나 비즈니스 규칙을 설명하여 코드의 유지보수를 지원합니다.
  • 임시 코드 비활성화 (주석 처리): 특정 코드 라인을 실행에서 제외할 목적으로 해당 라인 앞에 --를 붙여 비활성화할 수 있습니다.
    -- x := x + 1;  -- 이 라인은 주석 처리되어 실행되지 않음
    y := y + 1;
    
  • 자동화 도구를 위한 정보 제공: 문서 생성 도구나 코드 분석 도구는 특정 형식의 주석을 입력으로 사용합니다. 이러한 도구들은 주석을 읽어 API 문서를 생성하거나 코드의 정확성을 검증하는 데 사용합니다.

주석은 코드의 가독성과 유지보수성을 향상시킬 수 있으며, 협업에 활용될 수 있습니다.

3.6 프라그마 (pragma)

프라그마(pragma)는 Ada 언어의 표준 구문의 일부로서, 컴파일러에게 특정 지시 사항이나 정보를 전달하는 컴파일러 지시문(compiler directive)입니다.

프라그마는 프로그램의 실행 로직을 직접 구성하지는 않지만, 컴파일러의 최적화 방식, 런타임 검사 수행 여부, 외부 언어와의 연동 방식, 또는 경고 메시지 표시 여부 등 컴파일 및 실행 환경의 다양한 측면을 제어합니다.

기본 문법

프라그마는 pragma 예약어로 시작하며, 프라그마 식별자와 선택적인 인수(argument) 목록으로 구성됩니다.

pragma 식별자 [(인수, ...)];

프라그마는 선언부(declarative part) 내, 라이브러리 유닛(library unit)의 시작 부분, 또는 특정 선언(declaration) 바로 뒤 등 문법적으로 허용된 위치에 배치되어 해당 위치부터 유효합니다.

컴파일러는 자신이 인식하는 프라그마만 처리하며, 인식하지 못하는 프라그마는 무시합니다. 프라그마는 두 종류로 나뉩니다.

  1. 언어 정의 프라그마: assert, suppress 등 Ada 언어 표준 자체에 정의된 프라그마입니다.
  2. 구현 정의 프라그마: GNAT 컴파일러의 warningsstyle_checks 와 같이 특정 컴파일러(구현)에서만 제공하는 프라그마입니다. GNAT 컴파일러가 제공하는 구현 정의 프라그마의 전체 목록 및 설명은 GNAT 참조 매뉴얼6에서 확인할 수 있습니다.

이 책에서는 assert (12.7.1절), suppress (12.8.1절) 등 특정 기능과 연관된 프라그마는 해당 기능이 설명되는 장에서 설명합니다.

본 절에서는 코드의 컴파일 환경을 제어하는 GNAT 컴파일러의 구현 정의 프라그마 몇 가지를 소개합니다.

3.6.1 언어 버전 명시 (pragma ada_2012)

pragma ada_2012 계열의 프라그마는 컴파일러에게 해당 소스 코드가 어떤 Ada 표준 버전을 준수하는지 명시합니다.

-- 이 파일이 ada 2012 표준을 준수함을 명시합니다.
pragma ada_2012;

3.6.2 경고 메시지 제어 (pragma warnings)

pragma warnings는 GNAT 컴파일러가 생성하는 특정 경고 메시지를 비활성화(off)하거나 활성화(on)하는 데 사용됩니다. 이 프라그마는 경고를 발생시키는 코드 영역에서 적용할 수 있습니다.

pragma warnings는 모든 경고를 일괄 제어하거나, 특정 경고 플래그 또는 경고 메시지 텍스트 패턴을 지정하여 제어할 수 있습니다.

경고 제어의 특징

  1. 플래그 제어: 경고 플래그 문자열(예: "-gnatwu")을 사용하여 특정 카테고리의 경고를 켜거나 끌 수 있습니다.

    -- 이 지점부터 사용되지 않는 엔티티에 대한
    -- GNAT 경고(-gnatwu)를 비활성화합니다.
    pragma warnings (off, "-gnatwu");
    
    my_unused_variable : Integer; -- 이 선언은 -gnatwu 플래그에 의해 경고가 억제됩니다.
    
  2. 메시지 패턴 제어: 경고 메시지 텍스트와 일치하는 패턴(와일드카드 * 사용 가능)을 사용하여 개별 경고를 억제할 수 있습니다.

  3. 유효 범위: pragma warnings (off, ...)의 효력은 해당 프라그마가 위치한 지점부터 시작됩니다. 경고를 다시 활성화하는 pragma warnings (on, ...)이 뒤따르지 않으면, 해당 경고 억제는 현재 컴파일 단위의 끝까지 지속됩니다. 이 프라그마는 선언적 범위를 벗어나도 자동으로 효력이 종료되지 않습니다.

    -- 해당 경고를 다시 활성화합니다.
    pragma warnings (on, "-gnatwu");
    

3.6.3 스타일 검사 제어 (pragma style_checks)

pragma style_checks는 GNAT 컴파일러의 코딩 스타일 검사 기능을 비활성화(off)하거나 활성화(on)하는 데 사용됩니다. 자동 생성된 코드 영역이나 특정 스타일 가이드를 적용하는 코드 영역에서 스타일 경고를 억제할 때 활용할 수 있습니다. 또한 프로젝트별 코딩 규칙을 코드에 적용하거나 자동 생성된 코드 영역에서 스타일 경고를 억제하는 목적으로 사용할 수 있습니다.

-- 이 지점부터 스타일 검사를 비활성화합니다.
pragma style_checks (off);

4. 데이터 타입

4.1 타입과 객체의 기초

4.1.1 타입(Type)의 정의

타입(type)은 Ada 프로그램에서 데이터의 성질과 사용 규칙을 정의하는 기본 단위입니다. 타입은 값의 집합(set of values)과 해당 타입에 적용할 수 있는 기본 연산의 집합(set of primitive operations)으로 특징지어집니다.

모든 데이터는 반드시 특정 타입에 속하며, 이 타입은 해당 데이터가 프로그램 내에서 어떤 값들을 가질 수 있고, 어떤 연산이 가능한지를 결정합니다.

프로그래머는 타입 선언(type declaration)을 통해 이러한 타입을 정의합니다. Ada에서 타입 선언의 기본 구문은 type <타입_이름> is <타입_정의>; 형태를 따릅니다. 예를 들어, 색깔 타입을 다음과 같이 선언할 수 있습니다.

type Color is (Red, Green, Blue); -- 열거형 타입 정의 예시

<타입_정의> 부분에는 (Red, Green, Blue)와 같은 열거형 정의뿐만 아니라, 스칼라(열거형, 정수, 실수), 복합(배열, 레코드), 접근 타입 등 다양한 정의가 올 수 있으며, 이는 본 장의 여러 절에서 상세히 다룹니다.

타입이 정의하는 요소

  1. 값의 집합 (set of values): 해당 타입의 객체가 가질 수 있는 값들의 범위 또는 집합을 명시합니다. 예를 들어, Boolean 타입은 TrueFalse라는 두 개의 값만을 가질 수 있습니다.
  2. 연산의 집합 (set of operations): 해당 타입의 값들에 적용할 수 있는 유효한 연산자나 서브프로그램(함수, 프로시저)들을 정의합니다. 예를 들어, Integer 타입은 +, -와 같은 산술 연산을 허용하지만, Boolean 타입은 and, or와 같은 논리 연산을 허용합니다. 사용자가 정의한 연산 또한 타입의 일부로 간주됩니다.

Ada 컴파일러는 프로그램 코드를 분석할 때, 모든 연산과 할당이 해당 타입에 적합한지 검사합니다. 이 과정을 통해, 호환되지 않는 데이터의 사용을 방지합니다. 이러한 오류들은 컴파일 과정에서 정적으로 감지됩니다.

4.1.2 객체(object)의 개념

프로그램 실행 시점에 특정 타입의 값을 가지는 실체를 ‘객체(object)’라고 합니다. 모든 객체는 자신이 속한 타입에 의해 그 특성이 결정됩니다.

Ada에서 객체는 값을 보유하는 엔티티(entity)를 포괄적으로 지칭하며, 객체가 선언될 때 그 값이 변경 가능한지에 따라 상수 또는 변수로 나뉩니다. 이어지는 두 절에서는 이 두 가지 형태의 객체 선언 방법을 자세히 다룹니다.

4.1.3 상수 (constant) 선언

상수(constant)는 선언 시 초기화된 값을 프로그램 실행 동안 변경할 수 없는 객체입니다. 상수로 선언된 객체에 새로운 값을 할당하려고 시도하면, 프로그램 실행 전에 컴파일러가 이를 오류로 감지하고 컴파일을 중단합니다.

Ada에서 상수를 선언하는 방법은 두 가지가 있습니다.

1. 타입 지정 상수 (Typed Constant)

constant 키워드와 함께 타입을 명시적으로 지정하여 선언합니다.

-- 타입을 'Integer'로 명시적으로 지정
Limit : constant Integer := 100;
-- 타입을 'Float'로 명시적으로 지정
Pi_Approx : constant Float := 3.1416;

2. 숫자 선언 (Number Declaration)

숫자 리터럴을 타입 지정 없이 constant := 구문을 사용하여 선언할 수 있습니다. 이를 숫자 선언이라고 하며, 이렇게 선언된 상수는 특정 타입이 아닌 범용 타입(universal type)이 됩니다.

-- Seconds_Per_Day는 'Universal_Integer' 타입이 됨
Seconds_Per_Day : constant := 24 * 60 * 60;

-- Pi는 'Universal_Real' 타입이 됨
Pi : constant := 3.14159_26536;

범용 타입의 상수는 컴파일러가 문맥을 분석하여 구체적인 숫자 타입(예: Integer, Short_Integer, Float, Long_Float 등)으로 자동 변환할 수 있으며, 타입 변환 없이 다양한 숫자 타입과 연산할 수 있습니다.

4.1.4 변수 (variable) 선언과 초기화

변수(variable)는 선언 이후 값을 변경할 수 있는 객체입니다. 객체 선언 시 constant 키워드가 없으면 변수가 됩니다.

1. 초기화가 선택적인 경우 (Optional Initialization)

Integer, Float와 같은 제약된(constrained) 스칼라 타입이나, 컴포넌트에 기본값이 있는 레코드 타입 등은 선언 시 초기화가 문법적으로 필수는 아닙니다.

Count : Integer := 0;      -- 변수 선언과 동시에 초기화
Count : Integer;           -- 변수 선언만 수행 (초기화 생략)

초기화되지 않은 변수:

초기화 표현식 없이 선언된 스칼라 타입의 변수(예: Count : Integer;)는 유효하지 않은 값(invalid value)을 가질 수 있습니다. 이 변수에 새로운 값을 할당하기 전에 만약 이 값을 읽으려고 시도하면, 프로그램은 Constraint_Error 또는 Program_Error 예외를 발생시킬 수 있습니다.

단, 4.3절의 접근 타입(기본값 null)이나 4.5절의 Complex 레코드(컴포넌트 기본값 0.0)처럼 타입 자체에 기본 초기값이 명시된 경우는 예외입니다.

기본 초기값이 명시된 경우:

다음과 같이 타입 자체에 기본 초기값이 명시된 경우에는 초기화 표현식 없이 변수를 선언해도 유효한 기본값으로 자동 초기화됩니다.

  • 접근 타입(Access Type): 4.3절에서 설명하듯이, 접근 타입은 null을 암시적인 기본 초기값으로 가집니다.
  • 레코드 컴포넌트 기본값: 4.5절의 Complex 레코드처럼, 레코드 타입 선언 시 컴포넌트에 :=를 사용하여 기본값을 지정할 수 있습니다.
  • 타입 기본값 지정 (애스펙트 사용): 스칼라 서브타입 선언 시, 애스펙트(Aspect) 라는 메커니즘을 통해 해당 타입의 기본값을 명시할 수 있습니다. (애스펙트에 대한 자세한 설명은 4.1.5절을 참조하십시오.) 만약 타입에 이러한 기본값이 지정되어 있다면, 변수를 초기화 없이 선언해도 해당 값으로 초기화됩니다.
    -- Status 타입 선언 시 애스펙트로 기본값을 Pending으로 지정 (4.1.5절 참조)
    type Status is (Pending, Processing, Completed)
    with Default_Value => Pending;
    
    Current_Status : Status; -- Pending으로 자동 초기화됨
    

2. 초기화가 필수인 경우 (Mandatory Initialization)

Ada에서는 제약 없는 서브타입(unconstrained subtype)의 변수를 선언할 때, 초기화 또는 제약 지정을 생략하면 컴파일 오류가 발생합니다.

“제약이 없다”는 것은 타입 선언 시 크기나 범위가 고정되지 않았음을 의미합니다. 이러한 타입의 변수를 생성하려면, 컴파일러는 변수가 차지할 메모리 크기를 알아야 하므로 선언 시점에 반드시 제약(크기)을 지정해야 합니다.

제약을 지정하는 방법은 두 가지입니다.

  • 초기값 제공: 초기값의 크기로부터 제약이 추론됩니다.
  • 명시적 제약 지정: 타입 이름 뒤에 괄호 ()를 사용하여 제약을 명시합니다.

사례로는 4.4절에서 다룰 제약 없는 배열과 4.5절에서 다룰 기본값 없는 판별자를 가진 레코드가 있습니다.

-- 4.4절의 제약 없는 배열 타입 예시
type Vector is array (Integer range <>) of Float;

-- 4.5절의 제약 없는 레코드 타입 예시 (판별자 Size에 기본값 없음)
type Buffer(Size : Natural) is
 record
    Data : String(1 .. Size);
 end record;

다음은 위 타입들을 사용한 변수 선언 예시입니다.

-- 제약 없는 타입을 초기화 없이 선언 (컴파일 오류)
V1 : Vector;
S1 : String; -- String도 제약 없는 배열이므로 동일하게 오류 발생
B1 : Buffer;

-- 방법 1: 초기값으로 제약 지정 (정상)
V2 : Vector := (1.0, 2.0, 3.0); -- 제약이 (1 .. 3)으로 추론됨
S2 : String := "Hello";         -- 제약이 (1 .. 5)로 추론됨
B2 : Buffer := (Size => 10, Data => (others => ' ')); -- 제약이 (10)으로 추론됨

-- 방법 2: 명시적 제약 지정 (정상)
v3 : Vector(1 .. 10);
s3 : String(1 .. 80);           -- String의 범위를 명시적으로 지정
b3 : Buffer(Size => 128);

4.1.5 애스펙트(Aspect) 소개

애스펙트(Aspect)는 선언(declaration)에 추가 정보를 제공하여, 엔티티의 비-알고리즘적 속성(non-algorithmic property)을 명시하는 방법입니다. 타입의 메모리 표현 방식, 서브프로그램의 사전/사후 조건, 외부 언어와의 연동 규약 등을 지정하는 데 사용됩니다.

애스펙트는 일반적으로 선언 끝에 with 키워드와 함께 Aspect_Identifier => Expression 또는 Aspect_Identifier => Name 형태로 명시합니다.

4.1.4절에서는 변수의 기본 초기값을 지정하는 Default_Value 애스펙트가 사용되었습니다.

-- 예시: Type 선언에 Default_Value 애스펙트 명시
type Status is (Pending, Processing, Completed)
  with Default_Value => Pending; -- Status 타입의 기본값을 Pending으로 지정

with Default_Value => PendingStatus 타입 선언에 “이 타입의 기본값은 Pending이다”라는 추가 정보를 명시하는 애스펙트입니다.

다른 애스펙트의 예시는 다음과 같습니다.

-- 예시: Subprogram 선언에 Pre 애스펙트 명시
function Square_Root (X : Float) return Float
  with Pre => X >= 0.0; -- 사전 조건 명시

이 책에서는 각 기능(예: 기본값, 계약, 외부 연동)을 설명할 때 관련된 애스펙트를 함께 다룰 것입니다.

4.1.6 속성(attribute) 소개

속성(attribute)은 Ada에서 타입, 객체, 서브프로그램 등 이름으로 참조될 수 있는 다양한 프로그램 구성 요소, 즉 엔티티(entity)가 가지는 미리 정의된 특성(characteristic)이나 관련 연산(operation)을 나타냅니다. 여기서 엔티티란 선언(declaration)을 통해 이름이 부여되는 모든 것(예: 타입, 객체, 서브프로그램, 패키지 등)을 포괄적으로 지칭하는 용어입니다. 속성은 언어 자체에 정의되어 있으며, 개발자는 이를 사용하여 엔티티에 대한 정보를 얻거나 특정 동작을 수행할 수 있습니다.

속성의 구문

속성은 어퍼스트로피(') 기호를 사용하여 접근합니다. 일반적인 형태는 다음과 같습니다.

Prefix'attribute_name
  • Prefix: 속성을 조회하려는 엔티티(타입 이름, 객체 이름 등)를 나타냅니다.
  • attribute_name: 조회하려는 속성의 이름(식별자)입니다. 일부 속성은 추가적인 정적 인자(static argument)를 가질 수도 있습니다 (예: Array'length(n)).

예를 들어, Integer 타입의 가장 작은 값을 얻으려면 Integer'first 속성을 사용합니다.

속성의 종류

속성은 다음을 포함한 다양한 종류의 정보를 나타낼 수 있습니다.

  • : 엔티티의 특정 값 (예: 타입의 범위 경계 Integer'first, 객체의 크기 Data'size ).
  • 함수: 엔티티에 적용될 수 있는 연산 (예: 스칼라 타입의 다음 값 Day'Succ(Mon), 값의 문자열 표현 Integer'image(10) ).
  • 타입/서브타입: 엔티티와 관련된 다른 타입이나 서브타입 (예: 태그드 타입의 클래스 범위 타입 Point'class ).

학습 경로

Ada는 다양한 속성을 제공하며, 각 속성은 특정 종류의 엔티티와 관련이 있습니다. 본 장의 이후 절들과 후속 장들에서는 각 타입(스칼라, 접근, 배열, 레코드 등)과 객체를 설명할 때 해당 엔티티에 적용되는 주요 속성들을 함께 다룹니다.

4.1.7 Ada의 타입 계층 구조

Ada는 타입들을 그 구조적 특성에 따라 체계적인 계층 구조로 분류합니다. 이 구조를 이해하는 것은 Ada의 데이터 구성을 파악하는 데 필수적입니다.

모든 타입은 가장 상위 수준에서 기본 타입(elementary type)복합 타입(composite type)으로 나뉩니다.

기본 타입 (elementary type)

값이 논리적으로 더 이상 분해될 수 없는 타입입니다. 기본 타입은 다시 다음과 같이 나뉩니다.

  • 스칼라 타입 (scalar type): 이산(discrete) 타입(정수, 열거형)과 실수(real) 타입(부동 소수점, 고정 소수점)을 포함합니다. 모든 스칼라 타입은 순서가 정해져 있어 모든 관계 연산자가 미리 정의되어 있습니다.
    • 이산 타입 (discrete type): ‘이산(discrete)’이란 값이 연속적(continuous)이지 않고 하나하나 명확히 구분된다는 의미입니다. 예를 들어 실수 타입(real type)은 1.1과 1.2 사이에 1.11과 같은 무한히 많은 값이 존재하지만, 이산 타입은 1과 2 사이에 다른 정수 값이 없듯이 값이 뚝뚝 떨어져 있습니다. 이산 타입은 이러한 정수 타입열거형(enumeration) 타입을 포함합니다.
    • 실수 타입 (real type): 부동 소수점(floating point) 타입고정 소수점(fixed point) 타입을 포함합니다.
  • 접근 타입 (access type): 다른 객체나 서브프로그램의 메모리 위치(주소)를 가리키는 값을 제공하는 타입입니다.

복합 타입 (composite type)

값이 여러 개의 컴포넌트(component) 값들로 구성되는 타입입니다. 복합 타입에는 다음이 포함됩니다.

  • 배열 타입 (array type): 동일한 타입의 컴포넌트들로 구성된 집합입니다.
  • 레코드 타입 (record type) 및 레코드 확장 (record extensions): 서로 다른 타입의 명명된 컴포넌트들로 구성된 집합이며, 타입 확장을 통해 상속을 지원할 수 있습니다.
  • 인터페이스 타입 (interface type): 다중 상속을 지원하는 추상 태그드 타입입니다.
  • 태스크 타입 (task type): 독립적인 동시 실행(concurrency) 흐름을 나타내는 타입입니다.
  • 보호 타입 (protected type): 공유 데이터에 대한 상호 배타적인 접근을 제어하는 타입입니다.

전용 타입(private type)태그드 타입(tagged type)은 이 기본 계층 구조에서 다음과 같이 분류됩니다.

  • 전용 타입 (private type): private type 선언으로 정의되며 구현이 숨겨진 타입으로 정보 은닉에 사용됩니다. Ada의 공식 분류 체계는 이러한 타입을 복합 타입으로 간주합니다.

  • 태그드 타입: 이는 객체 지향 프로그래밍을 지원하기 위한 속성으로, 주로 레코드 타입이나 전용 타입에 적용됩니다. 태그드 타입 역시 복합 타입의 하위 분류에 명확히 포함됩니다.

본 장의 4.2절에서 4.5절까지는 기본 및 복합 타입의 구조적 측면을 다룹니다. 이어서 4.7절에서는 이러한 타입들 가운데 추상화와 객체 지향 프로그래밍에 필수적인 전용 타입과 태그드 타입의 용도와 특성을 상세히 학습합니다.[^ref]

4.1.8 강타입 시스템 (strong typing)

Ada 언어는 강타입 시스템(strong typing)을 채택하고 있습니다. 이는 언어의 타입 규칙이 프로그램 실행 전에, 즉 컴파일 시점에서 엄격하게 적용됨을 의미합니다.

강타입 시스템에서는 서로 다른 타입의 값들이 논리적으로 호환되지 않는 한 암시적으로(implicitly) 혼용될 수 없습니다. 예를 들어, Integer 타입의 객체와 Float 타입의 객체는 + 연산자로 직접 더할 수 없습니다.

이러한 규칙은 동일한 부모 타입에서 파생된 타입들에도 동일하게 적용됩니다. 예를 들어, 음수가 아닌 거리(미터)와 질량(킬로그램)을 나타내는 타입을 다음과 같이 Float을 기반으로 선언했다고 가정합니다.

-- Float'last는 Float 타입이 표현할 수 있는 가장 큰 값을 의미합니다.
type Meter    is new Float range 0.0 .. Float'last;
type Kilogram is new Float range 0.0 .. Float'last;

MeterKilogram은 둘 다 내부적으로는 Float으로 표현되고 동일한 범위 제약을 갖지만, Ada 컴파일러는 이 둘을 서로 구별되는(distinct) 타입으로 간주합니다. 따라서 Meter 타입 변수에 Kilogram 타입 변수의 값을 직접 할당하거나, 두 타입의 변수를 직접 더하는 연산은 허용되지 않습니다. 이는 거리를 나타내는 값과 질량을 나타내는 값을 혼용하는 것을 컴파일 시점에 방지합니다. 또한, range 0.0 .. Float'last 제약으로 인해 음수 값을 할당하려는 시도 역시 컴파일 시점 또는 실행 시점에 감지됩니다.

Ada의 타입 검사는 타입 불일치로 인해 발생할 수 있는 데이터 손상이나 논리적 오류를 실행 시점(run-time)이 아닌 컴파일 시점(compile-time)에 발견하도록 합니다.

서로 다른 타입 간의 연산이나 할당을 수행해야 한다면(단위 변환 등), 명시적인 타입 변환(explicit type conversion) 구문을 사용해야 합니다. 이러한 타입 변환의 구체적인 규칙은 본 장의 4.8절에서 자세히 다룹니다.

4.1.9 타입 뷰 (Views of a Type)

Ada 언어에서 하나의 타입(type)은 여러 가지 다른 뷰(view)를 가질 수 있습니다. 타입 뷰는 해당 타입에 대한 특정 관점이나 표현을 의미하며, 각 뷰마다 해당 타입에 적용할 수 있는 연산(operation)의 집합이 다를 수 있습니다.

타입 뷰는 타입을 정의하고 사용하는 단계에서 정보 은닉(information hiding), 재귀적 데이터 구조 정의, 구현 명세 분리 등의 목적을 위해 사용됩니다.

주요 타입 뷰는 다음과 같습니다.

  1. 불완전 뷰 (Incomplete View): 불완전 타입 선언(incomplete type declaration) (예: type Node;)을 통해 표현됩니다. 이는 타입의 이름만 먼저 알리고 실제 정의는 나중에 제공하는 방식으로, 접근 타입(access type)과 함께 재귀적이거나 상호 의존적인 데이터 구조(예: 연결 리스트의 노드)를 정의할 때 사용됩니다 (4.3절 참고). 이 뷰에서는 타입의 이름 외에 내부 구조나 연산에 대한 정보가 제한적입니다.

  2. 부분 뷰 (Partial View): 전용 타입 선언(private type declaration) (예: type Counter is private;)이나 전용 확장 선언(private extension declaration)을 통해 표현됩니다. 이 뷰는 패키지(package) 외부 사용자에게 타입의 이름과 명시적으로 제공된 연산만을 노출시키고, 타입의 실제 내부 구현(full view)은 숨깁니다. 이는 정보 은닉과 캡슐화를 지원하는 메커니즘입니다 (4.7.1절 참고).

  3. 완전 뷰 (Full View): 타입의 완전한 정의(예: type Integer is range ...;, type Point is record ...;)를 나타냅니다. 이 뷰는 타입이 가질 수 있는 값의 집합과 모든 기본 연산을 포함한 타입의 모든 세부 사항을 명시합니다.

Ada는 동일한 타입이라도 사용되는 문맥(context)에 따라 다른 뷰를 제공함으로써, 타입의 정의를 점진적으로 구체화하거나 구현 세부 사항을 숨길 수 있는 메커니즘을 제공합니다.

4.2 스칼라 타입 (scalar type)

4.1.2절에서 설명했듯이[^ref], 스칼라 타입(scalar type)은 기본 타입(elementary type)의 한 분류입니다. 스칼라 타입의 값은 논리적으로 더 이상 분해될 수 없으며, 모든 스칼라 타입은 순서가 정해져 있습니다. 이는 모든 스칼라 타입의 값들에 대해 <, <=, >, >=와 같은 관계 연산자(relational operator)가 미리 정의되어 있음을 의미합니다.

스칼라 타입은 그 특성에 따라 열거형 타입, 정수 타입, 실수 타입의 세 가지로 분류됩니다.

4.2.1 열거형 타입 (enumeration type)

열거형 타입은 프로그래머가 명시적으로 나열한 값들의 순서 있는 집합을 정의합니다. 미리 정의된 예로는 Boolean 타입(값이 FalseTrue)과 Character 타입(값이 'A', 'B' 등)이 있습니다.

사용자는 다음과 같이 자신만의 열거형 타입을 정의할 수 있습니다.

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

이 타입을 사용하여 변수를 선언하고 값을 조작할 수 있습니다.

with Ada.Text_IO;

procedure main is
  type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
  today : constant Day := Tue;
  tomorrow : Day;

begin
  tomorrow := Day'succ(today);  -- Today의 다음 값인 Wed가 tomorrow에 할당됨
  -- "내일은 WED" 출력 (Day'image 사용)
  Ada.Text_IO.put_line ("내일은 " & Day'image(tomorrow));
end main;

열거형 타입은 ‘이산 타입(discrete type)’으로 분류됩니다. 각 값은 0부터 시작하는 ‘위치 번호(position number)’를 가집니다. 예를 들어, Day'pos(Mon)0이고, Day'pos(Tue)1입니다. 반대로 'val 속성을 사용하여 위치 번호로부터 값을 얻을 수도 있습니다: Day'val(0)Mon입니다.

열거형 타입에는 다음과 같은 유용한 속성들이 미리 정의되어 있습니다.

  • 'pos(value): 주어진 값의 위치 번호(정수)를 반환합니다.
  • 'val(number): 주어진 위치 번호에 해당하는 값을 반환합니다.
  • 'succ(value): 주어진 값의 다음 값을 반환합니다. 마지막 값이면 Constraint_Error가 발생합니다.
  • 'pred(value): 주어진 값의 이전 값을 반환합니다. 첫 값이면 Constraint_Error가 발생합니다.
  • 'first: 타입(또는 서브타입)의 첫 번째 값을 반환합니다.
  • 'last: 타입(또는 서브타입)의 마지막 값을 반환합니다.
  • 'range: 타입(또는 서브타입)의 범위('first .. 'last)를 나타냅니다.
  • 'image(value): 주어진 값에 대한 문자열 표현을 반환합니다. 식별자는 대문자로, 문자 리터럴은 작은따옴표를 포함하여 문자열로 반환됩니다(예: "MON", "'A'").
Day'pos(Mon)   -- 0을 반환
Day'val(0)     -- Mon을 반환
Day'succ(Mon)  -- Tue를 반환 (다음 값)
Day'pred(Tue)  -- Mon을 반환 (이전 값)
Day'first      -- Mon을 반환 (범위의 첫 값)
Day'last       -- Sun을 반환 (범위의 마지막 값)
Day'range      -- Mon .. Sun 범위
Day'image(Mon) -- "MON"

4.2.2 정수 타입 (integer type)

정수 타입은 수학적인 정수 값을 표현합니다. Ada는 미리 정의된 Integer 타입을 제공하며, Natural (0 또는 양의 정수) 및 Positive (양의 정수)와 같은 서브타입도 제공합니다.

사용자는 range 키워드를 사용하여 원하는 범위의 정수 타입을 직접 정의할 수 있습니다.

type Sensor_Reading is range -1000 .. 1000;
type Page_Count is range 1 .. 500;

또한 mod 키워드를 사용하여, 정해진 값을 초과하면 순환하는(wrap-around) 모듈러(modular) 타입(부호 없는 정수)을 정의할 수 있습니다.

type Byte is mod 256; -- 0..255 범위, 255 + 1 = 0

정수 타입 역시 ‘이산 타입(discrete type)’에 속합니다. 열거형과 마찬가지로 ‘위치 번호’를 가지지만, 정수 타입의 위치 번호는 값 자체와 같습니다 (예: Integer'pos (10)는 10입니다).

4.2.3 실수 타입 (real type)

실수는 정수, 분수로 표현 가능한 유리수(예: 0.5, -0.75), 그리고 분수로 표현할 수 없는 무리수(예: π, √2)를 모두 포함하는 수 체계입니다. 컴퓨터는 유한한 비트를 사용하므로, Ada의 실수 타입은 유한한 비트로 인해 대부분의 실수를 근사치로 표현하지만, 일부 값(예: 특정 단위의 정수배)은 정확히 표현할 수 있습니다. 오차 한계를 명시하는 방식에 따라 두 가지 종류로 나뉩니다.

부동 소수점 (floating point) 타입

digits 키워드를 사용하여 유효 숫자의 개수, 즉 상대적인 오차 한계(relative error bounds)를 명시합니다. 이는 넓은 범위의 수를 표현할 수 있습니다.

type Mass is digits 8;

고정 소수점 (fixed point) 타입

delta 키워드를 사용하여 소수점 이하의 절대적인 오차 한계(absolute error bounds)를 명시합니다. 이 타입은 특정 정밀도를 기준으로 값을 표현하는 데 중점을 둡니다.

내부적으로, 고정 소수점 타입의 모든 값은 small이라고 불리는 최소 단위 값의 정수배로 구성되며, 이 small 값은 프로그래머가 지정한 delta보다 작거나 같아야 합니다 (small <= delta). 이렇게 small의 정수배가 되는 값들은 이진 표현 오차 없이 정확하게 표현되며, 그 외의 값들은 가장 가까운 small의 배수로 근사됩니다.

delta의 배수 값이 항상 정확하게 표현되는지는 고정 소수점 타입의 종류에 따라 다릅니다.

  • 십진 고정 소수점 (decimal fixed point) 타입: 이 타입은 delta가 반드시 10의 거듭제곱이어야 하며, smalldelta와 같습니다. 따라서 delta의 모든 정수배는 (small의 배수이므로) 정확하게 표현됩니다.

    type Money is delta 0.01 digits 15; -- small은 0.01. 0.01의 배수는 정확히 표현됨
    
  • 보통 고정 소수점 (ordinary fixed point) 타입: 이 타입은 delta가 10의 거듭제곱일 필요가 없으며, small은 컴파일러가 delta 요구사항을 만족하도록 선택하는 구현 정의된 2의 거듭제곱 값입니다 (지정되지 않은 경우). 만약 delta 자체가 2의 거듭제곱이 아니거나 small과 호환되지 않으면, delta의 배수가 small의 배수가 아닐 수 있어 정확히 표현되지 않을 수 있습니다. 개발자는 타입 선언 후에 속성 정의 절 (for Type_Name'small use ...;)을 사용하여 small 값을 직접 지정할 수도 있습니다. 보통 고정 소수점 타입 선언에는 반드시 range 절이 포함되어야 합니다.

    -- delta는 0.1, Range 필수
    type Precise_Measure is delta 0.1 range -100.0 .. 100.0;
    
    -- 컴파일러는 small을 0.0625 (1/16) 등으로 선택할 수 있음.
    for Precise_Measure'small use 0.0625; -- small을 명시적으로 1/16로 지정 가능
    

    이 경우 small 값이 0.0625로 결정되었으므로, Precise_Measure 타입은 0.0625의 정수배가 아닌 값, 예를 들어 0.1 (0.0625 * 1.6)은 정확하게 표현할 수 없으며 가장 가까운 small의 배수 값으로 근사됩니다.

4.3 접근 타입 (access type)

접근 타입(access type)은 Ada의 기본 타입(elementary type) 중 하나로, 다른 객체(object)나 서브프로그램(subprogram)의 메모리 위치에 대한 간접적인 접근을 제공하는 값을 정의합니다. 이는 다른 프로그래밍 언어의 포인터(pointer)나 참조(reference)와 유사한 개념이며, Ada는 접근 타입에 대한 특정 규칙을 적용합니다.

접근 타입의 종류

접근 타입은 지정하는 대상에 따라 두 가지 종류로 나뉩니다.

  1. 객체 접근 타입 (Access-to-Object Types)
    메모리 내의 데이터 객체를 가리킵니다. 이 타입은 다시 세부적으로 분류됩니다.
    • 풀-특정 접근 타입 (Pool-Specific Access Types): 특정 저장소 풀(storage pool)에 할당된 객체만 지정할 수 있습니다. access Subtype_Name 형태로 선언됩니다.
    • 일반 접근 타입 (General Access Types): 저장소 풀의 객체뿐만 아니라, aliased 키워드로 선언된 일반 변수나 객체의 컴포넌트 등 더 넓은 범위의 객체를 지정할 수 있습니다.
      • access all Subtype_Name: 지정된 객체를 읽고 수정할 수 있는 변수 접근(access-to-variable) 타입입니다.
      • access constant Subtype_Name: 지정된 객체를 읽을 수만 있고 수정할 수 없는 상수 접근(access-to-constant) 타입입니다.
  2. 서브프로그램 접근 타입 (Access-to-Subprogram Types)
    프로시저(procedure)나 함수(function)와 같은 서브프로그램을 가리킵니다. 이 타입은 서브프로그램의 매개변수 및 반환 타입 프로파일(profile)을 지정하여 정의합니다.
    type Integrand is access function (x : Float) return Float;
    

Null 값과 접근 값 얻기

모든 접근 타입은 아무것도 가리키지 않음을 나타내는 null 값을 포함합니다. 이는 접근 타입의 기본 초기값입니다. null이 아닌 접근 값은 다음 방법으로 얻을 수 있습니다.

  • 할당자 (allocator): new 키워드를 사용하여 저장소 풀에 새로운 객체를 생성하고, 이 객체를 가리키는 접근 값을 반환받습니다.
  • 'access 속성(attribute): 'access 속성은 이미 선언된 객체나 서브프로그램의 주소를 가리키는 접근 값을 반환합니다. 이 속성을 사용하기 위해서는, 대상이 되는 객체(또는 레코드 컴포넌트)가 aliased 키워드로 선언되어야 합니다. aliased는 컴파일러에게 해당 객체가 'access 속성을 통해 참조될 수 있음을 알리는 표시입니다.

aliased'Access 구문:

  • 일반 변수:

    My_Var : aliased Integer; -- 'aliased'로 변수 선언
    Var_Ptr : access all Integer := My_Var'Access; -- 'Access로 참조
    
  • 레코드 컴포넌트: 4.5절에서 다루지만, 레코드의 특정 필드만 aliased로 선언할 수도 있습니다.

    type Node is record
      Data : aliased String(1 .. 10);
    end record;
    
    My_Node : Node;
    Data_Ptr : access all String := My_Node.Data'Access;
    
  • 서브프로그램:

    type Integrand is access function (x : Float) return Float;
    My_Func_Ptr : Integrand := My_Function'access;
    

    'access 속성은 컴파일 시점에 접근성 규칙(accessibility rules) 검사를 통과하는 접근 값을 생성합니다. (접근성 규칙을 검사하지 않는 'Unchecked_Access 속성도 존재하며, 이는 본 절의 “안전성 관련 규칙” 하위 절에서 다룹니다.)

재귀적 데이터 구조

접근 타입은 자기 자신을 포함하는 데이터 구조, 즉 재귀적(recursive)이거나 상호 의존적인(mutually dependent) 데이터 구조(예: 연결 리스트, 트리)를 정의하는 데 사용될 수 있습니다. 이는 타입의 완전한 정의를 뒤로 미루는 불완전한 타입 선언(incomplete type declaration)과 함께 사용되기도 합니다.

안전성 관련 규칙

C나 C++의 포인터와 달리, Ada의 접근 타입은 포인터 연산(주소에 정수를 더하는 등)을 허용하지 않습니다.

또한, Ada는 접근성 규칙(accessibility rules)을 통해 접근 값이 가리키는 객체보다 더 오래 살아남지 않도록(outlive) 보장합니다. 이 규칙은 이미 해제된 메모리를 가리키는 접근 값, 즉 댕글링 참조(dangling reference)와 관련된 오류를 컴파일 시점에 정적으로 방지하는 것을 목적으로 합니다.

'Unchecked_Access 속성

Ada는 이러한 정적 안전성 규칙을 우회하는 메커니즘으로 'Unchecked_Access 속성을 제공합니다.

'Access 속성이 aliased로 명시된 객체에 대해서만 접근성 규칙을 검사하며 사용 가능한 것과 달리, 'Unchecked_Accessaliased로 선언되지 않은 객체를 포함하여 모든 객체에 대한 접근 값을 반환할 수 있습니다.

이 속성은 컴파일러의 접근성 검사를 수행하지 않으므로, 프로그래머의 책임 하에 댕글링 참조가 발생하지 않도록 관리해야 합니다. 'Unchecked_Access는 저수준(low-level) 프로그래밍이나 13장에서 다룰 외부 C 라이브러리와의 연동처럼 정적 검사를 우회해야 할 필요가 있을 때 사용됩니다.

이 속성과 관련된 예제는 13.2.3절에서 C 포인터 연동과 함께 다룹니다.

4.4 배열 타입 (array type)

배열 타입(array type)은 Ada의 복합 타입(composite type) 중 하나입니다. 배열 객체는 모두 동일한 서브타입을 가지는 컴포넌트(component)들의 집합으로 구성됩니다. 배열의 각 컴포넌트는 하나 이상의 인덱스(index) 값을 사용하여 접근하며, 이 인덱스 값은 명시된 이산 타입(discrete type)에 속합니다.

배열 타입은 인덱스의 범위가 타입 선언 시점에 고정되는지 여부에 따라 크게 두 가지로 나뉩니다.

1. 제약된 배열 타입 (Constrained Array Types)

제약된 배열 타입은 타입 선언 시 각 인덱스의 범위(bounds)가 명확하게 고정됩니다. 이 타입으로 선언된 모든 객체는 동일한 인덱스 범위와 크기를 가집니다.

  • 선언 구문:

    type <타입_이름> is array (<이산_서브타입_정의>, ...) of <컴포넌트_서브타입>;
    

    여기서 <이산_서브타입_정의>Integer range 1 .. 10 또는 Day range Mon .. Fri와 같이 명시적인 범위를 가집니다.

  • 예시:

    type Table is array (1 .. 10) of Integer;        -- 인덱스 1부터 10까지
    type Schedule is array (Day range Mon .. Fri) of Boolean; -- Day 타입의 Mon부터 Fri까지
    

2. 제약되지 않은 배열 타입 (Unconstrained Array Types)

제약되지 않은 배열 타입은 타입 선언 시 인덱스의 범위가 확정되지 않습니다. 범위는 <> (박스라고 불림) 기호로 표시되며, 이 타입의 객체를 선언하거나 생성할 때 비로소 각 객체의 실제 범위가 결정됩니다.

  • 선언 구문:

    type <타입_이름> is array (<인덱스_서브타입> range <>, ...) of <컴포넌트_서브타입>;
    

    여기서 <인덱스_서브타입>Integer, Positive 등 인덱스로 사용될 이산 타입을 지정합니다.

  • 예시:

    type Vector is array (Integer range <>) of Float;   -- 정수 인덱스, 범위 미정
    type Matrix is array (Integer range <>, Integer range <>) of Float; -- 2차원, 범위 미정
    

    제약되지 않은 타입의 객체를 선언할 때는 반드시 범위를 지정해야 합니다.

    My_Vector : Vector(1 .. 100); -- 객체 선언 시 범위 지정
    Another_Vector : Vector(-10 .. 10);
    

문자열 타입 (String Types)

Ada에는 미리 정의된 제약되지 않은 배열 타입으로는 String, Wide_String, Wide_Wide_String이 있습니다. 이들은 각각 Character, Wide_Character, Wide_Wide_Character를 컴포넌트로 가지며, Positive (1 이상의 정수)를 인덱스로 사용합니다.

배열 연산 및 속성

배열 타입에는 인덱스를 사용한 컴포넌트 접근(My_Array(Index)), 할당, 비교 연산 등이 정의됩니다. 또한 'first, 'last, 'length, 'range와 같은 속성(attribute)을 통해 배열의 경계와 길이에 대한 정보를 얻을 수 있습니다. 1차원 배열의 경우, 슬라이스(slice) 연산(My_Array(L .. R))을 통해 배열의 일부를 추출할 수도 있습니다.

익명 배열 타입 (Anonymous Array Types)

배열 타입은 type My_Array is array (...) of ...; 구문을 사용하여 명시적으로 이름을 부여하여 선언합니다. 그러나 Ada는 타입의 이름 없이 익명으로 배열 타입을 정의하는 것을 허용합니다.

익명 배열 타입은 다른 타입 정의 내부(예: 레코드 컴포넌트)나 객체 선언에서 직접 사용됩니다.

구문

익명 배열 타입 정의는 명시적인 타입 선언과 유사하지만, type <이름> is 부분이 생략됩니다.

-- 레코드 컴포넌트로 익명 배열 타입 사용
type Data_Packet is
  record
    Header : Header_Type;
    -- 익명 배열 타입 정의 (이름 없음)
    Payload : array (1 .. 128) of Byte;
  end record;

-- 변수 선언 시 익명 배열 타입 사용
My_Buffer : array (1 .. 10) of Character;

특징 및 제약

  1. 타입 호환성: 익명 배열 타입으로 선언된 객체들은 서로 다른 타입으로 간주됩니다. 배열의 구조(인덱스 범위, 컴포넌트 타입)가 동일하더라도, 각 익명 배열 선언은 고유한 타입을 정의합니다. 따라서 서로 다른 익명 배열 타입의 객체 간에는 직접적인 할당이나 비교가 허용되지 않습니다.

    Buffer_A : array (1 .. 10) of Integer;
    Buffer_B : array (1 .. 10) of Integer;
    -- Buffer_A := Buffer_B; -- 컴파일 오류 (서로 다른 익명 타입)
    

    이러한 타입 비호환성을 피하려면 명시적인 타입 선언(type My_Int_Array is array...)을 사용합니다.

  2. 서브프로그램 파라미터: 익명 배열 타입은 서브프로그램의 파라미터 타입으로 직접 사용될 수 없습니다. 서브프로그램에 배열을 전달하려면 명시적으로 선언된 배열 타입을 사용해야 합니다.

활용

익명 배열 타입은 다른 구조(주로 레코드)의 일부로 사용되거나, 특정 범위 내에서만 사용될 배열 객체를 선언할 때 사용될 수 있습니다. 타입 호환성 제약으로 인해, 재사용되거나 서브프로그램 간에 전달되어야 하는 배열에는 명시적인 타입 선언이 사용됩니다.

4.5 레코드 타입 (Record Types)

레코드 타입(record type)은 Ada의 복합 타입(composite type) 중 하나입니다. 레코드 객체는 이름(identifier)으로 식별되는 컴포넌트(component)들의 집합으로 구성됩니다. 배열 타입과 달리, 레코드의 컴포넌트들은 서로 다른 타입을 가질 수 있습니다. 레코드 객체의 값은 각 컴포넌트 값들의 조합입니다.

기본 레코드 선언

레코드 타입은 recordend record 예약어 사이에 컴포넌트 목록을 정의하여 선언합니다. 각 컴포넌트는 이름, 타입, 그리고 선택적으로 기본 초기값을 가집니다.

4.3절에서 설명했듯이, 레코드의 특정 컴포넌트(필드)가 'access 속성을 통해 참조될 수 있도록 하려면, 해당 컴포넌트를 aliased 키워드로 선언해야 합니다.

type Date is
   record
      Day   : Integer range 1 .. 31;
      Month : Month_Name;
      Year  : Integer range 0 .. 4000;
   end record;

type Complex is
   record
      Re : Float := 0.0; -- 기본 초기값 지정
      Im : Float := 0.0;
   end record;

-- 'aliased' 컴포넌트를 포함하는 레코드 예시
type Sensor_Node is
  record
    ID   : aliased Integer; -- 이 필드는 외부에서 'access 가능
    Value: Float;
  end record;

Null 레코드 (Null Record)

Ada는 컴포넌트가 전혀 없는 레코드 타입도 정의할 수 있습니다. 이를 null 레코드라고 하며, is null record; 구문을 사용하여 선언합니다.

type Placeholder is null record;

Null 레코드 타입의 값은 유일하며, null record 애그리게이트(aggregate)로 표현할 수 있습니다. Null 레코드는 타입 계층 구조에서 특정 위치를 표시하거나, 태그드 타입(tagged type) 계층의 루트(root) 타입으로 사용되는 등, 상태나 데이터를 저장할 필요 없이 타입 자체의 존재가 의미 있는 경우에 활용될 수 있습니다.

판별자 (Discriminants)

레코드 타입은 판별자(discriminant)를 가질 수 있습니다. 판별자는 레코드 타입의 매개변수와 같은 역할을 하며, 레코드 객체가 생성될 때 값이 결정됩니다. 판별자의 값은 레코드 내 다른 컴포넌트의 크기나 존재 여부에 영향을 줄 수 있습니다. 판별자는 타입 선언 시 괄호 안에 정의됩니다.

type Buffer(Size : Natural := 100) is -- Size가 판별자
   record
      Data : String(1 .. Size); -- 컴포넌트 크기가 판별자에 의존
   end record;

가변부 (Variant Parts)

레코드 타입은 가변부(variant part)를 가질 수 있습니다. 가변부는 case 문과 유사한 구조를 사용하여 특정 판별자의 값에 따라 레코드가 다른 컴포넌트들을 포함하도록 합니다.

type Peripheral(Unit : Device) is -- Device는 (Printer, Disk) 등
   record
      Status : State;
      case Unit is
         when Printer =>
            Lines_Printed : Natural;
         when Disk =>
            Cylinder : Track_Number;
      end case;
   end record;

위 예시에서 Peripheral 객체는 Unit 판별자의 값에 따라 Lines_Printed 컴포넌트 또는 Cylinder 컴포넌트 중 하나를 가집니다.

레코드 연산

레코드 객체의 컴포넌트는 점(.) 표기법을 사용하여 접근합니다 (예: My_Date.Month). 레코드 타입의 값은 레코드 애그리게이트(record aggregate)를 사용하여 생성할 수 있습니다. 제한되지 않은(nonlimited) 레코드 타입의 경우, 할당 연산과 동등 비교 연산이 미리 정의되어 있습니다.

4.6 타입을 정의하는 다양한 방법

4.6.1 서브타입 (Subtypes): 기존 타입에 제약 추가

서브타입(subtype)은 이미 존재하는 타입(이를 기반 타입(base type)이라고 함)에 제약(constraint)을 추가하여, 해당 타입이 가질 수 있는 값의 집합을 제한하는 방법입니다. 서브타입은 완전히 새로운 타입을 만드는 것이 아니라, 기존 타입의 값들 중 특정 조건을 만족하는 부분 집합에 새로운 이름을 부여하는 것입니다.

서브타입 선언

서브타입은 subtype 키워드를 사용하여 선언합니다.

subtype <서브타입_이름> is <기반_서브타입_이름> [제약];

<기반_서브타입_이름>은 제약을 가할 대상 타입(또는 이미 존재하는 다른 서브타입)의 이름입니다. [제약] 부분은 선택 사항이며, 값의 범위를 제한하는 조건을 명시합니다.

  • 예시 (스칼라 타입):

    subtype Day_Number is Integer range 1 .. 31;
    subtype Weekday is Day range Mon .. Fri; -- Day는 열거형 타입
    
  • 예시 (배열 타입):

    subtype Small_Vector is Vector(1 .. 5); -- Vector는 제약되지 않은 배열 타입
    
  • 예시 (레코드 타입):

    subtype Printer_Device is Peripheral(Unit => Printer); -- Peripheral은 판별자를 가진 레코드
    

제약의 종류

서브타입에 적용될 수 있는 주요 제약은 다음과 같습니다.

  • 범위 제약 (range_constraint): 스칼라 타입의 값 범위를 제한합니다.
  • 인덱스 제약 (index_constraint): 제약되지 않은 배열 타입의 인덱스 범위를 지정합니다.
  • 판별자 제약 (discriminant_constraint): 제약되지 않은 레코드 타입의 판별자 값을 고정합니다.
  • Null 제외 (null_exclusion): 접근 타입에서 null 값을 제외합니다.

제약이 없는 서브타입 선언(subtype My_Int is Integer;)도 가능하며, 이는 단순히 기존 타입(서브타입)에 다른 이름을 부여하는 역할을 합니다.

타입(Type)과의 차이점

서브타입 선언은 새로운 타입을 만들지 않습니다. Day_Number는 여전히 Integer 타입이며, Weekday는 여전히 Day 타입입니다. 따라서 같은 기반 타입에서 파생된 다른 서브타입의 객체 간에는 (값이 제약을 만족한다면) 직접적인 할당이나 연산이 가능합니다. 이는 4.6.2절에서 배울 파생 타입(derived type)이 완전히 새로운 타입을 만드는 것과 대조됩니다.

서브타입의 이점

서브타입은 다음과 같은 이점을 제공합니다.

  1. 가독성 향상: Day_Number와 같은 의미 있는 이름을 사용하여 코드의 의도를 명확히 할 수 있습니다.
  2. 신뢰성 향상: 서브타입의 제약 조건은 컴파일 시점 또는 실행 시점에 검사됩니다. 만약 어떤 값이 서브타입의 제약을 위반하면 Constraint_Error 예외가 발생하여 오류를 조기에 감지할 수 있습니다.

4.6.2 파생 타입 (Derived Types): 상속을 통한 새로운 타입 생성

파생 타입(derived type) 선언은 이미 존재하는 타입(이를 부모 타입(parent type)이라고 함)을 기반으로, 완전히 새로운 구별되는 타입을 만드는 방법입니다. 이는 4.6.1절에서 다룬 서브타입(subtype)이 기존 타입의 부분 집합에 이름을 부여하는 것과는 근본적으로 다릅니다.

파생 타입 선언

파생 타입은 new 키워드를 사용하여 선언합니다.

type <새_타입_이름> is new <부모_서브타입_이름> [제약];

<부모_서브타입_이름>은 새로운 타입의 기반이 될 기존 타입(또는 서브타입)의 이름입니다. 선택적으로 제약을 추가하여 파생된 타입의 첫 번째 서브타입의 범위를 제한할 수도 있습니다.

  • 예시:
    type Weight is new Float range 0.0 .. 1000.0;
    type Distance is new Float range 0.0 .. 1000.0;
    type Counter is new Integer range 0 .. Integer'Last;
    

상속과 특성

새로운 파생 타입은 부모 타입의 특성을 상속(inherit)합니다.

  1. 값의 집합: 부모 타입이 가질 수 있는 값의 집합을 그대로 물려받습니다 (단, 서브타입 제약에 따라 범위가 달라질 수 있습니다).
  2. 연산: 부모 타입에 대해 정의된 미리 정의된 연산자 (예: +, -, =)와 사용자가 정의한 기본 연산(primitive operations)들을 상속받습니다. 상속된 연산의 프로파일(profile)은 부모 타입을 새로운 파생 타입으로 체계적으로 대체하여 조정됩니다.

서브타입과의 핵심적인 차이점: 새로운 타입 생성

파생 타입 선언의 가장 중요한 특징은 완전히 새로운 타입을 만든다는 것입니다.

  • WeightDistance는 둘 다 Float에서 파생되었지만, Ada 컴파일러는 이 둘을 서로 호환되지 않는 별개의 타입으로 간주합니다.
  • 따라서 Weight 타입 변수에 Distance 타입 변수의 값을 직접 할당하거나 연산하는 것은 허용되지 않습니다. 이는 4.1.3절에서 설명한 강타입 시스템(strong typing) 규칙에 따른 것입니다.
  • 만약 두 타입 간의 변환이 필요하다면, 반드시 명시적인 타입 변환(Weight(My_Distance_Value))을 사용해야 합니다.

이는 동일한 기반 타입의 서브타입 간에는 자유롭게 값이 호환되는 서브타입과는 명확히 대조됩니다.

파생 타입의 목적

파생 타입은 다음과 같은 목적을 위해 사용됩니다.

  1. 개념적 분리: 비록 내부적인 표현 방식(예: Float)이 같더라도, 논리적으로 다른 개념(예: WeightDistance)을 나타내는 타입을 분리하여 프로그램의 의미를 명확하게 하고 오류를 방지합니다.
  2. 타입 확장: 4.7.2절에서 다룰 태그드 타입(tagged type)의 경우, 파생 타입을 선언하면서 새로운 컴포넌트를 추가하여 타입을 확장(상속)하는 기반이 됩니다.

4.7 추상화와 객체 지향을 위한 타입

4.7.1 전용 타입 (Private Types): 정보 은닉과 캡슐화

전용 타입(private type)은 Ada에서 정보 은닉(information hiding)과 캡슐화(encapsulation)를 구현하는 핵심적인 메커니즘입니다. 전용 타입으로 선언된 타입은 패키지 외부 사용자에게는 그 이름과 패키지가 제공하는 연산(서브프로그램)만 공개되고, 타입의 실제 내부 구조(예: 레코드 필드)는 숨겨집니다.

선언과 구현 분리: 부분 뷰와 풀 뷰

전용 타입은 패키지 명세부(specification)에서 두 부분으로 나뉘어 정의됩니다.

  1. 가시부 (Visible Part): 패키지 사용자에게 공개되는 부분입니다. 여기서는 타입을 private 키워드를 사용하여 선언합니다. 이 선언은 타입의 부분 뷰(partial view)를 제공합니다. 사용자는 이 뷰를 통해 타입의 존재와 이름만 알 수 있습니다.

    package Stacks is
       type Stack is private; -- 부분 뷰 (구현 숨김)
       procedure Push(S : in out Stack; Item : Integer);
       procedure Pop(S : in out Stack; Item : out Integer);
       -- ... other operations ...
    private -- private 키워드 뒤부터는 패키지 내부 구현 영역
       Max_Size : constant := 100;
       type Integer_Array is array (1 .. Max_Size) of Integer;
       type Stack is -- 풀 뷰 (실제 구현 정의)
          record
             Data : Integer_Array;
             Top  : Integer range 0 .. Max_Size := 0;
          end record;
    end Stacks;
    
  2. 비공개부 (Private Part): 패키지 명세부의 private 키워드 뒤에 오는 부분입니다. 여기서는 가시부에서 private으로 선언된 타입의 실제 구현, 즉 풀 뷰(full view)를 정의합니다. 위 예시에서는 Stack이 실제로는 레코드 타입임을 정의했습니다. 이 풀 뷰는 패키지 외부에서는 접근할 수 없습니다.

Ada 언어 규칙에 따라, 타입의 부분 뷰는 (그 풀 뷰가 무엇이든) 복합 타입(composite type)으로 간주됩니다.

제한된 연산

패키지 외부 사용자는 전용 타입 객체에 대해 다음과 같은 연산만 수행할 수 있습니다.

  • 패키지의 가시부에 명시적으로 선언된 서브프로그램(예: Push, Pop).
  • 할당 (:=).
  • 동등 비교 (=, /=).

제한된 전용 타입 (Limited Private Types)

만약 타입 선언 시 limited private 키워드를 사용하면, 할당과 동등 비교 연산마저 금지됩니다.

type File_Handle is limited private;

limited private 타입의 객체는 오직 해당 패키지가 명시적으로 제공하는 서브프로그램을 통해서만 조작될 수 있습니다. 이는 타입의 상태 변경 방식을 더욱 엄격하게 제어해야 할 때 유용합니다 (예: 파일 핸들러, 락(lock) 객체).

목적과 이점

전용 타입은 패키지 사용자에게 추상적인 인터페이스만을 제공하고 내부 구현 세부사항을 숨김으로써 다음과 같은 이점을 제공합니다.

  • 모듈성 향상: 패키지 사용자는 타입의 내부 구현에 의존하는 코드를 작성할 수 없으므로, 인터페이스와 구현이 분리됩니다.
  • 유지보수성 향상: 패키지 내부 구현(풀 뷰)이 변경되더라도, 인터페이스(부분 뷰와 제공된 연산)가 동일하게 유지된다면 패키지를 사용하는 외부 코드는 수정할 필요가 없습니다.
  • 캡슐화 강제: 데이터(타입의 내부 구조)와 해당 데이터를 조작하는 연산(서브프로그램)이 패키지라는 하나의 단위로 캡슐화됩니다.

4.7.2 태그드 타입과 타입 확장 (Tagged Types): 상속과 다형성

이 절은 Ada의 객체 지향 프로그래밍(Object-Oriented Programming, OOP)을 지원하는 핵심 기능인 태그드 타입과 타입 확장에 대해 설명합니다. 이 기능들은 전통적인 OOP의 상속(Inheritance)과 다형성(Polymorphism) 개념을 구현합니다.

태그드 타입(Tagged Type)의 정의

태그드 타입(Tagged Type)은 선언부에 tagged 예약어가 포함된 레코드 타입 또는 전용 타입을 말합니다. 인터페이스 타입이나 태스크 및 보호 타입도 태그드 타입이 될 수 있습니다.

‘태그드’라는 이름에서 알 수 있듯이, 태그드 타입의 모든 객체는 실행 시점에 해당 객체를 생성한 특정 타입을 식별하는 ‘태그(tag)’를 가집니다. 이 태그는 런타임 다형성을 구현하는 핵심적인 기반이 됩니다.

4.1.1절[^ref]에서 언급한 일반적인 ‘객체’(값을 가지는 실체)와 달리, 태그드 타입의 객체는 상태(데이터 컴포넌트)와 행위(기본 연산)를 결합한 OOP의 ‘객체’ 개념에 해당합니다.

타입 확장 (Type Extension)과 상속

타입 확장(Type Extension)은 Ada에서 상속을 구현하는 방식입니다. 태그드 타입은 다른 타입의 부모 타입이 될 수 있으며, 부모로부터 파생된 새로운 타입을 “타입 확장”이라고 부릅니다.

타입 확장은 new 키워드를 사용하여 부모 타입을 지정하고, with record 구문을 통해 새로운 컴포넌트를 추가하는 방식으로 정의합니다.

-- 부모 타입 정의
type Point is tagged
   record
      X, Y : Float;
   end record;

-- Point를 상속(확장)하는 자식 타입 정의
type Colored_Point is new Point with
   record
      Color : Some_Color_Type; -- 새로운 컴포넌트 추가
   end record;

Colored_Point 타입은 Point 타입의 X, Y 컴포넌트를 상속받으며, Color라는 새로운 컴포넌트를 가집니다. 또한 Point 타입을 위해 정의된 연산(기본 연산)들도 상속받으며, 이를 재정의(overriding)하거나 새로운 연산을 추가할 수 있습니다.

클래스-범위 타입과 다형성 (Polymorphism)

Ada는 다형성을 지원하기 위해 ‘클래스-범위 타입(Class-Wide Type)’이라는 개념을 사용합니다. T가 태그드 타입일 때, T'Class라는 속성은 T 타입 또는 T에서 파생된 모든 자손 타입을 포함하는 특별한 타입을 의미합니다.

이 클래스-범위 타입은 주로 서브프로그램의 매개변수나 접근 타입에 사용됩니다. 예를 들어, 매개변수가 T'Class 타입으로 선언되면, 해당 서브프로그램은 T 타입의 객체뿐만 아니라 T에서 파생된 모든 하위 타입(예: Colored_Point)의 객체를 인수로 받을 수 있습니다.

디스패칭 (Dispatching)

디스패칭(Dispatching)은 Ada에서 런타임 다형성이 동작하는 원리를 말합니다. 클래스-범위 타입(T'Class)의 객체를 대상으로 태그드 타입의 기본 연산을 호출하는 것을 ‘디스패칭 호출(dispatching call)’이라고 합니다.

디스패칭 호출이 발생하면, 프로그램은 컴파일 시점에 실행될 서브프로그램을 고정하지 않습니다. 대신, 프로그램은 실행 시점에 전달된 객체의 태그(tag)를 검사하여 해당 태그가 식별하는 실제 타입에 맞게 정의된 서브프로그램 본체를 선택하여 실행(dispatch)합니다.

procedure Draw(Item : in Point'Class) is ...
-- Item은 Point 또는 Point에서 파생된 모든 타입을 받을 수 있습니다.

My_Point : Point;
My_Colored_Point : Colored_Point;

Draw(My_Point);         -- Point의 Draw 연산 호출
Draw(My_Colored_Point); -- Colored_Point의 Draw 연산 호출 (디스패칭)

이처럼 태그드 타입, 타입 확장, 그리고 클래스-범위 타입을 통한 디스패칭은 Ada에서 유연하고 확장 가능한 객체 지향 설계를 가능하게 합니다.

4.7.3 인터페이스 타입 (Interface Types): 다중 상속 지원

인터페이스 타입(interface type)은 Ada에서 제한적인 형태의 다중 상속(multiple inheritance)을 지원하기 위해 도입된 특별한 종류의 추상 태그드 타입(abstract tagged type)입니다. 인터페이스는 구현(implementation) 없이 오직 연산의 명세(specification)만을 정의하는 계약(contract)과 같은 역할을 합니다.

인터페이스 타입 선언

인터페이스 타입은 interface 예약어를 사용하여 선언합니다. 다른 인터페이스들을 상속받을 때는 and 키워드를 사용합니다.

type Streamable is interface; -- 기본적인 인터페이스
procedure Read (Stream : access IO_Stream; Item : out Streamable) is abstract;
procedure Write(Stream : access IO_Stream; Item : in Streamable) is abstract;

type Serializable is interface and Streamable; -- Streamable 인터페이스를 상속
procedure Load (Item : out Serializable) is abstract;
procedure Save (Item : in Serializable) is abstract;

인터페이스 타입의 특징

  • 추상 타입: 모든 인터페이스 타입은 추상 타입입니다. 따라서 인터페이스 타입 자체의 객체를 직접 생성할 수는 없습니다.
  • 태그드 타입: 모든 인터페이스 타입은 태그드 타입입니다. 이는 클래스-범위 타입('Class)과 디스패칭(dispatching)을 지원함을 의미합니다.
  • 컴포넌트 없음: 인터페이스 타입은 데이터 컴포넌트(예: 레코드 필드)를 가질 수 없습니다.
  • 추상/Null 연산: 인터페이스 타입에 속한 모든 사용자 정의 기본 연산(primitive operation)은 반드시 추상 서브프로그램(abstract subprogram)이거나 아무 동작도 하지 않는 null 프로시저(null procedure)여야 합니다.

구현 상속이 아닌 명세 상속

일반적인 타입 확장(new ... with record ...)이 부모 타입의 구현(컴포넌트)과 연산을 모두 상속받는 것과 달리, 인터페이스 상속은 오직 연산의 명세(이름, 매개변수 프로파일)만을 상속합니다.

하나의 구체적인(non-interface) 태그드 타입, 태스크 타입, 또는 보호 타입은 하나의 부모 타입(타입 확장)과 여러 개의 인터페이스 타입(명세 상속)을 동시에 상속받을 수 있습니다.

type My_Data is new Parent_Type and Interface1 and Interface2 with
   record
      -- My_Data의 추가 컴포넌트
   end record;

-- My_Data는 Parent_Type의 구현과
-- Interface1, Interface2의 추상 연산 명세를 모두 상속받음.
-- Interface1과 Interface2의 모든 추상 연산에 대한 구현(overriding)을 제공해야 함.

이렇게 인터페이스를 구현하는 타입은 인터페이스로부터 상속받은 모든 추상 연산에 대해 구체적인 구현(서브프로그램 본체)을 제공해야 합니다.

특수 인터페이스

Ada는 동시성(concurrency) 제어와 관련된 특별한 인터페이스들도 정의합니다.

  • 동기화 인터페이스 (synchronized interface)
  • 태스크 인터페이스 (task interface)
  • 보호 인터페이스 (protected interface)

이러한 인터페이스들은 각각 태스크 타입이나 보호 타입만이 구현할 수 있도록 제한됩니다.

목적

인터페이스 타입은 서로 다른 타입 계층에 속한 타입들이 공통된 기능(연산 명세)을 공유하고 구현하도록 강제하는 강력한 추상화 메커니즘을 제공합니다. 이는 유연하고 확장 가능한 설계를 가능하게 합니다.

4.8 타입 변환 (type conversion)

4.1.3절에서 설명했듯이, Ada의 강타입 시스템은 서로 다른 타입 간의 암시적(implicit) 혼용을 엄격히 금지합니다. 프로그래머가 의도적으로 한 타입의 값을 다른 타입의 값으로 변경해야 할 때, 반드시 명시적 타입 변환(explicit type conversion) 구문을 사용해야 합니다.

타입 변환의 기본 구문은 변환하고자 하는 목표 타입(또는 서브타입)의 이름을 함수처럼 사용하는 것입니다.

Target_Type_Name (Value_Expression)

예를 들어, Integer 타입의 변수 iFloat 타입으로 변환하는 코드는 Float(i)와 같습니다.

타입 변환의 종류와 규칙

타입 변환은 모든 타입 간에 자유롭게 허용되지 않으며, Ada 언어 규칙에 의해 논리적으로 연관된 타입들 간에만 가능합니다. 이 절에서는 주요 변환 유형을 다룹니다.

  1. 숫자 타입 간의 변환: Integer 타입을 Float 타입으로, 또는 그 반대로 변환하는 것과 같이 서로 다른 숫자 타입 간의 변환입니다.

  2. 파생 타입 간의 변환: 4.6.2절에서 다룬 파생 타입과 그 부모 타입 간의 변환입니다. 이 변환은 동일한 “파생 클래스”에 속한 타입들 사이에서 값을 이동시킵니다.[^ref]

  3. 태그드 타입 간의 변환: 4.7.2절에서 다룬 태그드 타입 계층 구조 내에서의 변환입니다. 이는 객체 지향 프로그래밍에서 매우 중요하게 사용됩니다.[^ref]

  4. 뷰 변환 (View Conversion): 이 용어는 때때로 새로운 타입으로의 변환이 아니라, 기존 값에 새로운 제약(예: 서브타입)을 적용하거나, 타입의 모호성을 해결하기 위해 사용되는 변환을 지칭합니다.

타입 변환과 런타임 검사

명시적 타입 변환은 컴파일러의 정적 타입 검사를 통과하기 위해 사용되지만, 변환의 유효성은 실행 시점(run-time)에 검사될 수 있습니다.

만약 변환하려는 값이 목표 타입의 제약 조건(예: 서브타입의 범위)을 만족하지 못하면, 프로그램은 Constraint_Error 예외를 발생시키며 중단됩니다.

예를 들어, Integer100subtype Small_Int is Integer range 1 .. 10;으로 변환(Small_Int(100))하려 하면, 이 값은 Small_Int의 범위를 벗어나므로 Constraint_Error가 발생합니다.

5. 연산자와 표현식

연산자(operator)는 특정 연산을 수행하는 기호(예: +, =)이며, 표현식(expression)은 이러한 연산자와 피연산자(operand)를 결합하여 값을 계산하는 구문입니다. 프로그램의 로직은 표현식을 통해 구현됩니다.

이 장에서는 Ada에서 값을 계산하고 비교하는 데 사용되는 연산자와 표현식에 대해 학습합니다. 먼저 표현식의 기본 개념을 정의하고, 이후 산술, 관계, 논리 연산자 등 다양한 종류의 연산자를 소개합니다. 또한, 여러 연산자가 함께 사용될 때의 평가 순서를 결정하는 우선순위 규칙과 and then, or else와 같은 단락 평가 제어 구문, 타입 모호성을 해결하는 한정 표현식 등을 다룹니다.

5.1 표현식(Expression)의 기본 개념

표현식(expression)은 평가(evaluation)될 때 특정 타입의 값(value)을 생성하는 코드 구문입니다. 표현식은 Ada 프로그램에서 계산을 수행하는 방법입니다.

표현식은 다음과 같은 요소들을 하나 이상 조합하여 구성될 수 있습니다.

  • 리터럴(Literals): 123, 3.14, 'A', "Hello"와 같이 코드에 직접 작성된 값 (3.3절 참조).
  • 객체(Objects): 변수(variable) 또는 상수(constant)의 이름 (4.1.1절 참조).
  • 함수 호출(Function Calls): 값을 반환하는 서브프로그램 호출 (7.2절 참조).
  • 속성(Attributes): 엔티티의 특성을 나타내는 값 (예: Integer'First, 4.1.4절 참조).
  • 연산자(Operators): +, -, =, and 등 피연산자에 대해 연산을 수행하는 기호 (5.2-5.4절 참조).
  • 다른 표현식: 괄호 ()로 묶이거나 연산자와 결합된 표현식들.

프로그램 실행 시 표현식은 평가되어 단일 값을 생성합니다. Ada의 강타입 시스템(4.1.3절)에 따라, 모든 표현식은 컴파일 시점에 결정되는 타입을 가집니다.

예시

X + 1          -- X가 Integer 타입이면, 결과도 Integer 타입
Is_Ready       -- Is_Ready가 Boolean 타입 변수이면, 결과는 Boolean 타입
Square(Y)      -- Square 함수가 Float 타입을 반환한다면, 결과는 Float 타입
A(I)           -- 배열 A의 I번째 요소 값
My_Record.Field -- 레코드 My_Record의 Field 컴포넌트 값

표현식은 값을 생성하므로, 변수 할당문의 우변, if 문의 조건, 서브프로그램 호출의 인수 등 값이 필요한 모든 곳에 사용될 수 있습니다.

5.2 산술 연산자 (+, -, *, /, mod, rem, abs, **)

숫자 타입(정수, 실수 등)에 대한 기본적인 수학 계산을 수행하는 연산자입니다. 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/), 나머지(rem), 모듈로(mod), 절댓값(abs), 거듭제곱(**) 등이 포함됩니다.

5.3 관계 연산자 (=, /=, <, <=, >, >=)

두 값을 비교하여 참(True) 또는 거짓(False)Boolean 결과를 반환하는 연산자입니다. 동등 비교(=, /=)와 크기 비교(<, <=, >, >=)가 있습니다.

5.4 논리 연산자 (and, or, xor, not)

Boolean 값에 대한 논리 연산을 수행합니다. 논리곱(and), 논리합(or), 배타적 논리합(xor), 논리 부정(not)이 있습니다.

5.5 멤버십 테스트 (in, not in)

어떤 값이 특정 범위(range)나 서브타입(subtype)에 속하는지(in) 또는 속하지 않는지(not in)를 검사하여 Boolean 값을 반환합니다.

5.6 연산자 우선순위 (Operator Precedence)

하나의 표현식에 여러 연산자가 사용될 때, 어떤 연산이 먼저 계산될지를 결정하는 규칙입니다. Ada는 명확한 우선순위 레벨을 정의합니다 (예: ***보다 높고, *+보다 높음).

5.7 단락 평가 제어 구문 (and then, or else)

논리 연산 andor의 특별한 형태로, 첫 번째 피연산자만으로 전체 결과가 결정되면 두 번째 피연산자를 평가하지 않고 건너뜁니다 (short-circuit). and then, or else 구문을 사용합니다.

5.8 한정 표현식 (Qualified Expressions)

타입이 모호할 수 있는 표현식 앞에 타입이름'(표현식) 형태로 타입을 명시하여, 해당 표현식의 타입을 컴파일러에게 명확히 알려주는 구문입니다.

6. 제어 구조

6.1 문장(Statements)과 순차적 실행

6.1.1 프로그램 실행의 기본 흐름

Ada 프로그램의 실행은 기본적으로 순차적(sequential)입니다.

이는 서브프로그램(프로시저 또는 함수)이나 블록 문의 begin 키워드 다음부터 end 키워드 이전까지 나열된 문장(statement)들이, 소스 코드에 작성된 순서대로 위에서 아래로 하나씩 차례대로 실행됨을 의미합니다.

하나의 문장 실행이 완료되어야만 제어(control)가 그 다음 문장으로 이동합니다. 이 장의 나머지 절들에서 다루는 if, case, loop와 같은 제어 구조는 이러한 기본적인 순차 실행 흐름을 변경합니다.

6.1.2 널 문 (Null Statement)

널 문(Null Statement)은 어떠한 동작도 수행하지 않음을 명시적으로 나타내는 문장입니다.

문법은 다음과 같이 단일 키워드 null과 세미콜론으로 구성됩니다.

null;

이 문장은 Ada의 구문 규칙상 문장이 반드시 위치해야 하는 곳이지만, 프로그램의 논리상 해당 지점에서 수행할 동작이 없는 경우 사용됩니다.

널 문은 제어를 다음 문장으로 즉시 전달하는 것 외에는 아무런 효과가 없습니다. 예를 들어, case 문의 특정 선택지(alternative)나 예외 핸들러(exception handler)에서 아무런 작업을 수행하지 않도록 지정할 때 널 문을 배치할 수 있습니다.

6.2 블록 문 (Block Statements)

6.2.1 declare를 이용한 지역 변수 선언

블록 문(Block Statement)은 일련의 문장들을 하나의 단위로 묶고, 해당 단위에서만 사용될 지역(local) 객체(변수 또는 상수)를 선언할 수 있도록 합니다.

블록 문은 declare 키워드로 시작하며, begin 키워드 앞에 선언부(declarative part)를 가집니다.

기본 문법:

declare
   -- 선언부 (Declarative Part)
   -- 이 곳에 지역 변수, 상수 등을 선언합니다.
begin
   -- 실행부 (Sequence of Statements)
   -- 이 곳의 문장들은 선언부에서 선언된 객체를 사용할 수 있습니다.
exception
   -- (선택적) 예외 처리부
   ...
end;

declare 키워드는 특정 계산을 수행하는 데 임시적으로 사용되는 변수를 선언하는 데 사용됩니다.

예를 들어, 두 변수의 값을 교환(swap)하기 위해 임시 변수 Temp가 사용되는 경우, 다음과 같이 블록 문을 사용할 수 있습니다.

예제:

-- X와 Y는 블록 문 이전에 선언되었다고 가정합니다.
declare
   Temp : Integer := X; -- Temp는 이 블록에서만 유효한 지역 변수입니다.
begin
   X := Y;
   Y := Temp;
end;
-- 이 지점(end; 다음)부터 Temp 변수는 더 이상 존재하지 않습니다.

이 예제에서 Temp 변수는 declare에서 생성되고 end;에서 소멸합니다. 이 방식을 통해 Temp라는 이름이 블록 문 외부의 다른 식별자와 충돌하는 것을 방지하며, 객체의 사용 범위를 해당 블록으로 제한합니다.

6.2.2 블록의 범위(Scope)와 생명주기

블록 문은 그 안에서 선언된 지역 객체의 범위(Scope)생명주기(Lifetime)를 정의합니다.

  • 범위 (Scope)
    • 블록 문의 선언부(declarebegin 사이)에서 선언된 객체(변수, 상수 등)의 범위는 해당 선언 지점부터 블록 문이 끝나는 end; 키워드까지입니다.
    • 이는 해당 객체의 이름을 블록 내부에서만 인식하고 사용할 수 있음을 의미합니다. 블록 외부에서는 해당 객체에 접근할 수 없습니다.
    • 만약 블록 외부의 객체와 동일한 이름으로 블록 내부에 객체를 선언하면, 블록 내부에서는 지역 객체가 외부 객체를 가리게(hide) 됩니다.
  • 생명주기 (Lifetime)
    • 블록 문에서 선언된 지역 객체의 생명주기는 프로그램 실행이 해당 블록 문에 진입할 때 시작됩니다. 즉, declare 이후 선언부가 처리되면서 객체가 생성(elaborated)됩니다.
    • 객체의 생명주기는 프로그램 실행이 해당 블록 문을 빠져나갈 때(end;에 도달할 때) 종료됩니다. 이때 해당 객체는 소멸(finalized)되고, 할당되었던 메모리는 (만약 있다면) 반환됩니다.

6.3 조건문 (Conditional Statements)

6.3.1 if ... then ... end if; 구조

if 문은 특정 조건(condition)의 평가 결과에 따라 문장들의 실행 여부를 결정합니다. Ada에서 if 문에 사용되는 조건은 Boolean 타입(즉, True 또는 False 값)을 결과로 반환하는 표현식이어야 합니다.

if ... then ... end if; 구조는 다음과 같은 문법을 가집니다.

기본 문법:

if 조건_표현식 then
   -- (Sequence of Statements)
   -- 조건_표현식이 True일 때 실행될 문장들
end if;

프로그램 실행이 if 문에 도달하면, ‘조건_표현식’을 평가합니다.

  1. 평가 결과가 True이면, then 키워드와 end if; 키워드 사이에 위치한 문장들이 순차적으로 실행됩니다.
  2. 평가 결과가 False이면, thenend if; 사이의 문장들은 실행되지 않으며, 제어(control)는 end if; 다음의 문장으로 이동합니다.

예제:

-- Temperature가 0.0 미만일 때 프로시저를 호출합니다.
if Temperature < 0.0 then
   Put_Line("경고: 온도가 영하입니다.");
   Activate_Heater;
end if;
-- Temperature가 0.0 이상이면, end if; 다음 문장으로 이동합니다.

6.3.2 else 절: 택일 구조

if 문에 else 절을 추가하면, if의 조건 표현식이 False로 평가될 때 실행할 문장들을 지정할 수 있습니다. 이는 두 가지 실행 경로 중 하나를 선택하는 택일(two-way selection) 구조를 형성합니다.

기본 문법:

if 조건_표현식 then
   -- (Statements_1)
   -- 조건_표현식이 True일 때 실행될 문장들
else
   -- (Statements_2)
   -- 조건_표현식이 False일 때 실행될 문장들
end if;

프로그램 실행이 이 구조에 도달하면 ‘조건_표현식’을 평가합니다.

  1. 평가 결과가 True이면, then 절의 문장들(Statements_1)이 실행되고, else 절의 문장들(Statements_2)은 실행되지 않습니다.
  2. 평가 결과가 False이면, then 절의 문장들(Statements_1)은 실행되지 않으며, else 절의 문장들(Statements_2)이 실행됩니다.

두 경우 중 하나만 실행되며, 실행이 완료되면 제어는 end if; 다음의 문장으로 이동합니다.

예제:

-- A와 B 중 더 큰 값을 Max에 할당합니다.
if A > B then
   Max := A;
else
   Max := B;
end if;

6.3.3 elsif 절: 다중 조건 분기

elsif 절은 if 문 내에서 두 개 이상의 상호 배타적인 조건들을 순차적으로 검사할 때 사용됩니다.

기본 문법:

if 조건_표현식_1 then
   -- (Statements_1)
   -- 조건_표현식_1이 True일 때 실행될 문장들
elsif 조건_표현식_2 then
   -- (Statements_2)
   -- 조건_표현식_1이 False이고 조건_표현식_2가 True일 때 실행될 문장들
elsif 조건_표현식_3 then
   -- (Statements_3)
   ...
else
   -- (Statements_Else)
   -- (선택적) 위의 모든 조건이 False일 때 실행될 문장들
end if;

프로그램 실행 시, 조건 표현식은 위에서 아래로 순서대로 평가됩니다.

  1. 조건_표현식_1True이면 Statements_1이 실행되고, 나머지 elsifelse 절은 실행되지 않으며 end if; 다음으로 제어가 이동합니다.
  2. 조건_표현식_1False이면, 조건_표현식_2를 평가합니다. 조건_표현식_2True이면 Statements_2가 실행되고 end if; 다음으로 이동합니다.
  3. 이 과정은 True인 조건을 만나거나 모든 elsif 조건을 확인할 때까지 반복됩니다.
  4. 만약 모든 ifelsif 조건이 False로 평가되면, else 절이 있는 경우 Statements_Else가 실행됩니다. else 절이 없는 경우에는 아무 문장도 실행되지 않습니다.

예제:

-- 점수(Score)에 따라 등급(Grade)을 할당합니다.
if Score >= 90.0 then
   Grade := 'A';
elsif Score >= 80.0 then
   Grade := 'B';
elsif Score >= 70.0 then
   Grade := 'C';
else
   Grade := 'F';
end if;

6.4 다중 선택문 (Case Statements)

6.4.1 case 문의 기본 구조

case 문은 하나의 판별 표현식(selector expression)의 값에 따라 다수의 실행 경로(alternatives) 중 하나를 선택하여 실행합니다.

기본 문법:

case 판별_표현식 is
   when 선택지_1 =>
      -- 선택지_1과 값이 일치할 때 실행될 문장들
      Sequence_Of_Statements_1
   when 선택지_2 | 선택지_3 .. 선택지_4 =>
      -- 선택지_2 또는 선택지_3부터 선택지_4 범위의 값과 일치할 때 실행될 문장들
      Sequence_Of_Statements_2
   when others =>
      -- 위의 모든 선택지에 해당하지 않을 때 실행될 문장들
      Sequence_Of_Statements_Others
end case;

case 문의 판별 표현식은 이산 타입(Discrete Type)을 결과로 반환해야 합니다.

case 문은 Ada 언어 설계의 속성인 완전성(completeness)과 상호 배타성(mutual exclusivity) 규칙을 따릅니다.

  1. 완전성: 판별 표현식이 가질 수 있는 모든 가능한 값(범위)은 반드시 하나의 when 절에 의해 처리되어야 합니다.
  2. 상호 배타성: 하나의 값은 오직 하나의 when 절에만 일치해야 합니다.

예제:

type Color is (Red, Green, Blue, Yellow);
My_Color : Color := Blue;

case My_Color is
   when Red =>
      Put_Line("정지 신호");
   when Green =>
      Put_Line("진행 신호");
   when Blue | Yellow =>
      -- Blue와 Yellow 두 값은 하나의 경로로 처리됩니다.
      Put_Line("대기 신호");
end case;

6.4.2 선택지 (when)와 범위 (|, ..)

case 문의 when 절은 판별 표현식의 값과 비교될 선택지(choice)를 명시합니다. Ada는 when 절에 단일 값, 여러 값의 목록, 또는 값의 범위를 지정하는 구문을 제공합니다.

  1. 단일 값 (Single Value): 하나의 값만을 지정합니다.

    when 1 =>
       Put_Line("값은 1입니다.");
    
  2. 값의 목록 (| 사용): 수직선(|) 기호를 사용하여 상호 배타적인 여러 개의 이산 값을 하나의 when 절에 나열할 수 있습니다.

    when 1 | 3 | 5 | 7 | 9 =>
       Put_Line("값은 홀수 한 자리입니다.");
    
  3. 값의 범위 (.. 사용): 이중 점(..) 기호를 사용하여 연속적인 값의 범위를 지정할 수 있습니다. 이는 이산 타입(정수 또는 열거형)에만 적용됩니다.

    when 10 .. 19 =>
       Put_Line("값은 10대에 속합니다.");
    

이러한 구문들은 조합하여 사용할 수 있습니다. when 절에 명시되는 모든 선택지는 정적(static)이어야 하며, 컴파일 시점에 그 값을 알 수 있어야 합니다.

예제 (조합 사용):

type Day is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);
Today : Day := Tue;

case Today is
   when Mon .. Thu =>
      Put_Line("평일입니다.");
   when Fri =>
      Put_Line("금요일입니다.");
   when Sat | Sun =>
      Put_Line("주말입니다.");
end case;

6.4.3 when others의 사용

when others 절은 case 문의 마지막에 위치하여, 이전에 나열된 when 절들에서 명시되지 않은 판별 표현식의 모든 나머지 가능한 값들을 처리하는 단일 경로를 제공합니다.

when others의 용도는 case 문의 완전성(completeness) 규칙을 충족시키는 것입니다. 판별 표현식의 타입이 Integer와 같이 넓은 범위를 갖는 경우, 또는 열거형 타입의 모든 값을 개별적으로 나열하지 않는 경우 when others를 사용합니다.

규칙:

  1. when others 절은 case 문 내에서 오직 한 번만 사용할 수 있습니다.
  2. 사용될 경우, when others 절은 반드시 가장 마지막 선택지여야 합니다.

기본 문법:

case 판별_표현식 is
   when 선택지_1 =>
      ...
   when 선택지_2 .. 선택지_N =>
      ...
   when others =>
      -- 선택지 1부터 N까지에 해당하지 않는
      -- 판별_표현식의 다른 모든 가능한 값을 처리합니다.
end case;

예제:

Integer 타입의 서브타입인 Sensor_Reading이 0부터 100까지의 값을 가질 수 있다고 가정합니다.

subtype Sensor_Reading is Integer range 0 .. 100;
Value : Sensor_Reading := Get_Reading;

case Value is
   when 0 =>
      Put_Line("상태: 초기값");
   when 1 .. 99 =>
      Put_Line("상태: 정상 작동 범위");
   when 100 =>
      Put_Line("상태: 최대치 도달");
   when others =>
      -- Sensor_Reading 타입이 0..100 범위로 제한되므로,
      -- 이 예제에서 'when others'는 0, 1..99, 100을 제외한
      -- 나머지 값(이 경우 없음)을 처리합니다.
      -- 만약 판별 표현식의 타입이 'Integer'였다면,
      -- 'when others'는 0..100 범위를 제외한 모든 Integer 값을 처리합니다.
      null;
end case;

만약 5.4.1절의 Color 예제에서 when Red, when Green만 명시했다면, BlueYellow가 누락되어 컴파일 오류가 발생합니다. 이때 when others를 사용하면 BlueYellow를 한 번에 처리할 수 있습니다.

case My_Color is
   when Red =>
      Put_Line("정지 신호");
   when Green =>
      Put_Line("진행 신호");
   when others =>
      -- 이 경로는 Blue 또는 Yellow일 때 실행됩니다.
      Put_Line("기타 신호");
end case;

6.4.4 표현식을 이용한 case

Ada는 5.4.1절에서 설명한 case 문장(Statement) 외에, 값을 반환하는 case 표현식(Case Expression)도 제공합니다.

  • Case 문장 (Statement): 실행할 문장들의 순서(sequence of statements)를 선택합니다.
  • Case 표현식 (Expression): 평가할 표현식(expression)을 선택하여 하나의 값을 반환합니다.

case 표현식은 다른 표현식의 일부로 사용되어야 하며, 문법적으로 괄호로 묶어야 합니다.

기본 문법:

(case 판별_표현식 is
   when 선택지_1 => 결과_표현식_1,
   when 선택지_2 | 선택지_3 => 결과_표현식_2,
   ...
   when others => 결과_표현식_Others)

동작 방식:

  1. 판별_표현식 (selecting_expression)이 평가됩니다.
  2. 평가된 값은 case 문장과 동일한 규칙(완전성, 상호 배타성)에 따라 선택지 (discrete_choice_list)와 비교됩니다.
  3. 일치하는 선택지에 해당하는 결과_표현식 (dependent_expression)이 평가되며, 이 값이 case 표현식 전체의 결과 값이 됩니다.
  4. case 표현식의 모든 결과_표현식들은 호환 가능한 타입을 반환해야 합니다.

예제:

-- Grade(등급) 값에 따라 점수(Point)를 할당합니다.
type Grade is (A, B, C, F);
My_Grade : Grade := B;

Points : constant Natural := (case My_Grade is
                                when A => 4,
                                when B => 3,
                                when C => 2,
                                when F => 0);

이 구조는 if 표현식(ARM 4.5.7)과 함께, 표현식이 위치할 수 있는 모든 곳(예: 객체 선언, 할당문)에서 조건부 값을 제공하는 데 사용됩니다.

6.5 반복문 (Loop Statements)

반복문(Loop statements)은 일련의 문장들을 반복적으로 실행하기 위한 메커니즘을 제공합니다. Ada는 단순 반복부터 복잡한 병렬 처리에 이르기까지, 각기 다른 반복 요구사항을 위한 여러 형태의 루프를 제공합니다. 이 구문들은 프로그램의 실행 흐름을 제어하는 역할을 합니다.

6.5.1 기본 loop ... end loop; 구조

loop ... end loop; 구문은 Ada의 기본적인 반복문 형태입니다. 이 구조는 그 자체로는 종료 조건을 포함하지 않으며, 무한히 반복 실행되는 루프(infinite loop)를 생성합니다.

기본 문법:

loop
   -- (Sequence of Statements)
   -- 반복 실행될 문장들
end loop;

프로그램 실행이 loop 키워드에 도달하면, loopend loop; 사이의 문장들이 순차적으로 실행됩니다. end loop;에 도달한 후, 제어(control)는 즉시 다시 loop 키워드가 있는 시작 지점으로 돌아가 문장들의 실행을 반복합니다.

이 기본 루프는 5.5.2절과 5.5.3절에서 설명하는 exit 문과 같은 명시적인 탈출 구문을 만나기 전까지는 실행을 멈추지 않습니다.

6.5.2 exit 문: 루프 탈출

exit 문은 현재 실행 중인 반복문(loop)을 즉시 종료하는 데 사용되는 제어문입니다.

기본 문법:

exit;

프로그램 실행이 exit 문에 도달하면, 해당 exit 문을 가장 안쪽에서 둘러싸고 있는 루프의 실행이 즉시 중단됩니다. 제어(control)는 해당 루프의 end loop; 키워드 바로 다음 문장으로 이동합니다.

exit 문은 5.3절의 if 문과 같은 조건문 내부에 위치하여, 특정 조건이 충족되었을 때 루프를 탈출하는 용도로 사용됩니다.

예제:

Count : Integer := 0;

loop
   Count := Count + 1;
   Put_Line("Current count:" & Integer'Image(Count));

   if Count >= 10 then
      -- Count가 10 이상이 되면 루프를 탈출합니다.
      exit;
   end if;

   -- Count가 9일 때까지는 이 문장이 실행됩니다.
   Put_Line("...looping...");
end loop;

-- exit 문이 실행되면 제어는 이 지점으로 이동합니다.
Put_Line("루프가 종료되었습니다.");

6.5.3 exit when을 이용한 조건부 탈출

exit when 문은 조건부 루프 탈출 구문입니다.

이는 if 조건 then exit; end if; 구조를 단 하나의 문장으로 축약하여 표현합니다.

기본 문법:

exit when 조건_표현식;

프로그램 실행이 exit when 문에 도달하면 ‘조건_표현식’을 평가합니다.

  1. 평가 결과가 True이면, exit 문이 즉시 실행되어 해당 문을 가장 안쪽에서 둘러싸고 있는 루프를 종료합니다.
  2. 평가 결과가 False이면, 루프는 중단되지 않고 다음 문장으로 실행을 이어갑니다.

exit when 문은 루프의 어느 위치에나 배치될 수 있으며, 루프의 시작 지점(선-조건 검사)이나 끝 지점(후-조건 검사)에 배치하여 while 또는 until 루프와 유사한 동작을 구현하는 데 사용됩니다.

예제:

5.5.2절의 if 문을 사용한 예제는 exit when을 사용하여 다음과 같이 작성할 수 있습니다.

Count : Integer := 0;

loop
   Count := Count + 1;
   Put_Line("Current count:" & Integer'Image(Count));

   -- Count가 10 이상이 되면 루프를 탈출합니다.
   exit when Count >= 10;

   Put_Line("...looping...");
end loop;

Put_Line("루프가 종료되었습니다.");

6.5.4 while ... loop: 선(Pre-condition) 검사 반복

while ... loop 구문은 루프 본체의 문장들을 실행하기 전에 특정 조건을 먼저 검사하는 선-조건(pre-condition) 반복문입니다.

기본 문법:

while 조건_표현식 loop
   -- (Sequence of Statements)
   -- 조건_표현식이 True일 때 반복 실행될 문장들
end loop;

프로그램 실행이 while 문에 도달하면, ‘조건_표현식’ (반드시 Boolean 타입을 반환해야 함)을 먼저 평가합니다.

  1. 평가 결과가 True이면, loopend loop; 사이의 문장들이 순차적으로 실행됩니다. 문장들의 실행이 완료되면, 제어(control)는 다시 while 키워드로 돌아가 ‘조건_표현식’을 재평가합니다.
  2. 평가 결과가 False이면 (이것이 첫 번째 검사인 경우 포함), 루프 본체의 문장들은 실행되지 않으며 제어는 end loop; 다음의 문장으로 즉시 이동합니다.

이러한 특성으로 인해, while 루프의 본체는 조건이 처음부터 False일 경우 0번 실행될 수 있습니다.

예제:

-- Counter가 0보다 큰 동안 반복 실행합니다.
Counter : Integer := 10;

while Counter > 0 loop
   Put_Line("Counter:" & Integer'Image(Counter));
   Counter := Counter - 1;
end loop;

-- Counter가 0이 되어 조건이 False가 되면 루프가 종료됩니다.
Put_Line("루프 종료.");

6.5.5 for ... loop: 범위 기반 반복 (이산 범위)

for ... loop 구문은 명시된 이산 범위(discrete range) 내의 각 값에 대해 루프 본체를 한 번씩, 총 고정된 횟수만큼 실행합니다. 이산 범위에는 정수 타입뿐만 아니라 열거형 타입도 포함됩니다.

반복 횟수는 루프 진입 전에 결정된다는 특징이 있습니다.

기본 문법:

for 루프_제어_변수 in 이산_범위 loop
   -- (Sequence of Statements)
   -- 범위 내 각 값에 대해 반복 실행될 문장들
end loop;

for 루프는 다음과 같은 규칙을 따릅니다.

  1. 루프 제어 변수 (Loop Parameter):

    • 루프_제어_변수(예: I)는 for 루프에 의해 암시적으로 선언됩니다. 이 변수는 루프 이전에 따로 선언하지 않습니다.
    • 이 변수의 타입은 이산_범위의 타입(예: Integer 또는 열거형)에 따라 자동으로 결정됩니다.
    • 루프 본체(loopend loop; 사이) 안에서 이 변수는 상수(constant)로 취급됩니다. 즉, 루프 본체 안에서 이 변수에 새로운 값을 할당하려는 시도는 컴파일 오류를 발생시킵니다.
    • 이 변수의 유효 범위(scope)는 루프 본체로 한정되며, 루프가 종료되면 변수는 소멸합니다.
  2. 이산 범위 (Discrete Range):

    • 반복할 범위를 지정하며, 하한 .. 상한 (예: 1 .. 10)의 형태를 사용합니다.
    • 이 범위는 루프가 처음 시작될 때 단 한 번만 평가됩니다.

실행 과정:

  1. 루프가 시작될 때 이산_범위가 평가됩니다.
  2. 만약 범위가 비어있다면(예: 1 .. 0), 루프 본체는 0번 실행되고 제어는 end loop; 다음으로 즉시 이동합니다.
  3. 범위가 비어있지 않다면, 범위 내의 첫 번째 값부터 마지막 값까지 순서대로 루프_제어_변수에 할당되며, 각 할당마다 루프 본체의 문장들이 한 번씩 실행됩니다.

예제 1 (정수 범위):

-- 1부터 10까지의 합을 계산합니다.
Total : Integer := 0;

for I in 1 .. 10 loop
   -- I는 1, 2, 3, ..., 10의 값을 차례로 가집니다.
   Total := Total + I;
   -- I := I + 1; -- 이 문장은 I가 상수이므로 컴파일 오류입니다.
end loop;

Put_Line("1부터 10까지의 합:" & Integer'Image(Total));

예제 2 (열거형 범위):

type Day_Of_Week is (Mon, Tue, Wed, Thu, Fri, Sat, Sun);

-- 'range는 해당 타입의 전체 범위를 의미합니다.
for Day in Day_Of_Week'range loop
   Put_Line(Day_Of_Week'Image(Day));
end loop;

불변의 반복 횟수:

for 루프의 이산 범위는 루프가 시작되기 전에 단 한 번만 평가됩니다. 그 결과, 반복 횟수는 루프가 시작될 때 고정됩니다. 루프 본체 내부에서 범위의 상한이나 하한을 정의하는 데 사용된 변수의 값을 변경하더라도, 이미 결정된 반복 횟수에는 영향을 미치지 않습니다.

예제 3 (불변의 범위):

Upper_Bound : Integer := 3;

-- 루프 진입 시, 범위는 '1 .. 3'으로 결정됩니다.
for I in 1 .. Upper_Bound loop
   Put_Line("반복:" & Integer'Image(I));

   -- 루프 내부에서 Upper_Bound 변수를 변경합니다.
   Upper_Bound := 10;

   -- 이 변경은 현재 실행 중인 루프의 반복 횟수에 영향을 주지 않습니다.
end loop;

-- 루프는 '1 .. 3' 범위에 따라 3번만 실행됩니다.

6.5.6 for ... in reverse ... loop: 역방향 반복

for ... loop 구문에 reverse 키워드를 추가하면, 5.5.5절과는 반대로 명시된 이산 범위(discrete range)의 마지막 값에서 첫 번째 값까지 역방향(descending order)으로 반복 실행합니다.

기본 문법:

for 루프_제어_변수 in reverse 이산_범위 loop
   -- (Sequence of Statements)
   -- 범위 내 각 값에 대해 역방향으로 반복 실행될 문장들
end loop;

for ... in reverse ... loop의 규칙은 reverse 키워드가 없는 for 루프와 동일합니다.

  1. 루프 제어 변수: 암시적으로 선언되며, 루프 본체 내에서 상수로 취급되고, 루프 종료 시 소멸합니다.
  2. 이산 범위: 루프 시작 시 단 한 번만 평가됩니다.

실행 과정:

  1. 루프가 시작될 때 이산_범위 (예: 1 .. 10)가 평가됩니다.
  2. 범위가 비어있다면(예: 1 .. 0), 루프 본체는 0번 실행되고 제어는 end loop; 다음으로 즉시 이동합니다.
  3. 범위가 비어있지 않다면, 범위 내의 마지막 값(예: 10)부터 첫 번째 값(예: 1)까지 내림차순으로 루프_제어_변수에 할당되며, 각 할당마다 루프 본체의 문장들이 한 번씩 실행됩니다.

예제:

-- 10부터 1까지 역순으로 출력합니다.
Put_Line("카운트다운 시작:");

for I in reverse 1 .. 10 loop
   -- I는 10, 9, 8, ..., 1의 값을 차례로 가집니다.
   Put_Line(Integer'Image(I));
end loop;

Put_Line("종료.");

6.5.7 for ... of ... loop: 배열 및 컨테이너 반복

for ... of ... loop 구문은 배열(array)이나 순회 가능한 컨테이너(iterable container)의 요소(element) 자체를 직접 순회(iterate)하는 기능을 제공합니다.

이는 5.5.5절[^ref]의 for ... in ... loop 구문이 이산 범위(일반적으로 인덱스)를 순회하는 것과 구별됩니다.

이 구문은 iterator_specification (반복자 명세)의 한 형태이며, “배열 컴포넌트 반복자(array component iterator)” 또는 “컨테이너 요소 반복자(container element iterator)”로 동작합니다.

기본 문법 (배열):

for 요소_매개변수 of 배열_객체 loop
   -- 요소_매개변수(element parameter)를 사용하는 문장들
end loop;

동작 방식:

  1. 루프 매개변수 (Loop Parameter):

    • 요소_매개변수(예: Element)는 이 구문에 의해 암시적으로 선언됩니다.
    • 이 매개변수의 타입은 배열_객체컴포넌트(component) 타입입니다.
    • 배열_객체가 변수(variable)이면 요소_매개변수도 변수(variable) 뷰를 제공하여, 루프 본체 내에서 배열의 요소를 수정할 수 있습니다.
    • 배열_객체가 상수(constant)이면 요소_매개변수도 상수(constant) 뷰를 제공합니다.
  2. 순회 (Iteration):

    • 이 루프는 배열_객체의 모든 컴포넌트에 대해 순차적으로 실행됩니다.
    • 순회 순서는 캐노니컬 순서(canonical order), 즉 마지막 인덱스가 가장 빠르게 변하는 순서(row-major order)를 따릅니다 (배열에 Fortran 관례(convention)가 지정되지 않은 경우).

컨테이너 반복 (Container Iteration):

for ... of ... 구문은 Ada.Containers (A.18)의 자식 패키지에서 정의된 타입과 같이 “순회 가능한 컨테이너 타입(iterable container type)”에도 사용할 수 있습니다.

-- My_Vector가 Ada.Containers.Vectors의 인스턴스 객체라고 가정
for E of My_Vector loop
   ...
end loop;

이 경우, 루프 매개변수(E)의 타입은 컨테이너의 기본 요소(default element) 타입이 됩니다. 루프는 컨테이너가 제공하는 반복자(iterator)를 통해 각 요소를 순회합니다. iterable_name이 상수이거나 컨테이너 타입의 Variable_Indexing 에스펙트가 지정되지 않은 경우 루프 매개변수는 상수(constant)입니다.

예제 (배열):

My_Array : array (1 .. 10) of Integer := (others => 0);
Sum      : Integer := 0;

-- 5.5.5절의 인덱스 기반 순회
for I in My_Array'Range loop
   Sum := Sum + My_Array(I);
end loop;

-- 5.5.7절의 요소 기반 순회
Sum := 0;
for Element of My_Array loop
   Sum := Sum + Element;
end loop;

-- 요소 기반 수정을 위한 순회 (루프 매개변수가 변수임)
for E of My_Array loop
   E := E * 2;
end loop;

6.5.8 병렬 loop (Parallel loops) (Ada 2022)

Ada 2022 표준은 iteration_schemeparallel 키워드를 도입하여 병렬 루프(parallel loop) 구문을 제공합니다.

병렬 루프는 명시된 반복(iteration)을 여러 개의 청크(chunks)로 분할하며, 각 청크는 별도의 논리적 제어 스레드(logical thread of control)에서 실행됩니다. 이는 다중 프로세서 환경에서 루프의 반복 작업을 동시에 실행할 수 있도록 지원합니다.

기본 문법:

parallel [(chunk_specification)] [aspect_specification]
   for loop_parameter_specification | iterator_specification

parallel 키워드는 for ... in 구문(loop_parameter_specification) 및 for ... of 구문(iterator_specification)과 함께 사용될 수 있습니다. reverse 키워드는 병렬 루프 구문에서 허용되지 않습니다.

청크 명세 (Chunk Specification)

선택적인 chunk_specification은 병렬 실행에 사용될 최대 청크 수를 결정합니다. chunk_specification은 두 가지 형태가 있습니다:

  1. integer_simple_expression: 표현식의 값이 최대 청크 수를 지정합니다.
  2. defining_identifier in discrete_subtype_definition: 이산 서브타입에 속한 값의 개수가 최대 청크 수를 지정하며, 각 청크는 defining_identifier로 식별되는 청크 파라미터(chunk parameter)의 고유한 값을 가집니다.

chunk_specification이 있는 경우, 결정된 최대 청크 수가 0보다 큰지 확인하는 검사(check)가 수행됩니다. 이 검사가 실패하면 Program_Error가 발생합니다.

실행 (Execution)

병렬 루프가 실행될 때, 반복 범위는 여러 청크로 분할되어 각기 다른 논리적 제어 스레드에 할당됩니다.

  • loop_parameter_specification (for...in)의 경우: 각 논리적 스레드는 루프 파라미터의 고유한 하위 범위(subrange)를 담당하며, 모든 값이 중복이나 누락 없이 처리됩니다.
  • iterator_specification (for...of)의 경우: 병렬 배열 반복자는 배열 요소들을 정식 순서(canonical order)의 연속적인 청크로 분할합니다. 병렬 일반화 반복자(parallel generalized iterator)는 해당 반복자 타입의 Split_Into_Chunks 연산을 호출하여 작업을 분배합니다.

루프 문장은 (외부로의 제어 전송이 없는 경우) 모든 논리적 제어 스레드가 실행을 완료했을 때 완료됩니다.

예제 (청크 파라미터 활용):

다음 예제는 데이터를 여러 청크로 나누어 병렬로 처리하고, 각 청크의 부분 합을 별도의 배열에 저장하는 방법을 보여줍니다.

-- 병렬 루프 내에서 공유 변수(Partial_Sum)에 대한 동시 접근은
-- 스레드 안전성(thread-safety)을 보장해야 합니다.
-- 실제 구현에서는 'Atomic' 애스펙트나 보호 객체(Protected Objects)가 필요합니다.
-- 이 예제는 청크 파라미터의 사용법을 설명하기 위한 개념적 코드입니다.

declare
   subtype Chunk_Index is Natural range 1 .. 8;
   Partial_Sum : array (Chunk_Index) of Natural := (others => 0);
   Data_Grid   : array (1 .. 1_000) of Natural;
   ...
begin
   -- 데이터를 최대 8개의 청크로 나누어 병렬 처리합니다.
   -- 'Chunk'는 현재 실행 중인 청크의 인덱스(1..8)를 가집니다.
   parallel (Chunk in Chunk_Index)
      for I in Data_Grid'Range loop
         -- 'Chunk' 파라미터를 사용하여 해당 청크의 부분 합 배열에 접근
         Partial_Sum(Chunk) := Partial_Sum(Chunk) + Data_Grid(I);
      end loop;

   -- 모든 병렬 루프가 완료된 후, Partial_Sum 배열의 값을 합산하여 최종 결과를 도출합니다.
   ...
end;

6.5.9 루프 이름(Loop Naming)과 중첩 루프 탈출

Ada에서는 루프 문(loop statement)에 식별자(identifier)를 지정하여 이름(name)을 부여할 수 있습니다.

기본 문법:

루프_이름: loop
   ...
end loop 루프_이름;

loop 키워드 앞에 루프_이름:을 명시하고, 해당 루프의 end loop; 키워드 뒤에 동일한 루프_이름을 반복하여 명시해야 합니다.

루프 이름을 사용하는 주된 목적은 중첩 루프(nested loops) 구조에서 특정 루프를 명시적으로 식별하고 탈출하기 위함입니다.

5.5.2절에서 설명한 exit 문은 이름 없이 사용될 경우, 해당 exit 문을 가장 안쪽에서 둘러싸고 있는 루프(innermost loop)만을 종료시킵니다.

만약 내부 루프에서 하나 이상의 외부 루프(outer loop)를 즉시 종료해야 할 경우, exit 문에 탈출하고자 하는 루프의 이름을 지정합니다.

중첩 탈출 문법:

exit 루프_이름;

exit when 구문과 결합하여 사용할 수도 있습니다.

exit 루프_이름 when 조건_표현식;

예제:

2차원 배열(Matrix)에서 특정 값(Target)을 찾는 경우, 값을 찾았을 때 모든 루프를 한 번에 종료해야 합니다.

Matrix : array (1..10, 1..10) of Integer;
Target : Integer := ...;

Search_Loop:  -- 외부 루프에 'Search_Loop'라는 이름을 지정
for Row in Matrix'Range(1) loop
   for Col in Matrix'Range(2) loop
      if Matrix(Row, Col) = Target then
         Put_Line("값을 찾았습니다.");

         -- 'exit;'만 사용하면 내부 루프(Col)만 탈출합니다.
         -- 'Search_Loop'를 명시하여 외부 루프(Row)까지 탈출합니다.
         exit Search_Loop;
      end if;
   end loop;
end loop Search_Loop;

-- 'exit Search_Loop;'가 실행되면 제어는 이 지점으로 이동합니다.

6.5.10 반복자 필터 (Iterator Filter)

for ... in 루프 구문에 when 절을 추가하여, 반복 범위 내의 특정 조건을 만족하는 값에 대해서만 루프 본체를 실행하도록 필터링할 수 있습니다.

기본 문법:

for 루프_제어_변수 in 이산_범위 when 조건_표현식 loop
   -- (Sequence of Statements)
   -- 조건을 만족하는 루프_제어_변수에 대해서만 실행됨
end loop;

동작 방식:

루프는 이산_범위 전체를 순회하지만, 루프 본체는 조건_표현식True로 평가되는 루프_제어_변수의 값에 대해서만 실행됩니다. 조건_표현식 자체는 루프 제어 변수(예: I)를 포함할 수 있습니다.

예제:

-- 1부터 100까지의 범위에서 7의 배수만 출력합니다.
for I in 1 .. 100 when I mod 7 = 0 loop
   Put_Line(Integer'Image(I) & "은(는) 7의 배수입니다.");
end loop;

for ... of (요소 기반) 반복과 함께 사용하여 컨테이너의 특정 요소만 필터링할 수도 있습니다.

-- My_Array 배열의 요소 중 0보다 큰 요소(Element)만 두 배로 만듭니다.
for Element of My_Array when Element > 0 loop
   Element := Element * 2;
end loop;

6.6 무조건 분기문 (goto statement)

6.6.1 레이블(Label) 선언

goto 문(5.6.2절)의 대상이 되는 지점을 식별하기 위해 레이블(Label)이 사용됩니다.

Ada에서 레이블은 이중 꺾쇠 괄호(<< >>)로 식별자(identifier)를 감싸는 형태로 선언됩니다.

기본 문법:

<<레이블_이름>>

레이블은 단독으로 존재할 수 없으며, 반드시 실행 가능한 문장(statement) 바로 앞에 위치해야 합니다. 레이블이 부착된 문장을 “레이블된 문장 (labeled statement)”이라고 합니다.

예제:

-- 'Target_Location'이라는 이름의 레이블 선언
<<Target_Location>>
null;

<<Loop_Start>>
Counter := Counter + 1; -- 할당 문(statement)에 레이블 부착

선언된 레이블 식별자는 해당 선언이 포함된 본체(예: 서브프로그램 begin ... end 블록) 내에서 고유해야 합니다. 이 레이블은 goto 문에 의해 참조되어 프로그램의 실행 흐름을 해당 문장으로 이동시키는 데 사용됩니다.

6.6.2 goto 문의 사용과 제한

6.6.2 goto 문의 사용과 제한

goto 문은 goto 키워드와 5.6.1절에서 선언된 레이블(Label)의 식별자로 구성됩니다.

기본 문법:

goto 레이블_이름;

goto 문은 프로그램의 제어(control)를 해당 레이블_이름이 부착된 문장으로 무조건 이송(unconditional transfer)시킵니다.

Ada는 goto 문의 사용을 제한합니다.

주요 제한 사항:

  1. 동일 범위(Scope) 제한: goto 문과 대상 레이블은 반드시 동일한 서브프로그램 본체(body) 또는 declare 블록의 실행부(begin ... end) 내에 함께 존재해야 합니다.
  2. 복합 문(Compound Statement) 진입 불가: goto 문을 사용하여 현재 제어 위치의 외부에서 복합 문의 내부로 제어를 이송할 수 없습니다. 여기에는 다음이 포함됩니다.
    • if
    • case
    • loop
    • declare 블록
  3. 선택지 간 이동 불가:
    • if 문의 then, elsif, else 절 사이를 goto로 이동할 수 없습니다.
    • case 문의 서로 다른 when 절 사이를 goto로 이동할 수 없습니다.

goto 문은 if 문이나 declare 블록의 안쪽에서 바깥쪽으로 제어를 이송하는 데는 사용될 수 있습니다.

예제 (허용되는 사용):

-- 오류 발생 시 블록의 끝으로 제어를 이송합니다.
declare
   Value : Integer := Get_Value;
begin
   if Value < 0 then
      goto Error_Handler; -- 블록의 안쪽에서 바깥쪽으로 이동
   end if;

   -- 정상 처리
   ...

   <<Error_Handler>>
   Put_Line("오류가 감지되었습니다.");
end;

6.6.3 goto 문과 프로그램 구조

goto 문은 프로그램의 제어 흐름을 현재 실행 지점에서 소스 코드의 다른 지점(레이블)으로 직접 이송시킵니다. 이러한 비선형적인(non-linear) 제어 이송은 프로그램의 정적 구조와 실제 동적 실행 흐름 간의 불일치를 유발할 수 있습니다.

이러한 비선형적 제어 이송은 프로그램의 실행 경로를 소스 코드 상에서 순차적으로 추적하는 것을 어렵게 만들 수 있으며, 이는 가독성(readability)유지보수성(maintainability)에 영향을 미칠 수 있습니다.

구조적 프로그래밍(structured programming)은 단일 진입점과 단일 탈출점을 가진 제어 구조(예: if, case, loop)의 사용을 포함합니다. 이러한 구조는 프로그램의 논리적 흐름을 표현합니다.

Ada는 5.6.2절에서 설명한 제한 외에도, goto 문을 사용하지 않고 제어 흐름을 구현하는 다른 구문들을 제공합니다. 예를 들어, 중첩 루프를 탈출하는 데는 5.5.9절의 ‘루프 이름과 중첩 루프 탈출’을, 특정 지점으로 제어를 이송하는 데는 예외 처리(Exception Handling, 10장)나 declare 블록(5.2절)을 사용할 수 있습니다. goto 문 대신 이러한 구조화된 구문들을 사용하여 프로그램을 작성할 수 있습니다.

7. 서브프로그램 (Subprograms)

6장에서는 Ada에서 실행 가능한 알고리즘의 기본 단위인 서브프로그램(subprogram)에 대해 학습합니다. 서브프로그램은 서브프로그램 호출(subprogram call)에 의해 실행이 발동되는 프로그램 단위(program unit) 또는 고유 연산(intrinsic operation)입니다.

서브프로그램은 두 가지 형태로 구분됩니다. 하나는 특정 동작을 수행하는 프로시저(procedure)이며, 다른 하나는 값을 계산하여 반환하는 함수(function)입니다.

Ada의 서브프로그램 정의는 일반적으로 두 부분으로 나뉘어 제공될 수 있습니다.

  • 서브프로그램 선언부 (Subprogram Declaration): 서브프로그램의 인터페이스(interface)를 정의합니다.
  • 서브프로그램 본체 (Subprogram Body): 서브프로그램의 실제 실행(execution)을 정의합니다.

이 장에서는 이 두 가지 형태의 서브프로그램을 선언, 정의 및 호출하는 방법을 다룹니다.

7.1 프로시저 (procedure)

프로시저는 특정 동작 또는 일련의 작업을 수행하는 서브프로그램입니다. 프로시저는 값을 반환하지 않으며, 명령을 실행하거나 6.2절에서 다룰 파라미터를 통해 외부 상태를 변경하는 데 사용될 수 있습니다.

프로시저 호출(procedure call)은 하나의 명령문(statement)입니다.

프로시저의 정의는 선언부(specification)와 본체(body)로 구성됩니다.

선언부는 프로시저의 이름과 파라미터 프로파일을 정의하여 인터페이스를 명시합니다.

  • 구문:
    procedure 식별자 (파라미터_프로파일);
    

본체는 is 키워드 이후에 선언부(specification)를 반복하고, begin ... end 블록 내에 실행될 알고리즘(일련의 문장)을 포함합니다.

  • 구문:
    procedure 식별자 (파라미터_프로파일) is
       -- 로컬 선언 (변수, 상수 등)
    begin
       -- 실행될 문장들
    exception
       -- 예외 처리
    end 식별자;
    

프로시저(procedure)는 return 문을 가질 수 있습니다. 이 return 문은 값을 반환하지 않으며, 프로시저의 실행을 종료하는 용도로 사용됩니다.

7.2 함수 (function)

함수는 특정 값을 계산하여 하나의 값을 반환(return)하는 것을 목적으로 하는 서브프로그램입니다. Ada 언어에 내장된 연산자(operator) 및 열거형 리터럴(enumeration literal)도 함수의 한 형태로 간주됩니다.

함수 호출(function call)은 표현식(expression)을 구성하며, 호출된 지점에서 함수가 반환하는 값으로 대체됩니다.

함수의 정의는 선언부(specification)와 본체(body)로 구성됩니다.

선언부는 함수의 이름, 파라미터 프로파일, 그리고 반환될 값의 타입을 정의하여 인터페이스를 명시합니다.

  • 구문:
    function 식별자 (파라미터_프로파일) return 타입_이름;
    

본체는 is 키워드 이후에 선언부(specification)를 반복하고, begin ... end 블록 내에 실행될 알고리즘을 포함합니다. 함수의 본체는 선언부에 명시된 return 타입과 일치하는 값을 반환하는 하나 이상의 return 문을 포함해야 합니다.

  • 구문:
    function 식별자 (파라미터_프로파일) return 타입_이름 is
       -- 로컬 선언 (변수, 상수 등)
    begin
       -- 실행될 문장들
       return 결과값;
    exception
       -- 예외 처리
    end 식별자;
    

7.3 파라미터 모드 (Parameter Modes)

서브프로그램의 선언부에 정의되는 파라미터는 parameter_profile의 일부로, 서브프로그램과 호출자 간의 데이터 흐름 방향을 지정하는 모드(mode)를 명시할 수 있습니다.

파라미터 모드는 서브프로그램이 해당 파라미터를 읽을 수 있는지, 또는 파라미터에 값을 쓸 수 있는지를 제어합니다. 모드는 in, out, in out 세 가지 중 하나로 지정됩니다.

7.3.1 in 모드 (Mode in)

in 모드는 파라미터가 서브프로그램으로 값을 전달(input)하는 방향으로만 정보가 전송됨을 나타냅니다. 파라미터 선언 시 모드를 명시하지 않으면 in 모드가 기본값으로 적용됩니다.

in 모드의 포멀 파라미터(formal parameter)는 서브프로그램 본체 내에서 상수(constant) 뷰로 간주됩니다. 따라서 서브프로그램은 이 파라미터의 값을 읽을 수 있지만, 값을 수정(update)할 수는 없습니다.

서브프로그램 호출 시 in 모드에 대응하는 실제 파라미터(actual parameter)는 표현식(expression)으로 해석됩니다.

파라미터 전달 방식(6.x절 참조)이 ‘by copy’일 경우, 호출 시 포멀 파라미터 객체가 생성되고 실제 파라미터의 값이 포멀 파라미터의 서브타입으로 변환되어 할당됩니다. ‘by reference’ 방식일 경우, 포멀 파라미터는 실제 파라미터 객체의 뷰(view)를 나타냅니다.

7.3.2 out 모드 (Mode out)

out 모드는 파라미터가 서브프로그램으로부터 값을 반환받는(output) 방향으로 정보가 전송됨을 나타냅니다.

서브프로그램 본체 내에서 out 모드의 포멀 파라미터(formal parameter)는 변수(variable)로 취급되어 값을 할당할 수 있습니다. 서브프로그램 호출 시 out 모드에 대응하는 실제 파라미터(actual parameter)는 반드시 변수를 지정하는 이름(name)이어야 합니다.

파라미터 전달 방식(6.x절 참조)이 ‘by copy’일 경우, 호출 시 포멀 파라미터 객체가 생성됩니다. 이때 포멀 파라미터는 초기화되지 않은 상태일 수 있습니다 (일부 복합 타입 등 예외 존재). 서브프로그램이 정상적으로 완료되면, 포멀 파라미터의 최종 값이 실제 파라미터 변수로 변환되어 할당(copy back)됩니다.

‘by reference’ 방식일 경우, 포멀 파라미터는 실제 파라미터 객체 자체를 참조(denote)하며, 서브프로그램 내에서의 할당은 실제 파라미터 객체에 직접 반영됩니다.

7.3.3 in out 모드 (Mode in out)

in out 모드는 파라미터가 양방향으로 사용됨을 나타냅니다. 즉, 정보가 서브프로그램으로 전달(input)되고, 서브프로그램 내에서 수정된 값이 다시 외부로 반환(output)됩니다.

서브프로그램 본체 내에서 in out 모드의 포멀 파라미터(formal parameter)는 변수(variable)로 취급되며, 값을 읽고 수정하는 것이 모두 가능합니다. 서브프로그램 호출 시 in out 모드에 대응하는 실제 파라미터(actual parameter)는 반드시 변수를 지정하는 이름(name)이어야 합니다.

파라미터 전달 방식(6.x절 참조)이 ‘by copy’일 경우, 호출 시 포멀 파라미터 객체가 생성되고 실제 파라미터의 값이 포멀 파라미터의 서브타입으로 변환되어 할당됩니다. 서브프로그램이 정상적으로 완료되면, 포멀 파라미터의 최종 값이 실제 파라미터 변수로 변환되어 할당(copy back)됩니다.

‘by reference’ 방식일 경우, 포멀 파라미터는 실제 파라미터 객체를 참조(denote)하며, 서브프로그램 내에서의 읽기 및 수정은 실제 파라미터 객체에 직접 반영됩니다.

7.4 연산자 중복정의 (Operator Overloading)

Ada는 서브프로그램의 designator (6.1절 참조)로 일반 식별자(identifier) 대신 연산자 기호(operator_symbol)를 사용하는 것을 허용합니다.

연산자(operator)는 operator_symboldesignator로 갖는 함수(function)입니다. 연산자는 다른 함수와 마찬가지로 중복정의(overload)될 수 있습니다.

프로그램 내에서 단항 또는 이항 연산자를 사용하는 것은, 해당 operator_symbol을 함수 접두사(function_prefix)로 하고 피연산자(operand)를 순서대로 위치적 실제 파라미터(positional actual parameter)로 하는 함수 호출(function_call)과 동일합니다.

연산자 선언 규칙 및 관례

연산자 함수를 명시적으로 선언할 때는 다음 규칙을 준수해야 합니다.

  1. 파라미터 개수: 단항(unary) 연산자는 하나의 파라미터를 가져야 하며, 이항(binary) 연산자는 두 개의 파라미터를 가져야 합니다.
  2. 파라미터 모드 (연산자 함수): 연산자 함수의 모든 파라미터는 in 모드여야 합니다. in 모드는 피연산자가 서브프로그램 내에서 상수(constant) 뷰임을 나타내며(6.3.1절 참조), 피연산자가 변경되지 않도록 합니다. 결과적으로 연산자 호출은 값을 반환하는 표현식(expression)의 일부로 동작합니다. (예: A := B + C;)
  3. 연산자 프로시저: Ada 문법은 연산자를 프로시저(procedure)로 선언하는 것을 허용할 수 있습니다. 연산자를 in out 모드를 갖는 프로시저로 정의하면 (예: procedure "+"(Left : in out Complex...);), 해당 연산은 값을 반환하는 표현식이 아닌, 피연산자의 값을 직접 수정(in-place modification)하는 명령문(statement)(예: My_Complex + Another_Complex;)으로 동작합니다. Ada 관례(idiom)에서는 객체의 상태를 직접 수정할 때 연산자 프로시저 대신 Add_To와 같이 명명된 프로시저를 사용합니다.
  4. 기본값 금지: 연산자 함수의 파라미터에는 기본 표현식(default_expression)이 허용되지 않습니다.
  5. =/=의 관계: 결과 타입이 Boolean인 = 연산자를 명시적으로 선언하면, 그와 보완적인 결과(complementary result)를 반환하는 /= 연산자가 암시적으로 함께 선언됩니다.
  6. /=의 제약: /= 연산자를 명시적으로 선언할 경우, 그 결과 타입은 미리 정의된 Boolean 타입이 될 수 없습니다.
  7. 할당 연산자: 할당 연산자(:=)는 언어 표준에 의해 중복정의가 금지되어 있습니다.

예시:

다음은 Vector라는 타입을 위해 + 연산자를 선언하는 예시입니다.

-- '+' 연산자를 함수로 선언
function "+"(Left, Right : Vector) return Vector;

A, B, CVector 타입의 변수일 때, 아래의 두 문장은 동일한 함수 호출을 나타냅니다.

-- 1. 연산자 표기법
A := B + C;

-- 2. 일반 함수 호출 표기법
A := "+"(B, C);

7.5 서브프로그램 중복정의 (Subprogram Overloading)

Ada 언어는 동일한 유효 범위(scope) 내에서 같은 식별자(이름)를 공유하는 여러 서브프로그램을 선언하는 것을 허용합니다. 이를 서브프로그램 중복정의(subprogram overloading)라고 합니다. 이는 연산자 기호를 사용하는 함수인 연산자(operator)에도 동일하게 적용됩니다(6.4절 참조).

컴파일러는 서브프로그램 호출(subprogram call) 시, 호출에 사용된 실제 파라미터(actual parameter)의 개수, 순서, 타입 및 (함수의 경우) 예상되는 반환 타입을 분석하여, 중복 정의된 여러 서브프로그램 중 호출할 구체적인 선언을 결정합니다. 이 구별의 기준이 되는 정보 집합을 서브프로그램의 프로파일(profile)이라고 합니다. 프로파일에는 파라미터의 타입, 순서, 개수 및 (함수의 경우) 반환 타입이 포함됩니다.

호출 시점에 컴파일러가 주어진 인수 목록과 일치하는 단 하나의 서브프로그램 프로파일을 식별할 수 있다면, 해당 서브프로그램이 호출됩니다. 만약 일치하는 서브프로그램이 없거나 둘 이상 존재하여 모호한 경우, 컴파일러는 오류를 보고합니다.

서브프로그램 중복정의는 동일한 이름의 연산에 대해 다른 타입의 데이터를 처리하는 구현을 제공할 때 사용됩니다.

예시:

다음은 Integer 타입과 String 타입의 값을 출력하는 두 개의 Put 프로시저를 중복 정의하는 예시입니다.

-- 정수를 출력하는 프로시저
procedure Put (Item : in Integer);

-- 문자열을 출력하는 프로시저
procedure Put (Item : in String);

-- 호출 예시
Put (123);        -- Integer 버전을 호출
Put ("Hello");    -- String 버전을 호출

컴파일러는 Put 호출 시 전달된 인수의 타입(Integer 또는 String)을 보고 어떤 Put 프로시저를 호출할지 결정합니다.

7.6 재귀 서브프로그램 (Recursive Subprograms)

서브프로그램은 실행 중에 자기 자신을 호출할 수 있습니다[cite: 55]. 이러한 호출을 재귀(recursion)라고 합니다. Ada 언어는 서브프로그램의 재귀 호출을 지원합니다.

재귀는 두 가지 형태로 나타날 수 있습니다.

  1. 직접 재귀 (Direct Recursion): 서브프로그램이 자신의 본체 내에서 자기 자신을 직접 호출하는 경우입니다.
  2. 간접 재귀 (Indirect Recursion): 서브프로그램 A가 서브프로그램 B를 호출하고, 다시 서브프로그램 B가 서브프로그램 A를 호출하는 것처럼, 여러 서브프로그램이 서로를 호출하는 연쇄를 통해 원래의 서브프로그램이 다시 호출되는 경우입니다.

재귀 호출은 문제를 동일한 구조를 가진 더 작은 하위 문제로 분할하여 해결하는 알고리즘 구현에 사용될 수 있습니다. 재귀 서브프로그램은 하나 이상의 종료 조건(base case)을 포함해야 합니다. 종료 조건은 재귀 호출을 멈추고 결과값을 반환하는 조건입니다. 종료 조건이 없으면 서브프로그램은 자기 자신을 계속 호출하게 되어 스택 오버플로(Storage_Error)가 발생할 수 있습니다.

예시: 팩토리얼 함수

팩토리얼(Factorial)은 재귀를 사용하여 구현될 수 있습니다. N의 팩토리얼(N!)은 1부터 N까지의 모든 양의 정수를 곱한 값이며, N! = N * (N-1)! 로 정의되고 0! = 1 입니다.

function Factorial (N : Natural) return Positive is
begin
   if N = 0 then
      return 1; -- 종료 조건 (Base case)
   else
      -- 재귀 호출: N * Factorial(N-1)
      return N * Factorial (N - 1);
   end if;
end Factorial;

위 함수에서 N = 0일 때 1을 반환하는 부분이 종료 조건입니다. N > 0인 경우, 함수는 NN-1의 팩토리얼 결과를 곱하기 위해 자기 자신(Factorial (N - 1))을 호출합니다. 이 재귀 호출은 N이 0이 될 때까지 계속되며, 종료 조건에 도달하면 각 단계의 결과가 곱해져 최종 팩토리얼 값이 반환됩니다.

8. 패키지를 이용한 모듈화 (Modularization with Packages)

패키지(package)는 Ada에서 모듈화(modularization), 캡슐화(encapsulation) 및 정보 은닉(information hiding)을 구현하는 구조적 단위입니다. 패키지는 논리적으로 관련된 타입, 객체, 서브프로그램 및 기타 엔티티(entity)의 집합을 하나의 명명된 단위로 묶어 제공합니다.

8.1 패키지의 개념: 명세와 본체

Ada의 패키지는 일반적으로 명세부(specification)와 본체부(body)라는 두 개의 개별적인 컴파일 단위(compilation unit)로 분리되어 정의됩니다.

이러한 분리 구조는 패키지의 인터페이스(interface)와 구현(implementation)을 분리하는 Ada의 정보 은닉(information hiding) 메커니즘입니다.

1. 패키지 명세부 (Package Specification)

패키지 명세부(package ... is ... end;)는 패키지의 공개 인터페이스(public interface)를 정의합니다.

이는 패키지 외부의 다른 프로그램 단위(클라이언트)가 with 절을 통해 해당 패키지를 참조할 때, 접근하고 사용할 수 있는 모든 엔티티(entity)를 선언하는 부분입니다.

기본 문법:

package 패키지_이름 is
   -- (Public Declarations)
   -- 외부에 공개되는 타입 선언
   -- 외부에 공개되는 상수 및 변수 선언
   -- 외부에 공개되는 서브프로그램(프로시저, 함수) 선언
   ...
private
   -- (Private Declarations)
   -- (7.3절에서 설명)
   ...
end 패키지_이름;

명세부는 “패키지가 무엇을 제공하는지” (What)를 정의합니다. 여기에는 서브프로그램의 실제 동작 코드(구현)는 포함되지 않고, 오직 해당 서브프로그램을 호출하기 위한 선언(시그니처)만 포함됩니다.

2. 패키지 본체부 (Package Body)

패키지 본체부(package body ... is ... end;)는 패키지 명세부에 선언된 기능의 실제 구현(implementation)을 포함합니다.

본체부는 패키지 외부의 클라이언트에게는 보이지 않으며, 오직 패키지 내부에서만 사용됩니다.

기본 문법:

package body 패키지_이름 is
   -- (Private Declarations)
   -- 본체 내부에서만 사용되는 지역 타입, 변수, 서브프로그램 선언
   ...

   -- 명세부에 선언된 서브프로그램의 실제 구현 (본체)
   procedure 서브프로그램_이름(...) is
   begin
      ...
   end 서브프로그램_이름;
   ...

begin
   -- (선택적) 패키지 초기화 코드
   ...
exception
   -- (선택적) 패키지 초기화 예외 처리
   ...
end 패키지_이름;

본체부는 “패키지가 어떻게 동작하는지” (How)를 정의합니다. 여기에는 명세부에 선언된 서브프로그램의 완전한 begin ... end; 블록이 포함됩니다.

명세와 본체의 분리

모든 패키지 명세부가 반드시 패키지 본체부를 필요로 하는 것은 아닙니다. 예를 들어, 타입과 상수 선언만 모아둔 패키지(예: Ada.Characters.Latin_1)는 구현 코드가 없으므로 본체부가 필요 없을 수 있습니다.

하지만 명세부에 하나 이상의 서브프로그램 선언이 포함된 경우, 해당 서브프로그램의 구현을 제공하기 위해 반드시 패키지 본체부가 존재해야 합니다.

이러한 분리 구조의 주된 목적은 캡슐화입니다. 패키지를 사용하는 클라이언트는 명세부(인터페이스)에만 의존합니다. 패키지 개발자는 명세부를 변경하지 않는 한, 본체부(구현)의 내부 로직을 수정하거나 알고리즘을 변경할 수 있으며, 이러한 변경은 패키지를 사용하는 클라이언트 코드에 아무런 영향을 주지 않습니다. 이는 소프트웨어의 유지보수성을 향상시킵니다.

8.2 패키지 사용과 가시성 (Package Usage and Visibility)

7.1절에서 설명했듯이, 패키지 명세부(specification)는 외부에 공개되는 인터페이스를 정의하고, 본체부(body)는 내부 구현을 숨깁니다.

프로그램의 다른 단위(클라이언트)가 이 패키지를 사용하기 위해서는 해당 패키지에 대한 가시성(visibility)을 획득해야 합니다. Ada는 가시성을 제어하기 위해 with 절과 use 절을 사용합니다.

8.2.1 with 절: 의존성 선언

with 절은 특정 컴파일 단위(compilation unit)가 다른 패키지의 명세부에 접근해야 함을 컴파일러에 알리는 의존성(dependency) 선언입니다. 이 선언은 해당 단위가 컴파일되기 위해 참조하는 패키지의 정보가 필요함을 명시합니다.

기본 문법:

with Ada.Text_IO;  -- Ada.Text_IO 패키지에 의존함을 선언
with Ada.Integer_Text_IO; -- 여러 패키지를 'with' 할 수 있음

procedure Example_With is
   ...

with 절을 사용하면, with 목록에 명시된 패키지(예: Ada.Text_IO)의 명세부에 선언된 모든 공개 엔티티(public entity)가 해당 단위(이 경우 Example_With 프로시저)에 가시적(visible)이 됩니다. “가시적”이라는 것은 해당 엔티티가 존재함을 컴파일러가 인지함을 의미합니다.

with 절만 사용했을 경우, 이 가시적인 엔티티에 접근하기 위해 점 표기법(dotted notation)을 사용합니다. 점 표기법은 엔티티의 이름(예: Put_Line) 앞에 패키지의 이름(예: Ada.Text_IO)을 접두사로 붙여 이름을 한정(qualify)하는 방식입니다.

예제:

with Ada.Text_IO;
procedure Hello is
begin
   -- 점 표기법을 사용하여 'Put_Line' 프로시저를 호출
   Ada.Text_IO.Put_Line("Hello, World!");
end Hello;

점 표기법(Ada.Text_IO.Put_Line)은 Put_Line이라는 이름이 Ada.Text_IO 패키지에 속해 있음을 명시적으로 나타냅니다.

8.2.2 use 절: 직접 가시성

use 절은 패키지 이름으로 한정된(qualified) 점 표기법을 사용하지 않고, 패키지 내의 엔티티 이름에 직접 접근할 수 있도록 직접 가시성(direct visibility)을 부여합니다.

use 절은 7.2.1절의 with 절에 의해 가시적이 된 패키지에 대해서만 적용할 수 있으며, with 절 뒤에 위치해야 합니다.

기본 문법:

with Ada.Text_IO;
use Ada.Text_IO; -- Ada.Text_IO의 이름들을 직접 가시적으로 만듦

procedure Hello is
   ...

use 절이 선언되면, 해당 범위 내에서는 Ada.Text_IO.와 같은 접두사(prefix) 없이 Put_Line과 같은 엔티티 이름을 직접 사용할 수 있습니다.

예제:

with Ada.Text_IO;
use Ada.Text_IO; -- 'use' 절이 추가됨
procedure Hello is
begin
   -- 점 표기법(Ada.Text_IO.Put_Line) 없이 직접 호출
   Put_Line("Hello, World!");
end Hello;

서로 다른 두 패키지(예: Package_APackage_B)를 use하고, 두 패키지 모두에 Process라는 이름의 프로시저가 존재한다면, Process라는 이름만으로는 어떤 패키지의 프로시저를 호출하는 것인지 모호해집니다(ambiguity). 이러한 이름 충돌(name clash)이 발생하면, 컴파일러는 모호성을 해결하기 위해 다시 점 표기법(예: Package_A.Process)을 사용하도록 요구합니다.

패키지 이름과 타입 이름이 동일할 경우:

Ada는 패키지 이름과 그 패키지 내부에 선언된 타입 이름이 동일한 것을 문법적으로 허용합니다.

-- counter.ads
package Counter is
   type Counter is private; -- 패키지 이름 'Counter'와 타입 이름 'Counter' 동일

   -- 실용적인 연산들 선언
   procedure Increment (C : in out Counter);
   function Value (C : Counter) return Integer;

private
   Max_Value : constant := 1000; -- 기본값으로 수정 (예시 간결화)
   type Counter is record Val : Integer range 0 .. Max_Value := 0; end record;
end Counter;

-- counter.adb
package body Counter is
   procedure Increment (C : in out Counter) is
   begin
      if C.Val < Max_Value then
         C.Val := C.Val + 1;
      end if;
   end Increment;

   function Value (C : Counter) return Integer is
   begin
      return C.Val;
   end Value;
end Counter;

use Counter; 절은 Counter 패키지 내 이름(Counter 타입, Increment 프로시저, value 함수)에 직접 가시성을 부여합니다 (ARM 8.4). 그러나 컴파일러가 특정 위치에서 이름(식별자)을 해석할 때, 이름이 사용되는 문맥(context)에 따라 적용되는 이름 해석 규칙(ARM 8.3)이 우선합니다.

변수 선언(My_Var : Name;)과 같은 문맥에서는 컴파일러가 타입 이름을 기대합니다. 하지만 use Counter;가 있는 상태에서 My_Var : Counter;를 사용하면, Counter라는 이름이 패키지 이름으로도, 타입 이름으로도 해석될 수 있는 동형이의(homograph) 상태가 됩니다 (ARM 8.3 (9-11)). 이 경우, 특정 문맥에서는 타입 이름보다 패키지 이름 해석이 우선되어 모호성이 발생하거나 의도치 않은 해석으로 컴파일 오류가 발생할 수 있습니다.

-- main.adb
with Ada.Text_IO;
with Counter;
use Counter; -- 'use' 절 사용

procedure main is
   -- My_Var : Counter; -- 컴파일 오류 발생 가능:
                      -- 이 문맥에서 'Counter'가 패키지로 해석될 수 있음.

   -- 타입을 명확히 하기 위해 점 표기법 사용 필요
   My_Var : Counter.Counter;
begin
   -- 'use Counter;' 덕분에 서브프로그램은 직접 호출 가능
   Increment(My_Var);
   Increment(My_Var);

   Ada.Text_IO.Put_Line("Current Value: " & Integer'Image(Value(My_Var)));
end main;

이러한 모호성을 피하고 타입을 명확히 지정하기 위해서는, use 절 사용 여부와 관계없이 변수 선언 등 특정 문맥에서는 My_Var : Counter.Counter; 와 같이 점 표기법을 사용하여 타입을 명시적으로 한정해야 합니다.

문법적으로 허용되지만, 가독성을 고려하여 패키지 이름과 그 안의 주요 타입 이름을 다르게 지정하는 Ada 코딩 관례가 있습니다.

8.2.3 use type

use type 절은 use 절의 제한된 형태이며, use 절이 유발할 수 있는 이름 모호성(ambiguity) 없이 연산자(operator)를 사용할 수 있도록 합니다.

이 절은 특정 타입(또는 여러 타입)과 관련된 연산자들(예: +, -, <, =)에 대해서만 직접 가시성(direct visibility)을 부여합니다.

기본 문법:

use type 패키지_이름.타입_이름;

동작 방식:

use type 절은 use 절과 달리, 해당 패키지의 서브프로그램(프로시저, 함수)이나 다른 타입 선언 등은 직접 가시적으로 만들지 않습니다. 오직 명시된 타입(예: Vector_Ops.Vector)을 매개변수나 반환 값으로 사용하는 연산자들만 직접 가시적으로 만듭니다.

이 구문은 연산자를 중위 표기법(infix notation)으로 사용할 수 있도록 합니다.

예제:

Vector_Ops라는 패키지가 Vector 타입과 해당 타입을 위한 + 연산자를 정의했다고 가정합니다.

package Vector_Ops is
   type Vector is array (Integer range <>) of Float;
   function "+" (Left, Right : Vector) return Vector;
end Vector_Ops;

use type 절이 없다면, 두 벡터를 더하기 위해 다음과 같이 함수형 표기법(prefix notation)을 사용해야 합니다.

C := Vector_Ops."+"(A, B); -- 연산자를 함수처럼 호출

use type 절을 사용하면, + 연산자가 직접 가시화되어 중위 표기법을 사용할 수 있게 됩니다.

with Vector_Ops;
procedure Test_Vectors is
   use type Vector_Ops.Vector; -- 'Vector' 타입의 연산자만 직접 가시화

   A, B, C : Vector_Ops.Vector(1 .. 3);
begin
   ...
   C := A + B; -- 'use type'으로 중위 표기법 사용

   -- 만약 'Vector_Ops'에 'Process'라는 프로시저가 있어도,
   -- 'Process(A);'는 모호성 없이 'Vector_Ops.Process(A);'로 호출해야 합니다.
end Test_Vectors;

이 방식은 use 절과 달리 다른 이름(서브프로그램 등)과의 충돌을 방지하면서, 연산자의 중위 표기법 사용을 허용합니다.

8.3 private 타입을 이용한 정보 은닉 (Information Hiding via private Types)

7.1절에서 패키지 명세부는 공개 인터페이스를 정의한다고 설명했습니다. Ada는 이 명세부 내부에 private 키워드를 사용하여, 정보 은닉의 수준을 제어하는 두 번째 영역인 비공개부(private part)를 정의할 수 있습니다.

기본 문법 (패키지 명세부):

package 패키지_이름 is
   -- 1. 공개부 (Public Part)
   -- 패키지 외부의 모든 클라이언트에게 공개됩니다.
   type Public_Type is ...;

   type Private_Type is private; -- 'private'으로 선언 (불완전 선언)
   type Limited_Type is limited private; -- 'limited private'로 선언

   procedure Operate(V : in out Private_Type);
   ...
private
   -- 2. 비공개부 (Private Part)
   -- 클라이언트에게는 숨겨지지만, 자식 패키지(7.6절)나
   -- 제네릭 인스턴스화(8장)에는 가시적입니다.

   -- 'private' 타입의 완전한 정의
   type Private_Type is record
      Component : Integer;
   end record;

   -- 'limited private' 타입의 완전한 정의
   type Limited_Type is record
      Secret : Float;
   end record;

end 패키지_이름;

이 구조는 클라이언트가 타입의 존재는 알되, 그 내부 구조(예: 레코드 컴포넌트)는 알 수 없도록 합니다.

1. private 타입

타입이 private으로 선언되면 (예: type Private_Type is private;), 해당 타입의 이름은 패키지 외부(클라이언트)에 공개됩니다. 클라이언트는 이 타입의 객체를 선언하고, 패키지가 제공하는 서브프로그램(예: Operate)에 전달할 수 있습니다.

그러나 클라이언트는 이 타입의 내부 구조에 직접 접근할 수 없습니다. 예를 들어 My_Var : Private_Type;이 선언되었을 때, My_Var.Component와 같은 접근은 컴파일 오류를 발생시킵니다.

private 타입은 기본적으로 할당(assignment) 연산(:=)과 동등 비교(equality) 연산(=, /=)을 허용합니다 (해당 타입의 실제 정의가 이를 허용하는 경우).

2. limited private 타입

타입이 limited private으로 선언되면 (예: type Limited_Type is limited private;), private 타입의 모든 정보 은닉 특성을 가집니다.

이에 더해, limited private 타입은 외부에서의 할당(:=) 및 동등 비교(=, /=) 연산까지 금지합니다.

limited 타입의 객체는 오직 해당 패키지가 명시적으로 제공하는 서브프로그램을 통해서만 상태를 변경하거나 비교할 수 있습니다. 이는 객체의 상태를 제어해야 하는 경우(예: 파일 핸들러, 뮤텍스(Mutex) 객체)에 사용되며, 추상 데이터 타입(Abstract Data Type, ADT)을 구현하는 데 사용됩니다.

8.4 컴파일 단위

Ada 프로그램은 하나 이상의 컴파일 단위(compilation unit)로 구성됩니다 (ARM 10.1). 컴파일 단위는 Ada 컴파일러가 독립적으로 구문 분석하고 컴파일할 수 있는 소스 코드의 최소 단위입니다.

이러한 컴파일 단위들은 Ada 라이브러리(library)에 저장되며, 라이브러리 유닛(library unit)이라고 불립니다. 7.1절에서 다룬 패키지 명세부와 패키지 본체부는 라이브러리 유닛의 가장 대표적인 예입니다.

라이브러리 유닛에는 다음이 포함됩니다:

  • 패키지 명세부 (Package Specification)
  • 패키지 본체부 (Package Body)
  • 서브프로그램 선언 (Subprogram Declaration)
  • 서브프로그램 본체 (Subprogram Body) (예: 7.2.1절의 Hello 프로시저)
  • 제네릭 선언 (Generic Declaration) (8장)
  • 제네릭 인스턴스화 (Generic Instantiation) (8장)

독립 컴파일 (Separate Compilation)

Ada는 이러한 컴파일 단위 구조를 통해 독립 컴파일(separate compilation)을 지원합니다.

7.2.1절에서 설명한 with 절은 이러한 라이브러리 유닛 간의 의존성(dependency)을 명시하는 구문입니다. 컴파일러는 with 절을 분석하여 유닛 간의 컴파일 순서를 강제합니다.

어떤 유닛(예: 프로시저 Hello)을 컴파일하기 위해서는, 해당 유닛이 with하는 모든 패키지(예: Ada.Text_IO)의 명세부(.ads 파일)가 먼저 컴파일되어 라이브러리에 존재해야 합니다.

이 구조는 7.1절에서 설명한 인터페이스와 구현의 분리를 물리적으로 보장합니다. 패키지 본체부(구현, .adb 파일)가 변경되더라도, 명세부(인터페이스, .ads 파일)가 동일하게 유지되는 한, 해당 패키지를 with하는 클라이언트 코드들은 재컴파일(recompile)할 필요가 없습니다.

8.5 정교화

Ada 프로그램이 메인 서브프로그램(main subprogram)의 실행을 시작하기 전에, 프로그램이 의존하는 모든 라이브러리 유닛(패키지 등)을 사용 가능하도록 준비하는 런타임 과정을 정교화(Elaboration)라고 합니다 (ARM 10.2).

정교화는 선언된 모든 라이브러리 유닛을 “실행 준비 상태”로 만드는 과정입니다. 이 과정에는 타입 선언을 처리하고, 객체(변수)를 생성하며, 해당 객체에 초기값을 할당하는 작업이 포함됩니다.

패키지 초기화 (Package Initialization)

7.1절에서 설명했듯이, 패키지 본체부(package body)의 마지막에는 선택적으로 begin ... end; 블록을 포함할 수 있습니다.

package body 패키지_이름 is
   -- (내부 선언 및 서브프로그램 구현)
   ...
begin
   -- (패키지 초기화 코드)
   -- 이 코드는 정교화 과정에서 단 한 번 실행됩니다.
   ...
exception
   -- (선택적) 초기화 코드 예외 처리
   ...
end 패키지_이름;

begin ... end; 블록에 포함된 문장들을 초기화 코드(initialization code)라고 부릅니다. 이 코드는 프로그램 전체 생명주기 동안 단 한 번, 해당 패키지가 정교화될 때 실행됩니다.

이 초기화 코드는 패키지가 외부에 서비스를 제공하기 전에 수행해야 하는 내부 상태 설정에 사용됩니다. 예를 들어, 데이터 구조(예: 룩업 테이블)의 초기화, 하드웨어 장치의 등록, 또는 로그 파일의 개방(open)과 같은 작업을 수행할 수 있습니다.

정교화 순서 (Order of Elaboration)

정교화 순서는 7.2.1절에서 설명한 with 절에 의해 제어됩니다.

만약 Package_Awith Package_B;를 선언했다면, Ada 런타임 시스템은 Package_A를 정교화하기 전에 반드시 Package_B가 먼저 정교화되었음을 보장합니다.

프로그램 실행은 이 의존성 그래프(dependency graph)를 따라 모든 필요한 라이브러리 유닛의 정교화가 완료된 후에야 비로소 메인 서브프로그램의 실행을 시작합니다.

만약 패키지 초기화 코드(begin ... end; 블록) 실행 중에 처리되지 않은 예외(exception)가 발생하면, 정교화 과정이 실패합니다. 이 경우 일반적으로 Program_Error가 발생하며, 프로그램은 메인 서브프로그램이 시작되기도 전에 종료됩니다.

8.6 자식 패키지

자식 패키지(Child Package)는 기존 패키지(부모 패키지)에 계층적으로 종속되는 하위 패키지입니다 (ARM 10.1.1). 이 구조는 대규모의 단일 패키지를 여러 개의 개별 패키지로 구성된 서브시스템(subsystem)으로 조직화하는 메커니즘을 제공합니다.

기본 문법:

자식 패키지는 부모 패키지의 이름 뒤에 점(.)과 자식의 이름을 붙여 명명합니다.

  • 자식 패키지 명세부:
    package Parent.Child is
       ...
    end Parent.Child;
    
  • 자식 패키지 본체부:
    package body Parent.Child is
       ...
    end Parent.Child;
    

자식 패키지의 가시성 규칙

자식 패키지는 부모 패키지의 private 영역에 접근할 수 있습니다.

  1. 공개(Public) 영역 가시성: 자식 패키지의 명세부와 본체부는 부모 패키지 명세부의 공개(public) 영역(7.1절의 package ... is ... private 이전 부분)에 선언된 모든 엔티티에 대한 가시성을 가집니다.
  2. 비공개(Private) 영역 가시성: 자식 패키지의 private 영역과 본체(body)는 부모 패키지 명세부의 private 영역(7.6절에서 다룰)에 선언된 엔티티에 대한 가시성을 가집니다 (ARM 10.1.1(10)).

이러한 가시성 규칙은 부모 패키지의 구현 세부 사항(예: private 타입의 실제 정의)을 일반 클라이언트에게는 숨기면서(정보 은닉), 해당 세부 사항에 접근해야 하는 연관된 확장 기능(자식 패키지)에게 접근을 허용합니다.

예제:

Stacks 패키지가 private 타입으로 스택을 구현하고, Stacks.Debug라는 자식 패키지가 이 스택의 내부를 검사한다고 가정합니다.

  • 부모 패키지 (Stacks.ads):

    package Stacks is
       type Stack is limited private;
       procedure Push(S : in out Stack; Item : Integer);
       -- ...
    private
       Max_Size : constant := 100;
       type Stack_Data is array (1 .. Max_Size) of Integer;
       type Stack is limited record
          Data : Stack_Data;
          Top  : Natural := 0;
       end record;
    end Stacks;
    
  • 자식 패키지 (Stacks.Debug.ads):

    package Stacks.Debug is
       -- 이 자식 패키지는 부모의 'Stack' 타입을 알고 있습니다.
       procedure Dump(S : in Stacks.Stack);
    end Stacks.Debug;
    
  • 자식 패키지 본체 (Stacks.Debug.adb):

    package body Stacks.Debug is
       procedure Dump(S : in Stacks.Stack) is
       begin
          -- 이 본체는 부모의 'private' 영역에 접근 가능합니다.
          -- 따라서 'S.Data'나 'S.Top' 같은 'private' 컴포넌트에
          -- 직접 접근할 수 있습니다.
          for I in 1 .. S.Top loop
             -- (S.Data(I)에 접근하여 덤프 로직 수행)
             ...
          end loop;
       end Dump;
    end Stacks.Debug;
    

일반 클라이언트(예: Main 프로시저)는 Stacks.Stack 타입의 내부(Data, Top)에 접근할 수 없지만, Stacks.Debug 패키지는 부모의 private 멤버에 접근하여 디버깅 기능을 구현할 수 있습니다.

9. 제네릭 프로그래밍 (Generic Programming)

제네릭 프로그래밍(Generic Programming)은 데이터 타입이나 특정 값에 의존하지 않고, 일반화된(generic) 알고리즘과 자료 구조를 작성하는 프로그래밍 패러다임입니다. Ada는 이 패러다임을 언어 차원에서 지원하며, 이는 소프트웨어의 재사용성(reusability)을 위한 기능입니다.

9.1 제네릭의 개념과 재사용성

제네릭 유닛(Generic Unit)은 서브프로그램(프로시저, 함수) 또는 패키지를 위한 템플릿(template) 또는 청사진(blueprint)입니다. 제네릭 유닛 자체는 직접 호출하거나 사용할 수 없으며, 구체적인 유닛을 생성하기 위한 모델 역할을 합니다.

제네릭 프로그래밍의 핵심 목적은 소프트웨어 재사용성(reusability)을 달성하는 것입니다.

예를 들어, 두 Integer 값의 교환(swap) 로직과 두 Float 값의 교환 로직은 알고리즘 관점에서 동일합니다. 제네릭을 사용하지 않는다면, 프로그래머는 각 데이터 타입( Integer, Float, Color 등)에 대해 이름만 다르고 내용은 동일한 교환 프로시저를 반복적으로 작성해야 합니다.

제네릭 프로그래밍은 이러한 반복을 제거합니다. 프로그래머는 특정 타입에 의존하지 않는 단 하나의 제네릭 서브프로그램 (예: Generic_Swap)을 작성합니다. 이 템플릿은 구체적인 타입 대신, “어떤 타입이든 될 수 있는” 제네릭 형식 매개변수(generic formal parameter)를 사용하여 로직을 기술합니다.

프로그래머가 이 템플릿을 실제 Integer 타입이나 Float 타입과 함께 사용하고자 할 때, 인스턴스화(instantiation)라는 과정을 거칩니다. 인스턴스화는 컴파일러에게 해당 제네릭 템플릿을 기반으로 특정 타입을 위한 구체적인(concrete) 서브프로그램(예: Swap_Integers, Swap_Floats)을 생성하도록 지시하는 명시적 선언입니다.

이 방식을 통해, 단 하나의 제네릭 소스 코드를 재사용하여 컴파일 시점에 다양한 타입에 특화된, 타입-안전성(type-safe)이 보장되는 여러 개의 실행 코드를 생성할 수 있습니다.

9.2 제네릭 단위 선언 (Generic Declaration)

제네릭 단위 선언(Generic Declaration)은 8.1절에서 설명한 템플릿(template)을 정의하는 구문입니다. 제네릭 선언은 generic 키워드로 시작하며, 제네릭 부(generic part)와 제네릭 단위의 명세로 구성됩니다 (ARM 12.1).

  1. 제네릭 부 (Generic Part): generic 키워드와 제네릭 단위(서브프로그램 또는 패키지) 명세 사이에 위치하며, 템플릿이 받을 제네릭 형식 매개변수(generic formal parameters)를 선언합니다.
  2. 제네릭 단위 (Generic Unit): 제네릭 부에서 선언된 매개변수를 사용하는 서브프로그램 또는 패키지의 명세(specification)입니다.

기본 문법 (제네릭 서브프로그램):

generic
   -- (제네릭 형식 매개변수 선언)
procedure Generic_Procedure_Name( ... );

-- 또는
generic
   -- (제네릭 형식 매개변수 선언)
function Generic_Function_Name( ... ) return ...;

기본 문법 (제네릭 패키지):

generic
   -- (제네릭 형식 매개변수 선언)
package Generic_Package_Name is
   ...
end Generic_Package_Name;

제네릭 형식 매개변수 (Generic Formal Parameters)

제네릭 형식 매개변수는 인스턴스화(instantiation) 과정에서 클라이언트가 제공할 구체적인 값, 타입 또는 서브프로그램을 받아들이는 “자리 표시자(placeholder)”입니다.

주요 매개변수 유형은 다음과 같습니다.

1. 제네릭 형식 객체 (Generic Formal Objects)

제네릭 유닛이 사용할 값(상수)을 외부에서 받아옵니다.

-- 'Capacity'라는 이름의 Natural 타입 값을 받아옵니다.
Capacity : Natural;

-- 기본값을 지정할 수도 있습니다.
Default_Size : Positive := 100;

2. 제네릭 형식 타입 (Generic Formal Types)

제네릭 유닛이 작동할 데이터 타입을 외부에서 받아옵니다. 형식 타입은 is 뒤에 오는 구문에 따라 받아들일 수 있는 타입의 종류를 한정합니다.

  • type Item is private;
    • limited가 아닌 모든 타입을 받아들일 수 있습니다 (즉, 할당(:=)과 동등 비교(=)가 가능하다고 가정).
  • type Item is limited private;
    • limited 타입을 포함한 모든 타입을 받아들일 수 있습니다 (즉, 할당이나 동등 비교를 가정하지 않음).
  • type T is (<>); (Box 구문)
    • 모든 이산 타입(Discrete Type, 예: Integer, Character 또는 열거형)을 받아들입니다.
  • type T is range <>;
    • 모든 부호 있는(signed) 정수 타입을 받아들입니다.
  • type T is mod <>;
    • 모든 모듈로(modular) 타입을 받아들입니다.

3. 제네릭 형식 서브프로그램 (Generic Formal Subprograms)

제네릭 유닛 내부에서 사용할 서브프로그램(연산)을 외부에서 받아옵니다. with 키워드를 사용하여 선언합니다.

-- 'Item' 타입에 대한 "<" 연산자를 받아옵니다.
with function "<" (Left, Right : Item) return Boolean;

-- 연산자가 아닌 일반 프로시저를 받아옵니다.
with procedure Process(V : in out Item);

예제: 제네릭 선언 (Generic_Swap)

8.1절의 교환(swap) 프로시저를 제네릭으로 선언하는 예제입니다.

generic
   type Item is private; -- 1. 형식 타입: 할당이 가능한 모든 타입
procedure Generic_Swap(A, B : in out Item);

예제: 제네릭 선언 (Generic_Stack)

여러 형식 매개변수를 갖는 제네릭 패키지 선언 예제입니다.

generic
   Capacity : Positive;     -- 1. 형식 객체 (스택의 최대 크기)
   type Item is private;  -- 2. 형식 타입 (스택에 저장할 요소의 타입)
package Generic_Stack is
   procedure Push(V : in Item);
   procedure Pop(V : out Item);
   function Is_Empty return Boolean;
   function Is_Full return Boolean;
end Generic_Stack;

9.3 제네릭 파라미터 (Generic Parameters)

제네릭 파라미터는 인스턴스화 시점에 구체적인 실체(타입, 객체, 서브프로그램)로 대체될 ‘자리표시자(placeholder)’입니다.

9.3.1 제네릭 타입 파라미터 (Generic Type Parameters)

제네릭 타입 파라미터는 인스턴스화 시점에 구체적인 타입으로 대체될 자리 표시자입니다. Ada는 제네릭 선언 시점에 해당 타입이 어떤 범주(category)에 속하는지를 명시하도록 요구합니다.

이 선언은 제네릭 유닛의 본체(body)가 해당 타입의 객체에 대해 가정하고 사용할 수 있는 연산의 집합을 정의합니다.

예를 들어, type T is (<>); (정규 이산 타입)으로 선언된 파라미터는 제네릭 본체 내에서 'Pos, 'Val 같은 이산 타입 전용 속성(attribute)의 사용을 허용합니다. 반면, type T is private;로 선언된 파라미터는 이러한 속성을 사용할 수 없지만, 대신 할당(:=)이나 동등 비교(=) 연산을 사용할 수 있습니다.

주요 제네릭 형식 타입 구문과 그 의미는 다음과 같습니다 (ARM 12.5):

  • type T is private; (정규 비공개 타입) 할당과 동등 비교가 가능한 타입(제한되지 않은 타입)을 실제 파라미터로 받을 수 있습니다.

  • type T is limited private; (정규 제한된 비공개 타입) limited 타입을 포함하여 모든 타입을 받을 수 있습니다. 제네릭 본체는 이 타입에 대해 할당이나 동등 비교 연산을 가정할 수 없습니다.

  • type T is (<>); (정규 이산 타입, “box” 구문) 모든 이산 타입(정수, 열거형, 문자 타입 등)과 일치합니다.

  • type T is range <>; (정규 부호 있는 정수 타입) Integer와 같이 부호 있는 정수 타입의 범주와 일치합니다.

  • type T is mod <>; (정규 모듈로 타입) 모듈로(비부호) 정수 타입의 범주와 일치합니다.

  • type T is digits <>; (정규 부동소수점 타입) Float과 같은 부동소수점 타입의 범주와 일치합니다.

  • type T is delta <>; (정규 보통 고정소수점 타입) 보통 고정소수점 타입의 범주와 일치합니다.

  • type T is new Ancestor [with private]; (정규 파생 타입) 지정된 Ancestor 타입의 모든 파생 타입과 일치합니다.

  • type T is array (...) of ...; (정규 배열 타입) 지정된 인덱스 타입 및 컴포넌트 타입과 일치하는 배열 타입만 받습니다.

  • type T is access ...; (정규 액세스 타입) 지정된 접근(access) 정의와 일치하는 액세스 타입만 받습니다.

  • type T is interface; (정규 인터페이스 타입) 인터페이스 타입의 범주와 일치합니다.

9.3.2 제네릭 객체 파라미터 (Generic Object Parameters)

제네릭 객체 파라미터는 인스턴스화 시점에 제네릭 유닛으로 구체적인 객체(object)(상수 또는 변수)를 전달하기 위한 자리 표시자입니다 (ARM 12.4).

제네릭 형식 타입(8.3.1절)이 유닛이 사용할 타입을 지정하는 것과 달리, 제네릭 객체 파라미터는 해당 타입의 이나 변수 자체를 전달받습니다.

이 파라미터는 모드(mode)를 가지며, 모드에 따라 제네릭 유닛 내부에서의 동작이 결정됩니다.

기본 문법:

defining_identifier_list : mode subtype_mark [:= default_expression];

1. in 모드 (상수 파라미터)

in 모드는 파라미터의 기본 모드이며, mode 키워드가 생략될 경우 적용됩니다.

  • 동작: in 모드 파라미터는 제네릭 유닛의 본체 내부에서 상수(constant)로 취급됩니다. 즉, 그 값을 읽을 수는 있지만 수정할 수는 없습니다.
  • 용도: 주로 제네릭 유닛의 동작을 설정(configure)하는 정적인 값(예: 배열의 크기, 최대 용량, 타임아웃 값)을 전달하는 데 사용됩니다.
  • 실제 파라미터: 인스턴스화 시점에 전달되는 실제 파라미터(actual parameter)는 정적 표현식(static expression)일 수 있습니다.

2. in out 모드 (변수 파라미터)

in out 모드는 반드시 명시적으로 선언되어야 합니다.

  • 동작: in out 모드 파라미터는 제네릭 유닛의 본체 내부에서 변수(variable)로 취급됩니다. 제네릭 유닛은 이 파라미터를 통해 인스턴스화 시점에 전달된 외부 변수의 값을 읽고 수정할 수 있습니다.
  • 용도: 제네릭 유닛이 외부의 공유 상태(예: 공유 카운터, 상태 플래그)에 직접 접근하여 수정해야 할 때 사용됩니다.
  • 실제 파라미터: 인스턴스화 시점에 전달되는 실제 파라미터는 반드시 변수의 이름(name)이어야 합니다.

3. 기본값 (Default Expression)

in 모드와 in out 모드 파라미터 모두 := 기호를 사용하여 기본값(default expression)을 지정할 수 있습니다. 기본값이 지정된 파라미터는 인스턴스화 시점에서 생략될 수 있으며, 생략될 경우 이 기본값이 사용됩니다.

예제:

8.2절의 Generic_Stack 선언에서 Capacityin 모드 제네릭 객체 파라미터입니다.

generic
   -- 'in' 모드 객체 파라미터 (상수 취급)
   Capacity : Positive;

   -- 기본값을 가진 'in' 모드 객체 파라미터
   Default_Item : in Item := Item'First;

   -- 'in out' 모드 객체 파라미터 (변수 취급)
   Overflow_Counter : in out Natural;

   type Item is private;
package Generic_Stack is
   ...
end Generic_Stack;

9.3.3 제네릭 서브프로그램 파라미터 (Generic Subprogram Parameters)

제네릭 서브프로그램 파라미터는 인스턴스화 시점에 구체적인 서브프로그램(프로시저 또는 함수)을 제네릭 유닛으로 전달하기 위한 자리 표시자입니다 (ARM 12.6).

이 메커니즘은 8.3.1절에서 전달받은 제네릭 형식 타입(예: type Item)에 대해 제네릭 본체가 수행해야 할 연산(operation)이나 동작(behavior)을 외부로부터 주입받기 위해 사용됩니다.

기본 문법:

제네릭 서브프로그램 파라미터는 with 키워드를 사용하여 선언합니다.

-- (프로시저 파라미터)
with procedure Procedure_Name (parameter_list);

-- (함수 파라미터)
with function Function_Name (parameter_list) return Type_Mark;

예제: 연산자 전달

예를 들어, 임의의 Item 타입을 정렬하는 제네릭 프로시저를 작성한다고 가정합니다. 정렬 알고리즘은 두 Item을 비교하기 위한 “보다 작음”(<) 연산이 필요합니다.

generic
   type Item is private; -- (8.3.1) 어떤 타입이든 받음
   type Item_Array is array (Integer range <>) of Item;

   -- (8.3.3) 'Item' 타입을 비교할 함수를 파라미터로 받음
   with function "<" (Left, Right : Item) return Boolean;

procedure Generic_Sort(A : in out Item_Array);

제네릭 본체(Generic_Sortbody) 내부에서는, if A(I) < A(J) then ...와 같이 with 절로 전달받은 < 연산자를 사용하여 로직을 구현할 수 있습니다.

기본값 지정 (is <> Box 구문)

제네릭 서브프로그램 파라미터는 기본값(default)을 가질 수 있습니다. 기본값 지정 방식 중 하나는 is <> (일명 “box” 구문)입니다.

문법:

with function "<" (Left, Right : Item) return Boolean is <>;

is <>의 의미는 다음과 같습니다:

“인스턴스화 시점에 사용자가 < 함수를 명시적으로 제공하지 않으면, 컴파일러는 인스턴스화에 사용된 실제 타입(예: Integer)에 대해 가시적인(visible) < 연산자를 찾아서 사용하라.”

IntegerFloat과 같은 기본 타입들은 이미 < 연산자를 가지고 있으므로, is <> 구문을 사용하면 해당 제네릭을 이 타입들로 인스턴스화할 때 사용자가 비교 함수를 매번 전달할 필요가 없어집니다.

만약 인스턴스화 시점에 is <>에 해당하는 서브프로그램을 찾을 수 없으면 컴파일 오류가 발생합니다.

예제 (is <> 적용):

generic
   type Item is private;
   type Item_Array is array (Integer range <>) of Item;

   -- '<' 연산자를 자동으로 찾도록 기본값 지정
   with function "<" (Left, Right : Item) return Boolean is <>;

procedure Generic_Sort(A : in out Item_Array);

9.4 제네릭 인스턴스화 (Generic Instantiation)

제네릭 인스턴스화(Generic Instantiation)는 8.2절에서 선언된 제네릭 단위(템플릿)를 기반으로 구체적인(concrete) 패키지나 서브프로그램을 생성하는 선언(declaration)입니다 (ARM 12.3).

제네릭 단위 자체는 템플릿이므로 직접 사용(호출, with 등)할 수 없습니다. 인스턴스화 과정을 통해 이름이 부여된 실체(entity)를 생성해야 합니다.

기본 문법:

인스턴스화는 new 키워드를 사용하여 수행됩니다.

  • 제네릭 서브프로그램 인스턴스화:

    procedure New_Procedure_Name is
       new Generic_Procedure_Name (제네릭_실제_매개변수);
    
    function New_Function_Name is
       new Generic_Function_Name (제네릭_실제_매개변수);
    
  • 제네릭 패키지 인스턴스화:

    package New_Package_Name is
       new Generic_Package_Name (제네릭_실제_매개변수);
    

제네릭 실제 매개변수 (Generic Actual Parameters)

인스턴스화를 할 때, 제네릭 선언부(8.2절)에 정의된 제네릭 형식 매개변수(formal parameters)에 대응하는 제네릭 실제 매개변수(actual parameters)를 전달합니다.

  • 형식 매개변수가 type Item is private;이면, 실제 매개변수로 Integer, Float 또는 Boolean과 같은 구체적인 타입을 전달합니다.
  • 형식 매개변수가 Capacity : Positive;이면, 실제 매개변수로 100과 같은 구체적인 Positive 타입의 값을 전달합니다.
  • 형식 매개변수가 with function "<" ...이면, 실제 매개변수로 해당 시그니처와 일치하는 함수를 전달합니다 (단, 8.3.3절의 is <>와 같이 기본값이 지정된 경우 생략할 수 있습니다).

매개변수 전달 방식은 서브프로그램 호출과 동일하게 위치 표기법(positional notation)이나 이름 표기법(named notation)을 사용할 수 있습니다.

예제 1: Generic_Swap 인스턴스화 (8.2절 예제 기반)

8.2절의 Generic_Swap 템플릿으로부터 Integer 타입을 위한 구체적인 프로시저와 Boolean 타입을 위한 프로시저를 생성합니다.

-- (Generic_Swap 선언이 가시적이라고 가정)

-- 1. Integer 타입을 위한 인스턴스화
procedure Swap_Integers is
   new Generic_Swap (Item => Integer);

-- 2. Boolean 타입을 위한 인스턴스화
procedure Swap_Booleans is
   new Generic_Swap (Item => Boolean);

...
-- 생성된 구체적인 프로시저 사용
X, Y : Integer;
A, B : Boolean;
...
Swap_Integers(X, Y);   -- 'Integer' 버전을 호출
Swap_Booleans(A, B);   -- 'Boolean' 버전을 호출

예제 2: Generic_Stack 인스턴스화 (8.2절 예제 기반)

8.2절의 Generic_Stack 템플릿으로부터, Capacity (객체 파라미터)와 Item (타입 파라미터)을 전달받아 구체적인 스택 패키지를 생성합니다.

-- 100개의 'Integer'를 저장하는 스택 패키지 생성
package Integer_Stack is
   new Generic_Stack (Capacity => 100, Item => Integer);

-- 50개의 'Character'를 저장하는 스택 패키지 생성
package Char_Stack is
   new Generic_Stack (Capacity => 50, Item => Character);

...
-- 생성된 구체적인 패키지 사용
with Integer_Stack;
...
   Value : Integer;
   Integer_Stack.Push(Value);

이렇게 생성된 Integer_StackChar_Stack은 더 이상 제네릭이 아니며, with 절로 참조하고 사용할 수 있는 일반 패키지입니다.

9.5 제네릭 서브프로그램 (Generic Subprograms)

제네릭 서브프로그램(프로시저 또는 함수)은 특정 타입이나 값에 독립적인 알고리즘을 구현하는 템플릿입니다.

제네릭 서브프로그램은 다른 서브프로그램과 마찬가지로, 반드시 선언(specification)과 본체(body)를 가져야 합니다.

  1. 제네릭 선언 (Generic Declaration): 8.2절에서 설명한 generic ... procedure ...; 또는 generic ... function ...; 구문으로, 서브프로그램의 인터페이스와 제네릭 파라미터를 정의합니다.
  2. 제네릭 본체 (Generic Body): 제네릭 선언에 대응하는 procedure ... is ... end; 또는 function ... is ... end; 구문으로, 제네릭 파라미터를 사용하여 알고리즘의 실제 구현을 제공합니다.

제네릭 본체는 선언부와 분리되어 컴파일될 수 있습니다.

예제 1: Generic_Swap의 본체

8.2절에서 선언한 Generic_Swap 프로시저의 제네릭 본체는 다음과 같이 작성할 수 있습니다.

제네릭 선언 (복습):

generic
   type Item is private; -- 할당(:=)이 가능한 모든 타입
procedure Generic_Swap(A, B : in out Item);

제네릭 본체 (구현):

procedure Generic_Swap(A, B : in out Item) is
   Temp : constant Item := A; -- 'Item' 타입을 임시 변수 선언에 사용
begin
   A := B;
   B := Temp;
end Generic_Swap;

이 본체는 ItemInteger인지 Boolean인지에 관계없이, generic 선언부에 type Item is private;로 선언되었으므로 Item 타입의 객체를 선언하고 할당(:=) 연산을 사용할 수 있습니다.

예제 2: Generic_Sort의 본체

8.3.3절에서 선언한 Generic_Sort 프로시저의 제네릭 본체는 제네릭 타입 파라미터(Item)와 제네릭 서브프로그램 파라미터(<)를 모두 사용합니다.

제네릭 선언 (복습):

generic
   type Item is private;
   type Item_Array is array (Integer range <>) of Item;
   with function "<" (Left, Right : Item) return Boolean is <>;
procedure Generic_Sort(A : in out Item_Array);

제네릭 본체 (구현 - 삽입 정렬):

procedure Generic_Sort(A : in out Item_Array) is
   Key : Item;
   J   : Integer;
begin
   -- (삽입 정렬 알고리즘)
   for I in A'First + 1 .. A'Last loop
      Key := A(I);
      J   := I - 1;

      -- 제네릭 서브프로그램 파라미터 '<'를 사용하여 비교
      while J >= A'First and then A(J) > Key loop
         -- (만약 '>'가 정의되지 않았다면 'not (A(J) < Key or A(J) = Key)')
         A(J + 1) := A(J);
         J        := J - 1;
      end loop;

      A(J + 1) := Key;
   end loop;
end Generic_Sort;

이 제네릭 서브프로그램들은 8.4절에서 설명한 new 키워드를 사용한 인스턴스화를 통해서만 실제 호출 가능한 프로시저(예: Swap_Integers, Sort_Integers)가 됩니다.

9.6 제네릭 패키지 (Generic Packages)

제네릭 패키지는 특정 타입이나 값에 독립적인 관련 엔티티(타입, 객체, 서브프로그램 등)의 묶음을 정의하는 패키지 템플릿입니다.

8.5절의 제네릭 서브프로그램이 단일 알고리즘을 일반화하는 반면, 제네릭 패키지는 Stack, Vector, Map과 같은 전체 자료 구조 또는 관련 API의 집합을 일반화하는 데 사용됩니다.

제네릭 패키지 역시 선언(specification)과 본체(body)로 구성됩니다.

  1. 제네릭 패키지 선언 (Generic Package Declaration): 8.2절에서 설명한 generic ... package ... is ... end; 구문입니다. 이는 제네릭 파라미터(형식 타입, 형식 객체 등)와, 인스턴스화되었을 때 생성될 패키지의 공개 인터페이스(public part) 및 비공개부(private part)를 정의합니다.

  2. 제네릭 패키지 본체 (Generic Package Body): package body ... is ... end; 구문입니다. 이는 제네릭 명세부에 선언된 서브프로그램의 실제 구현을 제공합니다. 이 본체는 제네릭 파라미터를 사용하여 작성됩니다.

예제: Generic_Stack의 본체

8.2절에서 Generic_Stack의 제네릭 선언을 정의했습니다. 이제 이 템플릿의 실제 구현인 제네릭 본체를 작성할 수 있습니다.

제네릭 선언 (복습):

generic
   Capacity : Positive;     -- (8.3.2) 형식 객체
   type Item is private;  -- (8.3.1) 형식 타입
package Generic_Stack is
   procedure Push(V : in Item);
   procedure Pop(V : out Item);
   function Is_Empty return Boolean;
   function Is_Full return Boolean;

private
   -- 비공개부는 제네릭 파라미터를 사용할 수 있습니다.
   -- (이 부분은 'private' 선언이므로 Generic_Stack의
   -- 클라이언트에게는 보이지 않습니다.)
   Data : array (1 .. Capacity) of Item;
   Top  : Natural := 0;
end Generic_Stack;

제네릭 본체 (구현):

제네릭 본체는 제네릭 파라미터인 CapacityItem을 사용하여 로직을 구현합니다.

package body Generic_Stack is

   procedure Push(V : in Item) is
   begin
      if Is_Full then
         -- (예외 처리 - 10장에서 다룸)
         raise Stack_Error;
      else
         Top := Top + 1;
         Data(Top) := V; -- 'Item' 타입의 할당 사용
      end if;
   end Push;

   procedure Pop(V : out Item) is
   begin
      if Is_Empty then
         raise Stack_Error;
      else
         V   := Data(Top);
         Top := Top - 1;
      end if;
   end Pop;

   function Is_Empty return Boolean is
   begin
      return Top = 0;
   end Is_Empty;

   function Is_Full return Boolean is
   begin
      -- 'Capacity' 제네릭 파라미터를 사용
      return Top = Capacity;
   end Is_Full;

end Generic_Stack;

이 제네릭 패키지는 8.4절에서 설명한 대로 new 키워드를 통해 인스턴스화되어야만 Integer_Stack 또는 Char_Stack과 같은 실제 패키지로서 with 절에 사용되거나 호출될 수 있습니다.

10. 객체 지향 프로그래밍 (OOP)

10.1 Ada의 객체 지향 패러다임 개요

객체 지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 객체(object)들의 상호작용으로 모델링하는 프로그래밍 패러다임입니다. Ada는 절차적 프로그래밍과 더불어 객체 지향 프로그래밍을 지원합니다. 이 장에서는 Ada가 OOP의 개념인 캡슐화(encapsulation), 상속(inheritance), 다형성(polymorphism)을 어떻게 구현하는지 살펴봅니다.

Ada의 객체 지향 기능은 태그드 타입(tagged type)타입 확장(type extension)을 기반으로 합니다. 태그드 타입은 실행 시점에 자신의 타입을 식별하는 태그(tag)를 가지는 타입으로, 상속과 다형성에 사용됩니다. 타입 확장은 기존 태그드 타입으로부터 새로운 컴포넌트나 연산을 추가하여 새로운 타입을 파생시키는 메커니즘으로, 이는 상속에 해당합니다.

캡슐화는 데이터와 해당 데이터를 조작하는 연산을 하나로 묶고 내부 구현을 숨기는 개념입니다. Ada에서는 패키지(package)전용 타입(private type)을 통해 이를 구현합니다. 패키지는 관련된 선언들을 그룹화하고, 전용 타입은 타입의 표현(representation)을 사용자로부터 숨겨 정보 은닉을 제공합니다.

상속은 기존 타입의 특성(데이터 컴포넌트, 연산)을 물려받아 새로운 타입을 정의하는 기능입니다. Ada에서는 태그드 타입을 타입 확장하여 이를 구현합니다. 자식 타입(파생 타입)은 부모 타입의 컴포넌트와 기본 연산(primitive operation)을 상속받으며, 새로운 컴포넌트를 추가하거나 상속받은 연산을 재정의(override)할 수 있습니다.

다형성은 하나의 인터페이스(예: 서브프로그램 호출)가 다양한 타입의 객체에 대해 서로 다른 방식으로 동작하는 능력입니다. Ada에서는 클래스-범위 타입(class-wide type) (T'class) 과 디스패칭(dispatching) 을 통해 런타임 다형성을 지원합니다. 클래스-범위 타입의 객체를 대상으로 기본 연산을 호출하면, 객체의 런타임 태그(tag)에 따라 해당 타입에 맞는 연산 구현이 동적으로 선택되어 실행됩니다.

또한 Ada는 추상 타입(abstract type)추상 서브프로그램(abstract subprogram)을 정의하여 구현 없이 인터페이스만을 명시할 수 있게 하며, 인터페이스 타입(interface type)을 통해 제한적인 형태의 다중 상속(multiple inheritance)도 지원합니다.

본 장의 다음 절들에서는 이러한 Ada의 객체 지향 기능들(캡슐화, 상속, 다형성, 추상화 기법 등)을 더 상세하게 살펴보고 실제 사용 예제를 통해 그 활용법을 학습할 것입니다.

10.2 캡슐화와 정보 은닉 (Encapsulation and Information Hiding)

10.2.1 패키지를 이용한 추상화

Ada에서 추상화(abstraction)는 구현 세부 사항은 숨기고, 사용에 필요한 인터페이스만을 외부에 드러내는 프로그래밍 기법입니다. 이는 프로그램의 모듈성, 가독성, 유지보수성 지원을 목적으로 합니다. Ada 언어에서 패키지(package)는 이러한 추상화를 구현하는 기본 단위입니다.

패키지는 논리적으로 관련된 타입, 객체, 서브프로그램, 예외 등의 선언들을 하나의 단위로 묶는 역할을 합니다. 패키지는 일반적으로 명세부(specification)본체(body)라는 두 부분으로 나뉩니다.

패키지 명세부 (Package Specification)

package <패키지_이름> is ... end <패키지_이름>; 형태로 정의됩니다. 명세부는 패키지가 외부 사용자(클라이언트)에게 제공하는 인터페이스 역할을 합니다. 여기에는 외부에서 사용할 수 있는 타입, 상수, 변수, 서브프로그램 등의 선언이 포함됩니다. 사용자는 이 명세부를 통해 패키지가 제공하는 기능과 사용법을 식별할 수 있습니다.

패키지 본체 (Package Body)

package body <패키지_이름> is ... end <패키지_이름>; 형태로 정의됩니다. 본체에는 명세부에 선언된 서브프로그램의 실제 구현 코드나, 패키지 내부에서만 사용되는 추가적인 타입, 변수, 서브프로그램 등이 포함될 수 있습니다. 패키지 본체의 내용은 외부 사용자에게 숨겨지며(information hiding), 사용자는 명세부에 정의된 인터페이스만을 통해 패키지를 사용합니다.

이처럼 패키지는 명세부(인터페이스)와 본체(구현)를 분리하여 추상화를 제공합니다. 만약 패키지 본체의 구현 방식이 변경되더라도, 명세부에 정의된 인터페이스가 동일하게 유지된다면 해당 패키지를 사용하는 외부 코드는 영향을 받지 않습니다.

추가적으로, 패키지 명세부 내에서 전용 타입(private type) (7.2.2절[^ref]에서 상세히 다룸)을 사용하여, 타입의 이름과 연산만 공개하고 타입의 실제 구조는 명세부의 비공개부(private part)나 본체로 숨길 수 있습니다.

10.2.2 전용 타입 (Private Types)

전용 타입(private type)은 패키지(package)와 함께 Ada에서 캡슐화(encapsulation)와 정보 은닉(information hiding)을 구현하는 메커니즘입니다. 전용 타입으로 선언된 타입은 패키지 외부 사용자에게는 타입의 이름과 해당 타입에 대해 명시적으로 정의된 연산(서브프로그램)만 노출시키고, 타입의 실제 내부 데이터 구조(표현, representation)는 숨깁니다.

선언 구조: 부분 뷰와 풀 뷰

전용 타입의 정의는 패키지 명세부(specification) 내에서 두 부분으로 나뉩니다.

  1. 가시부 (Visible Part): 패키지 명세부의 private 키워드 이전 부분입니다. 여기서 타입은 private 예약어를 사용하여 선언됩니다.

    package Counter is
       type Object is private; -- 부분 뷰 선언
       procedure increment (c : in out Object);
       function value (c : Object) return Integer;
    private -- 이하 비공개부 시작
       -- ... 풀 뷰 정의 ...
    end Counter;
    

    이 선언은 타입의 부분 뷰(partial view)를 제공하며, 패키지 외부 사용자에게는 타입의 이름만 알려줍니다. 사용자는 이 부분 뷰를 통해 타입의 존재만 알 수 있고 내부 구조에는 접근할 수 없습니다. Ada 언어 규칙상, 부분 뷰는 그 풀 뷰가 무엇이든 복합 타입(composite type)으로 간주됩니다.

  2. 비공개부 (Private Part): 패키지 명세부의 private 키워드 이후 부분입니다. 여기서는 가시부에서 선언된 전용 타입의 실제 구현, 즉 풀 뷰(full view)를 정의합니다.

    package Counter is
       -- ... 가시부 ...
    private
       max_value : constant Integer := 1000;
       type Object is -- 풀 뷰 정의
          record
             current_value : Integer range 0 .. max_value := 0;
          end record;
    end Counter;
    

    이 풀 뷰(여기서는 record 타입)는 패키지 외부에서는 접근할 수 없으며, 패키지 본체(body)에서 서브프로그램(increment, value)을 구현할 때 사용됩니다.

허용되는 연산

패키지 외부 사용자는 전용 타입(Counter)의 객체에 대해 다음 연산들을 사용할 수 있습니다.

  • 패키지 명세부의 가시부에 명시적으로 선언된 서브프로그램 (예: increment, value).
  • 할당 연산 (:=).
  • 동등 비교 연산 (=, /=).

타입의 내부 구조에 의존하는 연산(예: 레코드 컴포넌트 직접 접근 my_counter.current_value)은 패키지 외부에서 허용되지 않습니다.

목적

전용 타입은 타입의 구현(representation)과 인터페이스(operations)를 분리합니다. 사용자는 인터페이스에만 의존하게 되므로, 패키지 개발자는 내부 구현을 변경할 수 있습니다 (예: 카운터를 정수 대신 다른 방식으로 구현). 인터페이스가 변경되지 않는 한, 이러한 내부 변경은 패키지 사용자 코드에 영향을 주지 않습니다. 이는 전용 타입을 통한 데이터 추상화의 결과입니다.

예제: Counter 사용

다음은 Counter 패키지를 사용하는 예제 코드입니다.

  • counter.ads

    package Counter is
       type Object is private; -- 부분 뷰 선언
       procedure increment (c : in out Object);
       function value (c : Object) return Integer;
    private -- 이하 비공개부 시작
       max_value : constant Integer := 1000;
       type Object is -- 풀 뷰 정의
       record
          current_value : Integer range 0 .. max_value := 0;
       end record;
    end Counter;
    
  • counter.adb (본체 구현)

    package body Counter is
    
       procedure increment (c : in out Object) is
       begin
          if c.current_value < max_value then
             c.current_value := c.current_value + 1;
          end if;
       end increment;
    
       function value (c : Object) return Integer is
       begin
          return c.current_value;
       end value;
    
    end Counter;
    
  • main.adb (패키지 사용 예제)

    with Ada.Text_IO;
    with Counter;
    
    procedure main is
       my_counter : Counter.Object; -- Counter.Object 타입 객체 선언 (내부 구조 모름)
    begin
       Ada.Text_IO.put_line ("Initial Value: " & Integer'image (Counter.value (my_counter)));
    
       Counter.increment (my_counter); -- 패키지가 제공하는 연산 사용
       Counter.increment (my_counter);
    
       Ada.Text_IO.put_line ("After Increment: " & Integer'image (Counter.value (my_counter)));
    
       -- my_counter.current_value := 0; -- 컴파일 오류! private 멤버 접근 불가
    end main;
    

이 예제에서 main 프로시저는 Counter.Object 타입의 내부가 record인지 알지 못하며, incrementvalue 인터페이스만을 통해 객체를 조작합니다. 이것이 캡슐화와 정보 은닉입니다.

10.2.3 제한된 타입 (Limited Types)

Ada 언어는 특정 타입에 대해 할당(assignment) 연산 (:=)과 미리 정의된 동등 비교(predefined equality) 연산 (=, /=)을 금지하는 제한된 타입(limited type) 개념을 제공합니다. 이는 객체의 복사나 비교를 프로그래머가 명시적으로 제어해야 하는 상황에 유용합니다.

제한된 타입 선언

타입은 다음과 같은 경우에 제한된 타입이 됩니다.

  1. 명시적 선언: 타입 정의에 limited 예약어를 포함하여 선언합니다.

    type File_Descriptor is limited record -- 명시적으로 제한된 레코드
       Handle : System.Address;
    end record;
    
    package IO_Pkg is
       type File_Handle is limited private; -- 명시적으로 제한된 전용 타입
       -- ...
    private
       type File_Handle is new File_Descriptor;
    end IO_Pkg;
    

    인터페이스 타입도 limited interface로 선언될 수 있습니다.

  2. 암시적 제한: 타입이 제한된 컴포넌트를 포함하는 경우, 해당 타입도 암시적으로 제한됩니다. 예를 들어, 컴포넌트 중 하나가 File_Descriptor 타입이라면 해당 레코드 타입은 limited 키워드가 없어도 제한된 타입이 됩니다. 태스크 타입과 보호 타입도 항상 제한된 타입입니다.

제한된 타입의 제약사항

제한된 타입의 객체에는 다음과 같은 제약사항이 따릅니다.

  • 할당 불가: := 연산자를 사용하여 한 객체의 값을 다른 객체에 복사할 수 없습니다.
  • 비교 불가: 미리 정의된 =/= 연산자를 사용할 수 없습니다. (단, 프로그래머가 직접 = 함수를 정의하는 것은 가능하며, 이를 ‘사용자 정의 동등 비교’라고 합니다.)
  • 함수 반환: 제한된 타입의 객체는 함수의 반환 값으로 직접 사용될 수 없습니다 (확장 return 문 등 특별한 경우 제외).
  • 매개변수 모드: 서브프로그램의 in 모드 매개변수로 제한된 타입의 객체를 전달할 수 없습니다 (단, 접근 타입 지정자는 가능). 주로 in out 모드나 접근 타입을 통해 다룹니다.

목적 및 활용

제한된 타입은 다음과 같은 상황에서 사용됩니다.

  • 고유 객체 표현: 파일 핸들, 락(lock), 태스크 등 각 인스턴스가 고유한 정체성을 가지며 복사되어서는 안 되는 객체를 모델링할 때 사용됩니다.
  • 복사 제어: 객체의 복사가 비효율적이거나 원하지 않는 부작용을 일으킬 수 있을 때, 복사를 원천적으로 금지합니다.
  • 리소스 관리: 제어 타입(controlled type)과 함께 사용될 때, 할당 (adjust) 및 종료 처리 (finalize) 연산을 사용자 정의하여 객체의 생성, 복사(금지됨), 소멸 시 리소스(예: 메모리, 파일)를 정밀하게 관리할 수 있습니다.

10.3 상속과 타입 확장 (Inheritance and Type Extension)

10.3.1 태그드 타입 (Tagged Types)

태그드 타입(tagged type)은 Ada에서 상속(inheritance)과 런타임 다형성(run-time polymorphism)을 지원하는 메커니즘입니다. 태그드 타입은 선언 시 tagged 예약어를 사용하여 표시되는 레코드 타입 또는 전용 타입입니다. 인터페이스 타입 및 인터페이스에서 파생된 태스크/보호 타입 또한 태그드 타입입니다.

태그 (Tag)

태그드 타입의 객체는 생성 시 자신의 구체적인 타입을 식별하는 런타임 값인 ‘태그(tag)’를 가집니다. 예를 들어, Point 타입과 이를 확장한 Colored_Point 타입이 있다면, 각 타입의 객체는 서로 다른 태그를 가집니다. 이 태그 정보는 객체의 실제 타입을 실행 시점에 식별하는 데 사용되며, 디스패칭(dispatching) 호출 시 실행할 연산 구현을 결정합니다.

선언 구문

태그드 타입은 일반적으로 레코드 타입 정의 앞에 tagged 키워드를 붙여 선언합니다. 전용 타입 또한 tagged로 선언될 수 있습니다 (type T is tagged private;).

type Point is tagged -- 'tagged' 키워드로 선언
   record
      X, Y : Float;
   end record;

type Shape is tagged private; -- 태그드 전용 타입

객체 지향 프로그래밍의 기반

태그드 타입은 Ada 객체 지향 프로그래밍의 다른 기능들의 기반이 됩니다.

  • 타입 확장 (Type Extension): 태그드 타입만이 다른 타입의 부모가 되어 확장될 수 있습니다 (7.3.2절 참고). 새로운 컴포넌트나 연산을 추가하여 자식 타입을 정의하는 것이 가능합니다.
  • 다형성 (Polymorphism): 클래스-범위 타입 (T'Class)과 디스패칭 호출을 통해, 태그드 타입 계층 구조 내의 여러 타입 객체를 일관된 인터페이스로 처리할 수 있습니다 (7.4절[^ref] 참고).

모든 태그드 타입은 상속 계층의 루트가 될 수 있으며, 이를 통해 객체 모델을 구축할 수 있습니다.

10.3.2 타입 확장 (Extension)과 프리미티브 연산 (Primitive Operations)

7.3.1절[^ref]에서 설명한 태그드 타입(tagged type)은 Ada에서 상속을 구현하는 기반입니다. 타입 확장(type extension)은 기존 태그드 타입(부모 타입)으로부터 새로운 타입을 파생시키는 메커니즘입니다. 이 과정에서 부모 타입의 컴포넌트와 연산이 상속되며, 새로운 특성이 추가될 수 있습니다.

타입 확장 선언

타입 확장은 new 키워드를 사용하여 부모 타입을 지정하고, 선택적으로 with record 구문을 통해 새로운 컴포넌트를 추가하여 정의합니다.

type Point is tagged
   record
      X, Y : Float;
   end record;

type Colored_Point is new Point with -- Point를 부모로 확장
   record
      Color : Some_Color_Type; -- 새로운 컴포넌트 추가
   end record;

Colored_PointPoint의 자식 타입(타입 확장)입니다. 이 타입은 부모 타입 PointX, Y 컴포넌트를 상속받으며, 추가적으로 Color 컴포넌트를 가집니다. 타입 확장은 태그드 타입을 부모로 해야 하며, with record 부분은 레코드 확장 파트(record extension part)라고 불립니다.

프리미티브 연산 (Primitive Operations)

프리미티브 연산(primitive operation) 또는 기본 연산은 타입 T의 기본적인 의미(semantics)를 구현하는 연산들의 집합입니다. 여기에는 다음이 포함됩니다:

  • 해당 타입에 대해 미리 정의된 연산자 (예: 숫자 타입의 +, 비교 연산자 등).
  • 부모 타입으로부터 상속된 사용자 정의 서브프로그램.
  • 열거형 타입의 경우, 열거형 리터럴 자체 (매개변수 없는 함수로 간주됨).
  • 타입과 동일한 선언 목록에 직접 선언된, 해당 타입을 피연산자나 결과로 사용하는 사용자 정의 서브프로그램 (패키지 명세부 등).

연산의 상속과 재정의

타입 확장을 통해 새로운 타입을 정의하면, 부모 타입의 프리미티브 연산들도 함께 상속됩니다. 상속된 연산의 프로파일(profile)은 부모 타입이 나타나는 부분이 새로운 자식 타입으로 변경되어 적용됩니다.

-- Point 타입의 프리미티브 연산 (예시)
procedure Move (P : in out Point; DX, DY : Float);

-- Colored_Point는 Move 연산을 상속받음.
-- 상속된 Move의 프로파일은 다음과 같음:
-- procedure Move (P : in out Colored_Point; DX, DY : Float);

자식 타입에서는 상속받은 프리미티브 연산을 재정의(override)하여 자식 타입에 맞는 동작을 구현할 수 있습니다. 또한, 자식 타입에만 적용되는 새로운 프리미티브 연산을 추가로 정의할 수도 있습니다.

프리미티브 연산의 상속과 재정의는 Ada의 객체 지향 프로그래밍에서 런타임 다형성(디스패칭)을 구현하는 데 사용됩니다.

10.3.3 연산 재정의 (overriding)

타입 확장을 통해, 상속받은 프리미티브 연산(primitive operation)을 파생 타입(derived type)에 특화된 새로운 구현으로 대체할 수 있습니다. 이러한 행위를 연산 재정의(overriding)라고 합니다.

여기서 “연산(Operation)”은 태그드 타입의 프리미티브 연산, 즉 해당 타입과 관련된 서브프로그램(프로시저, 함수)을 의미하며, +와 같은 “연산자(Operator)”만을 지칭하는 것이 아닙니다. 이는 다른 객체 지향 언어의 메소드 오버라이딩(Method Overriding)에 해당합니다. 연산자 기호 자체에 새로운 의미를 부여하는 것은 “연산자 중복정의(Operator Overloading)”라고 하며 6.4절에서 다룹니다.

연산 재정의는 파생 타입을 위한 새로운 서브프로그램을 선언하여 수행됩니다. 이 서브프로그램은 상속된 프리미티브 연산과 동일한 식별자(defining name)를 가져야 하며, 그 프로파일(profile)은 상속된 연산의 프로파일과 타입 순응(type conformant)해야 합니다. (프로파일 순응에 대한 자세한 규칙은 6.3.1절과 8.3절에서 다룹니다.)[^ref]

연산 재정의는 동일한 이름을 갖지만 파라미터 프로파일이 다른 여러 서브프로그램을 정의하는 연산자 중복정의(overloading)와는 구별됩니다. 재정의는 프로파일이 타입 순응 관계여야 합니다.

이 메커니즘은 다음 7.4절에서 설명할 다형성(polymorphism)과 동적 디스패치(dynamic dispatching)를 구현하는 정적 기반을 제공합니다.[^ref]

재정의 표시자 (Overriding Indicators)

Ada는 서브프로그램 선언 시 재정의 의도를 명시적으로 나타내는 표시자를 제공합니다. 이 표시자는 태그드 타입의 안정성을 지원하며, 프로그래머의 실수를 방지하고 코드의 명확성을 높이는 목적을 갖습니다.

  1. overriding 표시자

    서브프로그램 선언 앞에 overriding 키워드를 명시하면, 이 선언이 상속된 프리미티브 연산을 재정의(override)함을 컴파일러에 알립니다. 만약 해당 선언이 재정의하는 대상(동일한 이름과 타입 순응 프로파일을 가진 상위 타입의 연산)을 찾지 못할 경우, 컴파일러는 이를 오류로 처리합니다. 이는 서브프로그램 이름의 오타나 파라미터 프로파일의 미세한 차이로 인해 재정의가 의도대로 동작하지 않는 상황을 방지합니다.

  2. not overriding 표시자

    반대로, 서브프로그램 선언 앞에 not overriding 키워드를 명시하면, 이 선언이 새로운 연산을 정의하는 것이며 어떠한 상속된 연산도 재정의하지 않음을 명시합니다. 만약 이 선언이 의도치 않게 상속된 연산과 동일한 프로파일을 가져 재정의에 해당하게 될 경우, 컴파일러는 이를 오류로 보고합니다.

이러한 재정의 표시자 사용은 프로그래머의 의도를 명시하고, 상속 계층 구조에서 발생할 수 있는 잠재적인 오류를 컴파일 시점에 발견할 수 있도록 지원합니다.

예시:

-- 7.3.1절의 Point 타입 예시를 확장
type Point is tagged record
  X, Y : Float;
end record;

procedure Display(P : in Point);

-- 7.3.2절의 타입 확장 예시
type Circle is new Point with record
  Radius : Float;
end record;

-- 'Display' 연산을 'Circle' 타입에 맞게 재정의
overriding -- 'Point'로부터 상속받은 'Display'를 재정의함을 명시
procedure Display(C : in Circle);

-- 'Circle'에 새로운 프리미티브 연산 추가
not overriding -- 새로운 연산임을 명시
function Area(C : in Circle) return Float;

위 예시에서 Display 프로시저는 overriding으로 선언되어 Point 타입의 Display를 재정의함을 명시합니다. Area 함수는 not overriding으로 선언되어 이것이 Point로부터 상속된 연산이 아닌 Circle의 새로운 연산임을 명시합니다.

10.4 다형성 (Polymorphism)

다형성(Polymorphism)은 객체 지향 프로그래밍의 원리 중 하나로, 그리스어에서 유래하여 “다양한 형태”를 의미합니다. 프로그래밍 관점에서 다형성은 하나의 인터페이스(예: 서브프로그램 호출)가 서로 다른 타입의 객체들에 대해 각 타입에 맞는 방식으로 동작하는 능력을 의미합니다.

Ada는 9.3절에서 설명한 태그드 타입(tagged type)타입 확장(type extension)을 기반으로, 클래스-와이드 타입(class-wide type)과 동적 디스패치(dynamic dispatching) 메커니즘을 통해 런타임 다형성을 지원합니다.

이 절에서는 Ada의 다형성 구현 방식을 설명합니다. 설명을 위해, GUI 위젯 계층 구조(Widget, Window, Button 등) 예제를 사용하여 각 개념을 설명하고 관련 코드 조각을 제시합니다. 전체 예제 코드는 이 절의 마지막에 제공됩니다.

10.4.1 클래스-와이드 타입 (Class-Wide Types)

Ada의 런타임 다형성(polymorphism)은 클래스-와이드 타입(class-wide type) 개념을 기반으로 구현됩니다. 모든 태그드 타입(tagged type) T에 대해, T'Class라는 이름의 연관된 클래스-와이드 타입이 암시적으로 정의됩니다.

T'Class 타입은 T 자신과 T로부터 파생된 모든 자손 타입(descendant type)을 포함하는 전체 파생 클래스(derivation class)를 나타냅니다. T'Class 타입의 객체는 T 타입 또는 T의 자손 타입 중 어느 하나의 값을 가질 수 있습니다.

클래스-와이드 타입은 'Class 속성(attribute)을 통해 표기합니다. GUI 패키지의 Widget 타입을 예로 들면, Widget'ClassWidget, Window, Button, Toggle_Button, Checked_Button 타입을 모두 포함하는 클래스-와이드 타입입니다.

클래스-와이드 타입의 속성

  1. 다형적 파라미터: 클래스-와이드 타입은 런타임 다형성을 지원하는 서브프로그램 파라미터를 선언하는 데 사용됩니다. 서브프로그램의 파라미터가 Widget'Class 타입으로 선언되면, 해당 파생 클래스에 속하는 모든 특정 타입의 객체를 실제 파라미터로 전달할 수 있습니다.

    -- Widget'Class 타입을 파라미터로 받는 프로시저 선언
    procedure Process_Widget(Item : in Widget'Class);
    -- 이 프로시저는 Widget, Window, Button 등의 객체를 모두 받을 수 있습니다.
    
  2. 부정확 서브타입 (Indefinite Subtype): T'Class는 항상 부정확 서브타입(indefinite subtype)입니다. 이는 T'Class 타입의 객체가 가질 수 있는 구체적인 타입(및 크기)이 컴파일 시점에 고정되지 않기 때문입니다. 따라서 T'Class 타입의 객체는 선언 시 반드시 초기값을 지정해야 합니다 (4.1.2절 참조).

    My_Button : GUI.Button; -- Button 타입 객체
    -- Widget'Class 타입 객체 선언 시 초기화 필요
    Any_Widget : GUI.Widget'Class := My_Button;
    
  3. 컴포넌트 가시성: T'Class 타입을 통해 접근할 수 있는 컴포넌트는 루트 타입 T에 정의된 컴포넌트뿐입니다. Widget'Class 타입의 뷰를 통해서는 Widget에 정의된 Position_X, Position_Y, Visible 컴포넌트만 직접 접근할 수 있고, 자손 타입에서 추가된 Title이나 Label 같은 컴포넌트는 직접 접근할 수 없습니다.

10.4.2 동적 디스패치 (Dynamic Dispatching)

동적 디스패치(Dynamic Dispatching)는 클래스-와이드 타입(class-wide type)을 통해 Ada의 런타임 다형성(polymorphism)을 구현하는 메커니즘입니다. 이는 태그드 타입(tagged type)의 프리미티브 연산(primitive operation) 호출 시, 실행될 특정 서브프로그램 본체를 컴파일 시간이 아닌 실행 시간(run-time)에 결정하는 행위입니다.

이 메커니즘은 디스패칭 연산(dispatching operation)의 호출을 기반으로 동작합니다.

1. 디스패칭 연산 및 제어 피연산자 (Controlling Operands)

태그드 타입 T의 프리미티브 연산(9.3.2절 참조)은 디스패칭 연산입니다. GUI 패키지의 Show 프로시저가 이에 해당합니다.

이러한 연산 호출 시, 타입 T 또는 T'Class 타입의 파라미터를 제어 파라미터(controlling formal parameter)라고 하며, 이 파라미터에 대응하는 실제 파라미터(actual parameter)는 제어 피연산자(controlling operand)가 됩니다.

2. 제어 태그 (Controlling Tag) 결정

디스패칭 연산이 호출되면, 실행할 본체를 결정하기 위해 제어 태그 값(controlling tag value)이 식별됩니다.

  • 정적 태그 (Static Tag): 제어 피연산자가 특정 태그드 타입(예: GUI.Button)의 뷰라면, 제어 태그는 해당 타입(Button의 태그)으로 컴파일 시점에 고정됩니다. 이 경우 디스패치가 발생하지 않습니다.
  • 동적 태그 (Dynamic Tag): 제어 피연산자가 클래스-와이드 타입(예: GUI.Widget'Class)의 뷰이고 동적 태그를 갖는다면, 제어 태그는 정적으로 결정되지 않습니다. 대신, 실행 시점에 해당 피연산자의 실제 태그 값을 확인하여 제어 태그 값을 결정합니다. 이것이 디스패칭 호출(dispatching call)입니다.

3. 서브프로그램 본체 실행

디스패칭 호출이 실행될 때, 제어 태그 값에 의해 식별되는 특정 타입(예: Window, Button, Toggle_Button)에 해당하는 연산의 본체가 호출됩니다.

  • 만약 해당 특정 타입이 연산을 명시적으로 재정의(overriding)했다면, 재정의된 서브프로그램 본체가 실행됩니다.
  • 만약 재정의하지 않았다면, 조상 타입으로부터 상속받은 연산의 본체가 실행됩니다.

예시 (GUI 기반):

다음 Display_Widget 프로시저는 Widget'Class 타입의 파라미터를 받아 Show 프로시저를 호출합니다.

with GUI; use GUI;
with Ada.Text_IO; use Ada.Text_IO;

procedure Demonstrate_Dispatching is
   -- Widget'Class 파라미터를 받는 프로시저
   procedure Display_Widget(Item : in Widget'Class) is
   begin
      -- 'Item'은 Widget'Class 타입이므로, Show 호출은 디스패칭 호출이 됨.
      Show(Item);
   end Display_Widget;

   My_Window  : Window;
   My_Button  : Button;
   My_Toggle  : Toggle_Button;
begin
   -- 객체 초기화 (예시)
   My_Window.Title := To_Unbounded_String("Application");
   My_Button.Label := To_Unbounded_String("Submit");
   My_Toggle.Label := To_Unbounded_String("Mode");
   My_Toggle.Is_Toggled_On := True;

   Put_Line("--- Displaying Widgets via Dispatching ---");

   -- Display_Widget 호출 시 전달된 객체의 실제 타입에 따라
   -- 각기 다른 Show 구현이 실행됨
   Display_Widget(My_Window);  -- Item의 태그는 Window -> Window의 Show 실행
   New_Line;
   Display_Widget(My_Button);  -- Item의 태그는 Button -> Button의 Show 실행
   New_Line;
   Display_Widget(My_Toggle);  -- Item의 태그는 Toggle_Button -> Toggle_Button의 Show 실행
end Demonstrate_Dispatching;

Display_Widget 프로시저 내부의 Show(Item) 호출은 디스패칭 호출입니다. 이 프로시저를 My_Window, My_Button, My_Toggle 객체로 각각 호출하면, Item 파라미터가 실행 시점에 가지는 실제 태그(Window, Button, Toggle_Button)에 따라 해당 타입에 맞게 재정의된 Show 프로시저가 동적으로 선택되어 실행됩니다. 이것이 다형성의 동작 원리입니다.

접두사 표기법 호출 (Prefixed View Call - Ada 2022)

Ada 2022 표준에서는 태그드 타입(tagged type)의 객체에 대해, 프리미티브 연산 호출을 위한 접두사 표기법(prefixed view notation) 또는 점 표기법(dot notation) 사용을 허용합니다 (ARM 4.1.3(8/5)).

이는 타입 T의 프리미티브 연산 P가 있고, X가 타입 T 또는 T'Class 타입의 객체일 때, P(X, ...) 호출을 X.P(...) 형태로 작성할 수 있음을 의미합니다.

Display_Widget 프로시저 내부의 디스패칭 호출은 다음과 같이 점 표기법으로도 작성할 수 있습니다.

procedure Display_Widget(Item : in Widget'Class) is
begin
   -- 기존 방식: Show(Item);
   -- Ada 2022 접두사 표기법:
   Item.Show; -- 파라미터가 없으므로 괄호를 사용하지 않음
end Display_Widget;

이 접두사 표기법 호출(Item.Show)은 Item이 클래스-와이드 타입이므로 동적 디스패칭 호출로 동작합니다.

Ada 2022 표준 규칙에 따르면, 이러한 점 표기법 호출은 태그드 타입의 객체에 대해서만 허용됩니다. GUI 패키지의 Widget 및 그 자손들은 tagged로 선언되었으므로 이 구문을 사용할 수 있습니다. 태그드 타입이 아닌 객체(예: 9.2.2절의 Counter.Object)의 프리미티브 연산 호출에는 이 구문을 사용할 수 없습니다.

10.4.3 'tag'class 속성

Ada의 다형성 메커니즘은 'tag'class 속성을 기반으로 동작합니다. 이 속성들은 태그드 타입(tagged type)과 연관되어 런타임 타입 식별 및 클래스-와이드 타입 정의에 사용됩니다.

1. ‘class 속성 (Attribute)

'class 속성은 9.4.1절에서 설명한 바와 같이, 태그드 타입 T에 대해 T'Class라는 클래스-와이드 타입을 정의합니다.

  • T'Class는 타입 TT에서 파생된 모든 자손 타입을 포함하는 전체 파생 클래스를 나타내는 타입 자체를 지칭합니다.
  • 이 속성은 서브프로그램에서 다형적 파라미터를 선언하거나, 런타임에 여러 특정 타입을 가질 수 있는 객체를 선언하는 데 사용됩니다. GUI 패키지의 Widget 타입을 예로 들면, Widget'ClassWidget과 그 모든 자손(Window, Button 등)을 포함하는 타입입니다.

2. ‘tag 속성 (Attribute)

'tag 속성은 객체의 특정 타입을 식별하는 태그(tag) 값을 반환합니다 (ARM 3.9(7.5/2)). 이 태그의 타입은 Ada.Tags 패키지에 정의된 Tag 타입입니다.

이 속성은 두 가지 형태로 사용될 수 있습니다.

  • T'Tag (타입에 적용): T가 특정 태그드 타입의 이름(subtype mark)일 때, T'Tag는 해당 특정 타입 T에 연관된 태그 값을 반환합니다. 이 값은 컴파일 시간에 결정됩니다.
  • X'Tag (객체에 적용): X가 태그드 타입의 객체(특정 타입 또는 클래스-와이드 타입)일 때, X'TagX가 현재 보유한 값의 실행 시간(runtime) 태그를 반환합니다.

XT'Class 타입의 객체일 경우, X'Tag의 값은 X에 마지막으로 할당된 값의 특정 타입에 따라 동적으로 결정됩니다. 이 런타임 태그 값은 9.4.2절에서 설명한 동적 디스패치 호출 시, 실행할 프리미티브 연산의 본체를 선택하는 데 사용됩니다.


요약 및 대비

속성 표기 (예: Widget 타입) 반환 시점 주 용도
'class Widget'Class 타입(Type) 컴파일 시간 다형적 객체 및 파라미터 선언
'tag Widget'Tag 또는 Obj'Tag 값(Value) (타입 Tag) 컴파일 시간(타입) 또는 실행 시간(객체) 런타임 타입 식별, 동적 디스패치

예시 (GUI 기반):

'Tag 속성을 사용하여 클래스-와이드 타입 객체의 특정 타입을 런타임에 확인할 수 있습니다.

with Ada.Tags;
with GUI; use GUI;
with Ada.Text_IO;

procedure Demonstrate_Tag_Attribute is
   -- Widget'Class 파라미터를 받아 타입을 식별하는 프로시저
   procedure Identify_Widget(Item : in Widget'Class) is
      Widget_Tag : constant Ada.Tags.Tag := Item'Tag; -- (1) 런타임 태그 획득
   begin
      Ada.Text_IO.Put("객체 타입: ");
      if Widget_Tag = Widget'Tag then          -- (2) 특정 타입의 태그와 비교
         Ada.Text_IO.Put_Line("Widget");
      elsif Widget_Tag = Window'Tag then       -- (3) 다른 특정 타입의 태그와 비교
         Ada.Text_IO.Put_Line("Window");
      elsif Widget_Tag = Button'Tag then       -- Button 자체의 태그
         Ada.Text_IO.Put_Line("Button");
      elsif Widget_Tag = Toggle_Button'Tag then
         Ada.Text_IO.Put_Line("Toggle_Button");
      elsif Widget_Tag = Checked_Button'Tag then
         Ada.Text_IO.Put_Line("Checked_Button");
      else
         Ada.Text_IO.Put_Line("알 수 없음");
      end if;
      -- A'Tag = B'Tag 연산은 Ada.Tags 패키지의 "=" 연산자를 사용합니다.
   end Identify_Widget;

   My_Window  : Window;
   My_Button  : Button;
   My_Toggle  : Toggle_Button;
begin
   Identify_Widget(My_Window);
   Identify_Widget(My_Button);
   Identify_Widget(My_Toggle);
end Demonstrate_Tag_Attribute;

이 예시에서 (1) Item'TagItem 파라미터가 실행 시점에 보유한 값의 실제 태그를 반환합니다. (2), (3) 등은 이 런타임 태그를 Widget, Window, Button, Toggle_Button 등의 컴파일 시간 태그와 비교하여 타입을 식별합니다.

명시적인 타입 검사에 'Tag를 사용하는 것은 디스패칭 호출이 제공하는 자동화된 다형성 처리와는 다른 접근 방식입니다.

10.4.4 전체 예제 코드

다음은 이 절(9.4)에서 설명한 클래스-와이드 타입과 동적 디스패치를 보여주는 완전한 GUI 패키지(gui.ads, gui.adb) 및 사용 예제(main.adb) 코드입니다.

gui.ads (패키지 명세부)

with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with Ada.Text_IO;

package GUI is

   type Widget is tagged record
      Position_X : Integer := 0;
      Position_Y : Integer := 0;
      Visible    : Boolean := True;
   end record;

   -- Widget의 프리미티브 연산으로 Show 선언
   not overriding
   procedure Show(W : in Widget);

   -----------------------------------------------
   type Window is new Widget with record
      Title : Unbounded_String;
   end record;

   -- Window에 대한 Show 재정의 선언
   overriding
   procedure Show(Win : in Window);

   -----------------------------------------------
   type Button is new Widget with record
      Label : Unbounded_String;
   end record;

   -- Button에 대한 Show 재정의 선언
   overriding
   procedure Show(B : in Button);

   -----------------------------------------------
   type Toggle_Button is new Button with record
      Is_Toggled_On : Boolean := False;
   end record;

   -- Toggle_Button에 대한 Show 재정의 선언
   overriding
   procedure Show(TB : in Toggle_Button);

   -----------------------------------------------
   type Checked_Button is new Button with record
      Is_Checked : Boolean := False;
   end record;

   -- Checked_Button에 대한 Show 재정의 선언
   overriding
   procedure Show(CB : in Checked_Button);

end GUI;

gui.adb (패키지 본체부)

package body GUI is

   -- Widget의 기본 Show 구현
   procedure Show(W : in Widget) is
   begin
      Ada.Text_IO.Put ("Widget - Pos: (" &
                           Integer'Image(W.Position_X) & "," &
                           Integer'Image(W.Position_Y) & ")" &
                           " Visible: " & Boolean'Image(W.Visible));
      -- Put_Line 대신 Put 사용 (자식 Show에서 New_Line 추가 예정)
   end Show;

   -- Window의 Show 재정의 구현
   procedure Show(Win : in Window) is
   begin
      Show(Widget(Win)); -- 부모(Widget)의 Show 호출
      Ada.Text_IO.Put_Line(" | Window - Title: " & To_String(Win.Title));
   end Show;

   -- Button의 Show 재정의 구현
   procedure Show(B : in Button) is
   begin
      Show(Widget(B));
      Ada.Text_IO.Put_Line(" | Button - Label: " & To_String(B.Label));
   end Show;

   -- Toggle_Button의 Show 재정의 구현
   procedure Show(TB : in Toggle_Button) is
   begin
      Show(Button(TB)); -- 부모(Button)의 Show 호출 (New_Line 포함)
      Ada.Text_IO.Put_Line("      -> Toggle Button - Toggled: " & Boolean'Image(TB.Is_Toggled_On));
   end Show;

   -- Checked_Button의 Show 재정의 구현
   procedure Show(CB : in Checked_Button) is
   begin
      Show(Button(CB)); -- 부모(Button)의 Show 호출 (New_Line 포함)
      Ada.Text_IO.Put_Line("      -> Checked Button - Checked: " & Boolean'Image(CB.Is_Checked));
   end Show;

end GUI;

main.adb (사용 예제)

with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Strings.Unbounded; use Ada.Strings.Unbounded;
with GUI;                 use GUI;

procedure Main is

   -- 1. 다양한 위젯 타입의 객체 선언
   My_Window  : Window;
   My_Button  : Button;
   My_Toggle  : Toggle_Button;
   My_Checked : Checked_Button;

   -- 2. Widget'Class 타입의 배열 선언
   --    (다양한 위젯 타입을 담을 수 있음)
   All_Widgets : array (1 .. 4) of Widget'Class :=
     (1 => Widget'Class(My_Window),  -- Window 객체 (타입 변환)
      2 => Widget'Class(My_Button),  -- Button 객체
      3 => Widget'Class(My_Toggle),  -- Toggle_Button 객체
      4 => Widget'Class(My_Checked)  -- Checked_Button 객체
      ); -- 초기화는 배열 선언 시 또는 나중에 할당 가능

   -- 3. Widget'Class 배열을 받아 각 요소를 Display하는 프로시저
   procedure Display_All(Items : array (Positive range <>) of Widget'Class) is
   begin
      Put_Line("--- Displaying All Widgets ---");
      for I in Items'Range loop
         Show(Items(I)); -- 여기가 바로 동적 디스패칭! 🪄
         New_Line;        -- 각 위젯 출력 후 줄 바꿈
      end loop;
   end Display_All;

begin
   -- 4. 객체 초기화 (샘플 데이터)
   All_Widgets(1) := Widget'Class'(Window'(Title => To_Unbounded_String("Application")));
   All_Widgets(2) := Widget'Class'(Button'(Label => To_Unbounded_String("Submit")));
   All_Widgets(3) := Widget'Class'(Toggle_Button'(Label => To_Unbounded_String("Mode"), Is_Toggled_On => True));
   All_Widgets(4) := Widget'Class'(Checked_Button'(Label => To_Unbounded_String("Agree"), Is_Checked => False));


   -- 5. 다형성을 보여주는 Display_All 프로시저 호출
   Display_All(All_Widgets);

end Main;

10.5 추상 타입과 인터페이스

10.5.1 추상 타입 (Abstract Types) 및 서브프로그램 (Abstract Subprograms)

Ada는 상속 계층 구조에서 공통 인터페이스를 정의하고 특정 구현은 자손 타입에 위임하기 위한 메커니즘으로 추상 타입(abstract type)과 추상 서브프로그램(abstract subprogram)을 제공합니다.

추상 타입 (Abstract Types)

추상 타입은 인스턴스화(instantiation)될 수 없는, 즉 객체를 생성할 수 없는 태그드 타입입니다. 추상 타입은 다른 타입의 조상 타입으로만 사용될 목적으로 정의됩니다.

타입 선언에 abstract 키워드를 명시하여 해당 타입을 추상 타입으로 선언합니다.

type My_Abstract_Type is abstract tagged ... ;

인터페이스 타입(7.5.2절 참조) 역시 추상 타입의 한 형태입니다.

추상 타입 T 자체의 객체는 생성할 수 없지만, 해당 타입의 클래스-와이드 타입인 T'Class 타입의 객체는 선언할 수 있습니다. (이 경우, 객체는 반드시 T의 추상적이 아닌 자손 타입의 값으로 초기화되어야 합니다.)

추상 서브프로그램 (Abstract Subprograms)

추상 서브프로그램은 서브프로그램의 명세(specification)는 제공하지만, 구현부인 바디(body)를 갖지 않는 서브프로그램입니다. 이는 자손 타입에 의해 반드시 재정의(override)될 것을 전제로 합니다.

서브프로그램 명세 뒤에 is abstract 키워드를 사용하여 추상 서브프로그램을 선언합니다.

procedure My_Abstract_Proc(Param : My_Abstract_Type) is abstract;
function My_Abstract_Func(Param : My_Abstract_Type) return Integer is abstract;

추상 타입과 서브프로그램의 관계

추상 타입과 추상 서브프로그램의 주요 규칙은 다음과 같습니다.

  1. 추상 연산을 가진 타입은 반드시 추상 타입이어야 합니다. 어떤 태그드 타입의 프리미티브 연산(primitive operation) 중 하나라도 추상 서브프로그램이라면, 해당 태그드 타입은 반드시 abstract로 선언되어야 합니다.

  2. 추상 타입은 추상 연산을 가질 수 있습니다. 추상 타입으로 선언된 타입은 구현이 완료된 구체적인(concrete) 프리미티브 연산뿐만 아니라, 하나 이상의 추상 프리미티브 연산을 포함할 수 있습니다.

  3. 구체적인 자손 타입은 반드시 추상 연산을 재정의해야 합니다. 추상 타입을 조상으로 하는 타입이 추상적이 아닌 구체적인(non-abstract) 타입으로 선언되려면, 그 타입은 조상으로부터 상속받은 모든 추상 프리미티브 서브프로그램을 구체적인 구현(바디를 가진 서브프로그램)으로 재정의(overriding)해야 합니다. 만약 하나라도 재정의하지 않는다면, 해당 파생 타입 역시 abstract로 선언되어야 합니다.

  4. 추상 서브프로그램은 디스패칭 호출만 가능합니다. 추상 서브프로그램은 바디를 가지고 있지 않으므로, 정적인 호출(non-dispatching call)의 대상이 될 수 없습니다. 추상 서브프로그램에 대한 모든 호출은 반드시 디스패칭 호출(dispatching call, 7.4.2절 참조)이어야 합니다. 이는 런타임에 실제 객체의 태그를 기반으로 재정의된 구체적인 바디를 찾아 실행됨을 의미합니다.

예시:

package Shapes is
  -- 추상 타입 선언
  type Shape is abstract tagged null record;

  -- 추상 프리미티브 연산 선언
  function Area(S : Shape) return Float is abstract;
  procedure Display(S : Shape) is abstract;
end Shapes;

package body Shapes is
  -- 추상 연산이므로 바디가 없음
end Shapes;

with Shapes; use Shapes;
package Concrete_Shapes is
  type Point is new Shape with
     record
        X, Y : Float;
     end record;

  -- 'Point'는 'Shape'의 구체적인 자손이므로,
  -- 상속받은 추상 연산 'Area'와 'Display'를 반드시 재정의해야 함.
  overriding
  function Area(S : Point) return Float;
  overriding
  procedure Display(S : Point);

  type Circle is new Point with
     record
       Radius : Float;
     end record;

  -- 'Circle'은 'Point'로부터 구체적인 연산을 상속받지만,
  -- 'Area'를 다시 재정의함. 'Display'는 'Point'의 것을 상속함.
  overriding
  function Area(S : Circle) return Float;
end Concrete_Shapes;

10.5.2 인터페이스 타입 (Interface Types)과 다중 상속

Ada는 인터페이스 타입(interface type)을 제공하며, 이는 Ada에서 다중 상속을 구현하는 메커니즘입니다. 인터페이스는 다른 프로그래밍 언어의 추상 기반 클래스(abstract base class) 또는 인터페이스와 유사한 역할을 수행합니다.

인터페이스 타입 (Interface Types)의 정의

인터페이스 타입은 객체를 생성할 수 없는 추상 태그드 타입(abstract tagged type)입니다. 인터페이스는 구현(데이터 컴포넌트 또는 서브프로그램의 바디)을 포함하지 않고, 자손 타입들이 구현해야 하는 명세(specification)의 집합만을 정의합니다.

인터페이스는 is interface 키워드를 사용하여 선언합니다.

type My_Interface is interface;

인터페이스는 7.5.1절[^ref]의 abstract 키워드를 명시하지 않아도 그 자체로 추상 타입입니다. 인터페이스는 태그드 타입이므로, 프리미티브 연산을 가질 수 있습니다. 이러한 연산은 일반적으로 추상 서브프로그램(abstract subprogram)으로 선언되어, 인터페이스를 상속하는 구체적인(non-abstract) 타입이 이를 재정의(override)하도록 강제합니다.

package Schedulable is
   type Task_Interface is interface;

   -- 인터페이스의 프리미티브 연산 (일반적으로 추상 연산)
   procedure Execute(T : in out Task_Interface) is abstract;
   function Is_Done(T : Task_Interface) return Boolean is abstract;
end Schedulable;

다중 상속 (Multiple Inheritance)

Ada의 태그드 타입은 오직 하나의 구체적인 조상(parent) 태그드 타입으로부터만 확장(상속)될 수 있습니다. 그러나 타입 확장은 동시에 하나 이상의 인터페이스 타입을 상속(포함)할 수 있습니다.

이것이 Ada가 다중 상속을 지원하는 방식입니다. 즉, 구현은 단일 조상으로부터 상속받고, 명세는 다수의 인터페이스로부터 상속받을 수 있습니다.

타입 선언 시 and 키워드를 사용하여 상속할 인터페이스 목록을 지정합니다.

-- (1) 조상 태그드 타입 (단일 상속)
type Base_Task is tagged record
   ID : Integer;
end record;

-- (2) 인터페이스 타입
type Loggable is interface;
procedure Write_Log(L : Loggable) is abstract;

-- (3) 다중 상속: Base_Task(구현)와 Schedulable.Task_Interface(인터페이스) 상속
with Schedulable;
type My_Task is new Base_Task and Schedulable.Task_Interface with record
   -- My_Task에 특화된 데이터
   Priority : Integer;
end record;

-- My_Task는 'Schedulable.Task_Interface'의 추상 연산을 구현해야 함
overriding
procedure Execute(T : in out My_Task);
overriding
function Is_Done(T : My_Task) return Boolean;


-- (4) 다중 상속: Base_Task(구현)와 두 개의 인터페이스 상속
with Schedulable;
type Complex_Task is new Base_Task and Schedulable.Task_Interface and Loggable with record
   Data : String(1..100);
end record;

-- Complex_Task는 'Task_Interface'와 'Loggable'의
-- 모든 추상 연산을 구현해야 함
overriding
procedure Execute(T : in out Complex_Task);
overriding
function Is_Done(T : Complex_Task) return Boolean;
overriding
procedure Write_Log(L : Complex_Task);

위 예시에서 My_TaskBase_Task의 구현(컴포넌트 ID)을 상속받는 동시에 Schedulable.Task_Interface의 명세를 상속받습니다. Complex_TaskBase_Task의 구현과 함께 Task_InterfaceLoggable 두 인터페이스의 명세를 상속받습니다.

인터페이스 타입 자체도 다른 인터페이스로부터 상속받을 수 있습니다.

type Another_Interface is interface and Schedulable.Task_Interface and Loggable;

이러한 인터페이스 상속 메커니즘을 통해, 관련 연산의 집합을 계층적으로 구성하고, 구체적인 타입이 이러한 여러 인터페이스의 조합을 구현하도록 강제할 수 있습니다.

11. 함수형 프로그래밍 (Functional Programming)

11.1 Ada의 함수형 프로그래밍 패러다임 개요

Ada는 명령형 프로그래밍 언어의 특성을 기반으로 하면서도, 최신 표준(Ada 2012 및 Ada 2022)을 통해 함수형 프로그래밍(Functional Programming, FP) 패러다임의 원칙과 기법을 지원하는 기능들을 포함하고 있습니다.

함수형 프로그래밍은 계산을 수학적 함수의 평가로 취급하고, 상태 변경(state mutation)부작용(side effect)을 최소화하는 프로그래밍 스타일입니다. Ada에서 함수형 프로그래밍 관련 기능들은 다음과 같은 원칙을 중심으로 구성됩니다.

  1. 표현식 기반 계산 (Expression-Based Computation): 문장(statement)의 순차적 실행보다는, 값을 반환하는 표현식(expression)을 중심으로 로직을 구성합니다. Ada는 이를 위해 표현식 함수, 조건부 표현식, 리덕션/수량자 표현식 등을 제공합니다.
  2. 불변성 (Immutability): 데이터가 생성된 후 변경되지 않도록 하여 상태 변경을 제어합니다. Ada는 상수(constant) 선언과 in 모드 파라미터를 통해 이를 지원하며, 델타 애그리게이트 등은 불변 데이터 구조 스타일의 프로그래밍을 가능하게 합니다.
  3. 고차 연산 (Higher-Order Operations): 서브프로그램을 값처럼 다루어, 다른 서브프로그램에 파라미터로 전달하거나 결과로 반환하는 것을 의미합니다. Ada는 ‘서브프로그램에 대한 접근 타입’을 통해 이를 지원합니다.

본 장에서는 이러한 Ada의 함수형 프로그래밍 관련 기능들을 구체적으로 살펴봅니다.

11.2 불변성(Immutability)과 순수 함수 (Pure Functions)

함수형 프로그래밍은 불변성(immutability), 즉 데이터가 생성된 후 변경되지 않는다는 원칙을 사용합니다. 이는 상태 변경으로 인해 발생할 수 있는 복잡성과 오류를 줄이는 방식입니다.

Ada는 다음과 같은 기능을 통해 불변성을 지원합니다.

  • 상수 (Constants): constant 키워드로 선언된 객체는 초기화된 후 그 값을 변경할 수 없습니다.
  • in 모드 파라미터: 서브프로그램의 in 모드 파라미터는 서브프로그램 내부에서 상수(constant) 뷰로 취급되어 수정이 불가능합니다 (6.3.1절 참조).
  • 델타 애그리게이트 (Delta Aggregates) (Ada 2022): 기존 객체를 직접 수정하는 대신, 일부 컴포넌트 값만 변경된 새로운 객체를 생성하는 구문(My_Record with delta Component => New_Value)을 제공하여 불변 데이터 구조 스타일의 프로그래밍을 지원합니다.

순수 함수(Pure Function)는 함수형 프로그래밍의 또 다른 개념입니다. 순수 함수는 다음과 같은 두 가지 특성을 가집니다.

  1. 부작용 없음 (No Side Effects): 함수 실행이 함수 외부의 상태(예: 전역 변수, I/O 장치)를 변경하지 않습니다.
  2. 결정론적 (Deterministic): 동일한 입력 값에 대해 항상 동일한 출력 값을 반환합니다.

Ada는 애스펙트(aspect)를 사용하여 서브프로그램이나 패키지가 이러한 순수성 원칙을 따르도록 명시하고 검증하는 메커니즘을 제공합니다.

  • Pure 애스펙트: 패키지나 서브프로그램에 with Pure; 애스펙트를 명시하면, 해당 단위가 전역 변수를 수정하거나 I/O 작업을 수행하는 등의 부작용을 일으키지 않음을 컴파일러가 강제합니다. 이는 해당 단위가 상태 비저장(stateless)임을 나타냅니다.
  • Global 애스펙트: Global 애스펙트는 서브프로그램이 접근(읽기 또는 쓰기)하는 전역 변수를 명시적으로 선언하도록 요구합니다. Global => null로 지정된 서브프로그램은 어떠한 전역 변수에도 접근하지 않으므로 부작용이 없음을 나타냅니다.

11.3 고차 연산 (Higher-Order Operations)

고차 연산은 서브프로그램을 파라미터로 전달받거나, 서브프로그램을 결과로 반환하는 연산을 의미합니다. Ada는 이러한 기능을 ‘서브프로그램에 대한 접근 타입’을 통해 제공합니다.

11.3.1 서브프로그램에 대한 접근 타입

Ada는 서브프로그램에 대한 접근 타입(access-to-subprogram type)을 선언하는 기능을 제공합니다. 이는 특정 서브프로그램(프로시저 또는 함수)의 메모리 위치(주소)를 가리키는 값을 다루는 타입입니다 (4.3절 참조). 이 기능을 통해 서브프로그램 자체를 변수에 저장하거나, 다른 서브프로그램에 매개변수로 전달하거나, 함수에서 반환하는 등의 고차 연산(higher-order operation)을 구현할 수 있습니다.

선언 구문

서브프로그램 접근 타입은 access 키워드 뒤에 대상 서브프로그램의 프로파일(profile)(매개변수 및 반환 타입 명세)을 지정하여 선언합니다.

  • 프로시저 접근 타입:

    type Procedure_Access is access procedure (Parameter_List);
    
  • 함수 접근 타입:

    type Function_Access is access function (Parameter_List) return Return_Type;
    

접근 값 얻기 ('Access 속성)

특정 서브프로그램을 가리키는 접근 값을 얻기 위해서는 해당 서브프로그램의 이름 뒤에 'Access 속성을 사용합니다. 이 속성은 해당 서브프로그램의 메모리 주소를 나타내는 접근 타입의 값을 반환합니다.

procedure My_Procedure(X : Integer);
function My_Function(Y : Float) return Float;

-- 접근 타입 변수 선언
P_Ptr : Procedure_Access := My_Procedure'Access;
F_Ptr : Function_Access := My_Function'Access;

Null 값

모든 서브프로그램 접근 타입은 아무 서브프로그램도 가리키지 않음을 나타내는 null 값을 가질 수 있습니다.

Default_Handler : Procedure_Access := null;

호출

서브프로그램 접근 타입의 변수를 통해 실제 서브프로그램을 호출할 수 있습니다.

P_Ptr(10);        -- My_Procedure(10)을 호출
Result := F_Ptr(3.14); -- My_Function(3.14)를 호출하고 결과를 Result에 저장

활용

서브프로그램 접근 타입은 다음과 같은 함수형 프로그래밍 기법 및 디자인 패턴 구현에 사용됩니다.

  • 콜백 (Callbacks): 특정 이벤트 발생 시 호출될 서브프로그램을 등록하고 실행합니다.
  • 전략 패턴 (Strategy Pattern): 알고리즘의 특정 단계를 나타내는 서브프로그램을 동적으로 교체합니다.
  • 함수 전달: 함수를 다른 함수에 인수로 전달하여 적용합니다 (예: 리스트의 각 요소에 특정 함수 적용).

예제: 함수 전달

type Unary_Integer_Function is access function(X : Integer) return Integer;

function Square(N : Integer) return Integer is (N * N);
function Cube(N : Integer) return Integer is (N ** 3);

-- 함수(Op)를 인수로 받아 값(Value)에 적용하는 고차 함수
function Apply(Op : Unary_Integer_Function; Value : Integer) return Integer is
begin
   if Op = null then
      return 0; -- 또는 예외 발생
   else
      return Op(Value); -- 전달받은 함수 호출
   end if;
end Apply;

Result_Sq : Integer := Apply(Square'Access, 5); -- Square(5) 호출, Result_Sq는 25
Result_Cb : Integer := Apply(Cube'Access, 3);   -- Cube(3) 호출, Result_Cb는 27

11.3.2 람다 표현식 (Lambda Expressions)과 클로저

Ada 언어는 lambda와 같은 키워드를 사용한 익명 함수(anonymous function) 또는 람다 표현식(lambda expression)을 직접 지원하지 않습니다. Ada는 선언식 표현(declare expression) (RM 4.5.9)과 10.3.1절에서 설명한 'Access 속성을 조합하여 이와 유사한 기능을 구현합니다.

선언식 표현을 이용한 인라인 서브프로그램 접근

선언식 표현은 다른 표현식 내부에 declare ... begin ... end 블록을 포함하여 지역 선언(local declaration)과 실행 로직을 가질 수 있게 합니다. 이 블록 내부에 로컬 서브프로그램(local subprogram)을 선언하고, 이 로컬 서브프로그램의 'Access 값을 해당 선언식 표현의 결과로 반환함으로써, 익명 서브프로그램에 대한 참조를 생성하고 전달하는 효과를 구현합니다.

클로저 (Closure) 구현

클로저(closure)는 자신이 정의된 환경(lexical environment)을 “기억”하는 함수입니다. 이는 함수가 자신의 본문 외부에 선언된 변수(non-local variable)에 접근할 수 있는 기능입니다.

Ada에서 선언식 표현 내부에 선언된 로컬 서브프로그램이, 이 선언식 표현을 둘러싸고 있는 외부 유효 범위(enclosing scope)의 변수나 상수를 참조(capture)할 때, 해당 서브프로그램의 'Access 값은 클로저로 동작합니다. 이 접근 값은 생성될 당시의 외부 변수에 대한 참조를 유지하며, 나중에 이 접근 값을 통해 서브프로그램이 호출될 때 해당 외부 변수에 접근할 수 있습니다.

구조 예시:

-- (선언식 표현을 사용하여 로컬 함수 'Inner'를 정의하고,
--  'Inner'의 'Access' 값을 반환하는 함수 'Make_Adder')
function Make_Adder(X : Integer) return Adder_Function_Access is
   (declare
      -- 'X'는 Make_Adder의 파라미터 (외부 변수)
      function Inner(Y : Integer) return Integer is
      begin
         return X + Y; -- 외부 변수 'X'를 참조 (capture)
      end Inner;
   begin
      Inner'Access); -- 로컬 함수의 접근 값을 반환

예제: 클로저 사용

-- (10.3.1의 Unary_Integer_Function 접근 타입 사용)
type Adder_Function_Access is access function(Y : Integer) return Integer;

-- 클로저 생성 함수 (위 구조 예시와 동일)
function Make_Adder(X : Integer) return Adder_Function_Access is
   (declare
      function Inner(Y : Integer) return Integer is (X + Y);
   begin
      Inner'Access);

-- 클로저 생성: Add_5는 호출될 때 X=5인 환경을 기억함
Add_5 : constant Adder_Function_Access := Make_Adder(5);

-- 클로저 생성: Add_10은 호출될 때 X=10인 환경을 기억함
Add_10 : constant Adder_Function_Access := Make_Adder(10);

Result1 : Integer;
Result2 : Integer;
begin
   -- 클로저 호출: Add_5는 내부적으로 5 + 3을 계산
   Result1 := Add_5(3); -- Result1은 8

   -- 클로저 호출: Add_10은 내부적으로 10 + 3을 계산
   Result2 := Add_10(3); -- Result2는 13
end;

이 예제에서 Add_5Add_10은 각각 Make_Adder가 호출될 당시의 X 값(5 또는 10)을 “캡처”하여 기억하는 클로저입니다. 나중에 이 접근 값을 통해 함수가 호출될 때, 캡처된 X 값을 사용하여 계산을 수행합니다.

11.4 표현식 기반 프로그래밍 (Expression-Based Programming)

Ada는 문장(statement) 중심 구문 외에도, 값을 반환하는 식(expression)을 중심으로 로직을 구성하는 표현식 기반 프로그래밍을 지원합니다.

11.4.1 표현식 함수 (Expression Functions)

표현식 함수(Expression Function)는 함수의 본체(body)를 단일 표현식(expression)으로 정의하는 구문입니다 (ARM 6.8).

이는 is begin ... return ... end; 블록 구조 대신, 함수가 반환할 값을 계산하는 표현식 하나만으로 함수 구현을 정의할 때 사용됩니다.

기본 문법:

표현식 함수의 본체는 is 키워드 뒤에 괄호 ()로 둘러싸인 단일 표현식과 세미콜론 ;으로 구성됩니다.

function 함수_이름 (파라미터_목록) return 반환_타입 is
   (표현식);

동작 방식:

함수가 호출되면 괄호 안의 표현식이 평가되고, 그 평가 결과가 함수의 반환 값이 됩니다. 이 표현식은 함수의 반환_타입과 호환 가능해야 합니다.

예제:

정수 값을 제곱하는 함수 Square를 표현식 함수로 정의하는 예입니다.

-- 일반적인 함수 본체
function Square (X : Integer) return Integer is
begin
   return X * X;
end Square;

-- 표현식 함수 본체
function Square (X : Integer) return Integer is
   (X * X);

Square 함수의 기능은 동일합니다. 표현식 함수 구문은 단일 표현식으로 구현되는 함수를 정의하는 대체 구문입니다.

11.4.2 조건부 표현식 (Conditional Expressions)

조건부 표현식(Conditional Expression)은 Ada에서 문장(statement)이 아닌 표현식(expression) 형태로 조건부 로직을 구현하는 구문입니다 (ARM 4.5.7). 이는 if 문(5.3절)과 case 문(5.4절)에 대응하는 표현식 형태를 제공합니다.

조건부 표현식은 평가 결과로 하나의 값을 반환하며, 다른 표현식이 사용될 수 있는 곳(예: 변수 초기화, 할당문의 우변, 서브프로그램 인수)에서 사용될 수 있습니다. 조건부 표현식은 괄호 ()로 둘러싸여야 합니다.

1. if 표현식 (If Expression)

if 표현식은 하나 이상의 불리언 조건(boolean condition)을 순차적으로 평가하여, 참(True)으로 평가되는 첫 번째 조건에 해당하는 표현식의 값을 반환합니다.

기본 문법:

(if 조건_1 then
    표현식_1
 elsif 조건_2 then
    표현식_2
 ...
 else -- else 부분은 필수입니다.
    표현식_N)

동작 방식:

  • 조건들은 순서대로 평가됩니다.
  • 조건_i가 처음으로 True가 되면, 해당하는 표현식_i가 평가되고 이 값이 if 표현식 전체의 결과가 됩니다. 나머지 조건과 표현식은 평가되지 않습니다.
  • 모든 if/elsif 조건이 False이면, else 부분의 표현식_N이 평가되어 결과 값이 됩니다. else 부분은 반드시 존재해야 합니다.
  • 모든 결과 표현식(표현식_1표현식_N)은 서로 호환 가능한 타입을 가져야 합니다.

예제:

두 변수 AB 중 더 큰 값을 Max에 할당합니다.

Max : Integer := (if A > B then A else B);

2. case 표현식 (Case Expression)

case 표현식은 하나의 이산 타입(discrete type) 판별 표현식(selector expression)의 값에 따라 여러 대안 표현식 중 하나를 선택하여 그 값을 반환합니다. 이는 5.4.4절에서 이미 다루었습니다.

기본 문법 (복습):

(case 판별_표현식 is
   when 선택지_1 => 결과_표현식_1,
   when 선택지_2 | 선택지_3 => 결과_표현식_2,
   ...
   when others => 결과_표현식_Others)

동작 방식:

  • 판별_표현식이 평가됩니다.
  • 평가된 값과 일치하는 선택지에 해당하는 결과_표현식이 평가되고, 이 값이 case 표현식 전체의 결과 값이 됩니다.
  • case 문장과 동일하게 완전성(completeness)과 상호 배타성(mutual exclusivity) 규칙이 적용됩니다. (when others가 필요할 수 있습니다.)
  • 모든 결과_표현식들은 서로 호환 가능한 타입을 가져야 합니다.

예제:

문자 Grade 값에 따라 점수(Points)를 결정합니다.

Points : Natural := (case Grade is
                        when 'A' => 4,
                        when 'B' => 3,
                        when 'C' => 2,
                        when others => 0);

조건부 표현식은 값을 조건에 따라 결정하는 구문입니다.

11.4.3 리덕션 표현식 (Reduction Expressions) (Ada 2022)

리덕션 표현식(Reduction Expression)은 Ada 2022 표준에서 도입된 기능으로, 배열(array)이나 반복 가능한 컨테이너(iterable container)의 모든 요소(element)를 순회하며 하나의 단일 값으로 집약(reduction 또는 aggregation)하는 연산을 표현식 형태로 기술합니다 (ARM 4.5.10).

이는 함수형 프로그래밍의 fold 또는 reduce 연산과 유사합니다.

기본 문법:

리덕션 표현식은 'Reduce 속성(attribute)을 사용하여 구성됩니다.

-- 배열에 대한 리덕션
배열_객체'Reduce(Reducer => 집약_함수'Access,
                   Initial_Value => 초기값)

-- 컨테이너에 대한 리덕션
컨테이너_객체'Reduce(Reducer => 집약_함수'Access,
                     Initial_Value => 초기값)
  • Reducer: 두 개의 인수를 받는 함수에 대한 접근 값('Access)입니다. 첫 번째 인수는 현재까지 누적된 값이고, 두 번째 인수는 현재 순회 중인 요소입니다. 함수는 이 두 값을 결합하여 새로운 누적 값을 반환해야 합니다.
  • Initial_Value: 집약 연산의 시작 값(초기 누적 값)입니다.

Ada 2022는 병렬 처리를 위한 'Parallel_Reduce 속성도 제공합니다.

동작 방식:

  1. 누적 변수가 Initial_Value로 초기화됩니다.
  2. 배열 또는 컨테이너의 각 요소에 대해 순차적으로 (또는 병렬로) Reducer 함수가 호출됩니다.
  3. Reducer 함수는 현재 누적 값과 현재 요소를 입력받아 다음 누적 값을 계산하여 반환합니다.
  4. 모든 요소에 대한 처리가 완료되면 최종 누적 값이 'Reduce 표현식 전체의 결과 값이 됩니다.

예제:

정수 배열 My_Array의 모든 요소의 합계를 계산합니다.

My_Array : array (1 .. 10) of Integer := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

-- 합계를 계산하는 집약 함수
function Sum_Reducer(Accumulator : Integer; Element : Integer) return Integer is
   (Accumulator + Element);

Total : Integer;
begin
   -- 'Reduce' 속성을 사용하여 합계 계산
   Total := My_Array'Reduce(Reducer => Sum_Reducer'Access, Initial_Value => 0);

   -- Total은 55가 됩니다.
end;

리덕션 표현식은 반복문(loop)을 사용하지 않고, 배열이나 컨테이너의 집계 연산을 선언적으로 기술하는 방법을 제공합니다.

11.4.4 수량자 표현식 (Quantifier Expressions) (Ada 2022)

수량자 표현식(Quantifier Expression)은 Ada 2022 표준에서 도입된 기능으로, 배열(array)이나 반복 가능한 컨테이너(iterable container)의 요소(element)들이 특정 조건(술어, predicate)을 만족하는지 여부를 검사하여 부울(Boolean) 값 (True 또는 False)을 반환하는 표현식입니다 (ARM 4.5.8).

이는 명시적인 루프(loop) 없이 컬렉션의 속성을 선언적으로 검사하는 방법을 제공하며, 함수형 프로그래밍의 수량자(quantifier) 개념과 관련됩니다.

수량자 표현식은 for allfor some 두 가지 형태로 제공됩니다.

1. for all 수량자 (Universal Quantifier)

for all 수량자는 지정된 배열이나 컨테이너의 모든 요소가 주어진 술어(boolean expression)를 만족할 경우 True를 반환하고, 하나라도 만족하지 않으면 False를 반환합니다.

기본 문법:

(for all 요소_매개변수 of 배열_또는_컨테이너 => 술어_표현식)

-- 또는 이산 범위에 대해
(for all 루프_제어_변수 in 이산_범위 => 술어_표현식)

동작 방식:

  • 배열/컨테이너의 모든 요소 (또는 이산 범위의 모든 값)에 대해 술어_표현식이 평가됩니다.
  • 모든 평가 결과가 True이면, for all 표현식은 True를 반환합니다.
  • 하나라도 False인 평가 결과가 나오면, for all 표현식은 즉시 False를 반환하고 나머지 요소에 대한 평가는 생략될 수 있습니다 (단락 평가, short-circuit).
  • 배열/컨테이너가 비어있는(empty) 경우, for allTrue를 반환합니다.

예제:

배열 Values의 모든 요소가 0보다 큰지 검사합니다.

Values : array (1 .. 10) of Integer := (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
All_Positive : Boolean;
begin
   All_Positive := (for all V of Values => V > 0);
   -- All_Positive는 True가 됩니다.
end;

2. for some 수량자 (Existential Quantifier)

for some 수량자는 지정된 배열이나 컨테이너의 요소 중 하나 이상이 주어진 술어(boolean expression)를 만족할 경우 True를 반환하고, 모든 요소가 만족하지 않으면 False를 반환합니다.

기본 문법:

(for some 요소_매개변수 of 배열_또는_컨테이너 => 술어_표현식)

-- 또는 이산 범위에 대해
(for some 루프_제어_변수 in 이산_범위 => 술어_표현식)

동작 방식:

  • 배열/컨테이너의 요소 (또는 이산 범위의 값)에 대해 순차적으로 술어_표현식이 평가됩니다.
  • True인 평가 결과가 처음 나오면, for some 표현식은 즉시 True를 반환하고 나머지 요소에 대한 평가는 생략될 수 있습니다 (단락 평가, short-circuit).
  • 모든 요소에 대해 평가 결과가 False이면, for some 표현식은 False를 반환합니다.
  • 배열/컨테이너가 비어있는(empty) 경우, for someFalse를 반환합니다.

예제:

배열 Values에 음수인 요소가 하나라도 있는지 검사합니다.

Values : array (1 .. 5) of Integer := (10, 20, -5, 30, 40);
Has_Negative : Boolean;
begin
   Has_Negative := (for some V of Values => V < 0);
   -- Has_Negative는 True가 됩니다 (-5 때문에).
end;

수량자 표현식은 컬렉션의 전체 또는 일부 요소가 특정 속성을 만족하는지 여부를 명시적인 반복문 없이 표현하는 방법을 제공합니다.

11.5 반복자와 데이터 순회 (Iterators and Data Iteration)

이 절에서는 Ada의 데이터 컬렉션 순회 메커니즘, 특히 함수형 프로그래밍 스타일을 지원하는 고급 반복자 기능들을 소개합니다.

11.5.1 기본 반복 구문 (for...in, for...of) 복습

5.5절에서 Ada의 기본 for 루프 구문 두 가지를 학습했습니다.

  1. for ... in ... loop: 이 구문은 이산 범위(discrete range)를 순회합니다. 루프 파라미터(loop parameter)는 범위 내의 각 값을 순차적으로 (또는 reverse 사용 시 역순으로) 가지며, 루프 본체 내에서 상수(constant)로 취급됩니다 (5.5.5절, 5.5.6절 참조).

    -- 1부터 10까지 순회
    for I in 1 .. 10 loop ... end loop;
    
  2. for ... of ... loop: 이 구문은 배열(array)이나 반복 가능한 컨테이너(iterable container)의 요소(element)를 직접 순회합니다 (5.5.7절 참조). 루프 파라미터는 각 순회 단계에서 컬렉션의 요소 자체를 나타냅니다.

    My_Array : array (1 .. 5) of Integer;
    -- My_Array의 각 요소를 Element 변수로 순회
    for Element of My_Array loop ... end loop;
    

이러한 기본 반복 구문은 데이터를 순회하는 방법을 제공하며, 이어지는 절에서 다룰 일반화된 반복자 메커니즘의 기초가 됩니다.

11.5.2 일반화된 반복자 (Generalized Iterators: for...in Iterator_Object)

일반화된 반복자(Generalized Iterator)는 컬렉션(collection)을 순회하는 명시적인 방법을 제공합니다. 10.5.1절의 for...of 구문이 컨테이너 객체 자체를 순회 대상으로 지정하는 것과 달리, 일반화된 반복자는 반복자 객체(iterator object)를 for...in 구문과 함께 사용합니다 (ARM 5.5.1).

기본 문법:

for 요소_매개변수 in 반복자_객체 loop
   -- (Sequence of Statements)
   -- 각 요소에 대해 실행될 문장들
end loop;
  • 반복자_객체: 이 객체는 일반적으로 컨테이너 타입이 제공하는 함수(예: My_Container.Iterate, My_Container.Reverse_Iterate)를 호출하여 얻습니다. 이 객체는 순회 상태(예: 현재 위치)를 유지하고 다음 요소로 이동하는 로직을 캡슐화합니다.
  • 요소_매개변수: 각 순회 단계에서 현재 요소를 나타내는 암시적으로 선언된 변수 또는 상수입니다.

동작 방식:

for...in 루프는 반복자_객체가 제공하는 인터페이스(일반적으로 Ada.Iterator_Interfaces 패키지에 정의된 연산들)를 사용하여 컬렉션의 요소들을 순차적으로 접근합니다. 루프는 반복자 객체에게 다음 요소를 요청하고, 더 이상 요소가 없을 때 종료됩니다.

사용 목적:

일반화된 반복자는 다음과 같은 경우에 사용됩니다.

  1. 다양한 순회 방식 제공: 하나의 컨테이너 타입에 대해 여러 가지 순회 방법(예: 정방향, 역방향, 특정 조건 필터링)을 제공하고자 할 때, 각 방식에 해당하는 반복자 객체를 반환하는 함수를 정의할 수 있습니다.
  2. 복잡한 순회 로직: 순회 로직이 단순한 요소 접근 이상으로 복잡할 때 (예: 트리(tree) 구조의 특정 순서 순회).

예제 (개념):

-- (My_List_Package가 반복자 인터페이스를 구현하고,
-- Iterate 함수가 반복자 객체를 반환한다고 가정)

My_List : My_List_Package.List;
-- ... My_List 초기화 ...

-- 정방향 반복자를 사용하여 순회
for Item in My_List_Package.Iterate(My_List) loop
   Process(Item);
end loop;

-- (만약 Reverse_Iterate 함수가 있다면) 역방향 반복자를 사용하여 순회
-- for Item in My_List_Package.Reverse_Iterate(My_List) loop ...

이 방식은 순회 메커니즘을 컨테이너 자체와 분리하여 추상화와 유연성을 제공합니다.

11.5.3 절차적 반복자 (Procedural Iterators: for...of Iterator_Procedure)

절차적 반복자(Procedural Iterator)는 Ada에서 반복(iteration)을 구현하는 방식으로, 제어의 역전(inversion of control) 패턴을 사용합니다 (ARM 5.5.2).

10.5.1절의 for...of가 컨테이너 객체를 순회 대상으로 하고, 10.5.2절의 for...in반복자 객체를 사용하는 것과 달리, 절차적 반복자는 순회를 제어하는 프로시저(procedure) 자체를 for...of 구문의 대상으로 지정합니다.

기본 문법:

for (루프_파라미터_목록) of 반복_프로시저 loop
   -- (Sequence of Statements)
   -- 각 요소에 대해 실행될 문장들 (루프 본체)
end loop;
  • 반복_프로시저: 순회를 제어하는 프로시저의 이름입니다. 이 프로시저는 특정 프로파일(profile)을 가져야 하며, 루프 본문에 해당하는 다른 프로시저에 대한 접근 값(access value)을 파라미터로 받습니다.
  • (루프_파라미터_목록): 루프 본체가 각 순회 단계에서 받게 될 파라미터들을 선언합니다. 이 파라미터들의 타입은 반복_프로시저가 요구하는 프로파일과 일치해야 합니다.

동작 방식:

  1. for...of 반복_프로시저 loop ... end loop; 구문이 나타나면, 컴파일러는 loop ... end loop; 사이의 루프 본체를 암시적으로 하나의 프로시저로 간주합니다. 이 암시적 프로시저는 루프_파라미터_목록을 자신의 파라미터로 가집니다.
  2. 이 암시적 프로시저에 대한 접근 값('Access)이 생성됩니다.
  3. 반복_프로시저가 이 접근 값을 인수로 받아 호출됩니다.
  4. 반복_프로시저 내부 로직은 순회할 요소들을 생성하거나 접근하며, 각 요소에 대해 인수로 전달받은 접근 값(즉, 루프 본체 프로시저)을 호출합니다.
  5. 반복_프로시저가 반환하면 for 루프 전체가 종료됩니다.

사용 목적:

절차적 반복자는 순회 대상의 생성이나 접근 방식이 복잡하여, 순회 로직 자체를 프로시저 내부에 캡슐화하는 경우에 사용됩니다. 예를 들어, 운영체제의 환경 변수 목록이나 파일 시스템의 디렉토리 내용과 같이, 명시적인 컨테이너 객체가 없는 데이터 소스를 순회하는 데 사용될 수 있습니다.

예제: Ada.Environment_Variables.Iterate

Ada 표준 라이브러리의 Ada.Environment_Variables.Iterate 프로시저가 절차적 반복자의 예입니다.

with Ada.Text_IO;
with Ada.Environment_Variables;

procedure Show_Environment is
begin
   Ada.Text_IO.Put_Line("환경 변수 목록:");

   -- 'Iterate' 프로시저를 사용하여 환경 변수 순회
   for (Name, Value) of Ada.Environment_Variables.Iterate loop
      -- 루프 본문: 각 Name, Value 쌍에 대해 실행됨
      Ada.Text_IO.Put_Line(Name & "=" & Value);
   end loop;

end Show_Environment;

이 예제에서 loop ... end loop; 사이의 코드는 (Name : String; Value : String)을 파라미터로 받는 암시적 프로시저로 취급되며, 이 프로시저의 접근 값이 Ada.Environment_Variables.Iterate 프로시저에 전달됩니다. Iterate 프로시저는 내부적으로 운영체제로부터 환경 변수 목록을 얻어와, 각 변수의 이름과 값을 루프 본문 프로시저를 호출하여 전달합니다.

11.5.4 반복 가능한 타입 메커니즘 (Iterable Type Mechanism: Aspects)

Ada는 사용자 정의 컨테이너 타입이 10.5.1절에서 설명한 for ... of ... loop 구문을 직접 지원하도록 만드는 메커니즘을 제공합니다. 이는 타입 선언 시 특정 애스펙트(aspect) 를 명시하여 구현됩니다.

이 메커니즘은 타입이 “반복 가능(iterable)”함을 컴파일러에게 알리고, for...of 루프가 해당 타입의 객체를 순회하기 위해 필요한 정보(예: 어떤 반복자를 사용할지, 각 요소의 타입은 무엇인지)를 제공합니다.

주요 관련 애스펙트는 다음과 같습니다 (ARM 5.5.1, 5.5.2):

  • Iterable: 타입이 반복 가능한 컨테이너임을 명시합니다. 이 애스펙트는 다음과 같은 다른 애스펙트와 함께 사용됩니다.
    • Iterator_Element: for...of 루프의 루프 파라미터(요소 변수)가 가져야 할 타입을 지정합니다.
    • Default_Iterator: 이 타입을 for...of 루프에서 사용할 때, 기본적으로 어떤 반복자 객체(10.5.2절)나 반복 프로시저(10.5.3절)를 사용해야 하는지를 지정합니다. 이 애스펙트는 명시적인 반복자 객체 또는 프로시저에 대한 접근 타입을 값으로 가집니다.
  • Constant_Indexing / Variable_Indexing: (for...of 루프의 루프 파라미터가 상수(constant)인지 변수(variable)인지를 결정하는 데 사용될 수 있습니다.

이러한 애스펙트들은 Ada.Iterator_Interfaces 패키지에 정의된 인터페이스 및 관련 타입들과 함께 사용되어, 사용자 정의 컨테이너가 표준 반복자 패턴을 따르도록 합니다.

구조 예시 (개념):

-- (Ada.Iterator_Interfaces를 with/use 했다고 가정)

-- 사용자 정의 리스트 타입 선언
type My_List is tagged private
   with Iterable => True, -- 반복 가능함을 명시
        Iterator_Element => Integer, -- 요소 타입은 Integer
        Default_Iterator => Iterate'Access; -- 기본 반복자는 'Iterate' 함수/프로시저

private
   -- ... My_List의 실제 구현 ...
end My_List;

-- My_List 타입을 위한 반복자 함수 또는 프로시저 선언/구현
-- (예: function Iterate(Container : My_List) return Forward_Iterator'Class;)
-- ...

애스펙트를 사용하여 타입을 정의하면, 해당 타입의 객체 L에 대해 다음과 같이 for...of 루프를 직접 사용할 수 있습니다.

L : My_List;
-- ... L 초기화 ...
for Item of L loop -- 'Default_Iterator' 애스펙트로 지정된 반복자가 사용됨
   -- Item은 'Iterator_Element'로 지정된 Integer 타입
   Process(Item);
end loop;

11.5.5 반복자 필터 (Iterator Filters: when 절)

반복자 필터(Iterator Filter)는 for 루프 구문에 when 절을 추가하여, 반복 대상(이산 범위 또는 컬렉션)의 요소들 중 특정 조건을 만족하는 요소에 대해서만 루프 본체를 실행하도록 필터링하는 기능입니다 (ARM 5.5).

이는 if 문을 루프 내부에 사용하는 것과 유사한 결과를 제공하며, 필터링 조건을 루프 제어 구문 자체에 명시합니다.

기본 문법:

when 절은 loop 키워드 바로 앞에 위치하며, 부울(Boolean) 값을 반환하는 표현식을 포함합니다.

  • for...in 구문과 함께 사용:

    for 루프_제어_변수 in 이산_범위 when 조건_표현식 loop
       -- (Sequence of Statements)
       -- 조건을 만족하는 루프_제어_변수에 대해서만 실행됨
    end loop;
    
  • for...of 구문과 함께 사용:

    for 요소_매개변수 of 배열_또는_컨테이너 when 조건_표현식 loop
       -- (Sequence of Statements)
       -- 조건을 만족하는 요소_매개변수에 대해서만 실행됨
    end loop;
    

동작 방식:

  1. 루프는 명시된 이산_범위 전체 또는 배열_또는_컨테이너의 모든 요소를 순회합니다.
  2. 각 순회 단계에서 when 뒤의 조건_표현식이 평가됩니다. 이 조건 표현식은 현재 순회 중인 루프_제어_변수 또는 요소_매개변수를 참조할 수 있습니다.
  3. 조건_표현식의 평가 결과가 True인 경우에만 루프 본체(loop ... end loop; 사이의 문장들)가 실행됩니다.
  4. 평가 결과가 False이면, 루프 본체는 건너뛰고 다음 요소(또는 값)로 순회를 계속합니다.

예제 1 (for...in 사용):

-- 1부터 100까지의 범위에서 7의 배수만 출력합니다.
for I in 1 .. 100 when I mod 7 = 0 loop
   Put_Line(Integer'Image(I) & "은(는) 7의 배수입니다.");
end loop;

예제 2 (for...of 사용):

My_Array : array (1 .. 10) of Integer := (-1, 2, -3, 4, -5, 6, -7, 8, -9, 10);

-- My_Array 배열의 요소 중 0보다 큰 요소(Element)만 합산합니다.
Sum_Of_Positives : Integer := 0;
for Element of My_Array when Element > 0 loop
   Sum_Of_Positives := Sum_Of_Positives + Element;
end loop;
-- Sum_Of_Positives는 2 + 4 + 6 + 8 + 10 = 30이 됩니다.

반복자 필터는 컬렉션 순회 시 특정 요소만 선택적으로 처리하는 로직을 루프 제어부에 통합하는 방법을 제공합니다.

12. 예외 처리

12.1 예외 처리의 기본 개념

12.1.1 예외란 무엇인가?

Ada에서 예외(Exception)란 프로그램 실행 중 발생할 수 있는 ‘예외적인 상황’을 의미합니다. 예를 들어, 숫자를 0으로 나누려 하거나 존재하지 않는 파일에 접근하려는 경우가 이에 해당합니다. 이러한 상황이 실제로 발생하는 것을 예외 발생(exception occurrence)이라고 합니다.

이러한 예외적 상황에 대응하는 과정은 예외를 발생시키는(raising) 행위와 이를 처리하는(handling) 행위로 이루어집니다. 예외를 발생시키는 것은 예외적인 상황이 발생했음을 알리기 위해 정상적인 프로그램의 실행 흐름을 의도적으로 포기하는 행위이며, 이때 원래 진행되던 구문의 실행은 그 즉시 중단됩니다. 이렇게 실행이 중단되면, 발생한 예외에 대응하여 특정 동작을 수행하는 처리(handling) 과정이 이어집니다. 이 과정에서 프로그램의 제어권은 사용자가 미리 정의한 예외 처리기(exception handler)로 이전될 수 있으며, 만약 현재 위치에서 처리할 수 없다면 예외는 상위 실행 문맥으로 전파(propagate)될 수 있습니다.

이러한 예외 처리 메커니즘은 오류를 감지하면 정상적인 실행 흐름을 중단하고 예외를 발생시킨 후, 여기에 대응하여 프로그램의 안정적인 실행을 지속하거나 제어된 방식으로 종료함으로써 견고성(robustness)을 향상시킵니다.

12.1.2 예외 처리 모델: 종료 모델

예외 처리 메커니즘은 종료 모델(termination model)재개 모델(resumption model)로 나뉩니다.

  • 종료 모델 (termination model): 예외가 발생하여 예외 처리기로 제어권이 이전되면, 원래 예외가 발생했던 지점으로 돌아가지 않습니다. 예외 처리기의 실행이 끝난 후에는 해당 블록(begin ... end) 다음의 문장으로 실행이 이어집니다. 즉, 해당 블록의 실행은 예외 발생 시점에서 종료됩니다. Ada, C++, Java, Python 등이 이 모델을 사용합니다.

  • 재개 모델 (resumption model): 예외 처리기에서 특정 조치를 수행한 후, 예외가 발생했던 지점으로 돌아가 실행을 재개할 수 있는 모델입니다.

Ada는 종료 모델을 채택하고 있습니다. 예외가 발생하면 해당 코드 블록의 실행은 중단되며, 개발자는 예외 처리기에서 복구 또는 정리 작업을 수행한 후 프로그램의 다음 동작을 제어하는 흐름을 설계할 수 있습니다.

12.1.3 미리 정의된 예외

Ada 언어는 어떠한 컴파일 단위(compilation unit)에서든 바로 사용할 수 있는 4가지 핵심 예외, 즉 Constraint_Error, Program_Error, Storage_Error, Tasking_Error를 미리 정의하고 있습니다. 이 예외들은 Standard 패키지에 선언되어 있어 별도의 withuse 절 없이도 항상 접근 가능하며 프로그램 실행 중 특정 규칙 위반 여부를 확인하는 언어 정의 런타임 검사(language-defined run-time check)가 실패할 때 자동으로 발생합니다. 또한, 프로그래머가 raise 문을 사용하여 이 예외들을 직접 발생시킬 수도 있습니다. 언어 정의 런타임 검사에 대한 자세한 내용은 7.2.1절에서 설명합니다.

12.1.4 예외 선언

Ada에서는 exception 키워드를 사용하여 사용자 정의 예외를 선언(declare)할 수 있습니다. 이 선언은 특정 예외적 상황을 나타낼 고유한 이름을 정의하는 역할을 합니다.

구문 (syntax)

예외 선언의 일반적인 형식은 다음과 같습니다.

<예외_이름_목록> : exception;

<예외_이름_목록>에는 하나 또는 여러 개의 예외 이름을 쉼표로 구분하여 사용할 수 있습니다.

-- 하나의 예외 선언
Singular : exception;

-- 여러 예외를 한 번에 선언
Overflow, Underflow : exception;

선언은 항상 세미콜론(;)으로 끝납니다.

예외 선언의 의미 규칙

예외 선언에는 다음과 같은 의미 규칙이 있습니다.

  • 고유성: 각각의 예외 선언문은 완전히 다른 새로운 예외를 정의합니다. 예를 들어, 서로 다른 두 패키지에 Example_Error라는 이름의 예외가 각각 선언되었다면, 두 Example_Error 예외는 이름만 같을 뿐 서로 다른 별개의 예외입니다.
  • 정적 식별: 예외의 이름이 나타내는 특정 예외는 컴파일 타임에 결정됩니다. 즉, 선언된 예외의 고유한 정체성(identity)은 프로그램이 컴파일될 때 확정되며, 프로그램이 실행되는 동안 이 정체성은 변하지 않습니다.
  • 런타임 효과 없음: 예외 선언문의 정교화(elaboration)7는 런타임에 아무런 효과가 없습니다. 이는 변수 선언 시 메모리가 할당되고 초기화되는 것과는 다른 동작입니다.
  • 제네릭과 예외: 만약 제네릭 유닛 내에 예외가 선언된 경우, 해당 제네릭의 인스턴스를 생성할 때마다 각각의 인스턴스에는 서로 다른 고유한 예외가 있게 됩니다.

12.2 예외 발생시키기 (raising exceptions)

12.2.1 묵시적 발생 (implicit raising)

프로그래머가 예외를 접하는 일반적인 경로는 묵시적 발생입니다. 이는 Ada 언어가 프로그램의 안정성 및 정확성 보장을 위해 내장한 언어 정의 런타임 검사(language-defined check)가 실패했을 때 발생합니다.

런타임 검사는 프로그램 실행 중에 특정 연산이 유효한지를 확인하는 자동화된 과정입니다. 만약 검사 조건이 거짓으로 판명되면, 검사는 실패하고 그에 상응하는 미리 정의된 예외가 자동으로 발생합니다.

다음은 런타임 검사 실패로 인해 Constraint_Error가 묵시적으로 발생하는 사례들입니다.

인덱스 범위 위반 (index_check 실패)

배열에 정의된 범위를 벗어나는 인덱스로 접근하려 할 때 index_check가 실패하여 Constraint_Error가 발생합니다.

my_array : array (1 .. 4) of Integer;
value    : Integer;
...
value := my_array(5); -- 5는 1..4 범위를 벗어나므로 Constraint_Error 발생

0으로 나누기 (division_check 실패)

숫자를 0으로 나누려고 시도하면 division_check가 실패하여 Constraint_Error가 발생합니다.

a      : Integer := 10;
b      : Integer := 0;
result : Integer;
...
result := a / b; -- 0으로 나누므로 Constraint_Error 발생

널(null) 접근 (access_check 실패)

null 값을 가지는 접근 타입(access type) 변수를 역참조(dereference)하려 하면 access_check가 실패하여 Constraint_Error가 발생합니다.

type Int_Access is access Integer;
ptr   : Int_Access := null;
value : Integer;
...
value := ptr.all; -- null을 역참조하므로 Constraint_Error 발생

묵시적으로 발생할 수 있는 예외와 그와 관련된 언어 정의 런타임 검사를 정리하면 다음과 같습니다.

Constraint_Error는 어떤 값이 특정 제약 조건(constraint)을 위반할 때 다음과 같은 런타임 검사가 실패하여 발생합니다.

  • access_check: 값이 널(null)이 아니어야 하는 경우 이를 검사합니다.
  • discriminant_check: 복합 객체의 판별자(discriminant)가 올바른 값을 가졌는지 검사합니다.
  • division_check: 나눗셈 연산(/, rem, mod)의 두 번째 피연산자가 0이 아닌지 검사합니다.
  • index_check: 배열의 인덱스 값이 경계 내에 있는지 검사합니다.
  • length_check: 두 배열의 길이가 일치하는지 검사합니다.
  • overflow_check: 스칼라 값이 해당 타입의 기본 범위를 벗어나지 않는지 검사합니다.
  • range_check: 스칼라 값이 특정 범위 제약 조건을 만족하는지 검사합니다.
  • tag_check: 태그드 타입(tagged type)의 태그가 올바른지 검사합니다.

Program_Error는 프로그램의 실행 순서나 접근 규칙 등 논리적으로 실행될 수 없는 상황에서 다음과 같은 런타임 검사가 실패하여 발생합니다.

  • accessibility_check: 접근하려는 객체의 접근성 수준(accessibility level)을 검사합니다.
  • allocation_check: 할당자(allocator)가 호출될 때 태스크가 올바른 상태인지 등을 검사합니다.
  • elaboration_check: 호출하려는 서브프로그램 등의 몸체가 이미 정교화(elaborated)되었는지 검사합니다.
  • program_error_check: 그 외 다양한 언어 규칙 위반 상황을 검사합니다.

Storage_Error는 메모리 관련 문제가 발생했을 때 다음과 같은 런타임 검사가 실패하여 발생합니다.

  • storage_check: 할당 가능한 메모리 공간이 충분한지, 또는 태스크나 서브프로그램이 스택 공간을 초과하지 않았는지 검사합니다.

Tasking_Error는 태스크(task) 간의 통신이나 동기화 과정에서 문제가 발생했을 때 다음과 같은 런타임 검사가 실패하여 발생합니다.

  • tasking_check: 호출된 태스크가 아직 종료되지 않았는지 등을 검사합니다.

이러한 런타임 검사들을 비활성화하여 프로그램의 성능을 높이는 방법에 대해서는 7.8절에서 자세히 다룹니다.

12.2.2 명시적 발생: raise 문과 표현식

묵시적 발생 외에도, 프로그래머가 특정 조건이 충족되었을 때 코드에서 직접 예외를 발생시킬 수 있습니다. 이는 주로 예상된 오류 조건을 처리하거나, 서브프로그램의 사전/사후 조건을 강제하는 데 사용됩니다.

raise 문(statement)

raise 문은 예외를 발생시키는 독립적인 실행문입니다. 세 가지 형태로 사용할 수 있습니다.

1. 예외 이름으로 예외 발생시키기

가장 기본적인 형태로, 지정된 이름의 예외를 발생시킵니다.

-- Overflow는 이전에 선언된 예외 이름
raise Overflow;

2. 메시지와 함께 예외 발생시키기

예외 발생 시 특정 문자열 메시지를 함께 전달할 수 있습니다. 이 메시지는 나중에 예외 처리기에서 Ada.Exceptions.exception_message 함수를 통해 확인할 수 있어 디버깅에 유용합니다.

raise Constraint_Error with "입력 버퍼가 가득 찼습니다.";

3. 예외 다시 발생시키기 (re-raise)

raise; 형태는 오직 예외 처리기(exception handler) 내부에서만 사용할 수 있습니다. 이 구문은 현재 처리 중인 예외 발생(occurrence)을 그대로 상위 실행 문맥으로 다시 전파하는 역할을 합니다. 이는 예외를 부분적으로 처리(예: 로그 기록)한 후, 상위 처리기에게 마저 처리하도록 위임할 때 유용합니다.

begin
-- 예외가 발생할 수 있는 코드
exception
  when e : others =>
    Ada.Text_IO.put_line ("에러 발생: " & Ada.Exceptions.exception_message (e));
    raise; -- 처리 중인 예외 e를 그대로 다시 발생시킴
end;

raise 표현식 (expression)

raise 표현식은 문장이 아닌, 다른 표현식의 일부로 사용된다는 점에서 raise 문과 다릅니다. 주로 값을 반환하거나 변수를 초기화해야 하지만, 정상적인 값을 생성할 수 없는 상황에서 예외를 발생시키는 데 사용됩니다. raise 표현식은 어떤 타입이든 만족시킬 수 있으며, 평가될 때 지정된 예외를 발생시킵니다.

raise 표현식이 객체 선언, 타입 정의, 기본값 표현식 등의 일부로 사용될 경우, 반드시 괄호 ()로 감싸야 하는 문법적 제약이 있습니다.

-- 객체 선언 시 괄호가 필요함
x : Integer := (raise Program_Error);

다음은 변수 result를 초기화할 때, 조건에 따라 정상적인 값을 계산하거나 raise 표현식을 통해 예외를 발생시키는 예입니다.

result : Float := (if X >= 0.0 then
                     -- 조건이 참일 때의 값
                     Ada.Numerics.Elementary_Functions.sqrt(X)
                   else
                     -- 조건이 거짓일 때, raise 표현식이 예외를 발생시킴
                     (raise Constraint_Error with "음수 값에 대한 제곱근은 정의되지 않습니다."));

12.3 예외 처리하기

12.3.1 예외 처리기 구문 (beginexceptionend)

예외 처리기(exception handler)는 특정 예외가 발생했을 때 이에 대응하여 실행되는 코드 블록입니다. Ada에서는 이러한 처리기들을 일반 실행문과 분리된 별도의 구역에 배치합니다.

예외 처리기 구문의 일반적인 형태는 다음과 같습니다:

begin
   -- 정상적인 실행 흐름에 해당하는 문장들이 위치합니다.
   -- 이 영역에서 예외가 발생할 수 있습니다.
exception
   -- 예외 핸들러는 여기에 위치하며,
   -- 'when' 절로 시작합니다.
   when <예외_이름_1> =>
      -- 예외 1이 발생했을 때 실행할 코드
   when <예외_이름_2> | <예외_이름_3> =>
      -- 예외 2 또는 3이 발생했을 때 실행할 코드
   when others =>
      -- 위에서 명시되지 않은 모든 예외가 발생했을 때 실행할 코드
end;

이 구조의 실행 흐름은 두 가지 경우로 나뉩니다.

beginexception 사이의 코드에서 예외가 발생하지 않으면, exception 이하의 처리기 부분은 완전히 건너뛰고 end 다음으로 실행이 이어집니다.

만약 beginexception 사이의 코드에서 예외가 발생하면, 해당 지점에서 즉시 정상적인 실행을 포기합니다. 그 후, 프로그램 제어권은 exception 이하의 처리기 부분으로 이전되어 알맞은 처리기를 찾게 됩니다.

12.3.2 특정 예외 처리: when

exception 처리 구역은 하나 이상의 when 절로 구성됩니다. Ada 런타임은 발생한 예외와 일치하는 예외 이름을 가진 when 절을 찾아 해당 코드 블록을 실행합니다.

기본적인 형태는 다음과 같습니다.

begin
  -- ...
  i := Integer'value (s); -- s가 정수가 아니면 Constraint_Error 발생
  -- ...
exception
  when Constraint_Error =>
    Ada.Text_IO.put_line ("오류: 유효하지 않은 숫자 형식입니다.");
end;

여러 예외 동시 처리

하나의 처리기에서 여러 종류의 예외를 동일한 방식으로 처리하고자 할 때, 수직 막대 기호(|)를 사용하여 예외 이름들을 나열할 수 있습니다. 이를 통해 코드 중복을 줄일 수 있습니다.

when Name_Error | Use_Error =>
  Ada.Text_IO.put_line ("파일을 열거나 사용할 수 없습니다.");

예외 정보 접근하기

when 절은 발생한 예외에 대한 상세 정보에 접근할 수 있도록 선택 매개변수(choice parameter)를 선언하는 기능을 제공합니다. 이 매개변수는 예외 처리기 안에서만 사용 가능한 상수이며, 타입은 Ada.Exceptions.Exception_Occurrence 입니다.

선택 매개변수를 사용하면 Ada.Exceptions.exception_message와 같은 함수를 통해 예외와 관련된 구체적인 메시지를 얻어, 오류 보고나 로깅에 포함시킬 수 있습니다.

begin
  open (file, in_file, "non_existent_file.txt");
exception
  when e : Name_Error =>
    -- e는 발생한 Name_Error 예외에 대한 정보를 담고 있음
    Ada.Text_IO.put_line ("파일 열기 실패: " & Ada.Exceptions.exception_message (e));
end;

12.3.3 모든 예외 처리: when others

when others 절은 특정 when 절에서 명시적으로 처리되지 않은 모든 종류의 예외를 처리합니다. 이 절을 사용하여 예상치 못한 예외가 발생했을 때 프로그램이 비정상적으로 종료되는 것을 막고, 오류 기록이나 자원 정리 등의 동작을 수행할 수 있습니다.

구문 규칙

when others 절에는 다음과 같은 구문 규칙이 있습니다.

  • when othersexception 처리 구역의 가장 마지막에 위치해야 합니다.
  • when others는 다른 예외 이름과 | 기호를 사용하여 함께 나열할 수 없습니다.

when others 뒤에 다른 when 절이 위치하면, 해당 코드는 실행될 수 없으므로 컴파일 오류가 발생합니다.

사용 예시

begin
   -- ... 다양한 예외가 발생할 수 있는 코드 ...
exception
   when Constraint_Error =>
      Ada.Text_IO.put_line ("제약 조건 오류가 발생했습니다.");

   when Name_Error | Use_Error =>
      Ada.Text_IO.put_line ("파일 관련 오류가 발생했습니다.");

   when others =>
      -- 위에서 처리되지 않은 그 외 모든 예외를 여기서 처리합니다.
      Ada.Text_IO.put_line ("알 수 없는 오류가 발생했습니다.");
end;

선택 매개변수 사용

when others는 어떤 예외가 발생했는지 미리 알 수 없으므로, 선택 매개변수(choice parameter)를 사용하여 발생한 예외의 이름이나 메시지 등 구체적인 정보를 얻을 수 있습니다.

when e : others =>
  -- e는 발생한 예외에 대한 정보를 담고 있음
  Ada.Text_IO.put_line ("처리되지 않은 예외 발생!");
  Ada.Text_IO.put_line ("예외 종류: " & Ada.Exceptions.exception_name (e));

  -- 오류 정보를 기록한 후, 예외를 다시 발생시켜 상위 처리기에 알릴 수 있습니다.
  raise;
end;

12.3.4 처리기 내에서의 추가 동작: 정리, 변환, 재전파

예외 처리기에서는 오류 메시지 출력 외에 정리 작업, 예외 변환, 예외 재전파 등의 동작을 수행할 수 있습니다.

1. 지역적 정리 후 재전파 (Cleanup and Re-raise)

하나의 패턴은 예외 발생 시 현재 유효범위(scope)에서 정리 작업(예: 파일 닫기, 잠금 해제)을 수행하고, raise; 문을 사용하여 예외를 상위 호출자에게 전파하는 것입니다. raise; 문은 현재 처리 중인 예외를 원본 정보 손실 없이 다시 전파합니다. 이 동작 방식은 예외가 처음 발생했던 위치, 메시지 등 모든 원본 정보가 보존된 채로 상위 처리기에 전달되도록 합니다. 그 결과, 오류의 정확한 출처를 추적하는 디버깅이 가능해집니다.

raise; 구문

  • 예외 이름 없이 raise 키워드만 단독으로 사용합니다.
  • 오직 예외 처리기(exception ... end) 내부에서만 사용할 수 있습니다.
  • 현재 처리기가 포착한 예외 발생(occurrence)을 새로운 예외가 아닌, 원본 그대로 다시 발생시킵니다.

예시

procedure Process_File (File_Name : String) is
  File : Ada.Text_IO.File_Type;
begin
  Ada.Text_IO.Open (File, Mode => Ada.Text_IO.In_File, Name => File_Name);
  -- ... 데이터 처리 중 예외 발생 가능 ...
exception
  when E : others =>
    -- 1. 지역적인 정리 작업 (파일 닫기)
    Ada.Text_IO.Close (File);
    -- 2. 원본 예외를 호출자에게 다시 전파
    raise;
end Process_File;

2. 예외 변환 (Exception Translation)

예외 처리기는 저수준의 시스템 예외(예: Ada.IO_Exceptions.Name_Error)를 고수준의 응용 프로그램 예외(예: Application.Configuration_Error)로 변환하여 전파할 수 있습니다. 이를 예외 변환이라고 하며, 이러한 방식은 추상화 수준을 유지하고 내부 구현 정보를 노출하지 않습니다.

예외 처리기 내에서 raise <새로운_예외>; 구문을 사용하여 새로운 예외를 발생시키면 됩니다.

예시

package Application is
  -- 응용 프로그램 수준의 예외 선언
  Configuration_Error : exception;
  -- ...
end Application;

with Application; use Application;
function get_configuration_value (file_name : String) return Integer is
  -- ...
begin
  -- ... 설정 파일(file_name)을 열고 값을 읽는 코드 ...
exception
  when Ada.IO_Exceptions.Name_Error =>
    -- 저수준 I/O 예외를 고수준 응용 프로그램 예외로 변환
    raise Configuration_Error with "설정 파일을 찾을 수 없습니다: " & file_name;
end get_configuration_value;

위 예시에서 get_configuration_value 함수를 호출하는 코드는 내부적인 파일 I/O 예외 대신 Configuration_Error 예외를 처리합니다.

12.3.5 예외를 이용한 재시도 로직 (retry logic)

Ada의 종료 모델에서는 예외가 발생한 지점으로 제어가 돌아가지 않습니다. 그러나 loop 문과 예외 처리기를 결합하여 실패한 작업을 재시도(retry)하는 로직을 구현할 수 있습니다. 이러한 로직은 네트워크 연결이나 사용자 입력과 같이 일시적인 실패가 발생할 수 있는 작업에 적용될 수 있습니다.

구조

재시도 로직은 다음과 같은 구조를 가집니다.

  1. 실패 가능성이 있는 작업을 loop 문 내부에 둡니다.
  2. 작업이 성공하면 exit 문을 통해 루프를 종료합니다.
  3. 작업이 실패하여 예외가 발생하면, exception 처리기가 제어권을 얻습니다.
  4. 처리기는 재시도 횟수를 확인하고, 로그를 기록하는 등의 작업을 수행한 후 루프의 다음 반복으로 진행합니다.
  5. 정의된 재시도 횟수를 초과하면, 처리기는 raise 문을 통해 예외를 다시 발생시키거나 새로운 예외를 발생시켜 작업이 최종적으로 실패했음을 알립니다.

예시

다음 예시는 사용자에게 숫자 입력을 최대 3번까지 요청합니다. 유효하지 않은 형식의 문자가 입력되면 Constraint_Error가 발생하고, 예외 처리기는 재시도를 수행합니다.

with Ada.Text_IO, Ada.Integer_Text_IO, Ada.Exceptions, Ada.IO_Exceptions;

procedure retry_example is
  max_attempts : constant := 3;
  attempts     : Natural  := 0;
  user_value   : Integer;
begin
  loop
    begin
      -- 1. 실패 가능한 작업 시도 (사용자 입력)
      Ada.Text_IO.put ("1에서 100 사이의 숫자를 입력하세요: ");
      Ada.Integer_Text_IO.get (user_value);

      -- 2. 작업 성공 시 루프 탈출
      if user_value in 1 .. 100 then
        Ada.Text_IO.put_line ("입력된 값: " & Integer'image (user_value));
        exit;
      else
        raise Constraint_Error with "값이 범위를 벗어났습니다.";
      end if;

    exception
      when Ada.IO_Exceptions.End_Error =>
        Ada.Text_IO.new_line;
        Ada.Text_IO.put_line ("입력이 종료되었습니다.");
        -- 루프를 빠져나가기 위해 예외를 다시 발생시킴
        raise;

      when e : Constraint_Error =>
        -- 3. 예외 포착 및 재시도 횟수 증가
        attempts := attempts + 1;
        Ada.Text_IO.put_line ("잘못된 입력입니다. 다시 시도하세요.");
        Ada.Text_IO.put_line (Ada.Exceptions.exception_message (e));

        -- 4. 최대 재시도 횟수 확인
        if attempts >= max_attempts then
          Ada.Text_IO.put_line ("최대 시도 횟수를 초과했습니다.");
          -- 5. 최종 실패 처리
          raise;
        end if;
    end;
  end loop;

exception
  when Constraint_Error | Ada.IO_Exceptions.End_Error =>
    Ada.Text_IO.put_line ("프로그램을 종료합니다.");

end retry_example;

12.3.6 예외 처리부의 중첩

Ada에서는 문법적으로 예외 처리부(exception 구역) 안에 또 다른 begin ... end 블록을 중첩하여 배치할 수 있습니다.

begin
  -- ... 외부 블록의 실행문 ...
exception
  when others =>
    -- 외부 처리기
    begin
      -- ... 내부 블록의 실행문 ...
    exception
      when others =>
        -- 내부 처리기
        null;
    end;
end;

이러한 구조는 다음과 같은 특성을 가집니다.

  • 실행 흐름: 중첩된 구조는 코드의 실행 흐름과 예외 전파 경로를 복잡하게 할 수 있습니다.
  • 가독성: 코드 블록의 중첩은 가독성과 유지보수성에 영향을 줄 수 있습니다.
  • 대안 구조: 처리기 내부의 로직을 별도의 서브프로그램으로 분리하여 호출하는 구조 또한 가능합니다.

이러한 중첩 구조는 문법적으로 허용됩니다. 실제 설계에서는 예외 처리기의 로직을 단순하게 유지하거나 복잡한 작업을 서브프로그램으로 분리하는 방법을 사용하기도 합니다.

12.4 반환값 생성을 위한 예외 처리 (Ada 2022)

12.4.1 확장 return 문의 구문과 기능

Ada 2022 표준에 도입된 확장 return 문(extended return statement)은 함수가 반환할 객체의 선언과 초기화를 하나의 구문 블록 내에서 처리하는 기능입니다. 이 구문은 반환 객체의 초기화 과정을 하나의 블록으로 묶어주며, 이 블록 안에는 예외 처리부를 선택적으로 포함할 수 있습니다.

구문

확장 return 문의 일반적인 형태는 다음과 같습니다. exception 처리부는 선택적으로 사용할 수 있습니다.

return variable_name : Type_Name do
   -- 반환 객체 초기화 코드
   ...
exception
   -- 초기화 중 발생한 예외 처리 코드
   ...
end return;
  • return variable_name : Type_Name do: 함수가 반환할 타입(Type_Name)의 변수(variable_name)를 선언합니다. 이 변수의 유효 범위는 do ... end return 블록 내부로 한정됩니다.
  • do ... end return: 선언된 반환 변수의 최종 값을 결정하는 일련의 문장(statements)을 포함하는 블록입니다.
  • exception (선택 사항): do 블록 내의 코드가 실행되는 동안 예외가 발생하면, 제어가 exception 처리부로 이전됩니다. 예외 처리부에서는 발생한 예외에 따라 반환 변수의 값을 수정하는 등의 후속 조치를 수행할 수 있습니다.
  • end return: 이 지점에서 반환 변수의 값이 확정되어 함수 호출자에게 반환됩니다.

기능

이 구문은 반환 객체의 초기화가 여러 문장(statement)으로 구성되는 경우에 사용되며, 해당 초기화 과정에서 발생하는 예외를 처리하는 기능을 제공합니다. 확장 return 문의 주된 기능은 다음과 같습니다.

  1. 반환 객체 초기화 중 예외의 지역적 처리: do 블록 내에 exception 처리부를 두어, 반환 객체를 초기화하는 과정에서 발생하는 예외를 해당 블록 내에서 처리하도록 허용합니다. 이를 통해 예외가 함수 외부로 전파되기 전에 반환 객체의 상태를 사전에 정의된 값으로 설정할 수 있습니다.

  2. 반환값의 확정: do 블록 내에서 예외가 발생하지 않거나, 발생한 예외가 내부 exception 처리부에서 처리되는 경우, 함수는 end return 시점에 확정된 객체를 반환합니다. exception 처리부를 통해 반환 객체의 상태를 제어할 수 있으므로, 함수가 초기화가 완료되지 않은 상태의 값을 반환하는 상황을 방지할 수 있습니다.

구문 예시

다음 코드는 확장 return 문의 기본적인 사용법을 보여주는 사례입니다. 함수는 Status_Code라는 이름의 타입을 반환하며, return 문 안에서 code라는 변수를 선언하고 초기화합니다.

with Ada.Text_IO; use Ada.Text_IO;

procedure test_extended_return is

  -- 함수 반환 타입을 정의합니다.
  type Status_Code is range 100 .. 599;
  default_error_code : constant Status_Code := 500;

  -- Status_Code 타입의 값을 생성하는 함수
  function create_status (value : Integer) return Status_Code is
  begin
    return code : Status_Code do
      code := Status_Code (value); -- 여기서 Constraint_Error가 발생할 수 있습니다.
    exception
      when Constraint_Error =>
        -- 예외 발생 시, 반환될 변수 Code의 값을 안전하게 설정합니다.
        code := default_error_code;
    end return;
  end create_status;

begin
  -- 정상적인 경우
  put_line ("create_status (200)의 결과:" &
            Status_Code'image (create_status (200)));

  -- 예외가 발생하는 경우
  put_line ("create_status (999)의 결과:" &
            Status_Code'image (create_status (999)));
end test_extended_return;

실행 결과:

create_status (200)의 결과: 200
create_status (999)의 결과: 500

12.4.2 사용 사례: 객체 초기화 시 예외 처리

확장 return 문은 여러 필드로 구성된 객체(레코드)를 초기화하는 과정에서 오류가 발생했을 때, 객체를 사전에 정의된 상태로 반환하는 데 사용될 수 있습니다. 이 구문은 객체 초기화 과정과 해당 과정의 예외 처리를 하나의 구문 블록 내에 함께 기술할 수 있습니다.

예시: 설정(Configuration) 객체 생성

다음 예시는 문자열 형태의 설정값을 사용하여 Configuration 객체를 생성하는 함수입니다. 문자열을 정수로 변환하는 과정에서 예외가 발생하면, 함수는 예외를 전파하지 않고 사전에 정의된 기본값을 가진 Configuration 객체를 반환합니다.

with Ada.Text_IO;
use  Ada.Text_IO;
with Ada.Strings.Unbounded;
use  Ada.Strings.Unbounded;

procedure test_config_creation is

  -- 1. 여러 필드를 가진 레코드 타입(객체) 정의
  type Configuration is record
    host    : Unbounded_String;
    port    : Positive;
    timeout : Natural;
  end record;

  -- 초기화 실패 시 반환할 기본 설정값
  function default_config return Configuration is
  begin
    return (host    => to_unbounded_string ("localhost"),
            port    => 8080,
            timeout => 5_000);
  end default_config;

  -- 2. 외부 소스로부터 설정을 읽어와 객체를 생성하는 함수
  function load_config (host_str : String; port_str : String)
  return Configuration is
  begin
    -- 반환될 객체 config를 선언하고 초기화 블록을 시작합니다.
    return config : Configuration do
      -- 각 필드를 순서대로 초기화합니다.
      config.host    := to_unbounded_string (host_str);
      -- 여기서 Constraint_Error 발생 가능
      config.port    := Positive'value (port_str);
      config.timeout := 10_000; -- port 초기화 성공 시 설정되는 값
    exception
      when Constraint_Error =>
        -- port_str이 유효한 숫자가 아닐 경우, 전체 설정을 기본값으로 대체합니다.
        put_line ("(알림: 포트 번호 분석 실패. 기본 설정으로 복구합니다.)");
        config := default_config;
     end return;
  end load_config;

  -- 생성된 설정을 출력하는 프로시저
  procedure print_config (conf : Configuration) is
  begin
    put_line ("Host:" & to_string (conf.host));
    put_line ("Port:" & Positive'image (conf.port));
    put_line ("Timeout:" & Natural'image (conf.timeout));
  end print_config;

  config_a : Configuration;
  config_b : Configuration;

begin
  put_line ("--- 시나리오 1: 정상적인 초기화 ---");
  config_a := load_config (host_str => "example.com", port_str => "443");
  print_config (config_a);
  new_line;

  put_line ("--- 시나리오 2: 초기화 중 예외 발생 ---");
  config_b := load_config (host_str => "test-server", port_str => "bad_port");
  print_config (config_b);
end test_config_creation;

실행 결과

--- 시나리오 1: 정상적인 초기화 ---
Host:example.com
Port: 443
Timeout: 10000

--- 시나리오 2: 초기화 중 예외 발생 ---
(알림: 포트 번호 분석 실패. 기본 설정으로 복구합니다.)
Host:localhost
Port: 8080
Timeout: 5000

분석

  • 시나리오 1: port_str으로 전달된 문자열 "443"은 유효한 숫자이므로 Positive'value 변환이 성공합니다. do 블록이 예외 없이 완료되고, 초기화된 config 객체가 반환됩니다.
  • 시나리오 2: port_str으로 "bad_port"가 전달되어 Positive'value 변환 시 Constraint_Error가 발생합니다. 제어 흐름은 exception 처리부로 이동하며, 반환될 config 객체의 필드들이 default_config 함수의 값으로 덮어쓰기 됩니다.

이처럼 확장 return 문은 객체 생성 로직과 실패 시의 복구 로직을 하나의 구문 블록 안에 함께 배치할 수 있는 구조를 제공합니다.

12.5 예외 전파

예외가 발생했을 때 현재 실행 중인 블록에 해당 예외를 처리할 수 있는 when 절이 없으면, 예외는 소멸되지 않고 자신을 호출한 코드로 전파됩니다. 이처럼 예외가 호출 스택을 거슬러 올라가는 과정을 예외 전파(exception propagation)라고 합니다.

전파 과정

  1. 서브프로그램이나 블록문(declare ... begin ... end) 실행 중 예외가 발생합니다.
  2. 해당 블록의 정상적인 실행은 즉시 중단됩니다.
  3. Ada 런타임은 해당 블록의 exception 처리 구역에서 발생한 예외와 일치하는 when 절을 찾습니다.
  4. 만약 일치하는 when 절이 없으면 (또는 exception 구역 자체가 없으면), 예외는 전파됩니다.
  5. 전파된다는 것은, 현재 블록을 호출했던 지점에서 동일한 예외가 다시 발생하는 것을 의미합니다. 이 과정은 적절한 예외 처리기를 만나거나, 태스크의 최상위에 도달할 때까지 반복됩니다.

예시

다음 코드에서 procedure_bConstraint_Error에 대한 처리기가 없습니다. 따라서 예외가 발생하면, procedure_b를 호출한 procedure_a로 예외가 전파되어 그곳의 처리기가 실행됩니다.

with Ada.Text_IO;

procedure main_example is

  -- procedure_b를 미리 선언하여 procedure_a에서 호출 가능하게 함
  procedure procedure_b;

  -- procedure_b를 호출하는 procedure_a의 구현
  procedure procedure_a is
  begin
    Ada.Text_IO.put_line ("procedure_b를 호출합니다...");
    procedure_b;
  exception
    when Constraint_Error =>
      Ada.Text_IO.put_line ("procedure_a에서 예외가 처리되었습니다.");
  end procedure_a;

  -- 예외를 발생시키는 procedure_b의 구현
  procedure procedure_b is
    x : Integer := 0;
    y : Integer;
  begin
    Ada.Text_IO.put_line ("procedure_b 내부에서 예외를 발생시킵니다...");
    y := 1 / x;
    -- 위의 나눗셈에서 예외가 발생하므로 이 줄은 실행되지 않음
  end procedure_b;

begin
  -- 프로그램 실행을 시작하기 위해 procedure_a를 호출
  procedure_a;
end main_example;

전파의 규칙

예외 전파에는 다음과 같은 두 가지 규칙이 적용됩니다.

1. 선언부에서의 예외

서브프로그램이나 블록의 선언부(예: isbegin 사이)에서 예외가 발생하면, 해당 블록의 exception 처리기는 이를 처리하지 못하고 즉시 상위 실행 문맥으로 전파됩니다.

procedure example is
  -- 선언부에서 Constraint_Error 발생
  n : Positive := 0;
begin
  null;
exception
  -- 이 처리기는 선언부의 예외를 처리할 수 없음
  when Constraint_Error =>
    Ada.Text_IO.put_line ("이 메시지는 출력되지 않습니다.");
end example;

2. 태스크에서의 예외

처리되지 않은 예외가 태스크(task)의 최상위 레벨까지 전파되면, 더 이상 다른 곳으로 전파되지 않고 해당 태스크가 종료됩니다. 자세한 내용은 태스크를 다루는 9.1절에서 다시 설명하겠습니다.

12.5.1 예외 전파와 연산 재정의

9.3.3절에서 설명했듯이, 태그드 타입(tagged type)의 파생 타입은 부모 타입으로부터 상속받은 프리미티브 연산(primitive operation)을 재정의(overriding)할 수 있습니다. 예외 전파 메커니즘은 이러한 연산 재정의 상황에서도 적용됩니다.

Ada 언어는 재정의하는 서브프로그램이 발생시킬 수 있는 예외의 종류를 부모 타입의 서브프로그램이 발생시키는 예외로 제한하는 규칙을 가지고 있지 않습니다. 즉, 재정의하는 서브프로그램은 부모 타입의 연산과 다른 종류의 예외를 발생시킬 수 있으며, 새로운 예외를 추가로 발생시킬 수도 있습니다.

예외 전파 관점에서 호출 방식에 따라 동작이 구분됩니다.

  1. 정적 호출 (Non-dispatching call): 컴파일 시점에 호출할 서브프로그램이 결정되는 경우 (예: 특정 타입의 객체를 통해 호출), 예외가 발생하면 해당 서브프로그램의 본체로부터 예외가 전파됩니다. 이는 다른 서브프로그램 호출 시의 전파와 동일합니다.

  2. 동적 디스패치 호출 (Dispatching call): 클래스-범위 타입(T'Class)의 객체를 통해 프리미티브 연산을 호출하는 경우 (9.4.2절 참조), 실제 실행될 서브프로그램 본체는 런타임에 객체의 태그(tag)에 따라 결정됩니다. 이때, 실제로 호출된 (재정의된) 서브프로그램 본체 내에서 예외가 발생하면, 예외는 해당 본체의 실행을 중단하고 디스패치 호출이 이루어진 지점으로 전파됩니다.

예시:

package Shapes is
   type Shape is tagged null record;
   procedure Draw (S : Shape); -- Shape 타입의 Draw 연산

   type Circle is new Shape with record Radius : Float; end record;
   overriding -- Draw 연산 재정의
   procedure Draw (C : Circle);
end Shapes;

package body Shapes is
   procedure Draw (S : Shape) is begin null; end Draw; -- 기본 구현

   My_Error : exception; -- Circle의 Draw에서만 발생시키는 예외

   procedure Draw (C : Circle) is
   begin
      if C.Radius < 0.0 then
         raise My_Error with "반지름은 음수일 수 없습니다."; -- 재정의된 연산에서 예외 발생
      end if;
      -- 원 그리는 로직
   end Draw;
end Shapes;

with Shapes; use Shapes;
procedure Main is
   S : Shape'Class := Circle'(Radius => -1.0); -- Circle 객체, 잘못된 반지름
begin
   Draw (S); -- 디스패치 호출: Circle의 Draw가 실행됨
exception
   when My_Error =>
      -- Circle의 Draw에서 발생한 My_Error가 이곳으로 전파됨
      null;
end Main;

위 예시에서 Draw (S)는 디스패치 호출이며, S의 런타임 태그가 Circle이므로 CircleDraw 프로시저가 실행됩니다. 이 프로시저 내에서 My_Error 예외가 발생하면, 예외는 CircleDraw 본체를 벗어나 Main 프로시저의 Draw (S) 호출 지점으로 전파되어 Main의 예외 처리기에 의해 처리됩니다.

연산 재정의 상황에서도 예외 전파의 기본 규칙은 동일하게 적용됩니다. 예외는 발생한 서브프로그램의 실행을 중단시키고 호출 지점으로 전파되며, 재정의된 서브프로그램으로 인해 전파 방식이 달라지지는 않습니다.

12.6 Ada.Exceptions 패키지

Ada.Exceptions 패키지는 예외 관련 정보를 다루기 위한 타입과 서브프로그램을 제공합니다.

12.6.1 타입 (Types)

Ada.Exceptions 패키지에는 예외를 다루기 위한 타입과 관련 상수가 정의되어 있습니다.

Exception_Id 타입은 각각의 예외를 고유하게 식별하는 타입입니다. NULL_ID 상수는 Exception_Id 타입의 상수로, 어떠한 예외도 나타내지 않습니다. 이 값은 Exception_Id 타입의 기본 초기값으로 사용됩니다.

Exception_Occurrence 타입은 발생한 예외의 특정 인스턴스에 대한 정보를 나타내는 limited private 타입입니다. 예외 처리기에서 when e : others처럼 선언된 상수 e가 이 타입에 해당합니다. NULL_OCCURRENCE 상수는 Exception_Occurrence 타입의 상수로, 어떠한 예외 발생도 나타내지 않습니다. 이 값은 Exception_Occurrence 타입의 기본 초기값으로 사용됩니다.

Exception_Occurrence_Access 타입은 Exception_Occurrence 객체를 가리키는 접근(access) 타입이며 save_occurrence 함수와 함께 사용되어, 예외 발생 정보를 힙(heap)에 저장하거나 다른 태스크로 전달하는 등의 동적 처리를 가능하게 합니다.

12.6.2 예외 정보 조회 (information retrieval)

다음의 함수들은 발생한 예외(Exception_Occurrence)나 예외의 종류(Exception_Id)에 대한 정보를 반환하는 역할을 합니다.

exception_identity (x : Exception_Occurrence) return Exception_Id;
-- 이 함수는 주어진 예외 발생 인스턴스에 대한 예외의 고유 식별자(`Exception_Id`)를 반환합니다.

exception_name (id : Exception_Id) return String;
-- 이 함수는 예외의 고유 식별자인 `Exception_Id`를 매개변수로 받아, 해당 예외의 이름(예: `CONSTRAINT_ERROR`)을 반환합니다. `'identity` 속성으로 얻은 ID 값을 사용할 때 유용합니다.

exception_name (x : Exception_Occurrence) return String;
-- 이 함수는 실제로 발생한 예외의 인스턴스인 `Exception_Occurrence`를 매개변수로 받아, 해당 예외의 이름을 반환합니다. (내부적으로 `exception_name (exception_identity (x))`를 호출하는 것과 동일합니다.) 예외 처리기에서 `when e : others =>`와 같이 예외 발생 정보를 받았을 때 사용합니다.

exception_message (x : Exception_Occurrence) return String;
-- 예외 발생 시 연관된 메시지를 반환합니다. `raise with "메시지";`와 같이 발생한 예외의 경우 해당 메시지를 반환합니다.

exception_information (x : Exception_Occurrence) return String;
-- 구현에 따라 정의된, 디버깅에 유용한 상세 정보를 문자열로 반환합니다.

wide_exception_name (id : Exception_Id) return Wide_String;
-- `exception_name`과 유사하지만, 예외의 전체 확장 이름을 `Wide_String` 타입으로 반환합니다.

wide_exception_name (x : Exception_Occurrence) return Wide_String;
-- 주어진 예외 발생(x)에 대한 예외 이름을 `Wide_String` 타입으로 반환합니다.

wide_wide_exception_name (id : Exception_Id) return Wide_Wide_String;
-- 예외의 전체 확장 이름을 `Wide_Wide_String` 타입으로 반환합니다.

wide_wide_exception_name (x : Exception_Occurrence) return Wide_Wide_String;
-- 주어진 예외 발생(x)에 대한 예외 이름을 `Wide_Wide_String` 타입으로 반환합니다.
-- 예외 정보 조회 예시
with Ada.Text_IO, Ada.Exceptions;
procedure information_example is
  use Ada.Exceptions;
  My_Error : exception;
begin
  -- 예외와 함께 메시지 발생
  raise My_Error with "데이터 처리 중 오류 발생";
exception
  when e : others =>
    Ada.Text_IO.put_line ("예외가 포착되었습니다.");
    Ada.Text_IO.put_line ("예외 이름: " & exception_name (e));
    Ada.Text_IO.put_line ("예외 메시지: " & exception_message (e));
    Ada.Text_IO.put_line ("구현 정의 정보: " & exception_information (e));
end information_example;

12.6.3 예외 발생 및 전파 (Raising and Propagation)

다음의 프로시저들은 예외를 능동적으로 발생시켜 프로그램의 정상적인 실행 흐름을 중단시키는 역할을 합니다.

raise_exception (e       : in Exception_Id;
                 message : in String := "");
-- `Exception_Id`로 식별되는 예외를 주어진 메시지와 함께 발생시키는 프로시저입니다.

reraise_occurrence (x : in Exception_Occurrence);
-- 주어진 `Exception_Occurrence`를 다시 발생시킵니다. 이는 예외 처리기 내에서만 사용 가능한 `raise;` 문과 달리, 예외 처리기 외부에서도 원래의 예외 발생 정보를 그대로 전파할 수 있게 합니다.
-- 예외 발생 및 전파 예시:
-- 하위 루틴에서 발생한 예외를 상위 루틴으로 전달하고 메시지를 출력
with Ada.Text_IO, Ada.Exceptions;

procedure propagation_example is
  use Ada.Exceptions;

  procedure inner_routine is
    My_Custom_Error : exception;
  begin
    -- `raise_exception`을 사용하여 사용자 정의 예외와 메시지를 함께 발생시킴
    raise_exception (My_Custom_Error'identity, "입력값이 유효하지 않습니다.");
  exception
    when e : others =>
      Ada.Text_IO.put_line ("내부(하위) 루틴에서 예외 포착: " &
                            exception_name (e));
      -- 포착된 예외를 다시 발생시켜 외부(상위) 루틴으로 전달
      reraise_occurrence (e);
  end inner_routine;

begin
  begin
    inner_routine;
  exception
    when e : others =>
      Ada.Text_IO.put_line ("외부(상위) 루틴에서 예외 재포착: " &
                            exception_name (e));
      Ada.Text_IO.put_line ("메시지: " & exception_message (e));
  end;
end propagation_example;

12.6.4 예외 발생 정보 관리 (Occurrence Management)

다음의 서브프로그램들은 Exception_Occurrence 객체 자체를 복사, 저장, 또는 스트림을 통해 전송하는 등 관리하는 역할을 합니다.

save_occurrence (target : out Exception_Occurrence;
                 source : in  Exception_Occurrence);
-- `source` 예외 발생 인스턴스를 `target` 으로 복사합니다.

save_occurrence (source : Exception_Occurrence)
return Exception_Occurrence_Access;
-- 새로운 `Exception_Occurrence` 객체를 동적으로 할당하고 `source`의 내용을 복사한 후, 해당 객체를 가리키는 `Exception_Occurrence_Access` 타입의 값을 반환합니다.

write_exception_occurrence (
  stream : not null access Ada.Streams.Root_Stream_Type'class;
  item   : in Exception_Occurrence);
-- 예외 발생 인스턴스에 대한 표현을 스트림에 기록합니다.

read_exception_occurrence (
  stream : not null access Ada.Streams.Root_Stream_Type'class;
  item   : out Exception_Occurrence);
-- 스트림으로부터 예외 발생 인스턴스를 재구성합니다. 이는 다른 파티션(partition)에 기록된 예외도 포함합니다.
-- `save_occurrence`를 활용한 예외 정보 관리 예시
with Ada.Text_IO, Ada.Exceptions, Ada.Integer_Text_IO;
with Ada.IO_Exceptions;

procedure save_example is
  use Ada.Exceptions;

  function get_input return Integer is
    value : Integer;
  begin
    Ada.Text_IO.put ("숫자를 입력하세요 (0 입력 시 예외 발생): ");
    Ada.Integer_Text_IO.get (value);
    return value;
  end get_input;

  -- 예외 정보를 저장할 변수
  err_occ : Exception_Occurrence;

begin
  begin
    declare
      v : constant Integer := get_input;
    begin
      -- 입력값으로 나눗셈 수행 (0 입력 시 예외 발생)
      Ada.Text_IO.put_line ("결과: " & Integer'image (10 / v));
    exception
      -- 내부 블록에서 발생한 모든 예외 처리
      when e : others =>
        Ada.Text_IO.put_line ("예외가 발생했습니다 (내부 블록).");
        save_occurrence (err_occ, e);
    end;
  exception
    -- 입력 스트림 종료 예외 처리
    when e : Ada.IO_Exceptions.End_Error =>
      Ada.Text_IO.put_line ("오류: 입력 스트림이 종료되었습니다.");
      -- 예외 정보를 저장하여 일관된 처리 유지
      save_occurrence (err_occ, e);
    -- 외부 블록에서 발생한 기타 예외 처리
    when e : others =>
      Ada.Text_IO.put_line ("예외가 발생했습니다 (외부 블록).");
      save_occurrence (err_occ, e);
  end;

  Ada.Text_IO.put_line ("저장된 예외 정보:");
  if exception_identity (err_occ) /= NULL_ID then
    Ada.Text_IO.put_line ("예외 이름: " & exception_name (err_occ));
    Ada.Text_IO.put_line ("예외 메시지: " & exception_message (err_occ));
  else
    Ada.Text_IO.put_line ("저장된 예외가 없습니다.");
  end if;

end save_example;

12.6.5 'identity 속성

'identity 속성은 예외의 이름으로부터 시스템이 내부적으로 사용하는 고유 식별값(Exception_Id 타입의 값)을 얻어오는 데 사용됩니다. 이 속성은 프로그래머가 직접 선언한 예외와 Constraint_Error처럼 언어에 미리 정의된 예외 모두에 사용할 수 있습니다.

예를 들어, Ada.Exceptions.raise_exception 프로시저를 통해 특정 예외를 발생시키려면 해당 예외의 Exception_Id 값이 필요합니다. 이때 어떤 예외를 발생시킬지 이름을 알고 있다면 'identity 속성을 사용하고, 예외 처리기 안에서 어떤 예외가 발생했는지 모르는 상황이라면 exception_identity 함수를 사용하여 Exception_Id 값을 얻을 수 있습니다.

예시:

with Ada.Text_IO, Ada.Exceptions;
procedure id_attribute_example is
  use Ada.Exceptions;
  -- 사용자 정의 예외 선언
  Overflow  : exception;
  Underflow : exception;

begin
  -- 'identity 속성을 사용하여 예외 발생시키기
  Ada.Exceptions.raise_exception (Overflow'identity,
                                  "Overflow detected manually.");

exception
  when e : others =>
    Ada.Text_IO.put_line ("An exception was caught.");

    -- exception_identity 함수를 사용하여 발생한 예외의 ID를 얻음
    declare
      the_id : constant Ada.Exceptions.Exception_Id :=
        Ada.Exceptions.exception_identity (e);
    begin
      Ada.Text_IO.put_line ("Message: " & Ada.Exceptions.exception_message (e));

      -- 얻어낸 ID를 사용하여 어떤 예외인지 확인
      if the_id = Overflow'identity then
        Ada.Text_IO.put_line ("The exception was Overflow.");
      elsif the_id = Underflow'identity then
        Ada.Text_IO.put_line ("The exception was Underflow.");
      else
        Ada.Text_IO.put_line ("It was some other exception.");
      end if;
    end;

end id_attribute_example;

12.7 단언(assertion)과 예외

12.7.1 단언 구문: pragma assert

pragma assert는 코드의 특정 지점에서 부울 표현식이 참이어야 함을 명시하는 프라그마입니다. 이는 코드의 특정 상태에 대한 가정을 명시적으로 표현하고, 계약 기반 프로그래밍(Design by Contract)의 한 요소를 구현하는 방법입니다. 만약 실행 시점에 명시된 부울 표현식이 거짓(False)으로 평가되면, 프로그램은 Ada.Assertions.Assertion_Error 예외를 발생시킵니다.

이 프라그마는 선언이나 문장이 위치할 수 있는 모든 곳에 사용할 수 있으며, 다음과 같은 형태로 작성할 수 있습니다.

메시지가 없는 경우

메시지 없이 조건식을 인수로 사용하여 조건이 참인지를 검사합니다.

-- 조건식만 사용
pragma assert (count > 0);

-- `check` 식별자와 조건식만 사용
pragma assert (check => count > 0);

메시지를 포함하는 경우

조건이 거짓일 때 디버깅 시 참고할 수 있는 메시지를 포함할 수 있습니다.

-- 조건식과 메시지를 위치 기반으로 명시
pragma assert (count > 0, "count는 양수여야 합니다.");

-- `message` 식별자 사용
pragma assert (count > 0, message => "count는 양수여야 합니다.");

-- `check` 및 `message` 식별자 사용
pragma assert (check => count > 0, message => "count는 양수여야 합니다.");

예시

다음은 큐(queue)에서 원소를 꺼내는 서브프로그램의 시작 부분에서, 큐가 비어있지 않다는 사전 조건을 단언하는 예시입니다.

procedure dequeue (q : in out Queue_Type;  item : out Element_Type) is
begin
  -- 사전 조건: 큐가 비어있지 않아야 함을 단언
  pragma assert (not is_empty (q),
                 message => "Dequeue attempted on an empty queue.");

  -- 큐에서 원소를 꺼내는 로직
  -- ...
end dequeue;

is_empty (q)가 참인 상태에서 dequeue 프로시저가 호출되면, pragma assert의 부울 표현식은 거짓으로 평가됩니다. 그 결과, 프로그램은 “Dequeue attempted on an empty queue.” 메시지와 함께 Ada.Assertions.Assertion_Error 예외를 발생시킵니다. 이 dequeue 프로시저에는 별도의 예외 처리부가 없으므로, 발생한 예외는 프로시저를 호출한 외부 코드로 전달(전파)되며, 호출한 코드에서 해당 예외를 처리할 수 있습니다.

12.7.2 단언 정책 설정: pragma assertion_policy

런타임에 단언을 검사하는 것은 프로그램의 성능에 영향을 줄 수 있습니다. pragma assertion_policy는 단언의 검사 여부를 제어하는 정책을 설정하는 프라그마입니다. 이를 통해 개발 중에는 단언을 활성화하여 조건 검사를 수행하고, 최종 배포 버전에서는 단언을 비활성화하여 해당 검사를 생략할 수 있습니다.

구문 (syntax)

pragma assertion_policy는 두 가지 기본 형태로 사용됩니다.

1. 모든 단언에 일괄 정책 적용

가장 간단한 형태로, 특정 영역 내의 모든 종류의 단언에 하나의 정책을 일괄적으로 적용합니다.

pragma assertion_policy (<정책 식별자>);

정책 식별자 종류는 다음과 같습니다.

  • check: 실행 시점에 단언을 검사합니다.
  • ignore: 실행 시점에 단언을 검사하지 않습니다.
  • 컴파일러별 추가 정책: 컴파일러 제작사가 별도로 정의한 정책이 사용될 수 있습니다.

예시

-- 이 프라그마 이후부터 현재 선언부가 끝나는 지점까지 모든 단언을 검사하도록 설정
pragma assertion_policy (check);

-- 이 프라그마 이후부터 현재 선언부가 끝나는 지점까지 모든 단언을 무시하도록 설정
pragma assertion_policy (ignore);

2. 특정 단언에 선택 정책 적용

<단언 종류 표식> => <정책 식별자> 쌍을 쉼표(,)로 구분하여, 특정 종류의 단언에만 선택적으로 정책을 설정합니다.

-- 사전 조건(`pre`)은 검사하고, 사후 조건(`post`)은 무시하도록 설정
pragma assertion_policy (
  pre  => check,
  post => ignore
);

단언 종류 표식은 다음과 같습니다.

  • assert: pragma assert로 작성된 단언을 의미합니다.
  • static_predicate, dynamic_predicate: 서브타입에 정의된 정적 또는 동적 술어(predicate)를 의미합니다.
  • pre, pre'class: 서브프로그램의 사전 조건을 의미합니다. ('class는 클래스 전체 타입에 적용되는 사전 조건을 지정합니다.)
  • post, post'class: 서브프로그램의 사후 조건을 의미합니다. ('class는 클래스 전체 타입에 적용되는 사후 조건을 지정합니다.)
  • type_invariant, type_invariant'class: 타입 불변식을 의미합니다. ('class는 클래스 전체 타입에 적용되는 타입 불변식을 지정합니다.)
  • default_initial_condition: 타입의 기본 초기 조건을 의미합니다.
  • 이 외에 컴파일러 구현에 따라 추가적인 종류가 있을 수 있습니다.

적용 범위와 규칙

pragma assertion_policy에 허용되는 위치는 다음과 같습니다:

  • 선언부 (declarative part) 내부
  • 패키지 명세 (package specification) 내부
  • 설정 프라그마 (configuration pragma)

프라그마의 효력은 프라그마가 위치한 지점부터 해당 프라그마가 포함된 가장 안쪽의 선언부 영역(declarative region)이 끝나는 지점까지입니다. 예를 들어, 프로시저의 선언부에 위치했다면 프로시저 내부 전체에 영향을 줍니다. 여러 정책이 중첩된 영역에 적용될 경우, 가장 안쪽에 위치한 정책이 우선적으로 적용됩니다.

예시

다음은 특정 서브프로그램 내에서 사전 조건(pre) 검사를 비활성화하는 예시입니다.

procedure example_procedure (value : Integer) is
  -- 이 프로시저 내에서는 사전 조건 검사를 무시하도록 정책 설정
  pragma assertion_policy (pre => ignore);
begin
  -- ... 프로시저의 로직 ...
end example_procedure;

12.7.3 Ada.Assertions 패키지

Ada.Assertions는 단언(assertion)과 관련된 서비스를 제공하는 표준 라이브러리 패키지입니다. 이 패키지에는 단언 실패 시 발생하는 Assertion_Error 예외가 정의되어 있으며, pragma assert와는 다른 방식으로 단언을 수행하는 assert 프로시저를 제공합니다.

assert 프로시저

assert 프로시저는 코드 내에서 직접 호출하여 조건이 참인지를 검사하는 서브프로그램입니다. pragma assert와 마찬가지로 검사할 부울 조건과 선택적인 메시지 문자열을 인자로 받습니다.

구문 (syntax)

-- 메시지가 없는 경우
Ada.Assertions.assert (check => count > 0);

-- 메시지를 포함하는 경우
Ada.Assertions.assert (
   check   => count > 0,
   message => "count는 반드시 양수여야 합니다."
);

만약 check 매개변수에 전달된 조건이 거짓(False)이면, assert 프로시저는 Assertion_Error 예외를 발생시킵니다.

단언 정책으로부터 독립성

assert 프로시저는 pragma assertion_policy의 영향을 받지 않습니다. pragma assertassertion_policyignore로 설정되면 해당 검사는 컴파일 시 제거될 수 있습니다. 그러나 Ada.Assertions.assert 프로시저는 assertion_policy 설정과 관계없이 항상 실행되어 조건을 검사합니다.

assert 프로시저는 assertion_policy의 영향을 받지 않는 특성으로 인해, 컴파일러 정책에 의해 비활성화되지 않아야 하는 불변 조건이나 계약(contract)을 검증하는 데 사용됩니다.

12.7.4 표준 라이브러리에서의 단언 검사

Ada 표준 라이브러리의 특정 패키지들은 사전조건(pre), 정적 술어(static_predicate), 동적 술어(dynamic_predicate)와 같은 단언(assertion)의 준수 여부를 확인하기 위한 검사를 포함합니다.

각각의 단언 검사는 특정 표준 라이브러리 구성 요소(예: 패키지)와 그 안에 포함된 모든 후손 단위(descendant unit)에 적용됩니다. 후손 단위란 특정 구성 요소 내부에 한 단계 또는 여러 단계에 걸쳐 중첩된 모든 패키지, 서브프로그램 등을 의미합니다.

예를 들어 Ada 코드에서 A 내부에 B가 선언되고, 다시 B 내부에 C가 선언된 경우, BC는 모두 A의 후손 단위입니다.

-- 최상위 구성 요소 A
package A is
  -- ...
end A;

package body A is

  -- B는 A의 후손 단위입니다.
  package B is
    -- ...
  end B;

  package body B is

    -- C 또한 A의 후손 단위입니다.
    package C is
      -- ...
    end C;

    -- ...
  end B;
end A;

따라서 특정 단언 검사가 A에 적용된다면, 그 검사는 A의 모든 후손 단위인 BC 내부의 모든 선언에 효력을 미칩니다. 구체적인 적용 대상은 다음과 같습니다.

  • 후손 단위(BC 모두) 안에 선언된 모든 개체(변수, 타입 등)
  • 이러한 후손 단위(BC)에 속한 제네릭(Generic)을 바탕으로 생성된 인스턴스 내부의 모든 개체

다음은 레퍼런스 매뉴얼에 명시된 언어에 정의된 단언 검사들의 목록입니다. 이러한 검사가 실패할 경우, Assertion_Error 예외가 발생합니다.

  • calendar_assertion_check
    • Calendar 패키지와 관련된 단언을 검사합니다.
  • characters_assertion_check
    • Characters, Wide_Characters, Wide_Wide_Characters 패키지들과 관련된 단언을 검사합니다.
  • containers_assertion_check
    • Containers 패키지와 관련된 단언을 검사합니다.
  • interfaces_assertion_check
    • Interfaces 패키지와 관련된 단언을 검사합니다.
  • io_assertion_check
    • Sequential_IO, Direct_IO, Text_IO, Wide_Text_IO, Wide_Wide_Text_IO, Storage_IO, Streams.Stream_IO, Directories 패키지들과 관련된 단언을 검사합니다.
  • numerics_assertion_check
    • Numerics 패키지와 관련된 단언을 검사합니다.
  • strings_assertion_check
    • Strings 패키지와 관련된 단언을 검사합니다.
  • system_assertion_check
    • System 패키지와 관련된 단언을 검사합니다.

12.8 런타임 검사 억제

이 절에서는 7.2.1항에서 설명한 다양한 런타임 검사들을 pragma suppress를 통해 어떻게 비활성화하는지 알아봅니다.

12.8.1 pragma suppressunsuppress

pragma suppress는 컴파일러에 특정 런타임 검사를 생략하도록 지시하는 프라그마입니다. 반대로 pragma unsuppress는 이전에 부여된 검사 생략을 철회합니다.

구문

pragma suppresspragma unsuppress는 다음과 같은 형태로 사용하며, <검사 이름>에는 억제하거나 복원할 검사의 종류를 지정합니다.

pragma suppress (<검사 이름>);
pragma unsuppress (<검사 이름>);

<검사 이름>에는 index_check와 같이 7.2.1항에서 다룬 개별 런타임 검사 이름을 사용하거나, all_checks라는 식별자를 사용할 수 있습니다. Ada 레퍼런스 매뉴얼에 따르면 all_checks는 단언(assertion) 관련 검사를 제외한 모든 런타임 검사의 합집합을 나타냅니다.

사용 예시

먼저 pragma suppress가 없는 경우의 동작을 살펴보겠습니다. 아래 코드는 배열의 범위를 벗어난 인덱스 11에 접근하므로, index_check가 실패하여 Constraint_Error 예외를 발생시킵니다.

procedure default_behavior is
  my_array : array (1 .. 10) of Integer;
begin
  for i in 1 .. 11 loop
    my_array(i) := i; -- i가 11일 때 예외 발생
  end loop;
end default_behavior;

이제 pragma suppress를 사용하여 이 동작을 변경해 보겠습니다. 다음 예시에서 pragma suppress는 선언부에 위치하며 선언된 블록(declare ... end;) 내에서 검사를 억제합니다. 그래서 더 이상 예외가 발생하지 않고, 대신 오류 상태(erroneous)로 실행이 계속됩니다.

procedure scoped_suppress_example is
  my_array : array (1 .. 10) of Integer;
begin
  -- 이 바깥 블록에서는 index_check가 활성화된 상태입니다.
  declare
    -- 이 내부 블록의 선언부에서 index_check를 억제합니다.
    pragma suppress (index_check);
  begin
    -- 이 루프는 index_check가 억제되어 예외를 발생시키지 않습니다.
    -- 대신, 배열 경계를 넘어 메모리에 접근하는 오류 상태(erroneous)로 실행이 계속됩니다.
    for i in 1 .. 11 loop
      my_array(i) := i;
    end loop;
  end; -- 이 지점에서 `pragma suppress (index_check)`의 효력이 자동으로 사라집니다.

  -- 내부 블록이 종료되어 index_check는 다시 활성화되었습니다.
  -- 따라서 아래 코드는 Constraint_Error 예외를 발생시킵니다.
  my_array(0) := 0;

end scoped_suppress_example;

위 예시는 pragma unsuppress를 직접 사용하지 않더라도, pragma suppress가 블록의 범위를 벗어나면 그 효력이 자동으로 사라져 검사가 다시 활성화됨을 보여줍니다.

배치 규칙 및 적용 범위

pragma suppresspragma unsuppress검사 프라그마(checking pragma)로, 코드 내 특정 위치에만 배치될 수 있으며 그 위치에 따라 효력이 미치는 범위가 결정됩니다.

허용되는 위치는 다음과 같습니다:

  • 선언부 (declarative part) 내부
  • 패키지 명세 (package specification) 내부
  • 설정 프라그마 (configuration pragma)

프라그마의 효력 범위는 프라그마의 위치에 따라 다음과 같이 결정됩니다.

  • 서브프로그램(procedure, function)이나 declare 블록의 선언부에 프라그마가 위치할 경우, 프라그마가 있는 위치부터 해당 서브프로그램이나 블록이 끝나는 end 지점까지 효력이 있습니다.

  • 패키지 명세(.ads 파일) 내부에 프라그마가 위치할 경우, 프라그마가 있는 위치부터 해당 패키지 명세가 끝나는 end 지점까지 효력이 있습니다. 이 효력은 패키지 구현부(.adb 파일)에는 자동으로 적용되지 않습니다.

  • 설정 프라그마로 지정될 경우, 해당 프라그마가 적용되는 전체 컴파일 단위(compilation unit)에 효력을 미칩니다.

프라그마의 효력의 우선순위

프라그마는 중복 사용될 수 있습니다. 코드의 특정 지점에서 어떤 검사가 활성화 상태인지는, 해당 지점을 감싸는 여러 범위들 중 가장 안쪽 범위에 있는 프라그마가 우선합니다.

예를 들어, 외부 범위에서 pragma suppress (index_check)가 있더라도 내부 범위에 pragma unsuppress (index_check)가 있으면, 내부 범위 안에서는 pragma unsuppress (index_check)의 효과가 나타납니다. 또한, 외부 범위의 pragma suppress (index_check)와 내부 범위의 pragma suppress (access_check)는 서로 다른 검사에 대한 프라그마이므로, 내부 범위에서는 두 검사가 모두 억제되는 효과가 나타납니다. 이렇게 내부 범위에서 적용된 프라그마의 효력은 해당 범위가 끝나면 사라지고, 외부 범위의 프라그마 효력만 남게 됩니다.

12.8.2 억제의 효과와 위험성

pragma suppress는 특정 런타임 검사를 생략하도록 컴파일러에 지시하여 프로그램의 실행 효율을 높이는 데 활용될 수 있습니다. Ada 레퍼런스 매뉴얼은 억제된 검사에 대해 컴파일러가 관련 실행 코드를 가능한 한 최소화할 것을 권장합니다.

억제된 검사가 감지해야 할 오류 상황이 발생하면 프로그램은 오류 상태(erroneous)가 됩니다. 이때 컴파일러는 예외를 발생시키지 않고 해당 연산이 미정의된 결과(undefined result)를 반환하도록 할 수 있으며, 이 값은 데이터 손상이나 논리적 오류의 원인이 될 수 있습니다. 단, 컴파일러는 미정의된 결과가 프로그램의 외부 상호작용에 영향을 줄 경우에만 예외를 발생시킬 의무가 있으므로, 외부 영향이 없다면 프로그램은 예외 발생 없이 실행이 계속될 수 있습니다.

pragma suppress는 검사 제거를 보장하지 않습니다. 이는 컴파일러에 검사 생략을 허용하는 것이며, 강제하는 것은 아닙니다. 따라서 이 지시문은 효율성 향상 목적으로만 유용합니다.

12.9 최적화에 따른 예외 처리 방식의 변화

Ada 레퍼런스 매뉴얼에 따르면 프로그램의 외부적으로 관찰 가능한 효과가 동일하게 유지된다면, 즉, 최종 결과가 같다면 컴파일러는 예외 발생 시점이나 검사 코드를 제거하는 등의 최적화를 수행할 수 있습니다.

컴파일러는 언어에 정의된 런타임 검사가 실패하더라도 예외를 항상 발생시킬 필요는 없습니다. 만약 예외를 발생시키지 않아도 프로그램의 최종적인 외부 상호작용에 영향을 주지 않는다면, 컴파일러는 예외를 생략할 수 있습니다. 대신 해당 연산은 미정의된 결과(undefined result)를 반환할 수 있습니다. 컴파일러는 예외 처리 코드뿐만 아니라, 관련 검사 코드와 연산 자체를 제거하여 프로그램의 성능을 향상시킬 수 있습니다.

런타임 검사 실패로 인해 예외가 발생하는 경우, 컴파일러는 외부적으로 관찰되는 효과의 순서를 일부 변경할 수 있습니다. 프로그램의 외부 효과는 특정 코드 블록 내 어딘가에서 예외가 발생했다는 사실만 반영하면 되며, 실제 예외가 발생한 시점보다 더 이르거나 늦게 외부 효과가 나타날 수 있습니다.

이러한 최적화로 인해, 예외 발생 시 이전에 실행 중이던 할당문(assignment)과 같은 연산이 중단될 수 있습니다. 이 경우 객체가 부분적으로만 값이 변경되는 비정상 상태(abnormal)에 놓일 수 있으며, 이후 해당 객체를 사용하면 실행이 오류 상태(erroneous)가 될 수 있습니다.

결론적으로, 이러한 최적화 규칙들은 언어 정의 검사를 실패하는 프로그램에만 영향을 미칩니다. 런타임 검사를 항상 통과하는 프로그램의 동작은 컴파일러의 최적화 여부와 관계없이 일관되게 유지됩니다.

13. 다른 언어와의 인터페이스

13.1 System 패키지와 저수준 표현

Ada 프로그램이 실행되는 하드웨어나 운영체제와 관련된 저수준(low-level) 특성 및 기능에 접근하기 위해, Ada 표준 라이브러리는 System 패키지를 제공합니다. 이 패키지에는 대상 시스템(target system)의 구현 의존적인(implementation-dependent) 상수, 타입, 서브프로그램 등이 정의되어 있습니다.

System 패키지의 내용

System 패키지는 다음과 같은 시스템 관련 정보를 포함합니다.

  • 수치 타입 정보: Min_Int, Max_Int, Max_Digits 등 시스템이 지원하는 정수 및 부동소수점 타입의 범위와 정밀도에 대한 상수.
  • 메모리 관련 정보: Storage_Unit (메모리 주소 지정의 기본 단위 비트 수), Word_Size (단어 크기), Memory_Size (사용 가능한 메모리 크기 추정치).
  • 주소 타입: Address 타입과 관련 연산.
  • 태스크 관련 정보: Max_Priority, Default_Priority 등 태스크 우선순위 관련 상수.

System.Address 타입

System 패키지의 선언 중 하나는 Address 타입입니다. 이 타입은 메모리 주소를 나타내는 데 사용되며, 다음과 같은 특징을 가집니다.

  • Private 타입: Address는 private 타입으로 정의되어 있어, 그 내부 표현 방식은 구현(컴파일러 및 대상 시스템)에 따라 다릅니다.
  • 주소 연산: System.Storage_Elements 패키지는 Address 타입의 값에 오프셋(offset)을 더하거나 빼는 등의 주소 연산을 위한 서브프로그램을 제공합니다. 이는 저수준 메모리 조작을 가능하게 합니다.
  • Null_Address 상수: System 패키지에는 유효한 메모리 위치를 가리키지 않음을 나타내는 Null_Address 상수가 정의되어 있습니다.
  • 'Address 속성: 객체나 서브프로그램의 메모리 주소를 System.Address 타입의 값으로 얻기 위해 'Address 속성을 사용할 수 있습니다.

활용

System 패키지와 Address 타입은 다음과 같은 저수준 프로그래밍 작업에 사용됩니다.

  • 외부 연동: 13.2절에서 다룰 C 언어와의 연동 시, C 포인터(void*, int* 등)를 Ada의 System.Address 타입으로 매핑하여 사용합니다.
  • 메모리 직접 조작: 임베디드 시스템 프로그래밍에서 특정 하드웨어 레지스터나 메모리 영역에 직접 접근할 때 사용됩니다.
  • 표현 절 (Representation Clauses): 타입의 메모리 레이아웃을 명시적으로 제어할 때 주소 관련 정보를 활용합니다.

13.2 C 언어와의 연동 (Interfaces.C)

13.2.1 C 데이터 타입 연동 (Representation Aspects)

C 언어와 호환되는 인터페이스를 구축하려면, 서브프로그램의 호출 규약(13.2.2절)을 일치시키고, 두 언어 간에 교환되는 데이터 타입의 메모리 표현(representation)과 전달 방식을 일치시켜야 합니다.

Ada는 with 키워드를 사용하는 애스펙트(Aspect) 또는 표현 절(Representation Clause)을 통해 타입의 저수준(low-level) 특성을 명시적으로 제어하는 기능을 제공합니다. 기본적인 C 타입과의 매핑은 Interfaces.C 패키지를 통해 이루어집니다.

Interfaces.C 패키지의 기본 타입

Interfaces.C 패키지는 C 언어의 표준 정수 타입 (int, short, long, unsigned char 등), 부동소수점 타입 (float, double), 문자 타입 (char), 포인터 타입 (char_ptr, void_ptr) 등에 대응하는 Ada 타입을 제공합니다. C 코드와 데이터를 교환할 때는 이러한 Ada 타입을 사용해야 합니다.

열거형(Enumeration) 타입 연동

C 언어에서 enum 타입은 int 타입과 호환되는 정수 값으로 처리됩니다. 반면 Ada의 열거형은 정수와 구별되는 타입입니다.

열거형 타입을 C의 enum과 연동해야 할 경우, with Convention => C 애스펙트를 사용하여 해당 타입이 C와 호환되는 정수 표현을 갖도록 지정할 수 있습니다.

-- C의 'enum Status { OK = 0, FAILED = 1, PENDING = 2 };'와 연동
type Status is (OK, FAILED, PENDING)
  with Convention => C;

레코드(Record) 타입 연동 및 전달 방식

C 언어는 구조체(struct)를 함수 인자로 전달할 때 값에 의한 전달(pass-by-value) 방식을 사용합니다 (즉, 구조체 전체가 스택에 복사됨).

Ada에서 레코드 타입을 서브프로그램의 in 모드 파라미터로 전달할 때, 컴파일러는 참조에 의한 전달(pass-by-reference) 방식을 사용할 수 있습니다.

C 함수가 구조체를 값으로 전달받도록 (struct My_Struct s) 선언된 경우, Ada 레코드 타입을 C와 호환되도록 선언하고 C의 값 전달 방식을 따르도록 명시해야 합니다.

with Convention => C_Pass_By_Copy 애스펙트는 해당 레코드 타입의 객체가 파라미터로 전달될 때, C의 struct와 같이 값에 의한 전달(by-copy) 방식을 사용하도록 컴파일러에 지시합니다.

-- C의 'struct Point { double x; double y; };'와 연동
package C_Types is
  type Double is new Interfaces.C.double;

  type Point is record
    X : Double;
    Y : Double;
  end record
    -- 이 레코드가 C의 struct와 같이 값(by-copy)으로 전달되도록 지정
    with Convention => C_Pass_By_Copy;

end C_Types;

...

with C_Types;
-- C 함수: void draw_point(struct Point p);
procedure Draw_Point (P : in C_Types.Point)
  with import => True, Convention => C, External_Name => "draw_point";

이 애스펙트를 사용함으로써, Ada의 Draw_Point 프로시저를 호출할 때 Point 레코드가 C 함수에서 요구하는 방식(값에 의한 전달)으로 전달됩니다.

추가 C 타입 연동: Interfaces.C.Extensions

Ada 표준은 Interfaces.C 패키지의 자식 패키지로 Interfaces.C.Extensions를 정의합니다. 이 패키지는 C 언어 표준의 필수 부분은 아니지만 널리 사용되는 확장 기능이나 타입에 대한 Ada 연동 인터페이스를 제공합니다.

주요 내용 중 하나는 C99 표준 이후 도입된 _Bool 타입 (또는 stdbool.h 헤더의 bool 타입)에 대응하는 bool 타입입니다.

  • Interfaces.C.Extensions.bool: C의 부울 타입과 호환되는 Ada 타입입니다. 이 타입은 True 또는 False 값을 가지며, C의 _Bool (일반적으로 1 또는 0 정수 값)과 동일한 메모리 표현 및 전달 방식을 갖도록 정의됩니다.

예시 (bool 타입 사용):

// C 헤더 (my_lib.h)
#include <stdbool.h>
bool is_feature_enabled(int feature_id);
with Interfaces.C.Extensions; use Interfaces.C.Extensions;
with Interfaces.C; use Interfaces.C;

function Is_Feature_Enabled (Feature_Id : int) return bool
  with import        => True,
       Convention    => C,
       External_Name => "is_feature_enabled";

-- ... Ada 코드에서 호출
Enabled : constant bool := Is_Feature_Enabled (101);

이처럼 Interfaces.C.Extensions.bool을 사용하여 C의 부울 타입과 직접적으로 연동할 수 있습니다.

불완전 구조체 타입 연동 (Incomplete Struct Types)

C 언어에서는 구조체의 구체적인 정의(멤버)를 노출하지 않고 struct My_Opaque_Type; 와 같이 선언만 하여 불완전 타입(incomplete type) 또는 불투명 타입(opaque type)으로 사용합니다. 이러한 타입은 포인터(struct My_Opaque_Type *handle;)를 통해 다루어지며, 실제 구조체의 크기나 멤버는 해당 타입을 정의한 라이브러리 내부에서만 알려집니다.

Ada에서 C의 불완전 구조체 타입에 직접 대응하는 표현은 없습니다. C 인터페이스와 연동하기 위해 다음과 같은 방법을 사용합니다.

  1. 접근 타입을 이용한 불투명 포인터: C의 불완전 구조체 포인터(struct My_Opaque_Type *)를 Ada의 System.Address (13.1절 참조) 또는 Interfaces.C의 포인터 타입 (access ...)으로 매핑하는 방법이 있습니다. 이 경우 Ada 코드는 해당 포인터가 가리키는 실제 구조체의 내용에 직접 접근하지 않고, 포인터(핸들) 값 자체만을 C 라이브러리 함수에 전달하거나 반환받습니다.

    -- C: struct My_Opaque_Type;
    -- C: typedef struct My_Opaque_Type* Handle;
    -- C: Handle create_handle();
    -- C: void process_handle(Handle h);
    -- C: void destroy_handle(Handle h);
    
    with System;
    package My_Lib is
       -- C의 Handle (포인터)를 System.Address로 매핑
       type Handle is new System.Address;
       Null_Handle : constant Handle := System.Null_Address;
    
       function Create_Handle return Handle
         with import => True, Convention => C, External_Name => "create_handle";
    
       procedure Process_Handle (H : Handle)
         with import => True, Convention => C, External_Name => "process_handle";
    
       procedure Destroy_Handle (H : Handle)
         with import => True, Convention => C, External_Name => "destroy_handle";
    end My_Lib;
    
  2. null record 또는 불완전 타입 사용: Ada의 불완전 타입(type My_Opaque_Type;)이나 null record(type My_Opaque_Type is null record;) 선언을 사용하여 C의 불완전 구조체 타입을 형식적으로 표현할 수 있습니다. 그러나 이 방법은 해당 타입의 포인터를 C와 직접 교환하는 기능을 제공하지 않습니다. 이 방식은 C++ 연동 등에서 접근 타입과 함께 사용되거나, Ada 내부에서 타입 계층을 표현하기 위해 사용됩니다. C의 불투명 포인터 핸들을 다루는 연동에는 접근 타입을 사용합니다.

13.2.2 C 서브프로그램 연동 (import, export)

Ada 프로그램과 C 프로그램 간의 서브프로그램(함수) 호출을 구현하기 위해, Ada는 importExport 애스펙트(Aspect)를 제공합니다. 이 애스펙트들은 Convention 애스펙트와 함께 사용되어, Ada 컴파일러에게 해당 서브프로그램이 외부 C 코드와 상호작용함을 명시합니다.

C 함수 호출하기 (import)

Ada 코드에서 외부 C 함수를 호출하기 위해서는, with import => True 애스펙트를 사용하여 해당 C 함수의 Ada 서브프로그램 선언을 가져와야 합니다.

import 애스펙트는 다음 애스펙트와 함께 사용됩니다.

  • Convention => C: Ada 컴파일러에게 C 언어의 표준 호출 규약(calling convention)을 사용하여 파라미터를 전달하도록 지시합니다.
  • External_Name => "...": C 소스 코드에 정의된 실제 함수 이름(링커가 사용할 심볼 이름)을 문자열로 지정합니다.

예시:

C 소스 코드에 int add_integers(int a, int b);라는 함수가 정의되어 있다고 가정합니다.

// C 코드 (math_lib.c)
int add_integers(int a, int b) {
  return a + b;
}

Ada에서는 Interfaces.C 패키지의 C 호환 타입(int)을 사용하여 이 함수를 다음과 같이 선언하고 가져올 수 있습니다.

with Interfaces.C; use Interfaces.C;

procedure Call_C_Function is
  -- C 함수 'add_integers'를 위한 Ada 선언
  function c_add (a : int; b : int) return int
    with import        => True,
         Convention    => C,
         External_Name => "add_integers";

  result : int;
begin
  -- 'c_add' 호출은 실제 C 함수 'add_integers'를 호출합니다.
  result := c_add (10, 20);
end Call_C_Function;

Ada 서브프로그램을 C에서 호출하기 (Export)

반대로, C 코드에서 Ada 서브프로그램을 호출할 수 있도록 내보내기 위해서는 with Export => True 애스펙트를 사용합니다. 이는 Ada 코드를 라이브러리(예: .so, .dll)로 컴파일하여 C 애플리케이션에서 사용할 때 적용됩니다.

예시:

Ada로 작성된 감산 함수를 C에서 ada_subtract라는 이름으로 사용할 수 있도록 내보냅니다.

-- Ada 패키지 명세 (ada_math.ads)
with Interfaces.C; use Interfaces.C;

package Ada_Math is
  function subtract (a : int; b : int) return int
    with Export        => True,
         Convention    => C,
         External_Name => "ada_subtract";
end Ada_Math;
-- Ada 패키지 본체 (ada_math.adb)
package body Ada_Math is
  function subtract (a : int; b : int) return int is
  begin
    return a - b;
  end subtract;
end Ada_Math;

C 코드에서는 extern 키워드를 사용하여 ada_subtract 함수를 선언한 뒤 호출할 수 있습니다.

// C 코드 (main.c)
#include <stdio.h>

// Ada에서 Export한 함수의 프로토타입 선언
extern int ada_subtract(int a, int b);

int main() {
  // Ada 라이브러리 초기화 (필요한 경우)
  adainit();

  int result = ada_subtract(100, 40);
  printf("Result from Ada: %d\n", result); // "Result from Ada: 60"

  // Ada 라이브러리 종료 (필요한 경우)
  adafinal();
  return 0;
}

13.2.3 C 포인터 연동 및 생명주기 ('Unchecked_Access)

C 라이브러리와 연동할 때, C 함수는 Ada의 aliased 키워드나 접근성 규칙(accessibility rules)을 알지 못하는 포인터를 요구할 수 있습니다. 예를 들어, C 라이브러리가 콜백 함수 등록 시 Ada 객체의 주소를 요구하거나, 스택에 할당된 지역 변수의 주소를 C 함수에 전달해야 하는 경우가 있습니다.

이러한 경우, Ada의 안전한 'Access 속성은 컴파일 시점의 생명주기 검사(accessibility checks)로 인해 사용이 제한될 수 있습니다.

'Unchecked_Access 속성은 이러한 Ada의 정적 안전성 검사를 우회하여, aliased로 선언되지 않은 객체를 포함한 모든 객체의 주소에 대한 접근 값을 생성합니다.

'Unchecked_Access의 위험성 예제

'Unchecked_Access를 사용하면 Ada의 안전 장치가 해제되므로, 프로그래머는 C 포인터를 다룰 때와 마찬가지로 객체의 생명주기(lifetime)를 직접 관리해야 합니다.

다음 예제는 'Unchecked_Access가 어떻게 Ada의 생명주기 규칙을 우회하며, 그 결과로 댕글링 참조(dangling reference)가 발생할 수 있는지 보여줍니다. (이는 C 연동 시에도 동일하게 발생할 수 있는 위험입니다.)

declare
  type Int_Access is access all Integer;
  Ptr : Int_Access := null;

  procedure Set_Pointer is
    Local_Var : Integer := 10;
  begin
    -- Local_Var는 'aliased'가 아니며, Set_Pointer의 지역 변수임

    -- 'Unchecked_Access'는 접근성 검사를 우회하여
    -- 곧 소멸될 Local_Var의 주소를 Ptr에 할당
    Ptr := Local_Var'Unchecked_Access;
  end Set_Pointer;

begin
  Set_Pointer;
  -- 이 시점에서 'Set_Pointer' 프로시저는 종료되었고,
  -- 'Local_Var'가 차지하던 스택 메모리는 해제되었습니다.

  -- 'Ptr'은 이제 존재하지 않는 메모리(해제된 스택)를 가리키는
  -- 댕글링 참조가 됩니다.

  -- Ptr.all에 접근하는 것은 오류 상태(erroneous)이며
  -- 미정의 동작(undefined behavior)을 유발할 수 있습니다.
end;

C 라이브러리에 Ada 객체의 접근 값을 전달하기 위해 'Unchecked_Access를 사용할 때, Ada 프로그래머는 C 라이브러리가 해당 접근 값을 사용하는 동안(예: 콜백 호출), 가리키는 Ada 객체가 반드시 유효한 생명주기를 가지도록 보장해야 합니다.

13.2.4 C 헤더 파일 변환 도구 (GNAT)

C 라이브러리와 연동할 때, C 헤더 파일(.h)에 선언된 함수, 타입, 상수 등을 Ada에서 사용할 수 있도록 대응하는 Ada 패키지 명세부(.ads)를 작성해야 합니다. GNAT 컴파일러(GCC)는 이 과정을 위한 도구를 제공합니다.

-fdump-ada-spec 옵션을 사용하면 컴파일러가 C 헤더 파일을 분석하여 Ada 스펙 파일을 자동으로 생성합니다.

사용법:

gcc -c -fdump-ada-spec <헤더파일.h>
  • -c: 링크 없이 분석만 수행합니다.
  • -fdump-ada-spec: Ada 스펙 생성을 지시합니다.
  • <헤더파일.h>: 변환할 C 헤더 파일입니다.

이 명령을 실행하면, 현재 디렉토리에 입력 헤더 파일 이름에 _h를 붙인 .ads 파일이 생성됩니다. 예를 들어, 입력 파일이 my_c_library.h라면 my_c_library_h.ads 파일이 생성됩니다.

예시:

gcc -c -fdump-ada-spec my_c_library.h

(이 명령어는 my_c_library_h.ads 파일을 생성합니다)

주의사항:

  • 자동 생성의 한계: 이 기능은 기본적인 변환을 제공하지만, C 매크로, 전처리기 지시문, 특정 C 확장 기능 등은 변환하지 못할 수 있습니다. 생성된 .ads 파일은 수동으로 검토하고 수정해야 합니다.
  • 의존성: 변환할 헤더 파일이 다른 헤더 파일 (#include)을 참조하는 경우, -I 옵션을 사용하여 해당 헤더 파일이 위치한 디렉토리 경로를 컴파일러에 전달해야 합니다.
    gcc -c -fdump-ada-spec -I/path/to/dependency/headers my_c_library.h
    

이 도구는 C 연동을 위한 Ada 바인딩 작성의 초기 단계를 자동화합니다.

13.3 Fortran, C++ 연동 개요

Ada는 C 언어 외에도 Fortran 및 C++와의 연동 방안을 언어 명세(Annex B)를 통해 제공합니다.

Fortran 연동

Ada 표준은 Interfaces.Fortran 패키지(ARM B.5)를 통해 Fortran과의 상호운용성을 정의합니다. 이 패키지는 Fortran의 기본 타입에 대응하는 Ada 타입 (예: Integer, Real, Double_Precision, Logical)을 제공합니다.

C 연동과 유사하게, with Convention => Fortran 애스펙트를 import 또는 Export 애스펙트와 함께 사용하여 Ada 서브프로그램과 Fortran 서브루틴 간의 호출 규약을 일치시킬 수 있습니다.

C++ 연동

Ada 표준은 Interfaces.CPP라는 별도의 패키지를 정의하지는 않습니다. 대신, with Convention => CPP 애스펙트(ARM B.1)를 정의하여 C++ 호출 규약을 지원합니다.

C++와의 실제 연동은 Interfaces.C 패키지(ARM B.3)의 기능 및 GNAT 컴파일러가 제공하는 Interfaces.CPP와 같은 구현 정의 패키지를 통해 이루어집니다.

C++ 연동 메커니즘은 C 연동의 기능을 기반으로 다음과 같은 C++ 고유의 특성을 다룰 수 있도록 지원합니다.

  • Name Mangling: C++ 컴파일러가 생성하는 심볼 이름(mangled name)은 External_Name 애스펙트를 사용하여 Ada 측에서 명시적으로 지정할 수 있습니다.
  • 클래스 연동: C++ 클래스(class)는 Ada의 태그드 타입(tagged type)에 대응될 수 있습니다. 이를 통해 C++의 생성자(constructor), 소멸자(destructor), 멤버 함수(member function)를 Ada 측에서 호출하는 인터페이스를 정의할 수 있습니다.

14. 동시성 및 실시간 프로그래밍

Ada의 특징 중 하나는 동시성, 즉 병렬 처리를 위한 내장 지원입니다. 외부 라이브러리(예: C의 pthreads)나 플랫폼별 API에 의존하는 많은 언어와 달리, Ada의 동시성 기능은 언어 명세의 필수적인 부분입니다. 이는 이식성, 안전성, 정확성에 영향을 미칩니다. 동시성 Ada 프로그램은 작은 베어-메탈 임베디드 시스템에서부터 멀티코어 서버에 이르기까지 호환되는 컴파일러가 있는 모든 플랫폼으로 이식 가능하며, 동시성 의미론은 일관성이 보장됩니다. 컴파일러는 동시성 구조를 인지하고 있어 라이브러리 기반 접근 방식으로는 불가능한 검사와 최적화를 수행할 수 있습니다.

Ada는 표현력, 성능, 형식적 분석 가능성 사이의 트레이드오프 공간에서 서로 다른 지점에 적합한 다양한 도구를 제공하는 동시성 모델의 스펙트럼을 제공합니다.

14.1 태스킹 모델

Ada에서 동시성의 기본 단위는 task입니다. 태스크는 프로그램의 다른 태스크와 동시에 실행되는 독립적인 제어 스레드입니다. 패키지나 서브프로그램처럼, 태스크는 두 부분으로 정의됩니다: 공개 인터페이스를 정의하는 태스크 명세와 실행 코드를 포함하는 태스크 본체입니다.

태스크는 자동으로 활성화됩니다. 프로그램 실행이 태스크가 선언된 범위에 진입하면, 태스크는 자신을 선언한 코드(“마스터”)와 병렬로 실행을 시작합니다. 범위(예: 서브프로그램 또는 블록)는 그 안에 선언된 모든 태스크가 실행을 완료할 때까지 종료되지 않습니다. 이는 간단하고 견고한 형태의 동기화를 제공합니다.

with Ada.Text_IO;

procedure demonstrate_tasking is
  task T; -- 태스크 명세 (여기에는 공개 인터페이스 없음)

  task body T is -- 태스크 구현
  begin
    for i in 1 .. 5 loop
      Ada.Text_IO.put_line ("Task T is running...");
    end loop;
  end T;

begin -- 주 프로시저 실행 시작
  Ada.Text_IO.put_line ("Main procedure is running.");
  -- 주 프로시저는 이제 태스크 T가 종료될 때까지 여기서 대기함
end demonstrate_tasking;

Ada는 또한 태스크 형식을 지원하여, 단일 템플릿에서 여러 태스크를 선언할 수 있게 합니다. 이는 클래스에서 여러 객체를 생성하는 것과 유사합니다. 예를 들어, 작업자 태스크의 배열을 만드는 데 유용합니다.

예외 처리와 태스크

예외는 태스크 실행에 영향을 미칩니다. 태스크에서의 예외 처리는 서브프로그램의 예외 전파와 구별되는 특징이 있습니다.

  • 태스크 내부 예외 처리: 태스크 본체(task body)는 서브프로그램과 유사하게 exception 블록을 포함할 수 있으며, 이 블록은 태스크 내부에서 발생한 예외를 처리하는 데 사용됩니다.
  • 처리되지 않은 예외의 전파: 처리되지 않은 예외가 태스크 본체의 최상위 레벨까지 전파될 경우, 해당 태스크는 종료되며 더 이상 다른 실행 문맥으로 예외가 전파되지 않습니다. 이 예외는 태스크를 호출한 상위 태스크나 다른 태스크로 전파되지 않습니다.
  • 태스크 종료: 태스크가 예외로 인해 종료되면, 해당 태스크에 종속된 태스크들도 함께 종료됩니다.

14.2 동기화 및 통신: 랑데부

Ada에서 태스크 간의 직접적이고 동기적인 통신 및 동기화를 위한 원래 메커니즘은 랑데부(rendezvous)입니다. 이것은 클라이언트-서버 스타일 상호작용을 위한 모델입니다.

랑데부는 두 가지 구조를 기반으로 합니다:

  • entry: 엔트리는 태스크의 명세에 선언되며, 프로시저와 유사하게 공개적으로 호출 가능한 진입점 역할을 합니다. 상호작용을 위한 인터페이스를 정의합니다.

  • accept: accept 문은 태스크의 본체에 위치합니다. 태스크의 실행이 accept 문에 도달하면, 다른 태스크가 해당 entry를 호출할 때까지 대기합니다.

클라이언트 태스크가 엔트리를 호출하고 서버 태스크가 일치하는 accept 문에 있을 때, 두 태스크는 랑데부에서 동기화됩니다. 클라이언트 태스크는 서버 태스크가 accept 문의 do ... end 블록 내의 코드를 실행하는 동안 차단됩니다. 이 블록은 엔트리의 매개변수를 통해 데이터를 교환하는 데 사용될 수 있습니다. 서버가 accept 블록을 완료하면 랑데부가 끝나고, 두 태스크는 독립적으로 실행을 계속합니다.

task Server is
  entry request_data (value : out Integer);
end Server;

task body Server is
  current_value : Integer := 0;
begin
  loop
    current_value := current_value + 1;
    accept request_data (value : out Integer) do
      value := current_value; -- 이것은 랑데부 중에 발생함
    end request_data;
  end loop;
end Server;

-- 클라이언트 태스크에서:
declare
  my_data : Integer;
begin
  Server.request_data (my_data); -- 클라이언트는 엔트리를 호출하고, 랑데부가 완료될 때까지 차단됨
end;

더 복잡한 상황을 처리하기 위해, 태스크는 select 문을 사용하여 여러 엔트리에 대한 호출을 동시에 기다리거나, 특정 기간 내에 호출이 도착하지 않으면 시간 초과(또는 delay)하거나, 즉시 랑데부가 불가능할 경우 대안적인 조치(else)를 취할 수 있습니다.

14.3 공유 데이터를 위한 보호 객체

랑데부는 공유 데이터 접근을 관리하는 일반적인 문제에 비효율적일 수 있지만, Ada 95는 protected 형식을 도입했습니다. 보호 객체(protected object)는 개인 데이터를 캡슐화하고 이에 대한 상호 배타적인 접근을 보장하는 수동적인 데이터 구조입니다. 이는 임계 구역 문제에 대한 직접적이고 효율적인 해결책입니다.

보호 객체는 세 가지 종류의 연산을 제공합니다:

  • 프로시저: 개인 데이터에 대한 배타적인 읽기-쓰기 접근을 제공합니다. 언어 런타임은 주어진 보호 객체의 프로시저를 한 번에 하나의 태스크만 실행할 수 있도록 보장합니다.

  • 함수: 개인 데이터에 대한 공유 읽기 전용 접근을 제공합니다. 여러 태스크가 동일한 보호 객체의 함수를 동시에 실행할 수 있습니다.

  • 엔트리: 태스크 엔트리와 유사하지만, 불리언 베리어(barrier) 조건이 있습니다. 보호 엔트리를 호출하는 태스크는 엔트리의 베리어가 참으로 평가될 때까지 대기열에 추가되고 차단됩니다. 접근은 여전히 상호 배타적입니다.

protected type Shared_Counter is
  procedure increment;
  function get_value return Integer;
private
  value : Integer := 0;
end Shared_Counter;

protected body Shared_Counter is
  procedure increment is
  begin
    value := value + 1; -- 배타적 접근 보장
  end increment;

  function get_value return Integer is
  begin
    return value; -- 공유 읽기 전용 접근
  end get_value;
end Shared_Counter;

보호 객체를 사용하면 프로그래머가 데이터와 연산을 선언하고, 컴파일러와 런타임 시스템이 기본 잠금 메커니즘을 자동으로 처리합니다. 이는 경쟁 조건과 교착 상태를 구조적으로 방지하여, 동시성 프로그래밍을 더 안전하고 간단하게 만듭니다.

14.4 Ravenscar 프로파일

안전이 중요한 하드 실시간 시스템의 경우, Ada 동시성 기능의 전체 집합은 타이밍 동작과 스케줄링 가능성을 형식적으로 분석하기에 너무 복잡할 수 있습니다. 이를 해결하기 위해 Ravenscar 프로파일(Ravenscar Profile)이 개발되어 Ada 2005에서 표준화되었습니다.

Ravenscar 프로파일은 실시간 프로그래밍에 충분히 강력하면서도 형식적 정적 분석에 적합할 만큼 간단한, 선택된 Ada 태스킹 기능의 하위 집합입니다. 그 제한 사항은 일반적으로 다음을 포함합니다:

  • 동적 생성이나 종료가 없는 고정된 수의 태스크.
  • 일반적으로 보호 객체로 제한되는 단순화된 태스크 통신 모델.
  • else 부분이 있는 복잡한 랑데부나 select 문 없음.
  • 단순하고 예측 가능한 스케줄링 정책.

이 프로파일을 준수함으로써, 개발자는 교착 상태의 부재 및 하드 데드라인 충족과 같은 속성을 증명할 수 있는 동시성 시스템을 구축할 수 있습니다. 이는 항공 전자 공학의 DO-178C와 같은 안전 표준에 대해 Ada 소프트웨어를 인증하는 것을 가능하게 합니다.

15. 계약에 의한 설계(DbC)

Ada 2012에서 표준화된 계약에 의한 설계(Design by Contract, DbC)는 프로그래머가 형식적이고 검증 가능한 명세를 소스 코드에 직접 내장할 수 있게 하여 Ada의 신뢰성 기능을 향상시킵니다. 이러한 계약은 실행 가능한 문서 역할을 하며, 고수준 요구사항을 구현에 연결합니다. 컴파일러 스위치(-gnata)로 활성화하면, 이러한 계약은 실행 시간에 검사되어 위반 시 Assert_Failure 예외를 발생시킵니다.

Ada에서 DbC의 핵심 구성 요소는 다음과 같습니다:

  • 전제조건(Preconditions): 서브프로그램이 호출되기 전에 참이어야 하는 조건입니다. 이는 호출자 측의 의무입니다. with pre =>...로 지정됩니다.

    function square_root (x : Float) return Float with pre => x >= 0.0;
    
  • 후조건(Postconditions): 서브프로그램이 성공적으로 완료되었을 때 참이 될 것을 보장하는 조건입니다. 이는 서브프로그램 구현 측의 의무입니다. with post =>...로 지정됩니다. 후조건은 'old 속성으로 매개변수의 초기 값을, 'result 속성으로 함수의 결과를 참조할 수 있습니다.

    procedure increment (value : in out Integer) with post => value = value'old + 1;
    
  • 형식 불변식 및 술어(Type Invariants and Predicates): 주어진 형식의 모든 객체에 대해 항상 참이어야 하는 조건입니다. 술어(with static_predicate => ...)는 해당 형식의 값이 생성되거나 수정될 때마다 검사되는 속성입니다. 불변식(with type_invariant => ...)은 개인 형식의 속성으로, 공개 연산의 경계에서 검사됩니다.

계약은 유용한 도구입니다. 컴파일된 코드와 일치하는 문서를 제공합니다. 테스트와 디버깅을 위한 정확한 기반 역할을 하며, 형식적 검증 도구에 대한 중요한 입력이 됩니다.

16. SPARK 소개

SPARK(“SPADE Ada Kernel”의 합성어)는 고신뢰성 소프트웨어 개발을 위해 특별히 설계된 Ada 언어의 형식적으로 분석 가능한 하위 집합입니다.

SPARK는 두 가지 전략의 조합을 통해 정확성 증명을 가능하게 하는 목표를 달성합니다:

  1. 언어 부분집합화: 일반적인 접근 형식(에일리어싱 방지), 예외 처리(모든 런타임 오류가 없음을 증명하므로), 함수 내 부작용 등 형식적으로 분석하기 어려운 Ada의 기능을 제외합니다.

  2. 강화된 계약: Ada의 DbC 모델을 확장하여 데이터 의존성(어떤 입력이 어떤 출력에 영향을 미치는지), 정보 흐름 정책, 상태 추상화와 같은 속성을 지정할 수 있는 더 상세한 계약을 추가합니다.

GNATprove와 같은 도구 모음을 사용하여 개발자는 형식적 방법을 통해 수학적 증명의 높은 확신도로 SPARK 프로그램이 언어 정의 런타임 오류로부터 자유롭고, 기능적 명세를 준수하며, 중요한 안전 및 보안 속성을 준수함을 증명할 수 있습니다. 이는 테스트(버그의 존재만을 보여줄 수 있음)를 넘어 증명(버그의 부재를 보여줄 수 있음)으로 나아가는 높은 수준의 소프트웨어 보증을 나타냅니다. 강력한 형식 시스템에서부터 런타임 검사, 계약에 의한 설계, 그리고 SPARK를 이용한 형식적 증명에 이르기까지 이 계층적 접근 방식이 Ada 언어 제품군을 고무결성 소프트웨어 구축에 적합하게 만듭니다.

부록: Clair 코딩 스타일 가이드

Clair 코딩 스타일 가이드는 다른 주요 프로그래밍 언어의 일반적인 스타일을 Ada의 특성에 맞게 적용하여 Ada를 위한 현대적인 코딩 스타일을 제공합니다. 이 가이드는 변수와 서브프로그램에는 주로 snake_case를 사용하고, 타입에는 밑줄이 있는 Pascal_Case를 사용합니다. 이를 통해 다양한 개발자에게 직관적이면서도 명확성을 유지하는 스타일을 만듭니다.


들여쓰기: 탭 대신 2칸 공백을 사용합니다.

  • 근거: Ada의 명시적이고 종종 장황한 구문은 깊게 중첩된 코드 블록으로 이어질 수 있습니다. 2칸 들여쓰기는 과도한 가로 공간을 차지하지 않으면서도 명확한 시각적 구조를 제공하여, 줄이 지나치게 길어져 읽기 어려워지는 것을 방지합니다.

예약어 및 애스펙트: snake_case(전체 소문자)를 사용합니다.

  • 근거: 이는 Python, C, C++, Java와 같은 주요 프로그래밍 언어에서 언어 키워드를 소문자로 작성하는 거의 보편적인 관례와 일치합니다. 이 일반적인 관행을 채택하면 가독성이 향상되고 다른 언어에 익숙한 개발자가 코드를 더 직관적으로 이해할 수 있습니다.
  • 예시: package, is, begin, end, if, procedure, with, pre, post

프라그마: 프라그마 이름과 컨벤션 식별자에 snake_case(전체 소문자)를 사용합니다.

  • 근거: 이는 C, C++, Python과 같이 영향력 있는 언어에서 컴파일러 지시문을 소문자로 스타일링하는 지배적인 관례와 일치합니다. 널리 알려진 이 관행을 채택하면 개발자의 친숙도와 가독성이 향상됩니다. 이는 프라그마 자체(예: import)와 표준 컨벤션 식별자(예: c, intrinsic) 모두에 적용됩니다.
  • 예시: pragma import (c, my_c_func, "my_c_func"), pragma convention (c, My_Data_Type)

공백:

  • 서브프로그램 호출 및 선언: 서브프로그램 이름과 여는 괄호 ( 사이에 한 칸 공백을 사용합니다.
    • 근거: 이 규칙의 주된 목적은 서브프로그램의 이름과 파라미터 목록 사이에 명확한 시각적 구분을 만들어 가독성을 향상시키는 것입니다. 이 관례는 정확한 단어 단위 검색을 가능하게 하는 추가적인 이점을 제공합니다. 도구에서 ` subprogram_name `(이름 뒤에 공백)을 검색하여 동일한 접두사를 공유하는 다른 식별자와 일치시키지 않고 모든 호출 위치를 안정적으로 찾을 수 있습니다.
    • 예시 (호출): Clair.Error.get_error_message (errno_code);
    • 예시 (선언): procedure exit_process (status : Integer := EXIT_SUCCESS);
  • 타입 변환: 타입 이름과 여는 괄호 ( 사이에 공백을 사용하지 않습니다.
    • 근거: 프로그래머의 인지 부하를 줄여 코드 해석의 모호성을 없애기 위함입니다. 근접성의 원리에 따라 공백이 없는 Float(x)는 프로그래머에게 하나의 명확한 시각적 단위로 인식되기 때문입니다.
    • 예시: z := Float(x) + y;
  • 배열 인덱싱: 배열 이름과 여는 괄호 ( 사이에 공백을 사용하지 않습니다.
    • 근거: 공백이 필요한 서브프로그램 호출과 시각적으로 구별하기 위함입니다.
    • 예시: all_bids(n), my_matrix(row, col)
  • 범위 연산자 (..): 범위 연산자 양쪽에 한 칸 공백을 사용합니다.
    • 근거: 연산자를 범위의 경계와 시각적으로 분리하여, 특히 부동소수점 또는 고정소수점 리터럴과 함께 사용될 때 혼동을 방지하고 가독성을 향상시키기 위함입니다.
    • 예시 (타입 선언): range 0.0 .. 100.0
    • 예시 (루프): for i in 1 .. 10 loop

변수, 서브프로그램 및 엔트리: snake_case(밑줄이 있는 전체 소문자)를 사용합니다.

  • 근거: 모든 사용자 정의, 실행 가능 또는 데이터 보유 식별자에 대해 일관되고 읽기 쉬운 스타일을 유지하기 위함입니다.
  • 예시 (변수 및 서브프로그램): my_variable, get_pid
  • 예시 (엔트리): get_item, put_message
    protected body Buffer is
      entry get_item (item : out Data) when not is_empty is
        -- ...
      end get_item;
    end Buffer;
    
  • 반환 값 변수: 서브프로그램의 반환 값, 특히 상태 코드(예: 0, -1)를 담는 변수에는 retval을 사용하는 것을 선호합니다.
    • 근거: 이는 result 식별자와의 잠재적 충돌을 피하는 널리 알려진 관례입니다. 특정 데이터를 나타내는 반환 값의 경우, 더 설명적인 이름(예: bytes_written, new_fd)을 사용합니다.
    • 예시: retval := dlfcn_h.dlclose (self.handle);

속성: snake_case(전체 소문자)를 사용합니다.

  • 근거: 함수 및 변수와의 스타일 일관성을 유지하기 위함입니다. 속성은 종종 함수처럼 작동하거나(예: 'image는 값을 반환) 변수와 같은 속성을 나타냅니다(예: 'length). 이 가이드에서는 서브프로그램과 변수 모두 snake_case를 사용하므로, 속성을 동일하게 스타일링하면 이러한 관련 요소들 간의 일관성이 보장됩니다.
  • 예시: errmsg'length, c_path'address

타입, 서브타입, 예외 및 보호 객체: 각 단어의 첫 글자를 대문자로 하고 연속된 단어를 밑줄로 구분하는 Pascal_Case를 사용합니다.

  • 근거: 이 관례는 타입 이름의 가독성을 향상시키고, snake_case 변수 및 서브프로그램과 같은 다른 식별자와 시각적으로 구별합니다.
  • 중복 피하기: 패키지 내에 선언된 타입의 접두사로 패키지 이름을 반복하지 마십시오. 패키지 이름이 제공하는 문맥으로 충분합니다.
    • 근거: File.File_Descriptor와 같은 반복적인 이름은 장황하고 가독성을 떨어뜨립니다. 따라서 이 가이드는 간결한 형태인 File.Descriptor를 권장합니다.
  • 예시: Library_Load_Error, Symbol_Lookup_Error, Descriptor, Flags

루프 및 Goto 레이블: Pascal_Case를 사용합니다.

  • 근거: 이 규칙은 다른 프로그래밍 언어에서 레이블에 PascalCase를 사용하는 일반적인 관례를 적용한 것입니다. 여러 단어로 된 식별자의 가독성을 위해 밑줄을 사용하는 Ada의 관용적인 용법에 맞춰 스타일이 수정되었습니다.
  • 예시 (루프 이름):
    Main_Process_Loop:
      loop
        -- ...
        exit Main_Process_Loop when condition;
      end loop Main_Process_Loop;
    
  • 예시 (Goto 레이블):
    if has_error then
      goto Error_Handler;
    end if;
    -- ...
    <<Error_Handler>>
    log_error (error_code);
    

상수:

  • 컴파일 타임 상수: UPPER_CASE_WITH_UNDERSCORES를 사용합니다. 이 규칙은 표준 라이브러리의 상수를 포함한 모든 정적 상수에 적용됩니다.
    • 근거: 정적이고 고정된 값을 다른 모든 식별자와 명확하게 구별하기 위함입니다.
    • 프로젝트 정의 예시: EXIT_SUCCESS, MAX_BUFFER_SIZE
    • 표준 라이브러리 예시: System.NULL_ADDRESS, Interfaces.C.NUL, Interfaces.C.Strings.NULL_PTR
  • 런타임 상수: snake_case(변수처럼)를 사용합니다.
    • 근거: 서브프로그램 내에서 동적 값(예: 파라미터)으로 초기화되는 상수에 사용됩니다. 이러한 상수는 ‘읽기 전용 변수’로 취급합니다.
    • 예시: final_message : constant String := "Error: " & message;

패키지: Pascal_Case를 사용합니다.

  • 예시: Clair.Process
  • 예외: 두 글자로 이루어진 Dl의 경우 DL로 표기합니다. (예: Clair.DL, Clair.D1처럼 보일 수 있는 Clair.Dl이 아님).

표준 라이브러리 명명:

  • Interfaces.C: Interfaces.C 패키지 및 그 자식 패키지의 타입과 서브프로그램은 C 표준 라이브러리의 명명 규칙과 일치하도록 snake_case를 사용해야 합니다. 이 패키지의 상수는 컴파일 타임 상수에 대한 전역 UPPER_CASE 규칙을 따릅니다.
    • 근거: 프로젝트의 모든 상수가 일관된 모양을 갖도록 하면서 Ada와 C 간의 명확하고 일관된 정신적 매핑을 유지하기 위함입니다.
    • 예시 (타입/서브프로그램): Interfaces.C.int, Interfaces.C.char_array, Interfaces.C.Strings.chars_ptr
    • 예시 (상수): Interfaces.C.NUL, Interfaces.C.Strings.NULL_PTR

  1. Whitaker, William A. “Ada - The Project, The DoD High Order Language Working Group.” ACM SIGPLAN Notices, vol. 28, no. 3, 1993, http://archive.adaic.com/pol-hist/history/holwg-93/holwg-93.htm 

  2. Ada 2022 Reference Manual, Introduction, Design Goals 

  3. Booch, Grady, et al. Object-Oriented Analysis and Design with Applications. 3rd ed., Addison-Wesley, 2007. 

  4. Johnson, Ralph (August 26, 1991). “Designing Reusable Classes” (PDF). www.cse.msu.edu

  5. Stroustrup, Bjarne (February 19, 2007). “Bjarne Stroustrup’s C++ Glossary”. “polymorphism – providing a single interface to entities of different types.” 

  6. ‘정교화’란, 선언된 항목이 런타임에 처음으로 사용 가능하게 되는 과정을 의미합니다. 자세한 내용은 ‘5.4 정교화 (elaboration)’를 참조하십시오.