표명

컴퓨터 프로그래밍에서 표명(表明), 가정 설정문(假定設定文) 또는 어서션()은 프로그램 안에 추가하는 참·거짓을 미리 가정하는 문이다. 개발자는 해당 문이 그 문의 장소에서 언제나 참이라고 간주한다. 런타임 중에 표명이 거짓으로 평가되면 표명 실패(assertion failure)를 초래하며 이 상황에서는 일반적으로 실행이 중단된다.

 세부 설명 
다음의 코드에는 두 개의 표명 x > 0, x > 1이 있으며 실행 중에 이들은 주어진 위치에서 실제로 참으로 간주된다:
x = 1;
assert (x > 0);
x++;
assert (x > 1);
프로그래머들이 표명을 사용하면 프로그램들을 지정하고 프로그램 유효성을 추정할 수 있다. 이를테면 전제 조건(precondition), 즉 특정 코드 부분의 시작점에 위치한 표명은 코드가 실행될 것으로 예측하는 상태의 집합을 결정한다. 후행 조건(postcondition), 즉, 마지막에 위치한 표명은 실행 종료 시점에서 예측되는 상태를 기술한다. (예: x > 0 { x++ } x > 1)

상기의 예는 토니 호어가 그의 1969년 논문에 표명을 포함하기 위해 기술하고 있다.C.A.R. Hoare, An axiomatic basis for computer programming, Communications of the ACM, 1969. 해당 기술은 현존하는 주류 프로그래밍 언어에서는 이용할 수 없다. 그러나 프로그래머들은 프로그래밍 언어의 주석 기능을 이용하여 비검사형 표명(unchecked assertion)을 포함할 수 있다. 이를테면 C에서는 다음과 같다:
x = 5;
x = x + 1;
// {x > 1}
주석에 들어간 중괄호를 통해 다른 용도의 주석과 구별할 수 있다.

라이브러리를 통해 표명 기능을 이용할 수 있다. 이를테면 C99을 지원하는 glibc를 이용하는 C에서는 다음과 같다:
#include <assert.h>
x = 5;
x = x + 1;
assert(x > 1);

현대의 일부 프로그래밍 언어는 런타임 중에서나 이따금 정적인 상태에서 검사되는 문을 가리키는 검사형 표명(checked assertion)을 포함하기도 한다. 런타임 중에 표명이 거짓으로 평가되면 표명 실패(assertion failure)를 초래하여 실행이 중단되는 것이 일반적이다. 이를 통해 논리적 불일치가 감지된 위치에 집중할 수 있고 개발자는 의외의 결과가 나타나는 경우 보다 이 방법을 더 선호할 수 있다.

표명을 이용하면 프로그래머가 프로그램에 대한 설계, 개발, 추론을 도울 수 있다.

 용도 
에펠과 같은 언어에서 표명은 설계 프로세스의 일부를 형성한다. C나 자바와 같은 다른 언어들은 런타임에 가정을 확인하는 용도로만 이를 사용한다. 두 경우 모두 런타임에 유효성을 검사할 수 있지만, 보통은 억제(비활성화)할 수도 있다.

 계약에 의한 설계에서의 표명 
표명은 문서화의 한 형태로 기능할 수 있다. 코드가 실행되기 전에 기대하는 상태(사전 조건)와 실행이 끝났을 때 기대하는 결과 상태(사후 조건)를 설명할 수 있으며, 클래스의 불변 조건을 명시할 수도 있다. 에펠은 이러한 표명을 언어에 통합하고 이를 자동으로 추출하여 클래스를 문서화한다. 이는 계약에 의한 설계 방법론의 중요한 부분을 형성한다.

이 접근 방식은 표명을 명시적으로 지원하지 않는 언어에서도 유용하다. 주석으로 표명을 남기는 것보다 표명 문을 사용하는 것의 장점은 프로그램이 실행될 때마다 표명을 확인할 수 있다는 것이다. 표명이 더 이상 유효하지 않으면 오류를 보고할 수 있으며, 이를 통해 코드가 표명과 동기화되지 않는 것을 방지한다.

