Contracts는 왜 assert()의 대체재가 아닌가
계약 위반이 일어나면 무슨 일이 벌어지는가
앞 장에서 pre·post가 계약을 표현한다는 건 봤습니다. 그런데 실제로 그 계약이 깨지면 무슨 일이 일어날까요? 이 질문의 답이 애매하다는 게 Contracts를 실무에 들이기 전에 반드시 짚어야 할 첫 번째 함정입니다.
C++26의 위반 처리는 **위반 핸들러(violation handler)**라는 개념으로 동작합니다. 기본 동작은 핸들러를 호출한 뒤 프로그램을 종료하는 쪽에 가깝지만, 정확히 어떤 함수가 호출되고 그 함수를 사용자가 어디까지 바꿔치기할 수 있는지는 표준화 막바지까지 논쟁이 계속됐던 지점입니다. 즉 "계약이 깨지면 예외가 날아오나, 프로세스가 죽나, 로그만 남고 계속 실행되나"라는 질문에 **"컴파일러와 빌드 설정에 따라 다르다"**는 답을 준비해야 합니다. 이건 표준이 미완성이라서가 아니라, 프로덕션 임베디드 시스템(위반 시 즉시 정지가 맞는 선택)과 서버 애플리케이션(로그를 남기고 최대한 서비스를 유지하는 게 나은 선택)이 원하는 동작이 근본적으로 다르기 때문에 위원회가 의도적으로 유연성을 남긴 겁니다.
int divide(int a, int b)
pre(b != 0)
{
return a / b; // b == 0 이면 pre 위반 → 핸들러 호출, 이후 동작은 빌드 설정에 달림
}이 코드만 보고 "0으로 나누면 예외가 던져지겠지"라고 가정하면 안 됩니다. 그 가정이 맞는 빌드도 있고 아닌 빌드도 있습니다.
성능 — default 계약도 공짜가 아니다
앞 장에서 audit 수준을 비용이 큰 검사에 쓰라고 했는데, default 수준이라고 해서 비용이 0은 아닙니다. 함수 호출마다 조건식을 평가하는 코드가 실제로 삽입됩니다.
// 이 함수가 초당 수백만 번 호출되는 hot path 라면?
inline int clamp_index(int i, int size)
pre(size > 0)
post(r: r >= 0 && r < size)
{
return i < 0 ? 0 : (i >= size ? size - 1 : i);
}assert는 NDEBUG로 통째로 날려버릴 수 있다는 확실한 탈출구가 있었습니다. Contracts는 빌드 모드별로 계약 평가 여부를 세밀하게 조절할 수 있게 설계됐지만, 그 조절 스위치가 컴파일러마다 어떻게 노출될지는 지금 시점엔 아직 안정화되지 않았습니다. hot path에 계약을 무분별하게 붙였다가 릴리스 빌드에서 예상 못 한 오버헤드를 만나는 시나리오는 충분히 현실적입니다. 성능이 민감한 코드라면 계약을 붙이기 전에 그 컴파일러의 기본 평가 정책부터 확인하는 게 순서입니다.
assert()를 완전히 걷어내면 안 되는 이유
그렇다면 "이제 assert는 다 지우고 Contracts로 바꾸면 되겠다"고 생각하기 쉽습니다. 저는 이게 성급하다고 봅니다. 이유는 세 가지입니다.
첫째, 컴파일러 지원이 아직 부분적입니다. 앞 장에서 본 문법 자체는 표준에 확정됐지만, 위반 핸들러의 세부 동작·ABI 호환성·최적화 상호작용까지 완전히 구현하고 검증하는 데는 시간이 더 필요합니다. assert는 30년 가까이 모든 컴파일러가 동일하게 지원해온 기능이라 신뢰도가 다릅니다.
둘째, 계약은 함수 시그니처의 일부가 되므로 ABI에 영향을 줄 수 있습니다. 라이브러리의 공개 헤더에 pre/post를 넣으면, 그 라이브러리를 쓰는 모든 코드가 재컴파일 시 계약 검사 코드를 새로 받아들이게 됩니다. 내부 구현 세부사항이던 assert와 달리, Contracts는 인터페이스 변경에 가깝습니다. 공개 API에 계약을 붙일 땐 그게 바이너리 호환성에 미치는 영향까지 생각해야 합니다.
셋째, audit/axiom 구분을 팀 전체가 일관되게 이해하고 있어야 의미가 있습니다. 한 사람은 비용이 큰 검사를 default에 넣고 다른 사람은 audit에 넣으면, 릴리스 빌드마다 무엇이 검사되고 안 되는지가 팀 내에서도 뒤죽박죽이 됩니다. 이건 기능의 결함이 아니라 팀 컨벤션 문제지만, 컨벤션이 잡히기 전까지는 오히려 assert보다 관리 부담이 커질 수 있습니다.
결론적으로 이 기능은 새로 짜는 코드, 특히 공개 API 경계가 아닌 내부 함수에 조금씩 시험해보기 좋은 자리부터 시작하는 게 맞다고 봅니다. 기존에 잘 동작하던 assert 코드를 전부 걷어내고 교체할 이유는 아직 없습니다.