C언어를 공부하다 보면 volatile 키워드를 한 번쯤 접하게 된다.
하지만 많은 경우 “컴파일러 최적화를 막기 위한 키워드” 정도로만 이해하고 넘어가곤 한다.
이 설명이 틀린 것은 아니지만, 이 정도 수준으로 이해하고 실제 개발에 적용하면
특정 상황에서 코드가 정상적으로 동작하지 않는 문제를 만들 수 있다.
특히 임베디드 시스템, 인터럽트, AUTOSAR 환경에서는
volatile의 역할을 정확히 이해하는 것이 매우 중요하다.
이 글에서는 volatile이 왜 필요한지, 언제 사용해야 하는지, 그리고 잘못 사용하면 어떤 문제가 발생하는지를
실제 코드 예제 중심으로 정리한다.
volatile이란 무엇인가?
volatile은 컴파일러에게 다음과 같은 의미를 전달하는 키워드이다.
“이 변수의 값은 프로그램 흐름 외부에서 언제든지 변경될 수 있다.”
여기서 “외부”란 단순히 다른 코드가 아니라 다음과 같은 상황을 포함한다.
인터럽트, 다른 task 또는 쓰레드, 하드웨어 레지스터 (MMIO) 등등..
이렇게 값이 변경될 수 있는 변수에 대해, 컴파일러는 다음과 같은 최적화를 하면 안 된다.
- 값을 레지스터에 캐싱해서 재사용
- 불필요하다고 판단하고 메모리 접근 생략
즉, volatile이 붙은 변수는 항상 메모리에서 직접 값을 읽고 써야 한다.
왜 문제가 발생하는가 (최적화 관점 이해)
컴파일러는 프로그램 성능을 높이기 위해 다양한 최적화를 수행한다.
그 중 하나가 “값이 변하지 않는 변수는 반복해서 메모리에서 읽지 않는다”는 것이다.
아래 코드를 보자.
int flag = 0;
while (flag == 0)
{
// 대기
}
사람이 보기에는 단순히 flag 값이 1로 바뀌기를 기다리는 코드다.
하지만 컴파일러 입장에서는 다음과 같이 판단할 수 있다.
- flag 값이 이 코드 내부에서 변경되지 않는다.
- 따라서 한 번 읽은 값은 계속 동일하다.
이 판단이 적용되면, 실제로는 다음과 같은 형태로 최적화될 가능성이 있다.
while (1)
{
// 무한 루프
}
즉, 외부에서 flag 값을 변경해도 루프를 빠져나오지 못하는 상황이 발생할 수 있다.
이 문제를 방지하기 위해 사용하는 것이 바로 volatile이다.
실무 활용 가이드
1) 하드웨어 레지스터 접근 (MMIO)
임베디드에서 volatile이 필수인 대표적인 상황이다.
하드웨어 레지스터는 특정 메모리 주소에 매핑되어 있으며,
이 값은 CPU가 아닌 외부 하드웨어에 의해 변경된다.
잘못된 코드
#define STATUS_REG (*(unsigned int*)0x40000000)
while ((STATUS_REG & 0x01) == 0)
{
// 상태 변화 대기
}
이 경우 컴파일러는 STATUS_REG 값을 고정된 값으로 판단할 수 있고,
결과적으로 루프가 정상적으로 동작하지 않을 수 있다.
올바른 코드
#define STATUS_REG (*(volatile unsigned int*)0x40000000)
while ((STATUS_REG & 0x01) == 0)
{
// 실제 하드웨어 상태를 지속적으로 확인
}
하드웨어 레지스터는 항상 volatile로 선언하는 것이 기본이다.
2) Task 간 공유 변수 (AUTOSAR)
멀티태스킹 환경에서도 동일한 문제가 발생할 수 있다.
잘못된 코드
int sharedData = 0;
void Task1(void)
{
while (sharedData == 0)
{
// 대기
}
}
void Task2(void)
{
sharedData = 1;
}
이 경우에도 컴파일러는 sharedData 값이 변하지 않는다고 판단할 수 있다.
올바른 코드
volatile int sharedData = 0;
다른 실행 흐름(태스크)이 값을 변경하는 경우에는 반드시 고려해야 한다.
volatile 사용 기준 (실무 핵심 정리)
실무에서는 다음 3가지 상황에서 volatile 사용을 반드시 고려해야 한다.
1. 인터럽트와 공유되는 변수 (ISR ↔ Main)
2. 하드웨어 레지스터 접근 (MMIO)
3. 태스크/쓰레드 간 공유 변수
이 세 가지는 거의 “필수 사용 케이스”라고 봐도 된다.
정리
volatile을 한 문장으로 정리하면 다음과 같다.
“컴파일러에게 이 변수는 항상 다시 읽어야 한다고 알려주는 키워드”
이 키워드를 사용하지 않으면 다음과 같은 문제가 발생할 수 있다.
- 인터럽트에서 값이 변경되어도 감지하지 못함
- 하드웨어 레지스터 값을 제대로 읽지 못함
- 태스크 간 데이터 공유 오류 발생
마지막으로 가장 중요한 기준은 이것 하나다.
“이 변수의 값이 프로그램 흐름 외부에서 변경될 가능성이 있는가?”
YES라면 → volatile을 사용해야 한다.
'C 언어 > 실무' 카테고리의 다른 글
| C언어 비트 연산자 쉽게 설명하기 (임베디드 실무 관점에서 제대로 이해하기) (0) | 2026.04.14 |
|---|---|
| C언어 extern 변수 사용법 (파일 간 변수 공유 방법) (0) | 2026.04.13 |
| C언어 배열과 포인터 차이 (헷갈리는 이유부터 핵심 차이까지) (0) | 2026.04.12 |
| C언어 포인터 쉽게 설명하기 (개념부터 실무 예제까지 한 번에) (4) | 2026.04.11 |
| C언어 static 변수는 왜 쓰는가? (실무에서 안 쓰면 생기는 문제) (0) | 2026.04.11 |