본문으로 건너뛰기
Embedded C++ for Real Systems · 10/41

임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리

· Hawk · 6분 읽기

#한 줄 요약

“표준 라이브러리가 5개의 RAII 도구를 제공합니다.”lock_guard, scoped_lock, unique_lock, unique_ptr, ScopeGuard 패턴.

#어떤 문제를 푸는가

RAII 자체는 단순한 원리지만, 실제 사용에서는 여러 표준 패턴이 각자의 자리를 가집니다.

  • 단순 lock에는 std::lock_guard를 씁니다.
  • 여러 mutex를 동시에 잡을 때는 std::scoped_lock이 어울립니다.
  • 수동 unlock/relock이 필요하면 std::unique_lock을 씁니다.
  • generic 자원(FD, peripheral handle)은 custom deleter를 단 std::unique_ptr로 감쌉니다.
  • 임의 cleanup 코드는 ScopeGuard(Finally) 패턴으로 처리합니다.

각 도구가 언제, 왜, 어떻게 쓰이는지 정리합니다.

#패턴 1 — std::lock_guard

C++11에 도입된 가장 단순한 mutex RAII입니다. 생성자에서 lock, 소멸자에서 unlock합니다.

#include <mutex>
std::mutex m;
int counter = 0;
void increment() {
std::lock_guard<std::mutex> lock(m); // lock 획득
counter++;
// 소멸 시 unlock
}

C++17부터는 class template argument deduction이 가능합니다.

std::lock_guard lock(m); // 타입 추론

특징은 다음과 같습니다.

  • 복사와 이동이 불가능합니다.
  • unlock을 수동으로 호출할 수 없고 scope이 끝날 때까지 lock이 유지됩니다.
  • 단일 mutex만 다룹니다.

-fno-exceptions 환경에서도 동작합니다. mutex가 예외를 던지지 않는 한 무관합니다.

#임베디드 — RTOS 적응

std::mutex는 OS thread를 가정합니다. RTOS는 자체 mutex API를 쓰므로 RAII wrapper를 직접 만듭니다.

class FreeRtosMutex {
SemaphoreHandle_t handle_;
public:
FreeRtosMutex() : handle_(xSemaphoreCreateMutex()) {}
~FreeRtosMutex() { vSemaphoreDelete(handle_); }
SemaphoreHandle_t native() { return handle_; }
FreeRtosMutex(const FreeRtosMutex&) = delete;
};
class FreeRtosLockGuard {
SemaphoreHandle_t handle_;
public:
explicit FreeRtosLockGuard(FreeRtosMutex& m) : handle_(m.native()) {
xSemaphoreTake(handle_, portMAX_DELAY);
}
~FreeRtosLockGuard() {
xSemaphoreGive(handle_);
}
FreeRtosLockGuard(const FreeRtosLockGuard&) = delete;
};

사용은 다음과 같습니다.

FreeRtosMutex m;
int counter = 0;
void increment() {
FreeRtosLockGuard lock(m);
counter++;
}

#패턴 2 — std::scoped_lock (C++17)

여러 mutex를 deadlock 없이 동시에 lock합니다.

std::mutex m1, m2;
void transfer(Account& from, Account& to, int amount) {
std::scoped_lock lock(m1, m2); // 두 mutex deadlock-free
from.balance -= amount;
to.balance += amount;
}

내부적으로는 deadlock 회피 알고리즘(보통 try_lock + back-off)을 사용합니다. 두 mutex가 서로 다른 순서로 동시에 호출돼도 안전합니다.

// Thread A
std::scoped_lock lock(m1, m2);
// Thread B
std::scoped_lock lock(m2, m1); // 다른 순서 OK

std::lock_guard 두 개를 다른 순서로 잡으면 deadlock이 발생하지만, scoped_lock은 이를 피합니다.

C++17 이전에는 std::lock(m1, m2)std::lock_guard 두 개로 같은 효과를 냈습니다.

#패턴 3 — std::unique_lock

수동 unlock/relock이 필요한 경우에 씁니다. condition variable과 함께 자주 사용합니다.

std::mutex m;
std::condition_variable cv;
bool ready = false;
void wait_for_event() {
std::unique_lock lock(m);
cv.wait(lock, []{ return ready; }); // wait가 unlock/relock 함
// ready == true
}
void set_event() {
{
std::lock_guard lock(m);
ready = true;
}
cv.notify_one();
}

cv.wait는 unlock 후 대기하다 signal을 받으면 relock합니다. unique_lock이 수동 unlock을 지원하므로 함께 동작합니다.

특징은 다음과 같습니다.

  • unlock과 relock을 수동으로 호출할 수 있습니다.
  • 이동이 가능하므로 다른 함수로 lock을 넘길 수 있습니다.
  • 상태 추적 필드 때문에 lock_guard보다 약간 무겁습니다.

임베디드에서 condition variable을 쓰지 않는다면 lock_guardscoped_lock이면 충분합니다.

#패턴 4 — std::unique_ptr with Custom Deleter

unique_ptr은 RAII의 모범 사례입니다. 기본은 delete로 해제하지만, custom deleter로 어떤 자원이든 관리할 수 있습니다.