예를 들어, 다음은 C++(C++26 계약 사용)에서의 계약에 의한 설계를 보여준다.
int f(const int x)
    pre(x != 1) // 사전 조건 표명
    post(r : r == x && r != 2) // 사후 조건 표명; r은 f의 결과 객체 이름을 지정함
{
    contract_assert(x != 3); // 표명 문
    return x;
}

 런타임 검사를 위한 표명 
표명은 프로그램 구현 중에 프로그래머가 내린 가정이 프로그램 실행 시에도 여전히 유효한지 확인하는 데 사용될 수 있다. 예를 들어, 다음 자바 코드를 살펴보자.

int total = countNumberOfUsers();
if (total % 2 == 0) {
    // total이 짝수인 경우
} else {
    // total이 홀수이고 음수가 아닌 경우
    assert total % 2 == 1;
}

자바에서 %는 나머지 연산자이며, 첫 번째 피연산자가 음수이면 결과도 음수가 될 수 있다(수학에서 사용되는 나머지 연산과 다름). 여기서 프로그래머는 total이 음수가 아니라고 가정했으므로, 2로 나눈 나머지는 항상 0 또는 1이 된다. 표명은 이 가정을 명시적으로 만든다. 만약 countNumberOfUsers가 음수 값을 반환한다면 프로그램에 버그가 있는 것일 수 있다.

이 기술의 주요 장점은 오류가 발생했을 때 모호한 효과를 통해 나중에 발견되는 것이 아니라, 즉각적이고 직접적으로 감지된다는 것이다. 표명 실패는 보통 코드 위치를 보고하므로 추가적인 디버깅 없이도 오류 지점을 정확히 찾아낼 수 있는 경우가 많다.

표명은 때때로 실행이 도달해서는 안 되는 지점에 배치되기도 한다. 예를 들어, C, C++, 자바와 같은 언어의 switch 문에서 default 절에 표명을 배치할 수 있다. 프로그래머가 의도적으로 처리하지 않은 케이스가 발생하면 오류가 발생하고 프로그램이 중단되어, 잘못된 상태에서 자동으로 계속 실행되는 것을 방지한다. D에서는 switch 문에 default 절이 포함되지 않은 경우 이러한 표명이 자동으로 추가된다.

자바에서 표명은 1.4 버전부터 언어의 일부가 되었다. 표명 실패는 적절한 플래그와 함께 프로그램을 실행했을 때 AssertionError를 발생시키며, 플래그가 없으면 표명 문은 무시된다. C에서는 표준 헤더인 <assert.h>에서 실패 시 오류를 알리고 보통 프로그램을 종료하는 매크로로 assert(assertion)를 정의한다. C++에서는 <assert.h>와 <cassert> 헤더 모두에서 assert 매크로를 제공한다.

표명의 위험성은 메모리 데이터를 변경하거나 스레드 타이밍을 변경하여 부수 효과를 일으킬 수 있다는 점이다. 표명은 프로그램 코드에 부수 효과를 일으키지 않도록 신중하게 구현되어야 한다.

언어의 표명 구문을 사용하면 서드파티 라이브러리 없이도 간편하게 테스트 주도 개발(TDD)을 수행할 수 있다.

C 샤프에는 표명 매크로나 키워드가 없지만, 대신 Assert() 메서드를 제공하는 System.Diagnostics.Debug 및 System.Diagnostics.Trace 클래스가 있다.

러스트에는 assert!() 매크로가 있다.

 개발 주기 동안의 표명 

