C++ mutable Keyword | "mutable 키워드" 가이드
이 글의 핵심
const와 mutable, 캐시·동기화 패턴, 언제 쓰지 말아야 하는지까지 다룹니다.
mutable이란?
mutable 키워드는 const 멤버 함수에서도 수정 가능한 멤버 변수를 선언할 때 사용합니다.
class Cache {
mutable int accessCount = 0;
public:
int getData() const {
accessCount++; // OK: mutable
return 42;
}
};
왜 필요한가?:
- 캐싱: const 함수에서 캐시 업데이트
- 통계 수집: 읽기 함수에서 카운터 증가
- 동기화: const 함수에서 뮤텍스 잠금
- 지연 초기화: const 함수에서 리소스 초기화
// ❌ mutable 없이: 에러
class Counter {
int count = 0;
public:
void increment() const {
count++; // 에러: const 함수에서 수정 불가
}
};
// ✅ mutable 사용: OK
class Counter {
mutable int count = 0;
public:
void increment() const {
count++; // OK: mutable
}
};
mutable의 동작 원리:
class Example {
int normal;
mutable int mutableVar;
public:
void constFunc() const {
// normal = 10; // 에러: const 함수
mutableVar = 10; // OK: mutable
}
};
// 내부 동작 (개념적)
// const 함수는 this 포인터가 const Example*
// mutable 멤버는 const 제약에서 제외됨
논리적 const vs 물리적 const:
| 개념 | 설명 | 예시 |
|---|---|---|
| 물리적 const | 메모리 상태 변경 불가 | const int x = 10; |
| 논리적 const | 외부에서 보이는 상태 불변 | 캐싱, 통계 수집 |
class Cache {
mutable bool cached = false;
mutable double cachedValue = 0.0;
public:
// 논리적으로는 const (외부에서 보이는 상태 불변)
// 물리적으로는 non-const (캐시 업데이트)
double getValue() const {
if (!cached) {
cachedValue = expensiveComputation();
cached = true;
}
return cachedValue;
}
};
const 멤버 함수에서의 mutable
const 멤버 함수 안에서는 **this가 const T***로 고정되므로, 일반 데이터 멤버는 읽기 전용처럼 취급됩니다. 그런데 관측 가능한 동작(observable behavior)은 그대로인데 구현만 바꾸고 싶은 경우가 있습니다. 대표적으로 캐시·계측·동기화 객체입니다. 이때 해당 멤버만 **mutable**로 표시해 “논리적 constness는 유지하고, 구현 세부만 갱신한다”고 컴파일러와 독자에게 알립니다.
- 바깥 계약:
const메서드는 “객체의 추상적 상태를 바꾸지 않는다”는 뜻에 가깝습니다. - 안쪽 구현: 캐시나 락은 그 계약을 깨지 않으면서도 비트는 바뀔 수 있습니다.
반대로 mutable로 “실제 의미 있는 데이터”까지 const 안에서 바꾸기 시작하면 const가 무의미해지므로 설계가 흐트러집니다.
캐싱 패턴 심화
읽기 연산이 반복될 때 같은 입력에 대해 같은 결과를 보장한다면, 내부에 지연 계산 + 무효화를 둘 수 있습니다.
- 키: 입력이 바뀌면 캐시 비트를 끈다 (
valid = false등). - 스레드: 읽기만
const로 노출하고 캐시를mutable로 두되, 다중 스레드면 캐시 필드 접근도 뮤텍스나 원자 변수로 보호해야 합니다.mutable은 스레드 안전을 보장하지 않습니다.
class Stats {
mutable std::mutex m_;
mutable bool cacheValid_{false};
mutable double cache_{};
double input_{};
public:
void setInput(double x) { input_ = x; cacheValid_ = false; }
double meanExpensive() const {
std::lock_guard<std::mutex> lk(m_);
if (!cacheValid_) {
cache_ = input_ * 2; // 실제로는 무거운 계산
cacheValid_ = true;
}
return cache_;
}
};
뮤텍스와 mutable
std::mutex, std::shared_mutex 등은 잠그는 행위 자체가 객체 상태를 바꿉니다. 그런데 const 메서드에서도 “상태를 읽되, 동시 접근만 막고 싶다”는 요구는 흔합니다. 그래서 뮤텍스 멤버는 거의 항상 **mutable**입니다.
lock()/unlock()이const메서드에서 호출 가능해야 하므로, 뮤텍스가mutable이 아니면 설계가 꼬입니다.- 읽기/쓰기 잠금을 나눌 때는
std::shared_mutex+shared_lock/unique_lock패턴과 함께 쓰입니다.
class Registry {
mutable std::shared_mutex rw_;
std::map<std::string, int> data_;
public:
int get(const std::string& k) const {
std::shared_lock<std::shared_mutex> lk(rw_);
auto it = data_.find(k);
return it == data_.end() ? 0 : it->second;
}
void set(std::string k, int v) {
std::unique_lock<std::shared_mutex> lk(rw_);
data_[std::move(k)] = v;
}
};
언제 사용해야 하나
적합한 경우:
- 논리적 불변을 유지한 채, 프로파일링·캐시·지연 초기화만 필요할 때.
const인터페이스를 넓게 유지하고 싶을 때 (예:const객체에 대해서도size()가 캐시를 채움).- 동기화 프리미티브처럼 “계약과 무관한” 내부 도구를 둘 때.
다른 선택을 먼저 고려할 경우:
- 진짜 상태 변경이면
mutable대신 non-const 메서드로 올리는 편이 명확합니다. - 스레드 공유가 복잡하면
std::atomic, 외부 동기화, 또는 값 의미론으로 경쟁을 없애는 설계가 나을 수 있습니다.
남용 주의
mutable로 “비즈니스 데이터”를constAPI 안에서 숨겨 바꾸기 → 호출자가const참조로 안전하다고 믿기 어렵습니다.- 동일한
const메서드가 여러 스레드에서 캐시를 갱신하는데 락이 없음 → 데이터 경쟁. - **과도한
mutable**는 “이 객체는 사실상 항상 변한다”는 신호이므로, API를const가 아닌 메서드로 재구성하는 편이 낫습니다.
사용 이유
// const 함수에서 멤버 수정 필요
class Logger {
mutable std::mutex mtx;
public:
void log(const std::string& msg) const {
std::lock_guard<std::mutex> lock(mtx); // OK
// ...
}
};
실전 예시
예시 1: 캐싱
class ExpensiveCalculation {
mutable bool cached = false;
mutable double cachedValue = 0.0;
public:
double calculate() const {
if (!cached) {
// 복잡한 계산
cachedValue = /* ... */;
cached = true;
}
return cachedValue;
}
};
예시 2: 통계 수집
class DataProcessor {
mutable size_t readCount = 0;
mutable size_t writeCount = 0;
std::vector<int> data;
public:
int get(size_t index) const {
readCount++; // OK: mutable
return data[index];
}
void set(size_t index, int value) {
writeCount++;
data[index] = value;
}
size_t getReadCount() const {
return readCount;
}
};
예시 3: 지연 초기화
class Database {
mutable std::unique_ptr<Connection> conn;
public:
Connection* getConnection() const {
if (!conn) {
conn = std::make_unique<Connection>();
}
return conn.get();
}
};
예시 4: 멀티스레딩
#include <mutex>
class ThreadSafeCounter {
mutable std::mutex mtx;
int count = 0;
public:
void increment() {
std::lock_guard<std::mutex> lock(mtx);
count++;
}
int get() const {
std::lock_guard<std::mutex> lock(mtx); // OK: mutable
return count;
}
};
자주 발생하는 문제
문제 1: 남용
// ❌ 논리적 const 위반
class BadClass {
mutable int value;
public:
void setValue(int v) const {
value = v; // const 의미 상실
}
};
// ✅ 올바른 사용
class GoodClass {
mutable int cacheHits;
public:
int getData() const {
cacheHits++; // 통계만 수정
return 42;
}
};
문제 2: 스레드 안전성
// ❌ 경쟁 조건
class Counter {
mutable int count = 0;
public:
void increment() const {
count++; // 스레드 안전하지 않음
}
};
// ✅ 뮤텍스 사용
class Counter {
mutable std::mutex mtx;
mutable int count = 0;
public:
void increment() const {
std::lock_guard<std::mutex> lock(mtx);
count++;
}
};
문제 3: 캐시 무효화
class Cache {
mutable bool valid = false;
mutable int cachedValue;
public:
void invalidate() {
valid = false; // 비const 함수
}
int getValue() const {
if (!valid) {
cachedValue = compute();
valid = true;
}
return cachedValue;
}
};
문제 4: const 포인터
class MyClass {
mutable int* ptr;
public:
void modify() const {
ptr = new int(10); // OK: 포인터 수정
*ptr = 20; // OK: 가리키는 값 수정
}
};
사용 가이드라인
// ✅ 적절한 사용
// 1. 캐싱
mutable bool cached;
mutable double cachedValue;
// 2. 통계/로깅
mutable size_t accessCount;
// 3. 동기화
mutable std::mutex mtx;
// 4. 지연 초기화
mutable std::unique_ptr<Resource> resource;
// ❌ 부적절한 사용
// 논리적 const 위반
mutable int importantData;
실무 패턴
패턴 1: 지연 계산 캐시
class Matrix {
std::vector<std::vector<double>> data_;
mutable bool determinantCached_ = false;
mutable double cachedDeterminant_ = 0.0;
public:
Matrix(std::vector<std::vector<double>> data) : data_(std::move(data)) {}
double determinant() const {
if (!determinantCached_) {
cachedDeterminant_ = computeDeterminant();
determinantCached_ = true;
}
return cachedDeterminant_;
}
void modify(size_t i, size_t j, double value) {
data_[i][j] = value;
determinantCached_ = false; // 캐시 무효화
}
private:
double computeDeterminant() const {
// 복잡한 계산
return 1.0;
}
};
패턴 2: 스레드 안전 접근
#include <mutex>
#include <shared_mutex>
class ThreadSafeData {
mutable std::shared_mutex mtx_;
std::map<std::string, int> data_;
public:
int get(const std::string& key) const {
std::shared_lock lock(mtx_); // 읽기 잠금
auto it = data_.find(key);
return it != data_.end() ? it->second : 0;
}
void set(const std::string& key, int value) {
std::unique_lock lock(mtx_); // 쓰기 잠금
data_[key] = value;
}
};
패턴 3: 디버깅 정보
class Logger {
mutable size_t logCount_ = 0;
mutable std::chrono::steady_clock::time_point lastLog_;
public:
void log(const std::string& message) const {
logCount_++;
lastLog_ = std::chrono::steady_clock::now();
std::cout << "[" << logCount_ << "] " << message << '\n';
}
size_t getLogCount() const {
return logCount_;
}
};
FAQ
Q1: mutable은 언제 사용하나요?
A:
- 캐싱: const 함수에서 캐시 업데이트
- 통계 수집: 읽기 함수에서 카운터 증가
- 뮤텍스: const 함수에서 동기화
- 지연 초기화: const 함수에서 리소스 초기화
class Cache {
mutable bool cached_ = false;
mutable double cachedValue_ = 0.0;
public:
double getValue() const {
if (!cached_) {
cachedValue_ = compute();
cached_ = true;
}
return cachedValue_;
}
};
Q2: const 위반이 아닌가요?
A: 논리적 const는 유지됩니다. 외부에서 보이는 상태는 변하지 않고, 구현 세부사항만 수정합니다.
// 논리적 const: 외부에서 보이는 상태 불변
class Cache {
mutable int accessCount = 0; // 통계 (내부 상태)
public:
int getData() const {
accessCount++; // 외부에 영향 없음
return 42;
}
};
Q3: 스레드 안전한가요?
A: mutable 자체는 스레드 안전하지 않습니다. 뮤텍스를 함께 사용해야 합니다.
class ThreadSafe {
mutable std::mutex mtx_;
mutable int count_ = 0;
public:
void increment() const {
std::lock_guard lock(mtx_);
count_++; // 스레드 안전
}
};
Q4: 성능 영향은?
A: 없습니다. mutable은 컴파일 타임 키워드로, 런타임 비용이 없습니다.
// mutable 유무는 성능에 영향 없음
int count_; // 일반 멤버
mutable int count_; // mutable 멤버
Q5: 남용 위험은?
A: const의 의미가 상실될 수 있습니다. 신중히 사용해야 합니다.
// ❌ 남용: 논리적 const 위반
class Bad {
mutable int value_;
public:
void setValue(int v) const {
value_ = v; // const 의미 상실
}
};
// ✅ 적절한 사용: 통계만 수정
class Good {
mutable int accessCount_;
int value_;
public:
int getValue() const {
accessCount_++; // 통계만 수정
return value_;
}
};
Q6: 람다에서도 사용할 수 있나요?
A: 가능합니다. 람다의 operator()를 non-const로 만듭니다.
int x = 0;
auto lambda = [x]() mutable {
x++; // OK: mutable
return x;
};
lambda(); // 1
lambda(); // 2
Q7: mutable과 const_cast의 차이는?
A:
mutable: 컴파일 타임에 const 제약 해제const_cast: 런타임에 const 제거 (위험)
class Example {
mutable int x_;
public:
void func() const {
x_ = 10; // OK: mutable
// const_cast (위험)
const_cast<Example*>(this)->x_ = 10;
}
};
Q8: mutable 학습 리소스는?
A:
- “Effective C++” by Scott Meyers (Item 3)
- “C++ Concurrency in Action” by Anthony Williams
- cppreference.com - mutable
관련 글: const, mutex, lambda.
한 줄 요약: mutable은 const 멤버 함수에서도 수정 가능한 멤버 변수를 선언하는 키워드입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ const 완벽 가이드 | “const 정확성” 실전 활용
- C++ static 멤버 | “Static Members” 가이드
- C++ this Pointer | “this 포인터” 가이드
관련 글
- C++ const 완벽 가이드 |
- C++ const 에러 |
- C++ Initialization Order 완벽 가이드 | 초기화 순서의 모든 것
- C++ 람다 기초 완벽 가이드 | 캡처·mutable·제네릭 람다와 실전 패턴
- C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법