C++ volatile, 임베디드에서 쓰는 이유

volatile은 접근을 관측 가능한 부수 효과로 다루어 컴파일러가 죽은 코드처럼 지우거나 같은 스레드의 다른 관측 가능 부수 효과와 순서를 바꾸지 못하게 하는 한정자입니다. MMIO·ISR·시그널 핸들러처럼 프로그램 밖의 기록이 바뀌는 위치에 쓰며, 뮤텍스·원자 연산·스레드 안전의 대체는 아닙니다.

이 글은 cppreference의 cv 한정자 설명과 std::atomic 문서를 2026-08-27 기준으로 정리한 일반 설명이며, 표준 버전과 구현에 따라 세부 동작은 달라질 수 있습니다.

volatile은 컴파일러가 무엇을 못하게 하나?

한 줄 답: 이 한정 타입의 glvalue를 통한 모든 접근은 최적화 목적상 보이는 부수 효과로 취급되어, 단일 스레드 안에서 제거하거나 순서를 바꿀 수 없습니다.

cv 한정자는 함수 타입과 참조 타입을 제외한 타입에 적용됩니다. 비한정 타입, const 한정 타입, 이 한정 타입, const와 이 한정을 함께 적용한 타입은 서로 관련된 네 가지 타입을 이룹니다. 네 타입은 표현과 정렬 요구사항이 같습니다.

이 한정 타입의 glvalue 표현식을 통해 읽기·쓰기·멤버 함수 호출 등을 수행하면 각 접근이 최적화 목적상 보이는 부수 효과로 취급됩니다. 따라서 단일 실행 스레드에서 해당 접근과 sequenced-before 또는 sequenced-after 관계에 있는 다른 보이는 부수 효과와 순서를 바꾸거나 해당 접근을 최적화로 제거할 수 없습니다.

이 보장은 컴파일러 최적화에서 접근을 관측 대상으로 다루는 범위이며, 메모리 장벽이나 스레드 간 동기화를 뜻하지 않습니다. 한정 객체를 비한정 타입의 glvalue, 예를 들어 비한정 포인터나 참조로 접근하면 정의되지 않은 동작입니다.

volatile int n4 = 0;
n4 = 3;

공식 예시에서 n4 = 3;은 보이는 부수 효과로 취급됩니다. const와 이 한정을 함께 적용한 객체는 읽기 전용 성질과 보이는 접근 성질을 함께 가집니다.

MMIO와 ISR·시그널에서는 왜 쓰나?

한 줄 답: 컴파일러가 보지 못하는 외부 변경을 읽기·쓰기로 관측해야 하는 맥락에서 제거·캐싱을 피하는 단서가 되지만, 표준 문서가 직접 다루는 예시는 시그널 핸들러입니다.

MMIO, ISR이 갱신하는 플래그, 시그널 핸들러가 갱신하는 위치는 현재 실행 흐름이 직접 추적하지 못하는 이유로 값이 바뀔 수 있습니다. 이런 위치를 일반 객체처럼만 다루면 컴파일러가 반복 읽기를 유지하거나 결과에 필요하지 않다고 판단한 읽기·쓰기를 없애 외부 변경을 관측하지 못하게 할 수 있으므로, 펌웨어에서는 이 한정 타입을 사용해 접근을 관측 대상으로 표시합니다.

다만 C++의 표준 문서가 MMIO용 API나 특정 하드웨어 레지스터를 정의하는 것은 아닙니다. MMIO와 ISR에 관한 설명은 컴파일러가 볼 수 없는 변경과 보이는 접근이라는 공통 맥락에서 정리한 임베디드 설명이며, 특정 MCU의 IRQ·레지스터 명세가 아닙니다.

표준에서 직접 확인할 수 있는 대표 사례는 시그널 핸들러입니다. <csignal>std::sig_atomic_t는 비동기 시그널 인터럽트가 있어도 원자적 엔터티로 접근할 수 있는 정수형입니다.

#include <csignal>
#include <iostream>

namespace
{
    volatile std::sig_atomic_t gSignalStatus;
}

void signal_handler(int signal)
{
    gSignalStatus = signal;
}

C++11 이후 시그널 핸들러에 진입할 때 대부분 객체의 값은 unspecified이지만, 휘발성 std::sig_atomic_t 객체와 lock-free std::atomic 객체, 그리고 std::atomic_signal_fence를 통해 보이게 한 부수 효과는 예외입니다. 또한 C++14 이후에는 같은 스레드에서 시그널 핸들러를 포함해 해당 정수형 객체에 대한 두 접근이 데이터 레이스를 일으키지 않습니다. 이 규칙은 C++ 시그널 핸들러의 규칙이므로 MCU ISR의 구체적인 동작이나 인터럽트 명세를 대신하지 않습니다.

atomic이나 뮤텍스를 대신하나?

