[2026] C 언어 시리즈 #02 — 타입·승격(usual arithmetic)·정수 표현과 패딩
이 글의 핵심
`int`보다 작은 타입이 왜 먼저 `int`로 올라가는지, 혼합 타입 연산이 왜 비용·오버플로 의미를 바꾸는지, 그리고 이것이 포인터 연산·비트 연산과 어떻게 충돌하는지 설명합니다.
시리즈 안내
#02 | 이전: #01 기초·번역 단위 · 다음: #03 제어 흐름
1. 정수 승격(integer promotions)
int보다 작은 정수 타입(char, short, 비트 필드 등)은 여러 연산에서 먼저 int 또는 unsigned int로 승격된다. 이유는 레지스터 폭에 맞추기 쉽고, 대부분의 CPU가 네이티브 산술을 그 폭에서 수행하기 때문이다.
내부적으로 컴파일러 SSA나 중간 표현에서 확장(load) 연산이 명시적으로 생긴다. 이 비용은 작아 보여도 핫 루프에서는 누적된다.
2. 통상 산술 변환(usual arithmetic conversions)
이항 연산 a op b에서 두 피연산자를 같은 타입으로 맞추는 규칙이 있다. 요지는:
- 한쪽이
long double/double/float쪽이면 부동 쪽으로 끌어올림(세부는 표준 표 참조). - 그렇지 않으면 정수 규칙: 승격 후
unsigned혼합 시 더 넓은 부호 없는 타입으로 맞추는 등의 단계가 있다.
프로덕션 함정: size_t(보통 부호 없음)와 int를 비교하면 한쪽으로 맞춰지며, 음수가 큰 부호 없는 값으로 해석되는 큰 양수가 되어 루프 조건이 무너질 수 있다.
void scan(int *p, int n) {
/* 나쁜 예: i가 size_t로 올라가며 혼합 비교 함정 가능 */
for (int i = 0; i < n; i++) {
p[i] = i;
}
}
3. 고정폭 정수 <stdint.h>와 “바이트 크기”
int32_t 등은 옵션일 수 있다(플랫폼에 딱 맞는 폭이 없으면 typedef가 없음). 반면 int_least32_t, int_fast32_t는 다른 의미의 트레이드오프를 준다.
메모리 레이아웃: 네트워크 프로토콜에 uint32_t를 쓰더라도 엔디안은 별개다. “비트가 디스크에 이렇게 깔린다”는 보장은 memcpy와 명시적 변환으로만 안전하게 다룬다.
4. 부동·반올림 모드(FENV)
#pragma STDC FENV_ACCESS를 켜지 않은 상태에서 부동 연산과 FENV를 뒤섞으면 최적화가 FENV를 무시해도 된다는 전제가 깔릴 수 있다. 수치 라이브러리·게임 엔진·임베디드에서만큼은 컴파일 플래그·pragma·호출 경계를 문서화해야 한다.
5. 구조체와 타입 레이아웃
struct의 필드 순서는 보장되지만 패딩은 구현에 따른다. ABI가 고정된 플랫폼에서는 시스템 헤더의 구조체가 사실상 규약이 된다.
프로덕션 패턴:
- 직렬화용
struct는offsetof·정적 검증(_Static_assert)으로 스키마를 고정한다. - 컴파일러별
#pragma pack은 이식성과 UB 위험이 있으므로 팀 규약이 필요하다.
6. 열거형(enum)의 정수 폭
열거 상수는 int로 표현 가능해야 하지만, 호환 타입은 구현에 따라 다를 수 있다. GCC/Clang의 -fshort-enums 같은 옵션은 ABI를 바꾸므로 전체 프로젝트에 일관되게 적용해야 한다.
내부 동작과 핵심 메커니즘
이 글의 주제는 「[2026] C 언어 시리즈 #02 — 타입·승격(usual arithmetic)·정수 표현과 패딩」입니다. 여기서는 앞선 설명을 구현·런타임 관점에서 한 번 더 압축합니다. 구성 요소 간 책임 분리와 관측 가능한 지점을 기준으로 생각하면, “입력이 어디서 검증되고, 핵심 연산이 어디서 일어나며, 부작용(I/O·네트워크·디스크)이 어디서 터지는가”가 한눈에 드러납니다.
처리 파이프라인(개념도)
flowchart TD A[입력·요청·이벤트] --> B[파싱·검증·디코딩] B --> C[핵심 연산·상태 전이] C --> D[부작용: I/O·네트워크·동시성] D --> E[결과·관측·저장]
알고리즘·프로토콜 관점에서의 체크포인트
- 불변 조건(Invariant): 각 단계가 만족해야 하는 조건(예: 버퍼 경계, 프로토콜 상태, 트랜잭션 격리)을 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
- 결정성: 동일 입력에 동일 출력이 보장되는 순수한 층과, 시간·네트워크에 의해 달라질 수 있는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
- 경계 비용: 직렬화/역직렬화, 문자 인코딩, syscall 횟수, 락 경합처럼 “한 번의 호출이 아니라 누적되는 비용”을 의심 목록에 넣습니다.
프로덕션 운영 패턴
실서비스에서는 기능 구현과 함께 관측·배포·보안·비용이 동시에 요구됩니다. 아래는 팀에서 자주 쓰는 최소 체크리스트입니다.
| 영역 | 운영 관점에서의 질문 |
|---|---|
| 관측성 | 요청 단위 상관 ID, 에러율/지연 분위수, 주요 의존성 타임아웃이 보이는가 |
| 안전성 | 입력 검증·권한·비밀 관리가 코드 경로마다 일관적인가 |
| 신뢰성 | 재시도는 멱등한 연산에만 적용되는가, 서킷 브레이커·백오프가 있는가 |
| 성능 | 캐시 계층·배치 크기·풀링·백프레셔가 데이터 규모에 맞는가 |
| 배포 | 롤백 룬북, 카나리, 마이그레이션 호환성이 문서화되어 있는가 |
운영 환경에서는 “개발자 PC에서는 재현되지 않던 문제”가 시간·부하·데이터 크기 때문에 드러납니다. 따라서 스테이징의 데이터 양·네트워크 지연을 가능한 한 현실에 가깝게 맞추는 것이 중요합니다.
문제 해결(Troubleshooting)
| 증상 | 가능 원인 | 조치 |
|---|---|---|
| 간헐적 실패 | 레이스 컨디션, 타임아웃, 외부 의존성 불안정 | 최소 재현 스크립트 작성, 분산 트레이스·로그 상관관계 확인 |
| 성능 저하 | N+1 쿼리, 동기 I/O, 잠금 경합, 과도한 직렬화 | 프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거 |
| 메모리 증가 | 캐시 무제한, 클로저/이벤트 구독 누수, 대용량 객체의 불필요한 복사 | 상한·TTL·스냅샷 비교(힙 덤프/트레이스) |
| 빌드·배포만 실패 | 환경 변수·권한·플랫폼 차이 | CI 로그와 로컬 diff, 컨테이너/런타임 버전 핀(pin) |
권장 디버깅 순서: (1) 최소 재현 만들기 (2) 최근 변경 범위 좁히기 (3) 의존성·환경 변수 차이 확인 (4) 관측 데이터로 가설 검증 (5) 수정 후 회귀·부하 테스트.
요약
타입 규칙은 “문법”이 아니라 중간 코드에 삽입되는 확장·변환이다. 승격·통상 산술 변환을 머릿속에 두면 시프트·비교·오버플로 버그의 상당수가 설명된다.
다음: #03 제어 흐름·goto·setjmp