개발 주기 동안 프로그래머는 일반적으로 표명을 활성화한 상태로 프로그램을 실행한다. 표명 실패가 발생하면 프로그래머는 즉시 문제를 통지받는다. 많은 표명 구현체는 프로그램 실행을 중단시키기도 하는데, 이는 표명 위반 발생 후 프로그램이 계속 실행될 경우 상태가 오염되어 문제의 원인을 찾기 더 어려워질 수 있기 때문에 유용하다. 표명 실패로 제공되는 정보(실패 위치, 스택 추적, 또는 환경이 코어 덤프를 지원하거나 디버거에서 실행 중인 경우 전체 프로그램 상태 등)를 사용하여 프로그래머는 대개 문제를 수정할 수 있다. 따라서 표명은 디버깅에서 매우 강력한 도구를 제공한다.

 운영 환경에서의 표명 
프로그램이 운영 환경에 배포될 때는 오버헤드나 부수 효과를 피하기 위해 보통 표명을 끈다. 매크로를 통한 C/C++ 표명과 같이 배포된 코드에서 표명이 완전히 사라지는 경우도 있다. 자바와 같은 다른 경우에는 배포된 코드에 표명이 포함되어 있으며, 디버깅을 위해 현장에서 활성화할 수 있다.Programming With Assertions, Enabling and Disabling Assertions

표명은 또한 컴파일러에게 특정 에지 케이스 조건이 실제로 도달 가능하지 않음을 약속하여, 그렇지 않으면 불가능했을 특정 컴파일러 최적화를 허용하는 데 사용될 수도 있다. 이 경우 표명을 비활성화하면 오히려 성능이 저하될 수 있다.

 정적 표명 

컴파일 시간에 검사되는 표명을 정적 표명이라고 한다.

정적 표명은 컴파일 시간 템플릿 메타프로그래밍에서 특히 유용하지만, 표명이 실패할 경우에만 잘못된 코드를 도입함으로써 C와 같은 저수준 언어에서도 사용될 수 있다. C11 및 C++11은 static_assert를 통해 정적 표명을 직접 지원한다. 이전 C 버전에서는 예를 들어 다음과 같이 정적 표명을 구현할 수 있다.

#define SASSERT(pred) switch(0){case 0:case pred:;}

SASSERT( BOOLEAN CONDITION );

만약 (BOOLEAN CONDITION) 부분이 거짓으로 평가되면, 컴파일러가 동일한 상수를 가진 두 개의 case 레이블을 허용하지 않으므로 위 코드는 컴파일되지 않는다. 불리언 식은 컴파일 시간 상수 값이어야 한다. 예를 들어 (sizeof(int)==4)는 해당 문맥에서 유효한 식이 될 것이다. 이 구문은 파일 스코프(즉, 함수 외부)에서는 작동하지 않으므로 함수 내부에 감싸져야 한다.

또 다른 대중적인Jon Jagger, Compile Time Assertions in C, 1999. C에서의 표명 구현 방법은 다음과 같다.

static char const static_assertion[ (BOOLEAN CONDITION)
                                    ? 1 : -1
                                  ] = {'!'};

만약 (BOOLEAN CONDITION) 부분이 거짓으로 평가되면, 배열은 음수 길이를 가질 수 없으므로 위 코드는 컴파일되지 않는다. 만약 컴파일러가 음수 길이를 허용하더라도 초기화 바이트('!' 부분)로 인해 관대한 컴파일러조차 오류를 발생시키게 된다. 불리언 식은 컴파일 시간 상수 값이어야 한다.

이러한 방법들은 모두 고유한 이름을 생성하는 방법이 필요하다. 현대적인 컴파일러들은 각 컴파일 단위에 대해 단조 증가하는 숫자를 반환하여 고유한 이름 생성을 용이하게 하는 __COUNTER__ 전처리기 정의를 지원한다.GNU, "GCC 4.3 Release Series — Changes, New Features, and Fixes"

D는 static assert를 사용하여 정적 표명을 제공한다.

 표명 비활성화 

대부분의 언어는 표명을 전역적으로, 때로는 독립적으로 활성화하거나 비활성화할 수 있도록 허용한다. 표명은 개발 중에 활성화되고 최종 테스트 및 고객 출시 시에는 비활성화되는 경우가 많다. 표명을 확인하지 않으면 (표명에 부수 효과가 없다고 가정할 때) 정상적인 조건에서 동일한 결과를 생성하면서 표명을 평가하는 비용을 피할 수 있다. 비정상적인 조건에서 표명 확인을 비활성화하면 중단되었을 프로그램이 계속 실행될 수 있음을 의미한다. 이는 때때로 선호되는 방식이다.

