C++ mutable Keyword | "mutable 키워드" 가이드

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 멤버 함수 안에서는 **thisconst 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로 “비즈니스 데이터”를 const API 안에서 숨겨 바꾸기 → 호출자가 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:

관련 글: 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에서 람다 활용법