// File descriptor RAII
struct FdDeleter {
void operator()(int* fd) const {
if (fd && *fd >= 0) close(*fd);
delete fd;
}
};
using UniqueFd = std::unique_ptr<int, FdDeleter>;
UniqueFd open_file(const char* path) {
int fd = open(path, O_RDONLY);
if (fd < 0) return nullptr;
return UniqueFd(new int(fd));
}

new int가 들어가 조금 무겁습니다. 값 타입을 활용하면 더 가볍게 만들 수 있습니다.

struct Fd {
int value;
operator int() const { return value; }
};
struct FdDeleter {
void operator()(Fd* fd) const {
if (fd && fd->value >= 0) close(fd->value);
delete fd;
}
};

#임베디드 — Pool에서 할당된 객체

struct PoolDeleter {
Pool* pool;
void operator()(uint8_t* block) const {
if (block) pool->free(block);
}
};
using PoolPtr = std::unique_ptr<uint8_t, PoolDeleter>;
PoolPtr alloc_from_pool(Pool& p) {
auto* block = static_cast<uint8_t*>(p.alloc());
return PoolPtr(block, {&p});
}

unique_ptr이 pool에서 알아서 free하며, heap 자체는 사용하지 않습니다.

#함수 포인터 deleter — runtime 비용 발생

// 함수 포인터 — unique_ptr이 한 word 추가
using UniqueFile = std::unique_ptr<FILE, decltype(&fclose)>;
UniqueFile f(fopen("data.bin", "r"), &fclose);

sizeof(UniqueFile)이 8바이트가 됩니다(포인터 + 함수 포인터). struct나 lambda 같은 empty class deleter는 empty base optimization으로 4바이트로 줄어듭니다.

임베디드에서는 empty struct deleter를 권장합니다.

#패턴 5 — ScopeGuard / Finally 패턴

임의의 cleanup 코드를 RAII로 묶는 패턴입니다. 클래스를 따로 만들기 번거로운 일회성 자원에 어울립니다.

template<typename F>
class ScopeGuard {
F func_;
bool dismissed_ = false;
public:
explicit ScopeGuard(F f) : func_(std::move(f)) {}
~ScopeGuard() { if (!dismissed_) func_(); }
void dismiss() { dismissed_ = true; }
ScopeGuard(const ScopeGuard&) = delete;
};
// 헬퍼
template<typename F>
ScopeGuard<F> make_scope_guard(F f) { return ScopeGuard<F>(std::move(f)); }
// 매크로 (선택)
#define FINALLY(code) auto _guard_##__LINE__ = make_scope_guard([&]{ code; })

사용은 다음과 같습니다.

void process() {
char* buf = (char*)malloc(1024);
FINALLY(free(buf)); // 함수 끝에서 자동 free
if (validate(buf) < 0) return; // 정상 free
do_work(buf);
// 정상 free
}

C++ Core Guidelines의 gsl::finally도 같은 패턴입니다.

#임베디드 — peripheral 일시 활성

void send_uart_burst(const uint8_t* data, size_t len) {
USART2->CR1 |= USART_CR1_UE; // UART on
auto guard = make_scope_guard([]{
USART2->CR1 &= ~USART_CR1_UE; // UART off — 항상 실행
});
for (size_t i = 0; i < len; ++i) {
USART2->DR = data[i];
while (!(USART2->SR & USART_SR_TC));
}
}

함수 종료, return, 예외 어느 경로에서도 UART off가 보장됩니다.

#dismiss — commit 패턴

자원 반환을 취소하는 transaction 패턴입니다.

bool transfer(Account& from, Account& to, int amount) {
from.balance -= amount;
auto rollback = make_scope_guard([&]{ from.balance += amount; });
if (!to.deposit(amount)) return false; // rollback 발동
rollback.dismiss(); // 성공 — rollback 취소
return true;
}

#패턴 6 — Scoped 일반 wrapper

특정 enable/disable 패턴을 반복해서 쓸 때 작성합니다.

template<auto Enable, auto Disable>
struct Scoped {
Scoped() { Enable(); }
~Scoped() { Disable(); }
Scoped(const Scoped&) = delete;
};
// 사용
void enable_pins() { GPIOA->MODER |= 0x3; }
void disable_pins() { GPIOA->MODER &= ~0x3; }
using ScopedPins = Scoped<&enable_pins, &disable_pins>;
void use_pins() {
ScopedPins guard;
// pins 활성 상태
}

C++17의 auto non-type template parameter를 활용합니다. 함수 포인터가 컴파일 타임에 박히므로 오버헤드가 0입니다.

#패턴 7 — Resource Handle with Move

자원을 함수 간에 이전하고 싶을 때 쓰는 move-only 타입입니다.