C, YASS, C++를 포함한 일부 언어는 전처리기를 사용하여 컴파일 시간에 표명을 완전히 제거할 수 있다.

마찬가지로, 파이썬 인터프리터를 실행할 때 인수로 ""(최적화를 의미)를 전달하면 파이썬 코드 생성기가 표명에 대한 바이트코드를 생성하지 않게 된다.Official Python Docs, assert statement

자바는 표명을 활성화하기 위해 런타임 엔진에 옵션을 전달해야 한다. 옵션이 없으면 표명은 무시되지만, JIT 컴파일러에 의해 최적화되거나 프로그래머가 각 표명을 if (false) 절 뒤에 수동으로 배치하여 컴파일 시 제외하지 않는 한 항상 코드에 남아 있다.

프로그래머는 언어의 일반적인 표명 확인 메커니즘을 우회하거나 조작하여 항상 활성화된 검사 기능을 코드에 직접 구축할 수도 있다.

 오류 처리와의 비교 

표명은 일상적인 예외 처리와는 구별된다. 표명은 논리적으로 불가능한 상황을 기록하고 프로그래밍 오류를 발견하는 데 사용된다. 불가능한 상황이 발생한다면 프로그램의 근본적인 부분에 분명히 문제가 있는 것이다. 이는 오류 처리와는 다르다. 대부분의 오류 조건은 가능하며, 일부는 실제 상황에서 발생할 가능성이 매우 희박할 뿐이다. 표명을 범용적인 오류 처리 메커니즘으로 사용하는 것은 현명하지 않다. 표명은 오류로부터의 복구를 허용하지 않으며, 표명 실패는 대개 프로그램 실행을 갑작스럽게 중단시킨다. 또한 운영 코드에서는 표명이 비활성화되는 경우가 많다. 표명은 사용자 친화적인 오류 메시지도 표시하지 않는다.

오류 처리를 위해 표명을 사용하는 다음 예시를 살펴보자.

int* ptr = (int*)malloc(sizeof(int) * 10);
assert(ptr);
// ptr 사용
...

여기서 프로그래머는 메모리가 할당되지 않을 경우 malloc이 NULL 포인터를 반환할 것임을 알고 있다. 이는 가능한 상황이다. 운영 체제는 malloc에 대한 모든 호출이 성공할 것이라고 보장하지 않는다. 메모리 부족 오류가 발생하면 프로그램은 즉시 중단될 것이다. 표명이 없다면 프로그램은 ptr이 역참조될 때까지, 혹은 사용 중인 하드웨어에 따라 더 오래 계속 실행될 것이다. 표명이 비활성화되지 않는 한 즉각적인 종료가 보장된다. 그러나 우아한 실패(graceful failure)를 원한다면 프로그램이 해당 실패를 처리해야 한다. 예를 들어 서버에 여러 클라이언트가 있거나, 깔끔하게 해제되지 않을 리소스를 보유하고 있거나, 데이터 저장소에 써야 할 커밋되지 않은 변경 사항이 있을 수 있다. 이러한 경우 갑자기 중단되는 것보다 단일 트랜잭션을 실패 처리하는 것이 더 낫다.

또 다른 오류는 표명의 인수로 사용된 식의 부수 효과에 의존하는 것이다. 표명의 유일한 목적은 항상 참이어야 하는 조건이 실제로 참인지 확인하는 것이므로, 표명이 전혀 실행되지 않을 수도 있음을 항상 명심해야 한다. 결과적으로 프로그램에 오류가 없다고 판단되어 출시될 때 표명이 비활성화되면 더 이상 평가되지 않는다.

이전 예제의 다른 버전을 살펴보자.