한 줄 답: 아닙니다. 이 한정자는 다른 실행 스레드와의 통신용이 아니며, 동시 접근에는 원자 타입과 필요한 동기화 도구를 선택해야 합니다.

이 한정자는 접근의 제거와 특정 순서 변경을 막는 규칙만 제공하며, 상호 배제·락 소유권·임계 구역 의미를 제공하지 않습니다. 따라서 여러 실행 스레드가 같은 객체에 동시에 접근하는 상황을 해결하는 도구로 사용할 수 없습니다.

<atomic>std::atomic은 C++11부터 각 인스턴스와 완전 특수화가 원자 타입을 정의합니다. 한 스레드가 원자 객체에 쓰는 동안 다른 스레드가 읽어도 동작이 정의되며, 원자 객체에 대한 접근은 std::memory_order에 따라 스레드 간 동기화를 수립하고 비원자 메모리 접근의 순서를 정할 수 있습니다.

라이브러리 원자 연산의 기본 메모리 순서는 sequentially consistent입니다. 상호 배제와 락 소유권이 필요한 임계 구역은 뮤텍스의 역할로 구분해야 하며, 한정 타입 하나를 추가한다고 그 의미가 생기지 않습니다.

C++ 기술면접 질문 10가지는 별도의 C++ 핵심 개념을 점검할 때 참고할 수 있는 관련 글입니다.

C++20 이후 주의할 사용은?

한 줄 답: C++20부터 일부 내장 증감·대입과 함수 매개변수·반환형, 구조적 바인딩에서의 사용은 deprecated이므로 새 코드에서는 대안을 검토해야 합니다.

C++20부터 volatile 타입의 lvalue를 내장 증감 연산자 ++·--의 피연산자로 사용하는 용법이 deprecated입니다. 또한 unevaluated context 또는 discarded-value expression인 경우를 제외하면, 내장 직접 대입의 왼쪽 피연산자로 사용하는 용법도 deprecated입니다.

함수 매개변수형이나 반환형에 휘발성 객체 타입을 사용하는 용법도 deprecated입니다. 구조적 바인딩 선언에서 이 한정자를 사용하는 용법 역시 deprecated 목록에 포함됩니다.

조사한 문서는 이 항목들을 C++20부터 deprecated로 표시하므로, C++26에서 이미 제거되었다고 단정할 수 없습니다. 새 코드는 외부 변경 관측, 시그널 처리, 스레드 간 통신 중 목적을 구분한 뒤 대상 표준과 구현을 확인해야 합니다.

std::memory_order는 원자 연산 주변의 일반 비원자 메모리 접근을 포함해 접근 순서를 지정합니다. 따라서 이는 스레드 간 순서를 다루는 별도 도구이며, C++20부터 deprecated로 표시된 모든 용법을 자동으로 대체한다는 의미는 아닙니다.

FAQ

한 줄 답: 외부 변경을 관측하는 용도와 여러 스레드의 동기화 용도를 먼저 나누면 흔한 오해를 줄일 수 있습니다.

질문마다 보장 범위와 적용 맥락을 구분해야 합니다. 아래 답변은 조사한 공식 문서의 범위에 맞춘 요약입니다.

질문답변
이 한정자는 모든 하드웨어에서 메모리 장벽입니까?아닙니다. 조사한 근거는 모든 하드웨어에서 메모리 장벽이라고 보장하지 않으며, 이 한정 타입의 규칙은 최적화 목적의 보이는 접근에 관한 것입니다.
모든 하드웨어에서 원자적입니까?아닙니다. 원자성에 대한 일반 보장은 없으며, 스레드 간 통신에는 std::atomic과 필요한 메모리 순서를 검토해야 합니다.
MMIO 주소를 직접 캐스팅한 예제를 넣어도 됩니까?이번 글에서는 공식 문서의 짧은 예시만 사용하며, 특정 벤더의 레지스터 맵과 주소는 제시하지 않습니다.
시그널 핸들러와 MCU ISR은 같은 표준 규칙입니까?아닙니다. 시그널 관련 규칙은 C++ 시그널 핸들러에 대한 것이며, MCU NVIC·IRQ 명세를 대신하지 않습니다.
C++20 이후 모두 사용할 수 없습니까?아닙니다. cppreference가 표시한 것은 일부 용법의 deprecated이며, 대상 용법과 표준 버전, 구현을 확인해야 합니다.

출처

한 줄 답: cv 한정의 최적화 규칙은 cppreference cv 문서, 시그널·원자 연산·메모리 순서는 각각 해당 cppreference 문서에서 확인합니다.

아래 문서는 본문의 최적화 규칙, 시그널 핸들러, 원자 연산, 메모리 순서를 확인하는 공식 cppreference 출처입니다. 확인 기준일은 2026-08-27이며, 표준 버전과 구현에 따라 세부 동작을 함께 확인해야 합니다.