Ada 프로그래밍 (초안)
머리말
이 책은 기존 프로그래밍 경험이 있는 개발자를 대상으로 하는 Ada 프로그래밍 언어 학습서입니다. 모든 기술적인 내용은 Ada 2022 레퍼런스 매뉴얼을 기반으로 합니다.
Ada는 신뢰성, 유지보수성, 효율성에 중점을 두고 설계되어, 소프트웨어의 오류가 치명적인 결과로 이어질 수 있는 고신뢰성 시스템 개발에 사용되어 왔습니다. 이 책은 Ada의 기본 문법과 주요 특징을 공부하고, 그 외 C/C++ 연동 등의 실용적인 주제를 학습하는 것을 목표로 합니다.
이 책의 모든 예시 소스코드는 부록에 수록된 Clair 코딩 스타일 가이드를 따릅니다. 이 가이드는 타 언어 사용자들이 Ada에 쉽게 적응할 수 있도록, 다른 주요 프로그래밍 언어의 일반적인 코딩 스타일을 Ada의 특성에 맞게 조정한 것입니다.
이 저작물은 크리에이티브 커먼즈 저작자표시-비영리-변경금지 4.0 국제 라이선스에 따라 이용할 수 있습니다.
목차
- 머리말
- 1. Ada 언어 소개
- 2. 개발 환경 구성
- 3. 어휘 요소 (Lexical Elements)
- 4. Ada 형식 시스템
- 5. 제어 흐름과 문장
- 6. 서브프로그램과 패키지를 이용한 구조화
- 7. 예외 처리
- 8. 외부 시스템과의 연동
- 9. 동시성 및 실시간 프로그래밍
- 10. 계약에 의한 설계(DbC)
- 11. SPARK 소개
- 부록: Clair 코딩 스타일 가이드
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 type system)입니다. 이 시스템은 서로 다른 데이터 타입 간의 연산을 허용하지 않습니다. 예를 들어, 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는 블록과 제어 구문의 범위를 명시적인 키워드로 한정합니다. 예를 들어, 서브프로그램이나 패키지는 begin
과 end
로, 조건문은 if
와 end 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 개발 환경을 설정하려면:
- Visual Studio Code를 설치합니다.
- GNAT 툴체인을 설치하고 시스템의
PATH
에 있는지 확인합니다. - 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
키워드, 이는 프로시저의 헤더와 선언부를 분리합니다.is
와begin
사이의 공간은 지역 변수, 형식 또는 중첩된 서브프로그램이 선언될 위치입니다. -
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 Elements)
이 장에서는 Ada 프로그램을 구성하는 가장 기본적인 단위인 ‘어휘 요소’에 대해 배웁니다. 소스 코드를 작성할 때 사용할 수 있는 문자의 종류부터 변수 이름을 짓는 규칙, 숫자나 문자열 같은 값을 표현하는 방법 등을 학습합니다.
3.1 문자 집합 (Character Set)
Ada 프로그램의 소스 코드는 ISO/IEC 10646, 즉 유니코드(Unicode) 문자 집합 전체를 사용하여 작성할 수 있습니다. Ada 구현체(컴파일러)는 UTF-8로 인코딩된 소스 코드를 반드시 지원해야 합니다. 이는 영문자 외에도 한글이나 다른 여러 언어의 문자를 주석, 문자열, 심지어 식별자(변수 이름 등)에도 사용할 수 있음을 의미합니다.
3.2 분리자와 구분자 (Separators and Delimiters)
Ada 코드는 어휘 요소들의 연속으로 이루어집니다. 분리자(Separator)는 이러한 어휘 요소들을 서로 떨어뜨리는 역할을 하며, 공백(space), 탭(tab), 줄바꿈(end of line) 등이 해당됩니다. 구분자(Delimiter)는 그 자체로 의미를 가지는 특별한 문자로, +
, ;
와 같은 단일 문자나 :=
, =>
와 같은 복합 문자 쌍으로 구성됩니다.
3.3 식별자 (Identifiers)
식별자(Identifier)는 변수, 타입, 서브프로그램 등 프로그래머가 정의하는 대상에 부여하는 이름입니다. 식별자는 반드시 글자(letter)로 시작해야 하며, 그 뒤에는 글자, 숫자, 또는 밑줄(_
) 문자가 올 수 있습니다. Ada의 식별자는 대소문자를 구분하지 않으므로 My_Variable
과 my_variable
은 동일한 이름으로 간주됩니다.
3.4 숫자 리터럴 (Numeric Literals)
숫자 리터럴(Numeric Literal)은 소스 코드에 숫자 값을 직접 표현하는 방법입니다. 점(.
)이 없는 정수 리터럴과 점이 있는 실수 리터럴로 나뉩니다. 기본적인 10진수 표기법(123
, 3.14
) 외에도, 밑줄을 사용하여 자릿수를 구분(123_456
)할 수 있으며, 16#FF#
나 2#1011#
과 같이 2진수부터 16진수까지 다양한 진법으로 값을 표현하는 것도 가능합니다.
3.5 문자 리터럴 (Character Literals)
문자 리터럴(Character Literal)은 단일 문자를 표현하며, 작은따옴표('
)로 문자를 감싸서 만듭니다. 예를 들어 'A'
, '*'
, '''
(작은따옴표 문자 자체)와 같이 사용합니다. 문자 리터럴은 문자 타입(Character
, Wide_Character
등)의 값이 됩니다.
3.6 문자열 리터럴 (String Literals)
문자열 리터럴(String Literal)은 0개 이상의 문자열을 표현하며, 큰따옴표("
)로 문자열을 감싸서 만듭니다. 예를 들어 "Hello, Ada!"
와 같이 사용하며, 아무 내용도 없는 ""
는 빈 문자열을 의미합니다. 문자열 내부에 큰따옴표를 포함시키려면 ""
처럼 두 번 연속해서 사용해야 합니다.
3.7 주석 (Comments)
주석(Comment)은 코드를 설명하기 위해 사용되며, 컴파일러는 주석을 무시합니다. 주석은 두 개의 하이픈(--
)으로 시작하여 해당 줄의 끝까지 이어집니다. 주석은 프로그램의 어느 줄에나 나타날 수 있습니다.
3.8 프라그마 (Pragmas)
프라그마(Pragma)는 프로그래머가 컴파일러에게 특정 동작을 지시하기 위해 사용하는 컴파일러 지시문입니다. pragma
라는 키워드로 시작하며, 최적화 수준을 조절하거나(pragma Optimize
), 특정 런타임 검사를 비활성화하는(pragma Suppress
) 등 다양한 목적으로 사용됩니다.
3.9 예약어 (Reserved Words)
예약어(Reserved Word)는 begin
, if
, procedure
처럼 Ada 언어에서 특별한 문법적 의미를 가지도록 미리 예약된 단어들입니다. 예약어는 식별자(변수 이름 등)로 사용할 수 없습니다. Ada의 예약어는 대소문자를 구분하지 않습니다.
4. Ada 형식 시스템
Ada의 형식 시스템(type system)은 언어의 신뢰성과 정확성 목표를 달성하기 위한 중심 기능이자 주요 도구입니다. 이는 문제 영역을 모델링하고, 애플리케이션별 규칙을 코드에 내장하며, 프로그램이 실행되기 전에 컴파일러가 광범위한 논리적 오류를 감지할 수 있도록 하는 메커니즘입니다.
4.1 강력한 형식 지정 원칙
Ada는 강력한 형식 지정(strongly typed) 언어입니다. 이는 컴파일 시간 오류 감지를 높이기 위한 설계상의 선택입니다. 이 시스템은 몇 가지 주요 규칙에 기반합니다:
-
이름 동등성(Name Equivalence): 두 변수는 동일한 형식 이름을 사용하여 선언된 경우에만 동일한 형식입니다. 기본 구조나 표현이 동일하다는 것만으로는 충분하지 않습니다. 예를 들어, 두 개의 정수 형식을 고려해 봅시다:
type Apples is range 0 .. 1_000; type Oranges is range 0 .. 1_000; num_apples : Apples; num_oranges : Oranges;
-
묵시적 변환 없음(No Implicit Conversions): 컴파일러는 다른 형식, 특히 숫자 형식 간에 묵시적인 형식 변환을 수행하지 않습니다. 프로그래머가
Apples
형식의 값을Oranges
형식의 변수에 할당하려면, 명시적으로num_apples := Apples (Num_Oranges);
와 같이 해야 합니다. 이는 프로그래머가 변환을 인지하도록 강제하여 의도를 명확히 하고 미묘한 오류를 방지합니다. 묵시적 변환의 위험성을 보여주는 대표적인 예는 정수 나눗셈입니다. C++나 Java에서float result = 5 / 2;
표현식은2.0
을 산출하는데, 이는 먼저 정수 나눗셈이 수행되고 그 결과(2
)가 묵시적으로 float로 변환되기 때문입니다. Ada에서 이에 상응하는 연산은result := Float (5) / Float (2);
와 같이 명시적인 변환을 요구하며, 이는 부동소수점 나눗셈을 올바르게 수행하여2.5
를 산출합니다.이러한 엄격함은 형식 시스템이 도메인 모델링 도구로서 기능하게 합니다. 서로 다른 물리량(예:
Meters
,Kilograms
,Seconds
)에 대해 별개의 형식을 생성함으로써, 프로그래머는 컴파일러를 사용하여 이러한 양들이 절대 잘못 결합되지 않도록 보장할 수 있으며, 이로써 논리적 및 물리적 모델링 오류의 한 종류를 방지할 수 있습니다.
4.2 스칼라 형식
스칼라 형식(Scalar types)은 숫자나 문자와 같은 단일 값을 나타냅니다. Ada는 사용자 정의 스칼라 형식을 정의하기 위한 일련의 기능을 제공합니다.
정수 형식
Ada는 미리 정의된 Integer
형식을 제공하지만, 범위가 제한된 문제별 정수 형식을 생성할 것을 권장합니다.
type Day_Of_Month is range 1 .. 31;
type Engine_RPM is range 0 .. 7_000;
Day_Of_Month
형식의 변수를 선언하면 값이 지정된 범위로 제한될 뿐만 아니라, Engine_RPM
과 같은 다른 정수 형식과 호환되지 않게 됩니다. 정의된 범위 밖의 값을 할당하려는 시도는 컴파일 시간에 포착되지 않으면 실행 시간에 미리 정의된 Constraint_Error
예외를 발생시킵니다.
표준 라이브러리는 또한 Integer
의 유용한 두 가지 미리 정의된 서브타입을 제공합니다:
subtype Natural is Integer range 0 .. Integer'last;
subtype Positive is Integer range 1 .. Integer'last;
열거 형식
열거 형식(Enumeration types)은 그 값들이 정렬된 식별자 목록으로 지정되는 형식입니다. 상태, 모드 또는 명명된 값들의 집합을 모델링하는 데 유용합니다.
type Traffic_Light_Color is (Red, Amber, Green);
type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
값들은 순서가 있으므로, <
및 >
와 같은 관계 연산자가 정의됩니다(예: Red < Amber
는 참입니다).
실수 형식 (부동소수점 및 고정소수점)
실수를 포함하는 계산을 위해 Ada는 두 종류의 형식을 제공합니다:
-
부동소수점 형식(Floating-Point Types): 상대 오차가 중요한 근사 계산에 사용됩니다. 프로그래머는 필요한 십진수 정밀도 자릿수를 지정합니다.
type Percentage is digits 7 range 0.0 .. 100.0; type Mass is digits 15; -- 범위 제약이 없는 부동소수점 형식
-
고정소수점 형식(Fixed-Point Types): 금융 계산과 같이 절대 오차가 중요한 근사 계산에 사용됩니다. 프로그래머는 절대 오차 한계인 델타(delta)를 지정합니다.
type Dollars is delta 0.01 range 0.0 .. 1_000_000.00;
이는
Dollars
형식의 값이 0.01보다 크지 않은 오차로 표현됨을 보장합니다.
서브타입
서브타입(Subtype)은 새로운 형식을 생성하지 않습니다. 대신, 기존 기본 형식에 선택적 제약을 제공합니다. 서브타입은 그 기본 형식 및 동일한 기본 형식의 다른 서브타입과 호환됩니다.
subtype Work_Day is Day_Of_Week range Monday .. Friday;
today : Day_Of_Week;
pay_day : Work_Day;
...
today := pay_day; -- 항상 합법적임
-- 합법적이지만, Today가 Saturday나 Sunday일 경우 실행 시간에 Constraint_Error를 발생시킴
pay_day := today;
새로운 type
을 생성할지 subtype
을 생성할지 선택하는 것은 기본적인 설계 결정입니다. 새로운 형식을 생성하면 엄격한 분리를 강제하고 개념의 우발적인 혼합(예: Celsius
와 Fahrenheit
)을 방지합니다. 서브타입을 생성하면 제약을 강제하면서 호환성을 허용하므로, 단일 개념을 정제하는 데 유용합니다(예: Work_Day
는 Day_Of_Week
의 부분집합).
4.3 복합 형식
복합 형식(Composite types)은 다른 형식들의 컬렉션을 나타냅니다.
배열
배열(Arrays)은 모두 동일한 형식의 구성 요소들의 컬렉션이며, 이산 형식(정수 또는 열거 형식)으로 인덱싱됩니다. Ada는 경계가 컴파일 시간에 고정되는 제한된 배열(constrained arrays)과 경계가 실행 시간에 결정될 수 있는 비제한 배열(unconstrained arrays)을 구분합니다.
-
제한된 배열:
type Vector is array (1 .. 10) of Float; my_vector : Vector; -- 경계가 1 .. 10으로 고정됨
-
비제한 배열: 경계는
<>
(“box”)로 표시됩니다.type Matrix is array (Integer range <>, Integer range <>) of Float; -- 객체가 선언될 때 경계가 지정됨 m1 : Matrix (1 .. 10, 1 .. 20); m2 : Matrix (0 .. 4, 0 .. 4);
미리 정의된 String
형식은 비제한 배열입니다: type String is array (Positive range <>) of Character;
.
레코드
레코드(Records)는 다른 형식일 수 있는 명명된 구성 요소들의 컬렉션입니다. C의 struct
와 유사합니다.
type Date is record
year : Integer range 1900 .. 2100;
month : Integer range 1 .. 12;
day : Day_Of_Month; -- 이전에 정의된 형식을 사용
end record;
type Person is record
name : String (1 .. 30);
birthdate : Date;
end record;
4.4 접근 형식 (“포인터”)
Ada의 접근 형식(Access types)은 다른 언어의 포인터나 참조에 해당하지만, 안전성을 강조하여 설계되었습니다.
-
강력한 형식 지정: 접근 형식은 지정할 수 있는 특정 형식에 “묶여” 있습니다. 형식 없는(
void*
) 포인터가 없어, 일반적인 형식 오류의 원인을 방지합니다. -
포인터 연산 없음: 접근 값에 대해 산술 연산을 수행하는 것은 불법입니다. 이 제한은 메모리 손상 버그와 보안 취약점의 한 종류를 제거합니다.
-
선언 및 사용:
access
형식은access
키워드로 선언됩니다.new
할당자는 힙에 객체를 생성하고 해당 객체를 지정하는 접근 값을 반환하는 데 사용됩니다..all
접미사는 접근 값을 역참조하여 지정된 객체에 접근하는 데 사용됩니다.
procedure main is
-- Integer 객체만 가리킬 수 있는 접근 형식 선언
type Integer_Access is access Integer;
-- 특정 객체 및 접근 형식으로 제네릭 패키지를 인스턴스화하여
-- 할당 해제를 위한 프로시저 선언.
procedure free is new Ada.Unchecked_Deallocation (
object => Integer,
name => Integer_Access
);
-- 기본적으로 null로 초기화되는 접근 변수 선언
p : Integer_Access;
begin
-- 힙에 값 42를 가진 새로운 Integer 객체를 할당하고,
-- p가 그 객체를 가리키게 함.
p := new Integer'(42);
-- .all을 사용하여 지정된 객체의 값에 접근
Ada.Text_IO.put_line (Integer'image (p.all)); -- " 42"를 출력
p.all := p.all + 1; -- 지정된 객체는 이제 43
free (p);
end main;
이러한 규칙을 강제함으로써, Ada의 접근 형식은 전통적인 포인터와 관련된 위험 없이 동적 데이터 구조의 강력한 기능을 제공합니다.
5. 제어 흐름과 문장
Ada의 제어 흐름 구조는 명확성, 명시성, 그리고 흔한 구조적 프로그래밍 오류 방지에 중점을 두고 설계되었습니다. 구문은 C 계열 언어들보다 더 장황한데, 이는 모호성을 제거하고 장기적인 유지보수성을 향상시키기 위한 선택입니다.
5.1 조건문
if
문
Ada는 완전히 블록화된 if ... then ... elsif ... else ... end if
구조를 사용합니다. if
나 elsif
키워드 뒤의 조건은 반드시 사전 정의된 Boolean
타입(True
또는 False
)으로 평가되어야 합니다.
이 구조의 특징은 의무적인 end if;
종료자입니다. 이 구문적 요구사항은 C나 C++처럼 블록 구분자가 선택적인 언어에서 발생할 수 있는 “매달린 else
(dangling else
)” 모호성을 제거합니다. Ada에서는 else
절이 어떤 if
에 속하는지 불분명한 상황을 만드는 것이 구문적으로 불가능합니다. 이는 언어 구문 자체를 사용하여 특정 종류의 버그를 방지하는 Ada의 설계 철학을 보여주는 예입니다.
-- 완전한 if-elsif-else 구조의 예
if temperature > HIGH_TEMP_THRESHOLD then
activate_emergency_coolant;
elsif temperature > NORMAL_TEMP_LIMIT then
increase_fan_speed;
else
-- 조치 필요 없음, 온도가 정상 작동 범위 내에 있음
null;
end if;
case
문
case
문은 이산 타입(즉, 모든 정수 또는 열거형 타입)의 단일 표현식 값에 기반한 다방향 분기를 위한 명확하고 안전한 메커니즘을 제공합니다.
type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
today : Day_Of_Week := Wednesday;
...
case today is
when Monday .. Friday =>
schedule_work_meeting;
when Saturday | Sunday =>
plan_weekend_activity;
end case;
case
문은 두 가지 안전 기능을 가집니다.
-
“Fall-through” 없음: 실행이 한
case
에서 다음case
로 넘어가지 않습니다. 각when
분기는 독립적이므로,break
문을 잊어버려 의도치 않은 실행을 유발하는 C 계열 언어의 흔한 버그 원인을 제거합니다. -
컴파일 타임 커버리지 검사: Ada 컴파일러는
case
표현식 타입의 모든 가능한 값이when
절에 의해 처리되는지 검증합니다. 만약 어떤 값이 누락되면, 모든 나머지 가능성을 처리하는when others
절이 제공되지 않는 한 컴파일러는 오류를 발생시킵니다. 이 기능은 유지보수 중에 프로그래머에게 도움이 됩니다. 프로그래머가 열거형 타입에 새 값을 추가하여 수정하면(예: 상태 기계에 새 상태 추가), 컴파일러는 해당 타입을 사용하는 코드베이스의 모든case
문이 불완전하다고 자동으로 표시해 줍니다. 이는 유지보수의 한 측면을 자동화하여, 어떤 상태도 처리되지 않고 남겨지지 않도록 보장합니다.
-- 이 case 문은 만약 Day_Of_Week에 처리되지 않은 다른 값이 포함되어 있고
-- 'when others'가 추가되지 않았다면 컴파일 타임 오류를 유발할 것임
case Today is
when Monday =>
...
when Tuesday =>
...
-- 등등 모든 7일에 대해
end case;
-- 'when others'를 사용한 유효한 대안
case User_Input is
when 'Y' | 'y' =>
confirm_action;
when 'N' | 'n' =>
cancel_action;
when others =>
report_invalid_input;
end case;
5.2 반복문 (루프)
일반적으로 루프라고 알려진 반복문은 일련의 문장들을 반복적으로 실행하기 위한 메커니즘을 제공합니다. Ada는 단순 반복부터 복잡한 병렬 처리에 이르기까지, 각기 다른 반복 요구사항에 적합한 여러 형태의 루프를 제공합니다. 이 구문들은 프로그램의 실행 흐름을 제어하는 데 근본적인 역할을 합니다.
5.2.1 기본 루프
기본적인 루프 구조는 기본 loop
입니다. 이는 무한히 반복되는 일련의 문장들을 정의합니다. 무한 루프를 방지하기 위해서는, 반복을 종료하고 루프 다음의 문장으로 제어를 이전시키는 exit
문이 필요합니다.
구문:
loop
-- 일련의 문장들
exit when condition;
-- 추가 문장들
end loop;
exit when
문은 매 반복마다 조건을 평가합니다. 만약 조건이 True
이면, 루프는 종료됩니다.
예제: 다음 코드는 별표(*
)가 나타날 때까지 문자를 읽습니다.
with Ada.Text_IO;
procedure read_until_stop is
use Ada.Text_IO;
current_character : Character;
begin
loop
get (current_character);
exit when current_character = '*';
end loop;
new_line;
put_line ("Loop terminated.");
end read_until_stop;
5.2.2 while
루프
while
루프는 명시된 조건이 True
로 유지되는 동안 그 본문을 실행합니다. 조건은 매 반복이 시작되기 전에 검사됩니다. 만약 조건이 초기에 False
이면, 루프 본문은 전혀 실행되지 않을 것입니다.
구문:
while condition loop
-- 일련의 문장들
end loop;
예제: 이 루프는 입찰 가격이 특정 상한가 미만인 동안 입찰을 처리합니다.
-- bid, price, record_bid, cut_off가 다른 곳에 선언되었다고 가정
while bid(n).price < cut_off.price loop
record_bid (bid(n).price);
n := n + 1;
end loop;
5.2.3 for
루프
for
루프는 확정적 반복을 위한 기본적인 구문으로, 일련의 문장들을 지정된 횟수만큼 실행하도록 설계되었습니다. 이 횟수는 루프가 시작되기 전에 한 번만 평가되는 이산 범위(discrete range)에 의해 결정됩니다.
Ada에서 이산 타입(discrete type)은 값들 사이에 명확하고 뚜렷한 구분이 있는 타입을 의미합니다. 이 범주에는 다음이 포함됩니다.
- 정수 타입 (예:
Integer
,Natural
,Positive
) - 사전 정의된
Boolean
과Character
타입을 포함하는 열거형 타입.
값들이 셀 수 있고 순서가 있기 때문에, 루프는 각 값을 순차적으로 반복할 수 있습니다. 이처럼 반복 횟수를 미리 알 수 있다는 결정론적(deterministic) 특성은 for
루프의 핵심적인 특징입니다. Float
이나 String
과 같은 비이산 타입에는 사용할 수 없습니다.
루프는 루프 파라미터(loop parameter)를 도입하는데, 이는 각 반복마다 이산 범위 내의 연속된 원소 값을 갖는 식별자입니다. 이 루프 파라미터는 세 가지 중요한 속성을 가집니다.
- 루프 자체에 의해 묵시적으로 선언됩니다. 별도의 선언이 필요하지 않습니다.
- 그 스코프(scope)는
loop
키워드부터end loop
키워드까지의 루프 본문으로 제한됩니다. 루프 밖에서는 보이지 않습니다. - 루프 내에서 상수(constant)입니다. 그 값은 각 반복이 시작될 때 루프 메커니즘에 의해 자동으로 갱신되며, 루프 본문 안의 코드로는 수정할 수 없습니다.
이산 범위 반복 (Iteration over Discrete Ranges)
가장 흔한 형태는 ..
연산자로 명시된 값의 범위를 반복하는 것입니다. 루프는 오름차순(기본값)으로 진행하거나 reverse
키워드를 사용하여 내림차순으로 진행할 수 있습니다.
예제 (정수 범위):
with Ada.Text_IO;
procedure integer_loop_example is
begin
-- 1부터 10까지 반복
for i in 1 .. 10 loop
Ada.Text_IO.put_line ("Ascending: " & Integer'image (i));
end loop;
Ada.Text_IO.new_line;
-- 10부터 1까지 반복
for i in reverse 1 .. 10 loop
Ada.Text_IO.put_line ("Descending: " & Integer'image (i));
end loop;
end integer_loop_example;
예제 (열거형 범위): 이 예제는 사용자 정의 열거형 타입을 반복하는 것을 보여줍니다.
with Ada.Text_IO;
procedure enum_loop_example is
type Day_Of_Week is (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
begin
for day in Day_Of_Week'range loop -- 'range는 전체 범위를 제공
Ada.Text_IO.put (Day_Of_Week'image (day) & " ");
end loop;
Ada.Text_IO.new_line;
-- 열거형의 일부 범위만 반복
for day in Wednesday .. Friday loop
Ada.Text_IO.put_line ("Workday: " & Day_Of_Week'image (day));
end loop;
end enum_loop_example;
단일 범위 평가의 원칙 (The Principle of Single Range Evaluation)
Ada for
루프의 결정적인 특징은 그 이산 범위가 단 한 번만 평가된다는 점입니다. 이는 첫 반복이 시작되기 전에 범위가 완전히 계산되고 그 경계가 고정됨을 의미합니다. 이 단일 평가는 루프 파라미터를 위한 불변의 값 시퀀스를 설정하며, 결과적으로 반복 횟수는 루프 본문이 실행되기 전에 결정됩니다.
이 원칙은 null 범위(null range)를 가진 루프의 동작을 직접적으로 관장합니다. 범위의 하한이 상한보다 크면 (예: 5 .. 1
) 그 범위는 null로 간주됩니다. 범위가 null일 때, 루프는 즉시 완료되며 0번의 반복을 수행합니다. 루프 본문 안의 문장들은 전혀 실행되지 않습니다.
reverse
키워드는 범위의 경계를 맞바꾸지 않으므로, 범위 자체의 유효성이 아닌 반복의 방향에만 영향을 미친다는 점을 이해해야 합니다. 이러한 이유로 for j in reverse 1 .. 0 loop
루프는 실행되지 않습니다. 범위 1 .. 0
은 reverse
키워드가 반복 순서를 위해 고려되기 전에 null로 결정됩니다. 이는 안전하고 예측 가능한 결과를 제공합니다.
예제: Null 범위의 동작
다음 예제는 null 범위를 가진 루프가 reverse
키워드를 사용하더라도 그 본문을 실행하지 않음을 보여줍니다.
with Ada.Text_IO;
procedure null_range_example is
use Ada.Text_IO;
loop_1_executed : Boolean := False;
loop_2_executed : Boolean := False;
begin
-- 이 루프는 5가 1보다 크므로 실행되지 않음
for i in 5 .. 1 loop
loop_1_executed := True;
end loop;
if not loop_1_executed then
put_line ("Loop with range 5 .. 1 did not execute.");
end if;
-- 'reverse'를 사용해도 null 범위는 유효해지지 않음
-- 이 루프 또한 1이 0보다 크므로 실행되지 않음
for j in reverse 1 .. 0 loop
loop_2_executed := True;
end loop;
if not loop_2_executed then
put_line ("Loop with range 'reverse 1 .. 0' did not execute.");
end if;
end null_range_example;
출력:
Loop with range 5 .. 1 did not execute.
Loop with range 'reverse 1 .. 0' did not execute.
이러한 설계는 for
루프가 예측 가능하고 결정론적임을 보장합니다. 매 반복 전에 종료 조건을 재평가하고 루프 내에서 수정할 수 있는 C 언어의 for
루프와 달리, Ada for
루프의 반복 횟수는 루프 본문 안에서 범위의 경계값을 변경해도 영향을 받지 않습니다.
예제: 불변의 반복 횟수
다음 코드는 루프의 범위를 정의하는 데 사용된 변수가 루프 내에서 수정되더라도, 원래의 반복 횟수는 변하지 않음을 보여줍니다.
with Ada.Text_IO;
procedure immutable_bounds_example is
use Ada.Text_IO;
upper_bound : Integer := 3;
begin
put_line ("Loop will start with range 1 .. " & Integer'image (upper_bound));
new_line;
-- '1 .. upper_bound' 범위는 여기서 '1 .. 3'으로 평가됨
-- 이제 루프는 정확히 3번 실행되도록 고정됨
for i in 1 .. upper_bound loop
put_line ("Start of iteration " & Integer'image (i) &
": upper_bound is " & Integer'image (upper_bound));
if i = 2 then
upper_bound := 10; -- 이 변경은 루프의 실행 기간에 영향을 주지 않음
put_line (" -> upper_bound changed to 10 inside the loop.");
end if;
end loop;
new_line;
put_line ("Loop finished. It executed 3 times as determined initially.");
put_line ("Final value of upper_bound: " & Integer'image (upper_bound));
end immutable_bounds_example;
출력:
Loop will start with range 1 .. 3
Start of iteration 1: Upper_Bound is 3
Start of iteration 2: Upper_Bound is 3
-> Upper_Bound changed to 10 inside the loop.
Start of iteration 3: Upper_Bound is 10
Loop finished. It executed 3 times as determined initially.
Final value of Upper_Bound: 10
이 동작은 우발적인 무한 루프나 조기 종료와 같은 흔한 종류의 오류를 방지하여 프로그램의 안전성과 신뢰성을 향상시킵니다. 또한 프로그래머가 루프의 선언만 보고도 정확한 반복 횟수를 결정할 수 있게 하여 코드 가독성을 높입니다.
배열 및 컨테이너 반복 (Array and Container Iteration)
for
루프는 배열이나 반복 가능한 컨테이너의 원소들을 반복하는 데에도 사용될 수 있습니다.
-
배열 인덱스 반복:
'range
속성은 배열 인덱스의 범위를 제공하며, 각 원소를 인덱스로 접근하는 데 유용합니다.buffer : String (1 .. 10); -- ... (buffer가 초기화됨) for i in buffer'range loop if buffer (i) /= ' ' then Ada.Text_IO.put (buffer (i)); end if; end loop;
-
배열 원소 반복 (컴포넌트 반복): Ada는 배열의 구성 요소(원소)를 직접 반복하는 것을 허용합니다. 이 경우 루프 파라미터는 원소를 직접 가리킵니다. 만약 배열이 상수가 아니라면 루프 파라미터는 변수가 되어, 배열 원소의 직접적인 수정이 가능합니다.
my_array : array (1 .. 5) of Integer := [10, 20, 30, 40, 50]; begin for element of my_array loop element := element * 2; -- my_array의 원소를 수정함 end loop; end;
반복자 필터 (Iterator Filter)
when
절을 가진 iterator_filter
를 for
루프에 추가하여 특정 조건을 만족하는 값에 대해서만 루프 본문을 조건부로 실행할 수 있습니다.
예제: 이 루프는 범위 내의 홀수만 처리합니다.
for i in 1 .. 10 when i mod 2 /= 0 loop
Ada.Text_IO.put_line ("Processing odd number: " & Integer'image (i));
end loop;
5.2.4 루프 이름짓기
루프에는 이름을 부여할 수 있습니다. 이 관행은 가독성을 향상시키며, 중첩된 루프 구조 내에서 특정 루프를 빠져나올 때 필수적입니다. 이름은 루프의 시작과 끝 모두에 나타나야 합니다.
구문:
Loop_Name:
loop
-- ...
exit Loop_Name when condition;
-- ...
end loop Loop_Name;
예제:
Outer_Loop:
for i in 1 .. 10 loop
Inner_Loop:
for j in 1 .. 10 loop
-- ...
exit Outer_Loop when i * j > 50;
end loop Inner_Loop;
end loop Outer_Loop;
5.2.5 병렬 루프 (Ada 2022)
Ada 2022는 병렬 루프를 도입하여, for
루프의 반복들이 여러 논리적 제어 스레드에 의해 동시에 실행될 수 있게 합니다. 이 기능은 루프의 작업 부하를 분산시켜 멀티코어 프로세서에서 계산 집약적인 작업의 성능을 크게 향상시킬 수 있습니다.
구현 참고: 병렬 루프 기능은 Ada 2022 표준에 추가된 사항입니다. GCC 15 기준으로, GNAT 컴파일러는 아직
parallel
키워드를 지원하지 않습니다. 따라서 다음 구문과 예제들은 Ada 2022 명세에 기반한 것이며, 현재의 GNAT 버전으로는 컴파일되지 않을 수 있습니다.
병렬 루프는 표준 for
루프 구문 앞에 오는 parallel
키워드로 표시됩니다. 런타임 시스템은 루프의 반복들을 하나 이상의 “청크(chunk)”로 분할하며, 각 청크는 별도의 논리적 스레드에 의해 처리됩니다. 어떤 형태의 병렬 루프에서도 reverse
키워드는 허용되지 않는다는 점에 유의해야 합니다.
Ada 2022는 세 가지 주요 형태의 병렬 반복을 정의합니다.
1. 이산 범위에 대한 병렬 for
루프
이것은 병렬 루프의 직접적인 형태로, 이산 범위(예: 정수 또는 열거형 범위)에 대한 반복이 병렬화됩니다.
구문:
parallel for loop_parameter in discrete_subtype_definition loop
-- 일련의 문장들
end loop;
예제:
-- 각 인덱스에 대한 독립적인 계산을 병렬로 수행
parallel for i in 1 .. 100 loop
process_data (i);
end loop;
2. 배열 및 컨테이너를 위한 병렬 반복자
이 형태는 배열의 원소나 병렬 반복자 인터페이스를 제공하는 모든 컨테이너 타입(특히, Ada.Iterator_Interfaces.Parallel_Iterator
에서 파생된 타입)에 대한 병렬 반복을 허용합니다.
구문:
parallel for element of Iterable_Container loop
-- 일련의 문장들
end loop;
예제 (병렬 컴포넌트 반복): 이 예제는 2차원 배열의 각 원소를 병렬로 두 배로 만듭니다.
-- Board는 Float 타입의 2차원 배열
parallel for element of Board loop
element := element * 2.0;
end loop;
3. 병렬 절차적 반복자
이 고급 형태는 프로시저를 사용하여 반복을 제어합니다. 지정된 프로시저가 Parallel_Iterator => True
애스펙트를 가지고 있다면 루프는 병렬화됩니다. 이는 프로시저 자체가 여러 스레드에서 루프 본문을 안전하게 호출하도록 설계되었음을 나타냅니다.
구문:
parallel for (parameters) of Iterator_Procedure loop
-- 일련의 문장들
end loop;
청크 명세 (Chunk Specification)
프로그래머는 chunk_specification
을 제공하여 작업 부하가 어떻게 분할되는지에 영향을 줄 수 있습니다. 이는 병렬 실행의 세분성을 미세 조정할 수 있게 합니다. 청크를 명시하는 두 가지 방법이 있습니다.
- 최대 개수로: 정수 표현식이 최대 청크 수를 정의합니다.
- 청크 서브타입으로: 이산 서브타입이 청크의 집합을 정의합니다. 이 형태는 또한 루프 내에서 특정 청크를 식별하는 데 사용할 수 있는 청크 파라미터를 묵시적으로 선언하여, 결과를 분할하는 데 유용합니다.
예제 (서브타입을 사용한 청크 명세): 이 루프는 최대 8개의 청크를 사용하여 부분 계산을 병렬로 수행합니다. Chunk_Number
타입의 Chunk
파라미터는 루프 내에서 어떤 스레드가 데이터의 어느 부분을 작업하고 있는지 구별하는 데 사용할 수 있습니다.
declare
subtype Chunk_Number is Natural range 1 .. 8;
partial_sum : array (Chunk_Number) of Natural := (others => 0);
grid : array (1 .. 1_000) of Boolean;
-- ... grid가 초기화됨
begin
parallel (chunk in Chunk_Number)
for i in grid'range loop
if grid(i) then
-- 이 연산은 스레드-안전해야 함. 예제는 간결함을 위해 '@'를 사용했지만,
-- 실제 구현에서는 스레드-안전한 업데이트를 위해 원자적 연산이나
-- 보호 객체가 필요할 것임
partial_sum(chunk) := @ + 1;
end if;
end loop;
-- ... partial_sum으로부터 최종 결과를 집계
end;
5.2.6 고급 반복자: 간략한 개요
앞선 절들에서는 Ada의 기본적이고 가장 흔한 루프 구문들을 다루었습니다. 하지만, 언어의 반복 능력은 이러한 형태들을 훨씬 넘어서, 데이터 구조를 순회하기 위한 표현력 있고, 타입-안전하며, 확장 가능한 패턴들을 제공합니다. 이러한 고급 기능들은 프로그래머가 자신만의 타입을 위한 사용자 정의 반복 동작을 정의하고, 고급 반복 제어 메커니즘을 사용할 수 있게 합니다.
이 절은 이러한 고급 개념들에 대한 높은 수준의 소개를 제공합니다. 상세한 구현 예제를 포함한 포괄적인 처리는 6장 “고급 반복과 컨테이너 설계”에서 제시됩니다.
일반화된 반복자 (Generalized Iterators)
for element of container
구문이 배열이나 컨테이너의 원소들을 순회하는 직접적인 방법을 제공하는 반면, Ada는 일반화된 반복자를 사용하는 더 명시적인 형태의 반복도 지원합니다. 이 형태는 컨테이너 객체와 of
키워드를 사용하는 대신, 반복자 객체와 in
키워드를 사용합니다.
이 패턴은 한 타입이 여러 가지 순회 방법을 제공할 때(예: 정방향, 역방향, 또는 키-값 순회) 또는 반복 로직이 컨테이너의 기본 동작이 되기에는 너무 복잡할 때 유용합니다.
개념적 구문:
-- My_Container.Iterate는 반복자 객체를 반환
for item in My_Container.Iterate (Mode => By_Value) loop
-- item 처리
null;
end loop;
이 구조에서 My_Container.Iterate
는 반복자 객체를 반환하는 함수이며, for ... in
루프는 이 객체를 사용하여 시퀀스를 순회합니다.
절차적 반복자 (Procedural Iterators)
절차적 반복자는 루프의 제어를 역전시키는 독특한 반복 패턴입니다. 루프가 컨테이너로부터 값을 가져오는 대신, 루프는 자신의 본문을 프로시저 파라미터로써 실행을 제어하는 반복 프로시저에게 제공합니다.
루프 본문은 묵시적으로 프로시저에 대한 접근 값으로 변환되어 지정된 프로시저에 전달됩니다. 이는 환경 변수 순회와 같이 Ada 표준 라이브러리의 일부에서 사용되는 매우 다용도의 패턴입니다.
개념적 구문:
-- Iterate는 프로시저에 대한 접근을 받는 프로시저
for (name, value) of Ada.Environment_Variables.Iterate loop
put_line (name & "=" & value);
end loop;
이는 구문적으로 루프 본문을 위한 지역 프로시저를 선언하고, 그 'Access
속성을 Iterate
프로시저에 전달하는 것과 동일합니다. 이 수준의 추상화는 6장에서 상세히 다루어질 것입니다.
반복 가능한 타입 메커니즘 (The Iterable Type Mechanism)
사용자 정의 컨테이너 타입에 대해 직접적인 for...of...
구문을 사용하는 능력은 내장된 것이 아니라, 주로 Ada.Iterator_Interfaces
에 정의된 애스펙트와 같은 언어 기능들에 의해 활성화됩니다. 프로그래머는 Default_Iterator
나 Iterator_Element
와 같은 애스펙트를 사용자 정의 타입에 명시함으로써, 해당 타입을 Ada의 반복 프레임워크에 직접 통합할 수 있습니다. 이 메커니즘은 새롭고, 재사용 가능하며, 효율적인 반복 가능 컨테이너를 만드는 데 필수적입니다.
이러한 고급 능력들은 Ada 반복 모델의 구조적인 설계를 보여주며, 기본 루프부터 완전히 사용자화 가능한, 타입-안전한 순회 패턴에 이르기까지 여러 추상화 계층을 제공합니다. 이 기능들의 사용 및 구현에 대한 완전한 가이드는 6장을 참조하십시오.
5.3 블록과 스코프
declare ... begin ... end
블록은 문장이 허용되는 곳이라면 어디든 삽입할 수 있습니다. 이 구문은 코드의 작은 부분에만 필요한 임시 변수를 선언하기 위한 지역 스코프를 만드는 데 유용하며, 이를 통해 지역성을 향상시키고 외부 스코프의 변수가 우발적으로 수정될 위험을 줄입니다. 블록은 또한 7장에서 상세히 다루어질 지역화된 예외 핸들러를 정의하는 주된 메커니즘입니다.
-- 외부 스코프
...
declare
-- 이 블록 안에서만 보이는 지역 변수들
temp : Float;
swap_var : Integer;
begin
-- temp와 swap_var를 사용하는 연산들
...
end;
-- temp와 swap_var는 여기서 더 이상 존재하지 않음
...
6. 서브프로그램과 패키지를 이용한 구조화
프로그램이 복잡해질수록 추상화를 위한 메커니즘이 중요해집니다. Ada는 서브프로그램, 패키지, 제네릭을 통해 이러한 복잡성을 관리하는 시스템을 제공합니다. 이러한 기능들은 함께 작동하여 긴 수명 주기 동안 모듈화되고, 재사용 가능하며, 유지보수 가능한 소프트웨어를 구축할 수 있게 합니다.
6.1 서브프로그램: 프로시저와 함수
서브프로그램(Subprograms)은 Ada에서 실행 가능한 코드의 기본 단위입니다. 일련의 연산을 단일의 호출 가능한 엔티티로 캡슐화하여 알고리즘적 추상화를 제공합니다. 언어는 두 가지 형태의 서브프로그램을 명확하게 구분합니다:
-
프로시저(Procedures): 프로시저는 어떤 동작을 수행하며 독립적인 문장으로 호출됩니다. 값을 반환하지 않습니다.
-
함수(Functions): 함수는 값을 계산하고 반환하므로 항상 표현식의 일부가 됩니다. 함수의 반환 값은 무시할 수 없으며, 변수에 할당되거나 다른 표현식에서 사용되어야 합니다.
모든 서브프로그램은 명세(공개 인터페이스)와 본체(구현)로 구성되며, 이들은 별도로 컴파일될 수 있습니다.
매개변수 모드
Ada에서 서브프로그램 설계의 중요한 측면은 매개변수 모드(parameter modes)를 명시적으로 정의하는 것입니다. 이는 각 매개변수에 대한 데이터 흐름의 의도된 방향을 지정합니다. 이는 서브프로그램과 호출자 사이의 계약 역할을 합니다.
-
in
: 매개변수의 값이 서브프로그램으로 전달되며, 서브프로그램은 이를 읽기만 할 수 있습니다. 형식 매개변수는 서브프로그램 내에서 상수처럼 동작합니다. 아무것도 지정되지 않으면 이것이 기본 모드입니다. -
in out
: 매개변수의 값은 서브프로그램에 의해 읽히고 수정될 수 있습니다. 변경 사항은 호출자가 제공한 실제 매개변수에 반영됩니다. -
out
: 서브프로그램이 매개변수에 값을 할당할 것으로 예상됩니다. 실제 매개변수의 초기 값은 무관하며, 서브프로그램 내에서 형식 매개변수는 값이 할당될 때까지 초기화되지 않은 것으로 간주됩니다.
procedure compute_roots (
a, b, c : in Float;
root_1 : out Float;
root_2 : out Float;
is_valid: out Boolean) is
...
end compute_roots;
매개변수 연관
서브프로그램을 호출할 때 매개변수는 두 가지 표기법을 사용하여 전달될 수 있습니다:
-
위치적 연관(Positional Association): 인수는 목록에서의 순서에 따라 형식 매개변수와 일치됩니다.
-
이름 지정 연관(Named Association): 인수는
=>
기호를 사용하여 이름으로 형식 매개변수와 명시적으로 일치됩니다.
-- 위치적 호출
compute_roots (1.0, -5.0, 6.0, r1, r2, v);
-- 이름 지정 호출 (순서는 중요하지 않음)
compute_roots (
a => 1.0,
b => -5.0,
c => 6.0,
root_1 => r1,
root_2 => r2,
is_valid => v);
이름 지정 연관은 여러 매개변수를 가진 서브프로그램에 권장되며, 코드의 명확성을 향상시키고 인수를 잘못된 순서로 전달할 위험을 줄여줍니다.
표현식 함수
Ada 2012에서 도입된 표현식 함수(expression functions)는 본체가 단일 반환 표현식으로 구성된 간단한 함수를 정의하기 위한 간결한 구문을 제공합니다.
function square (x : Integer) return Integer is (x * x);
6.2 모듈화를 위한 패키지
package
는 Ada의 모듈화, 캡슐화 및 정보 은닉을 위한 주요 메커니즘입니다. 프로그래머가 논리적으로 관련된 엔티티(형식, 상수, 변수, 서브프로그램 등)의 모음을 잘 정의된 인터페이스를 가진 단일의 명명된 모듈로 그룹화할 수 있게 해줍니다.
패키지는 항상 두 개의 구별되는 부분으로 나뉘며, 별도의 파일에 저장되고 독립적으로 컴파일됩니다:
-
패키지 명세(
.ads
파일): 패키지의 공개 인터페이스를 정의합니다. 프로그램의 다른 부분(패키지의 “클라이언트”)에게 보이고 사용 가능하도록 의도된 모든 선언을 포함합니다. 이것은 패키지가 외부 세계에 제공하는 계약입니다. -
패키지 본체(
.adb
파일): 명세에 선언된 엔티티의 구현을 포함합니다. 여기에는 모든 서브프로그램의 완전한 본체뿐만 아니라, 구현에 필요하지만 클라이언트에게는 숨겨져야 하는 내부 데이터 구조, 도우미 서브프로그램 또는 상수가 포함됩니다.
이러한 인터페이스와 구현의 엄격한 분리는 Ada에서 대규모 소프트웨어 공학의 핵심 원칙입니다. 이를 통해 팀은 먼저 패키지 명세를 정의하고 합의할 수 있습니다. 명세가 컴파일되면, 다른 개발자들은 컴파일러가 합의된 인터페이스를 강제할 것이라는 확신을 가지고 병렬로 패키지 본체를 구현하는 작업을 할 수 있습니다. 더욱이, 명세가 변경되지 않는 한, 패키지 본체 내부의 구현은 변경되고 재컴파일될 수 있으며, 이에 의존하는 클라이언트 코드를 재컴파일할 필요가 없습니다.
private
형식을 이용한 정보 은닉
캡슐화를 제공하기 위해, 패키지는 형식의 내부 구조를 클라이언트로부터 숨길 수 있습니다. 이는 패키지 명세의 보이는 부분에서 형식을 private
으로 선언함으로써 달성됩니다. 형식의 전체 정의는 명세 끝의 특별한 private
섹션으로 미뤄집니다.
-- 명세 파일: stack.ads
package Stack is
type T is private; -- 클라이언트는 'T'라는 이름만 볼 수 있음
-- private 형식에 대한 연산
procedure push (S : in out T; Value : in Integer);
function pop (S : in out T) return Integer;
function is_empty (S : T) return Boolean;
private
-- 전체 정의는 클라이언트에게 숨겨짐
MAX_SIZE : constant := 100;
subtype Index is Integer range 0 .. MAX_SIZE;
type Integer_Array is array (Positive range <>) of Integer;
type T is record
elements : Integer_Array (1 .. MAX_SIZE);
top : Index := 0;
end record;
end Stack;
이 패키지의 클라이언트는 Stack.T
형식의 변수를 선언하고 push
및 pop
프로시저를 호출할 수 있지만, 레코드의 elements
또는 top
필드에 직접 접근할 수는 없습니다. 이것은 추상화를 강제하고 클라이언트가 스택의 내부 상태를 손상시키는 것을 방지합니다.
6.3 컴파일 단위 (compilation unit)
컴파일 단위는 Ada 컴파일러가 한 번에 처리하는 독립적인 소스 코드 단위를 의미합니다. Ada 프로그램은 이러한 컴파일 단위들의 집합으로 구성됩니다. 각 컴파일 단위는 보통 별도의 파일에 저장됩니다.
컴파일 단위는 프로그램의 모듈성과 재사용성을 높이는 핵심 요소이며, 주요 종류는 다음과 같습니다.
- 서브프로그램 선언 및 본체:
procedure
나function
의 선언 또는 구현부입니다. - 패키지 명세 및 본체:
package
의 명세(.ads
파일)와 구현부(.adb
파일)입니다. 이 둘은 서로 다른 컴파일 단위입니다. - 제네릭 선언 및 본체: 제네릭
package
나subprogram
의 선언 또는 구현부입니다. - 서브유닛: 다른 컴파일 단위에 속한 몸체를 별도의 파일로 분리한 것입니다.
프로그램의 다른 컴파일 단위에 정의된 기능을 사용하기 위해서는 with
절을 통해 명시적으로 의존성을 선언해야 합니다. 컴파일러와 링커는 이 의존성 관계를 분석하여 올바른 순서로 컴포넌트들을 결합하고 실행 파일을 생성합니다.
6.4 정교화 (elaboration)
정교화(elaboration)는 프로그램 실행이 시작될 때, 주(main) 서브프로그램의 코드가 실행되기 전에 선언된 모든 개체(패키지, 변수 등)를 생성하고 초기화하는 동적인 과정입니다.
정교화의 역할
- 변수 초기화: 변수가 선언될 때 초기값이 지정된 경우, 정교화 과정에서 해당 값이 변수에 할당됩니다.
- 패키지 초기화: 패키지 본체에 포함된 초기화 코드가 정교화 시점에 실행됩니다.
- 의존성 관리:
with
절을 통해 다른 패키지에 의존하는 패키지는 해당 패키지가 먼저 정교화됩니다. 컴파일러는 정교화 순서를 결정하며, 순환 의존성이 발생할 경우 오류를 보고합니다.
-- example_package_a.ads
package example_package_a is
procedure do_something;
end example_package_a;
-- example_package_a.adb
package body example_package_a is
-- 이 패키지의 정교화(elaboration) 과정에서 10으로 초기화되는
-- 패키지 내부 변수입니다.
initial_value : Integer := 10;
procedure do_something is
begin
-- Do something
end do_something;
begin -- 패키지 초기화부 시작
-- 이 블록은 example_package_a 패키지가 정교화될 때 단 한 번 실행됩니다.
-- 패키지 상태를 최종적으로 설정하는 역할을 합니다.
initial_value := initial_value + 1; -- 따라서 initial_value의 최종값은 11이 됩니다.
end example_package_a;
위 예시에서 initial_value
변수와 패키지 본체의 초기화 블록은 해당 패키지가 정교화될 때 실행됩니다.
정교화 순서
대부분의 정교화 순서는 Ada 컴파일러가 자동으로 결정합니다. 하지만 특정 상황에서 순서를 명시적으로 제어할 필요가 있을 수 있으며, 이를 위해 Elaborate
및 Elaborate_Body
와 같은 프라그마(pragma)가 사용됩니다.
6.5 제네릭 단위
제네릭(Generics)은 Ada에서 높은 수준의 추상화를 제공하여, 형식이나 다른 프로그램 엔티티로 매개변수화된 재사용 가능한 “템플릿” 서브프로그램과 패키지를 생성할 수 있게 합니다. 이는 재사용 가능하고 형식 안전한 알고리즘과 데이터 구조의 개발을 가능하게 합니다.
제네릭 단위는 직접 사용할 수 없습니다. 먼저 형식 제네릭 매개변수에 대한 실제 매개변수를 제공하여 인스턴스화해야 합니다. new
키워드를 사용하는 이 인스턴스화는 다른 것과 마찬가지로 사용할 수 있는 새로운, 구체적인 서브프로그램이나 패키지를 생성합니다.
-- swap 프로시저에 대한 제네릭 명세
generic
type Item is private; -- 제네릭 형식 매개변수
procedure swap (left, right : in out Item);
-- 제네릭 본체
procedure swap (left, right : in out Item) is
temp : Item := Left;
begin
left := right;
right := temp;
end swap;
-- Integer에 대한 인스턴스화
procedure swap_integers is new swap (Item => Integer);
-- 사용자 정의 레코드 형식에 대한 인스턴스화
procedure swap_persons is new swap (Item => Person);
Ada의 제네릭 형식 매개변수는 풍부한 옵션 집합을 제공하며, 이는 제네릭 단위와 그 인스턴스 생성자 사이의 계약 역할을 합니다. 제네릭 단위는 매개변수가 가져야 할 속성을 정확하게 지정할 수 있습니다. 예를 들어, 제네릭 정렬 알고리즘은 요소 형식에 “<
” 연산자가 정의되어 있어야 한다고 요구할 수 있습니다. 그러면 컴파일러는 이 계약을 충족하는 형식으로만 제네릭이 인스턴스화되도록 보장하여, 컴파일 시간에 오류를 방지합니다.
7. 예외 처리
7.1 예외 처리의 기본 개념
7.1.1 예외란 무엇인가?
Ada에서 예외(Exception)란 프로그램 실행 중 발생할 수 있는 ‘예외적인 상황’을 의미합니다. 예를 들어, 숫자를 0으로 나누려 하거나 존재하지 않는 파일에 접근하려는 경우가 이에 해당합니다. 이러한 상황이 실제로 발생하는 것을 예외 발생(exception occurrence)이라고 합니다.
이러한 예외적 상황에 대응하는 과정은 예외를 발생시키는(raising) 행위와 이를 처리하는(handling) 행위로 이루어집니다. 예외를 발생시키는 것은 예외적인 상황이 발생했음을 알리기 위해 정상적인 프로그램의 실행 흐름을 의도적으로 포기하는 행위이며, 이때 원래 진행되던 구문의 실행은 그 즉시 중단됩니다. 이렇게 실행이 중단되면, 발생한 예외에 대응하여 특정 동작을 수행하는 처리(handling) 과정이 이어집니다. 이 과정에서 프로그램의 제어권은 사용자가 미리 정의한 예외 처리기(exception handler)로 이전될 수 있으며, 만약 현재 위치에서 처리할 수 없다면 예외는 상위 실행 문맥으로 전파(propagate)될 수 있습니다.
이러한 예외 처리 메커니즘은 오류를 감지하면 정상적인 실행 흐름을 중단하고 예외를 발생시킨 후, 여기에 대응하여 프로그램의 안정적인 실행을 지속하거나 제어된 방식으로 종료함으로써 견고성(robustness)을 향상시킵니다.
7.1.2 예외 처리 모델: 종료 모델
예외 처리 메커니즘은 종료 모델(termination model)과 재개 모델(resumption model)로 나뉩니다.
-
종료 모델 (termination model): 예외가 발생하여 예외 처리기로 제어권이 이전되면, 원래 예외가 발생했던 지점으로 돌아가지 않습니다. 예외 처리기의 실행이 끝난 후에는 해당 블록(
begin ... end
) 다음의 문장으로 실행이 이어집니다. 즉, 해당 블록의 실행은 예외 발생 시점에서 종료됩니다. Ada, C++, Java, Python 등이 이 모델을 사용합니다. -
재개 모델 (resumption model): 예외 처리기에서 특정 조치를 수행한 후, 예외가 발생했던 지점으로 돌아가 실행을 재개할 수 있는 모델입니다.
Ada는 종료 모델을 채택하고 있습니다. 예외가 발생하면 해당 코드 블록의 실행은 중단되며, 개발자는 예외 처리기에서 복구 또는 정리 작업을 수행한 후 프로그램의 다음 동작을 제어하는 흐름을 설계할 수 있습니다.
7.1.3 미리 정의된 예외
Ada 언어는 어떠한 컴파일 단위(compilation unit)에서든 바로 사용할 수 있는 4가지 핵심 예외, 즉 Constraint_Error
, Program_Error
, Storage_Error
, Tasking_Error
를 미리 정의하고 있습니다. 이 예외들은 Standard
패키지에 선언되어 있어 별도의 with
나 use
절 없이도 항상 접근 가능하며 프로그램 실행 중 특정 규칙 위반 여부를 확인하는 언어 정의 런타임 검사(language-defined run-time check)가 실패할 때 자동으로 발생합니다. 또한, 프로그래머가 raise
문을 사용하여 이 예외들을 직접 발생시킬 수도 있습니다. 언어 정의 런타임 검사에 대한 자세한 내용은 7.2.1절에서 설명합니다.
7.1.4 예외 선언
Ada에서는 exception
키워드를 사용하여 사용자 정의 예외를 선언(declare)할 수 있습니다. 이 선언은 특정 예외적 상황을 나타낼 고유한 이름을 정의하는 역할을 합니다.
구문 (syntax)
예외 선언의 일반적인 형식은 다음과 같습니다.
<예외_이름_목록> : exception;
<예외_이름_목록>
에는 하나 또는 여러 개의 예외 이름을 쉼표로 구분하여 사용할 수 있습니다.
-- 하나의 예외 선언
Singular : exception;
-- 여러 예외를 한 번에 선언
Overflow, Underflow : exception;
선언은 항상 세미콜론(;
)으로 끝납니다.
예외 선언의 의미 규칙
예외 선언에는 다음과 같은 의미 규칙이 있습니다.
- 고유성: 각각의 예외 선언문은 완전히 다른 새로운 예외를 정의합니다. 예를 들어, 서로 다른 두 패키지에
Example_Error
라는 이름의 예외가 각각 선언되었다면, 두Example_Error
예외는 이름만 같을 뿐 서로 다른 별개의 예외입니다. - 정적 식별: 예외의 이름이 나타내는 특정 예외는 컴파일 타임에 결정됩니다. 즉, 선언된 예외의 고유한 정체성(identity)은 프로그램이 컴파일될 때 확정되며, 프로그램이 실행되는 동안 이 정체성은 변하지 않습니다.
- 런타임 효과 없음: 예외 선언문의 정교화(elaboration)6는 런타임에 아무런 효과가 없습니다. 이는 변수 선언 시 메모리가 할당되고 초기화되는 것과는 다른 동작입니다.
- 제네릭과 예외: 만약 제네릭 유닛 내에 예외가 선언된 경우, 해당 제네릭의 인스턴스를 생성할 때마다 각각의 인스턴스에는 서로 다른 고유한 예외가 있게 됩니다.
7.2 예외 발생시키기 (raising exceptions)
7.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
: 호출된 태스크가 아직 종료되지 않았는지 등을 검사합니다.
이러한 런타임 검사들을 비활성화하여 프로그램의 성능을 높이는 방법에 대해서는 6.8절에서 자세히 다룹니다.
7.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 "음수 값에 대한 제곱근은 정의되지 않습니다."));
7.3 예외 처리하기
7.3.1 예외 처리기 구문 (begin
… exception
… end
)
예외 처리기(exception handler)는 특정 예외가 발생했을 때 이에 대응하여 실행되는 코드 블록입니다. Ada에서는 이러한 처리기들을 일반 실행문과 분리된 별도의 구역에 배치합니다.
예외 처리기 구문의 일반적인 형태는 다음과 같습니다:
begin
-- 정상적인 실행 흐름에 해당하는 문장들이 위치합니다.
-- 이 영역에서 예외가 발생할 수 있습니다.
exception
-- 예외 핸들러는 여기에 위치하며,
-- 'when' 절로 시작합니다.
when <예외_이름_1> =>
-- 예외 1이 발생했을 때 실행할 코드
when <예외_이름_2> | <예외_이름_3> =>
-- 예외 2 또는 3이 발생했을 때 실행할 코드
when others =>
-- 위에서 명시되지 않은 모든 예외가 발생했을 때 실행할 코드
end;
이 구조의 실행 흐름은 두 가지 경우로 나뉩니다.
begin
과 exception
사이의 코드에서 예외가 발생하지 않으면, exception
이하의 처리기 부분은 완전히 건너뛰고 end
다음으로 실행이 이어집니다.
만약 begin
과 exception
사이의 코드에서 예외가 발생하면, 해당 지점에서 즉시 정상적인 실행을 포기합니다. 그 후, 프로그램 제어권은 exception
이하의 처리기 부분으로 이전되어 알맞은 처리기를 찾게 됩니다.
7.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;
7.3.3 모든 예외 처리: when others
when others
절은 특정 when
절에서 명시적으로 처리되지 않은 모든 종류의 예외를 처리합니다. 이 절을 사용하여 예상치 못한 예외가 발생했을 때 프로그램이 비정상적으로 종료되는 것을 막고, 오류 기록이나 자원 정리 등의 동작을 수행할 수 있습니다.
구문 규칙
when others
절에는 다음과 같은 구문 규칙이 있습니다.
when others
는exception
처리 구역의 가장 마지막에 위치해야 합니다.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;
7.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
예외를 처리합니다.
7.3.5 예외를 이용한 재시도 로직 (retry logic)
Ada의 종료 모델에서는 예외가 발생한 지점으로 제어가 돌아가지 않습니다. 그러나 loop
문과 예외 처리기를 결합하여 실패한 작업을 재시도(retry)하는 로직을 구현할 수 있습니다. 이러한 로직은 네트워크 연결이나 사용자 입력과 같이 일시적인 실패가 발생할 수 있는 작업에 적용될 수 있습니다.
구조
재시도 로직은 다음과 같은 구조를 가집니다.
- 실패 가능성이 있는 작업을
loop
문 내부에 둡니다. - 작업이 성공하면
exit
문을 통해 루프를 종료합니다. - 작업이 실패하여 예외가 발생하면,
exception
처리기가 제어권을 얻습니다. - 처리기는 재시도 횟수를 확인하고, 로그를 기록하는 등의 작업을 수행한 후 루프의 다음 반복으로 진행합니다.
- 정의된 재시도 횟수를 초과하면, 처리기는
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;
7.3.6 예외 처리부의 중첩
Ada에서는 문법적으로 예외 처리부(exception
구역) 안에 또 다른 begin ... end
블록을 중첩하여 배치할 수 있습니다.
begin
-- ... 외부 블록의 실행문 ...
exception
when others =>
-- 외부 처리기
begin
-- ... 내부 블록의 실행문 ...
exception
when others =>
-- 내부 처리기
null;
end;
end;
이러한 구조는 다음과 같은 특성을 가집니다.
- 실행 흐름: 중첩된 구조는 코드의 실행 흐름과 예외 전파 경로를 복잡하게 할 수 있습니다.
- 가독성: 코드 블록의 중첩은 가독성과 유지보수성에 영향을 줄 수 있습니다.
- 대안 구조: 처리기 내부의 로직을 별도의 서브프로그램으로 분리하여 호출하는 구조 또한 가능합니다.
이러한 중첩 구조는 문법적으로 허용됩니다. 실제 설계에서는 예외 처리기의 로직을 단순하게 유지하거나 복잡한 작업을 서브프로그램으로 분리하는 방법을 사용하기도 합니다.
7.4 반환값 생성을 위한 예외 처리 (Ada 2022)
7.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
문의 주된 기능은 다음과 같습니다.
-
반환 객체 초기화 중 예외의 지역적 처리:
do
블록 내에exception
처리부를 두어, 반환 객체를 초기화하는 과정에서 발생하는 예외를 해당 블록 내에서 처리하도록 허용합니다. 이를 통해 예외가 함수 외부로 전파되기 전에 반환 객체의 상태를 사전에 정의된 값으로 설정할 수 있습니다. -
반환값의 확정:
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
7.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
문은 객체 생성 로직과 실패 시의 복구 로직을 하나의 구문 블록 안에 함께 배치할 수 있는 구조를 제공합니다.
7.5 예외 전파
예외가 발생했을 때 현재 실행 중인 블록에 해당 예외를 처리할 수 있는 when
절이 없으면, 예외는 소멸되지 않고 자신을 호출한 코드로 전파됩니다. 이처럼 예외가 호출 스택을 거슬러 올라가는 과정을 예외 전파(exception propagation)라고 합니다.
전파 과정
- 서브프로그램이나 블록문(
declare ... begin ... end
) 실행 중 예외가 발생합니다. - 해당 블록의 정상적인 실행은 즉시 중단됩니다.
- Ada 런타임은 해당 블록의
exception
처리 구역에서 발생한 예외와 일치하는when
절을 찾습니다. - 만약 일치하는
when
절이 없으면 (또는exception
구역 자체가 없으면), 예외는 전파됩니다. - 전파된다는 것은, 현재 블록을 호출했던 지점에서 동일한 예외가 다시 발생하는 것을 의미합니다. 이 과정은 적절한 예외 처리기를 만나거나, 태스크의 최상위에 도달할 때까지 반복됩니다.
예시
다음 코드에서 procedure_b
는 Constraint_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. 선언부에서의 예외
서브프로그램이나 블록의 선언부(예: is
와 begin
사이)에서 예외가 발생하면, 해당 블록의 exception
처리기는 이를 처리하지 못하고 즉시 상위 실행 문맥으로 전파됩니다.
procedure example is
-- 선언부에서 Constraint_Error 발생
n : Positive := 0;
begin
null;
exception
-- 이 처리기는 선언부의 예외를 처리할 수 없음
when Constraint_Error =>
Ada.Text_IO.put_line ("이 메시지는 출력되지 않습니다.");
end example;
2. 태스크에서의 예외
처리되지 않은 예외가 태스크(task)의 최상위 레벨까지 전파되면, 더 이상 다른 곳으로 전파되지 않고 해당 태스크가 종료됩니다. 자세한 내용은 태스크를 다루는 8.1절에서 다시 설명하겠습니다.
7.6 Ada.Exceptions 패키지
Ada.Exceptions
패키지는 예외 관련 정보를 다루기 위한 타입과 서브프로그램을 제공합니다.
7.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)에 저장하거나 다른 태스크로 전달하는 등의 동적 처리를 가능하게 합니다.
7.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;
7.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;
7.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;
7.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;
7.7 단언(assertion)과 예외
7.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
프로시저에는 별도의 예외 처리부가 없으므로, 발생한 예외는 프로시저를 호출한 외부 코드로 전달(전파)되며, 호출한 코드에서 해당 예외를 처리할 수 있습니다.
7.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;
7.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 assert
는 assertion_policy
가 ignore
로 설정되면 해당 검사는 컴파일 시 제거될 수 있습니다. 그러나 Ada.Assertions.assert
프로시저는 assertion_policy
설정과 관계없이 항상 실행되어 조건을 검사합니다.
assert
프로시저는 assertion_policy
의 영향을 받지 않는 특성으로 인해, 컴파일러 정책에 의해 비활성화되지 않아야 하는 불변 조건이나 계약(contract)을 검증하는 데 사용됩니다.
7.7.4 표준 라이브러리에서의 단언 검사
Ada 표준 라이브러리의 특정 패키지들은 사전조건(pre
), 정적 술어(static_predicate
), 동적 술어(dynamic_predicate
)와 같은 단언(assertion)의 준수 여부를 확인하기 위한 검사를 포함합니다.
각각의 단언 검사는 특정 표준 라이브러리 구성 요소(예: 패키지)와 그 안에 포함된 모든 후손 단위(descendant unit)에 적용됩니다. 후손 단위란 특정 구성 요소 내부에 한 단계 또는 여러 단계에 걸쳐 중첩된 모든 패키지, 서브프로그램 등을 의미합니다.
예를 들어 Ada 코드에서 A
내부에 B
가 선언되고, 다시 B
내부에 C
가 선언된 경우, B
와 C
는 모두 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
의 모든 후손 단위인 B
와 C
내부의 모든 선언에 효력을 미칩니다. 구체적인 적용 대상은 다음과 같습니다.
- 후손 단위(
B
와C
모두) 안에 선언된 모든 개체(변수, 타입 등) - 이러한 후손 단위(
B
나C
)에 속한 제네릭(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
패키지와 관련된 단언을 검사합니다.
7.8 런타임 검사 억제
이 절에서는 7.2.1항에서 설명한 다양한 런타임 검사들을 pragma suppress
를 통해 어떻게 비활성화하는지 알아봅니다.
7.8.1 pragma suppress
와 unsuppress
pragma suppress
는 컴파일러에 특정 런타임 검사를 생략하도록 지시하는 프라그마입니다. 반대로 pragma unsuppress
는 이전에 부여된 검사 생략을 철회합니다.
구문
pragma suppress
와 pragma 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 suppress
와 pragma 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)
는 서로 다른 검사에 대한 프라그마이므로, 내부 범위에서는 두 검사가 모두 억제되는 효과가 나타납니다. 이렇게 내부 범위에서 적용된 프라그마의 효력은 해당 범위가 끝나면 사라지고, 외부 범위의 프라그마 효력만 남게 됩니다.
7.8.2 억제의 효과와 위험성
pragma suppress
는 특정 런타임 검사를 생략하도록 컴파일러에 지시하여 프로그램의 실행 효율을 높이는 데 활용될 수 있습니다. Ada 레퍼런스 매뉴얼은 억제된 검사에 대해 컴파일러가 관련 실행 코드를 가능한 한 최소화할 것을 권장합니다.
억제된 검사가 감지해야 할 오류 상황이 발생하면 프로그램은 오류 상태(erroneous)가 됩니다. 이때 컴파일러는 예외를 발생시키지 않고 해당 연산이 미정의된 결과(undefined result)를 반환하도록 할 수 있으며, 이 값은 데이터 손상이나 논리적 오류의 원인이 될 수 있습니다. 단, 컴파일러는 미정의된 결과가 프로그램의 외부 상호작용에 영향을 줄 경우에만 예외를 발생시킬 의무가 있으므로, 외부 영향이 없다면 프로그램은 예외 발생 없이 실행이 계속될 수 있습니다.
pragma suppress
는 검사 제거를 보장하지 않습니다. 이는 컴파일러에 검사 생략을 허용하는 것이며, 강제하는 것은 아닙니다. 따라서 이 지시문은 효율성 향상 목적으로만 유용합니다.
7.9 최적화에 따른 예외 처리 방식의 변화
Ada 레퍼런스 매뉴얼에 따르면 프로그램의 외부적으로 관찰 가능한 효과가 동일하게 유지된다면, 즉, 최종 결과가 같다면 컴파일러는 예외 발생 시점이나 검사 코드를 제거하는 등의 최적화를 수행할 수 있습니다.
컴파일러는 언어에 정의된 런타임 검사가 실패하더라도 예외를 항상 발생시킬 필요는 없습니다. 만약 예외를 발생시키지 않아도 프로그램의 최종적인 외부 상호작용에 영향을 주지 않는다면, 컴파일러는 예외를 생략할 수 있습니다. 대신 해당 연산은 미정의된 결과(undefined result)를 반환할 수 있습니다. 컴파일러는 예외 처리 코드뿐만 아니라, 관련 검사 코드와 연산 자체를 제거하여 프로그램의 성능을 향상시킬 수 있습니다.
런타임 검사 실패로 인해 예외가 발생하는 경우, 컴파일러는 외부적으로 관찰되는 효과의 순서를 일부 변경할 수 있습니다. 프로그램의 외부 효과는 특정 코드 블록 내 어딘가에서 예외가 발생했다는 사실만 반영하면 되며, 실제 예외가 발생한 시점보다 더 이르거나 늦게 외부 효과가 나타날 수 있습니다.
이러한 최적화로 인해, 예외 발생 시 이전에 실행 중이던 할당문(assignment)과 같은 연산이 중단될 수 있습니다. 이 경우 객체가 부분적으로만 값이 변경되는 비정상 상태(abnormal)에 놓일 수 있으며, 이후 해당 객체를 사용하면 실행이 오류 상태(erroneous)가 될 수 있습니다.
결론적으로, 이러한 최적화 규칙들은 언어 정의 검사를 실패하는 프로그램에만 영향을 미칩니다. 런타임 검사를 항상 통과하는 프로그램의 동작은 컴파일러의 최적화 여부와 관계없이 일관되게 유지됩니다.
8. 외부 시스템과의 연동
Ada는 자체 완결적인 시스템을 구축하도록 설계되었지만, 기존 코드, 특히 C로 작성된 라이브러리와의 상호운용성을 제공하는 언어이기도 합니다. Ada는 언어 표준의 부록 B에 정의된 표준화된 메커니즘을 통해 이러한 상호운용성을 제공합니다. 이 메커니즘은 임시방편적인, 컴파일러별 기능이 아니라 언어 자체의 이식 가능한 부분이므로, 모든 호환 Ada 컴파일러에서 일관된 방식으로 연동 로직을 작성할 수 있도록 보장합니다.
8.1 C 언어와의 연동
C 상호운용성을 위한 주요 도구는 미리 정의된 라이브러리 패키지인 Interfaces.C
와 그 자식 패키지들(예: Interfaces.C.Strings
, Interfaces.C.Pointers
)입니다. 이 패키지들은 두 언어 간의 연동에 필요한 형식과 서브프로그램을 제공합니다.
형식 매핑
중요한 첫 단계는 데이터 형식을 올바르게 매핑하는 것입니다. Interfaces.C
패키지는 주어진 플랫폼에서 C의 대응 형식과 동일한 크기와 표현을 가짐이 보장되는 Ada 형식 집합을 제공합니다. 이는 언어 경계에서 데이터 손상을 방지합니다.
C 서브프로그램 및 변수 가져오기
Ada에서 C 함수를 호출하려면, 해당하는 Ada 서브프로그램을 선언하고 이를 가져오기(import)용으로 표시해야 합니다. 이는 with import => True, convention => c
애스펙트(또는 구식의 pragma import
)를 사용하여 수행됩니다. convention => c
부분은 Ada 컴파일러에 매개변수 전달 및 반환 값 처리에 C 호출 규약을 사용하도록 지시합니다.
// C 헤더 파일, my_lib.h에서
int multiply_by_two(int value);
-- Ada 소스 파일에서
with Interfaces.C; use Interfaces.C;
procedure call_c_function is
-- C 함수에 매핑되는 Ada 함수 선언
function multiply_by_two (value : int) return int
with import => True,
convention => c,
external_name => "multiply_by_two"; -- C 함수의 이름
result : int;
begin
result := multiply_by_two (10); -- C 함수를 호출함
end call_c_function;
C 전역 변수도 비슷한 방식으로 가져올 수 있습니다.
Ada 서브프로그램을 C에서 호출 가능하도록 내보내기
그 반대도 가능합니다. with export => True, convention => c
애스펙트를 사용하여 Ada 서브프로그램을 C 코드에서 호출 가능하게 만들 수 있습니다. Ada 컴파일러는 C와 호환되는 링크를 가진 함수를 생성하여 모든 C 모듈에서 호출할 수 있게 합니다.
-- Ada 패키지 명세에서
package Ada_Library is
function add (a, b : int) return int
with export => True,
convention => c,
external_name => "ada_add";
end Ada_Library;
안전 경계 관리
연동 패키지의 설계는 Ada의 안전 지향 설계를 반영합니다. 이들은 Ada와 C 사이의 전환을 관리하는 도구를 제공합니다. 이는 문자열과 포인터 처리에서 명백하게 드러납니다.
C 스타일 문자열은 단순한 널-종단 문자 배열(char*
)로, 버퍼 오버플로 취약점의 흔한 원인입니다. 반면, Ada의 네이티브 String
형식은 항상 자신의 길이를 아는 경계가 있는 객체입니다. Interfaces.C.Strings
패키지는 이 두 표현 사이를 안전하게 변환하는 함수(to_c
, to_ada
)를 제공합니다. 또한 C 문자열을 처리하기 위한 특별한 접근 형식인 chars_ptr
과 chars_ptr
을 안전한 Ada String
으로 변환하는 value
와 같은 함수를 제공합니다.
이 설계는 프로그래머가 언어 경계를 의식하도록 강제합니다. 안전하지 않은 C 구조는 경계에서 처리됩니다. 예를 들어, value
함수에 C의 널 포인터가 전달되면 충돌이나 미정의 동작을 일으키지 않고, 대신 처리 가능한 Ada 예외를 발생시킵니다. 이런 방식으로 Ada는 C 코드와의 인터페이스에서 안전 의미론을 적용하여 경계를 관리하고 위험을 억제하려고 시도합니다.
9. 동시성 및 실시간 프로그래밍
Ada의 특징 중 하나는 동시성, 즉 병렬 처리를 위한 내장 지원입니다. 외부 라이브러리(예: C의 pthreads)나 플랫폼별 API에 의존하는 많은 언어와 달리, Ada의 동시성 기능은 언어 명세의 필수적인 부분입니다. 이는 이식성, 안전성, 정확성에 영향을 미칩니다. 동시성 Ada 프로그램은 작은 베어-메탈 임베디드 시스템에서부터 멀티코어 서버에 이르기까지 호환되는 컴파일러가 있는 모든 플랫폼으로 이식 가능하며, 동시성 의미론은 일관성이 보장됩니다. 컴파일러는 동시성 구조를 인지하고 있어 라이브러리 기반 접근 방식으로는 불가능한 검사와 최적화를 수행할 수 있습니다.
Ada는 표현력, 성능, 형식적 분석 가능성 사이의 트레이드오프 공간에서 서로 다른 지점에 적합한 다양한 도구를 제공하는 동시성 모델의 스펙트럼을 제공합니다.
9.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
블록을 포함할 수 있으며, 이 블록은 태스크 내부에서 발생한 예외를 처리하는 데 사용됩니다. - 처리되지 않은 예외의 전파: 처리되지 않은 예외가 태스크 본체의 최상위 레벨까지 전파될 경우, 해당 태스크는 종료되며 더 이상 다른 실행 문맥으로 예외가 전파되지 않습니다. 이 예외는 태스크를 호출한 상위 태스크나 다른 태스크로 전파되지 않습니다.
- 태스크 종료: 태스크가 예외로 인해 종료되면, 해당 태스크에 종속된 태스크들도 함께 종료됩니다.
9.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
)를 취할 수 있습니다.
9.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;
보호 객체를 사용하면 프로그래머가 데이터와 연산을 선언하고, 컴파일러와 런타임 시스템이 기본 잠금 메커니즘을 자동으로 처리합니다. 이는 경쟁 조건과 교착 상태를 구조적으로 방지하여, 동시성 프로그래밍을 더 안전하고 간단하게 만듭니다.
9.4 Ravenscar 프로파일
안전이 중요한 하드 실시간 시스템의 경우, Ada 동시성 기능의 전체 집합은 타이밍 동작과 스케줄링 가능성을 형식적으로 분석하기에 너무 복잡할 수 있습니다. 이를 해결하기 위해 Ravenscar 프로파일(Ravenscar Profile)이 개발되어 Ada 2005에서 표준화되었습니다.
Ravenscar 프로파일은 실시간 프로그래밍에 충분히 강력하면서도 형식적 정적 분석에 적합할 만큼 간단한, 선택된 Ada 태스킹 기능의 하위 집합입니다. 그 제한 사항은 일반적으로 다음을 포함합니다:
- 동적 생성이나 종료가 없는 고정된 수의 태스크.
- 일반적으로 보호 객체로 제한되는 단순화된 태스크 통신 모델.
else
부분이 있는 복잡한 랑데부나select
문 없음.- 단순하고 예측 가능한 스케줄링 정책.
이 프로파일을 준수함으로써, 개발자는 교착 상태의 부재 및 하드 데드라인 충족과 같은 속성을 증명할 수 있는 동시성 시스템을 구축할 수 있습니다. 이는 항공 전자 공학의 DO-178C와 같은 안전 표준에 대해 Ada 소프트웨어를 인증하는 것을 가능하게 합니다.
10. 계약에 의한 설계(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 => ...
)은 개인 형식의 속성으로, 공개 연산의 경계에서 검사됩니다.
계약은 유용한 도구입니다. 컴파일된 코드와 일치하는 문서를 제공합니다. 테스트와 디버깅을 위한 정확한 기반 역할을 하며, 형식적 검증 도구에 대한 중요한 입력이 됩니다.
11. SPARK 소개
SPARK(“SPADE Ada Kernel”의 합성어)는 고신뢰성 소프트웨어 개발을 위해 특별히 설계된 Ada 언어의 형식적으로 분석 가능한 하위 집합입니다.
SPARK는 두 가지 전략의 조합을 통해 정확성 증명을 가능하게 하는 목표를 달성합니다:
-
언어 부분집합화: 일반적인 접근 형식(에일리어싱 방지), 예외 처리(모든 런타임 오류가 없음을 증명하므로), 함수 내 부작용 등 형식적으로 분석하기 어려운 Ada의 기능을 제외합니다.
-
강화된 계약: 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
-
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 ↩
-
Ada 2022 Reference Manual, Introduction, Design Goals ↩
-
Booch, Grady, et al. Object-Oriented Analysis and Design with Applications. 3rd ed., Addison-Wesley, 2007. ↩
-
Johnson, Ralph (August 26, 1991). “Designing Reusable Classes” (PDF). www.cse.msu.edu. ↩
-
Stroustrup, Bjarne (February 19, 2007). “Bjarne Stroustrup’s C++ Glossary”. “polymorphism – providing a single interface to entities of different types.” ↩
-
‘정교화’란, 선언된 항목이 런타임에 처음으로 사용 가능하게 되는 과정을 의미합니다. 자세한 내용은 ‘5.4 정교화 (elaboration)’를 참조하십시오. ↩