int* ptr;
// 아래 문장은 malloc()이 NULL을 반환하면 실패하지만,
// -NDEBUG로 컴파일할 때는 전혀 실행되지 않는다!
assert(ptr = (int*)malloc(sizeof(int) * 10));
// ptr 사용: -NDEBUG로 컴파일할 때 ptr은 초기화되지 않는다!
...

이것은 malloc의 반환 값을 ptr에 할당하고 그것이 NULL인지 확인하는 것을 한 단계에 처리하는 영리한 방법처럼 보일 수 있지만, malloc 호출과 ptr에 대한 할당은 assert 조건을 형성하는 식을 평가할 때 발생하는 부수 효과다. 프로그램에 오류가 없다고 간주되어 출시될 때처럼 컴파일러에 NDEBUG 매개변수가 전달되면 assert() 문이 제거되므로 malloc()이 호출되지 않아 ptr이 초기화되지 않은 상태로 남게 된다. 이는 잠재적으로 프로그램 실행 훨씬 뒷부분에서 세그멘테이션 오류나 유사한 널 포인터 오류를 발생시킬 수 있으며, 하이젠버그처럼 산발적이거나 추적하기 어려운 버그의 원인이 된다. 프로그래머들은 때때로 이 문제를 완화하기 위해 유사한 VERIFY(X) 정의를 사용한다.

현대적인 컴파일러는 위와 같은 코드를 만났을 때 경고를 표시할 수 있다.

 역사 
1947년 존 폰 노이만과 허먼 골드스타인의 IAS 머신 설계에 관한 보고서에서Goldstine and von Neumann. "Planning and Coding of problems for an Electronic Computing Instrument" . Part II, Volume I, 1 April 1947, p. 12. 그들은 초기 형태의 순서도를 사용하는 알고리즘을 설명하면서 표명을 포함했다. "C가 실제로 흐름도의 특정 지점에 도달할 때마다 하나 이상의 바인딩된 변수가 필연적으로 특정 지정된 값을 갖거나, 특정 속성을 갖거나, 서로 특정 속성을 만족시키는 것이 사실일 수 있다. 나아가 우리는 그러한 지점에서 이러한 제한 사항의 유효성을 나타낼 수 있다. 이러한 이유로 우리는 그러한 제한 사항의 유효성이 표명되는 각 영역을 '표명 박스(assertion box)'라고 부르는 특별한 박스로 표시할 것이다."

프로그램의 정확성을 증명하기 위한 표명 방법은 앨런 튜링에 의해 옹호되었다. 1949년 6월 24일 케임브리지에서 열린 "대규모 루틴 점검(Checking a Large Routine)" 강연에서 튜링은 다음과 같이 제안했다. "어떻게 하면 대규모 루틴이 올바른지 확인하는 의미에서 점검할 수 있을까? 점검하는 사람의 작업이 너무 어렵지 않도록 프로그래머는 개별적으로 확인할 수 있고 전체 프로그램의 정확성이 쉽게 도출되는 여러 명확한 표명을 만들어야 한다."Alan Turing. Checking a Large Routine, 1949; quoted in C. A. R. Hoare, "The Emperor's Old Clothes", 1980 Turing Award lecture.

 같이 보기 
 예외 처리
 정적 프로그램 분석
 불변량
 assert.h

 각주 

 외부 링크 
  Programming With Assertions in Java by Qusay H. Mahmoud, (Oracle Corp.), 2005
  A historical perspective on runtime assertion checking in software development by Lori A. Clarke, David S. Rosenblum in: ACM SIGSOFT Software Engineering Notes 31(3):25-37, 2006
  The benefits of programming with assertions by Philip Guo (Stanford University), 2008
  Assertions: a personal perspective by C.A.R. Hoare in: Annals of the History of Computing, IEEE, Volume: 25, Issue: 2 (2003), Page(s): 14 - 25
  My Compiler Does Not Understand Me by Poul-Henning Kamp in: ACM Queue 10(5), May 2012

분류:정형 기법
분류:컴퓨터 과학 내 논리
분류:조건문
분류:디버깅