class FileHandle {
int fd_ = -1;
public:
explicit FileHandle(int fd) : fd_(fd) {}
~FileHandle() { if (fd_ >= 0) close(fd_); }
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Move
FileHandle(FileHandle&& other) noexcept : fd_(other.fd_) {
other.fd_ = -1;
}
FileHandle& operator=(FileHandle&& other) noexcept {
if (this != &other) {
if (fd_ >= 0) close(fd_);
fd_ = other.fd_;
other.fd_ = -1;
}
return *this;
}
int get() const { return fd_; }
};
FileHandle open_data() {
FileHandle f(open("data.bin", O_RDONLY));
return f; // move (또는 RVO)
}
void process() {
FileHandle f = open_data(); // 이전
// 사용
}

자원이 복사되지 않고 오직 이전만 가능합니다. unique_ptr과 유사하지만 별도의 heap 객체를 두지 않습니다.

#패턴 8 — Critical Section with Interrupt Preservation

ARM Cortex-M에서 interrupt 상태를 보존하는 패턴입니다.

class CriticalSection {
uint32_t primask_;
public:
CriticalSection() : primask_(__get_PRIMASK()) {
__disable_irq();
}
~CriticalSection() {
__set_PRIMASK(primask_); // 원래 상태 복원
}
CriticalSection(const CriticalSection&) = delete;
};

nested critical section에서도 안전합니다. 이전 상태가 enabled였다면 enable로, disabled였다면 그대로 복원됩니다.

void outer() {
CriticalSection cs1; // disable interrupt
counter++;
inner(); // 안에서 또 cs
// cs1 소멸 → 원래(enabled) 복원
}
void inner() {
CriticalSection cs2; // 이미 disabled — 변화 없음
// ...
// cs2 소멸 → 여전히 disabled (outer 안)
}

#패턴 비교 요약

패턴자원특징사용 시점
lock_guard단일 mutex단순, copy-only-delete가장 흔한 case
scoped_lock여러 mutexdeadlock 회피2개+ mutex
unique_lock단일 mutexunlock/relockcv.wait
unique_ptr + custom deletergenericheap pointer동적 자원
ScopeGuard임의 코드lambda 기반일회성 cleanup
Scoped<>enable/disable함수 포인터 template반복 패턴
Move-only handle자원 ownership함수 간 이전FD, peripheral handle
CriticalSectioninterrupt state보존 + 복원nested ISR sync

#자주 보는 함정과 안티패턴

#1. temporary lock_guard

std::lock_guard(m); // 임시 객체 — 즉시 소멸 → lock 안 됨!

임시 객체는 즉시 소멸하므로 lock이 유지되지 않습니다. 반드시 변수에 바인딩해 std::lock_guard lock(m) 형태로 씁니다.

#2. unique_ptr의 함수 포인터 deleter

std::unique_ptr<FILE, decltype(&fclose)> f(fopen(...), &fclose);

sizeof가 두 배로 늘어납니다. struct deleter를 권장합니다.

#3. ScopeGuard 함수 호출 빠짐

auto guard = make_scope_guard([&]{ cleanup(); });
guard.dismiss(); // 의도와 다르게 호출
do_more();
// dismiss 되어 cleanup 안 함

dismiss는 commit이 성공한 경우에만 호출해야 합니다.

#4. lock_guard scope가 너무 짧음

{
std::lock_guard lock(m);
}
counter++; // unlock 상태에서 접근

보호 범위가 실제 접근 코드를 포함해야 합니다.

#5. RAII로 자원의 시작만 묶고 종료는 다른 곳

class Starter {
Starter() { peripheral_init(); }
// 소멸자 정의 안 함 → cleanup 누락
};

생성과 소멸은 대칭이어야 합니다.

#6. Move 후 destructor가 자원을 두 번 해제

Handle(Handle&& other) : fd_(other.fd_) {} // other.fd_ 안 비움
~Handle() { close(fd_); } // 두 객체 모두 close → double close

move 시 원본을 other.fd_ = -1처럼 무효화해야 합니다.

#측정 — lock_guard 오버헤드

# 수동 lock/unlock
manual_lock:
bl mutex_lock
ldr r3, [counter]; adds r3, #1; str r3, [counter]
bl mutex_unlock
bx lr
# lock_guard 사용
raii_lock:
bl mutex_lock ; constructor 인라인
ldr r3, [counter]; adds r3, #1; str r3, [counter]
bl mutex_unlock ; destructor 인라인
bx lr

완전히 동일합니다. RAII는 zero-cost입니다.

#정리

  • 표준 RAII 패턴은 5가지입니다 — lock_guard, scoped_lock, unique_lock, unique_ptr+deleter, ScopeGuard.
  • 임베디드에서는 Scoped enable/disable, Move-only handle, Critical section with preservation 패턴이 추가됩니다.
  • Custom deleter는 struct나 lambda로 작성해 empty class optimization을 받으며, 함수 포인터는 sizeof를 늘립니다.
  • ScopeGuard는 일회성 cleanup의 보편 패턴이며 gsl::finally도 같은 역할을 합니다.
  • 모든 RAII는 zero-cost이며 컴파일러가 생성자와 소멸자를 인라인합니다.

#관련 항목

#다음 글

Part 2-03: constexpr 기초 — 컴파일 타임 계산으로 런타임 코드를 제거합니다. -Os보다 더 강력한 기능적 0 비용을 달성합니다.

Embedded C++ for Real Systems · 11 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교