[2026] C++ call_once | 한 번만 호출 가이드
이 글의 핵심
std::call_once 는 C++11에서 도입된 함수로, 여러 스레드에서 호출되어도 함수를 정확히 한 번만 실행하도록 보장합니다. std::once_flag와 함께 사용하여 스레드 안전한 초기화를 구현합니다.
call_once란?
std::call_once 는 C++11에서 도입된 함수로, 여러 스레드에서 호출되어도 함수를 정확히 한 번만 실행하도록 보장합니다. std::once_flag와 함께 사용하여 스레드 안전한 초기화를 구현합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <mutex>
std::once_flag flag;
void init() {
std::cout << "초기화" << std::endl;
}
void func() {
std::call_once(flag, init); // 한 번만
}
왜 필요한가?:
- 스레드 안전 초기화: 여러 스레드에서 동시 호출 시에도 안전
- 성능: 초기화 후 빠른 체크 (double-checked locking 불필요)
- 예외 안전: 초기화 실패 시 재시도 가능
- 간결성: 복잡한 동기화 코드 불필요 다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 수동 동기화: 복잡하고 오류 가능
std::mutex mtx;
bool initialized = false;
void init() {
std::lock_guard<std::mutex> lock(mtx);
if (!initialized) {
// 초기화
initialized = true;
}
}
// ✅ call_once: 간단하고 안전
std::once_flag flag;
void init() {
std::call_once(flag, {
// 초기화 (한 번만)
});
}
call_once의 동작 원리:
call_once는 내부적으로 원자적 연산을 사용하여 첫 번째 호출만 함수를 실행하고, 이후 호출은 즉시 반환합니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 개념적 동작
std::once_flag flag;
void call_once(std::once_flag& flag, Callable&& func) {
// 원자적으로 상태 확인
if (flag.already_called()) {
return; // 이미 호출됨, 빠른 반환
}
// 첫 호출: 락 획득
lock();
if (!flag.already_called()) {
func(); // 함수 실행
flag.mark_called();
}
unlock();
}
once_flag의 특성:
- 복사 불가:
once_flag는 복사할 수 없음 - 이동 불가:
once_flag는 이동할 수 없음 - 상태 유지: 한 번 호출되면 영구적으로 “호출됨” 상태 유지
std::once_flag flag1;
// std::once_flag flag2 = flag1; // 에러: 복사 불가
// std::once_flag flag3 = std::move(flag1); // 에러: 이동 불가
기본 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag initFlag;
bool initialized = false;
void initialize() {
std::cout << "초기화 중..." << std::endl;
initialized = true;
}
void process() {
std::call_once(initFlag, initialize);
// 첫 호출만 initialize 실행
}
실전 예시
예시 1: 싱글톤
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Singleton {
static std::once_flag initFlag;
static Singleton* instance;
Singleton() {
std::cout << "Singleton 생성" << std::endl;
}
public:
static Singleton& getInstance() {
std::call_once(initFlag, {
instance = new Singleton();
});
return *instance;
}
};
std::once_flag Singleton::initFlag;
Singleton* Singleton::instance = nullptr;
예시 2: 자원 초기화
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Database {
static std::once_flag connFlag;
static Connection* conn;
public:
static Connection& getConnection() {
std::call_once(connFlag, {
conn = new Connection("localhost");
std::cout << "DB 연결" << std::endl;
});
return *conn;
}
};
예시 3: 설정 로드
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag configFlag;
Config config;
void loadConfig() {
std::cout << "설정 로드" << std::endl;
config = Config::load("config.json");
}
Config& getConfig() {
std::call_once(configFlag, loadConfig);
return config;
}
예시 4: 람다 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag flag;
int value = 0;
void func() {
std::call_once(flag, [&value]() {
value = expensiveComputation();
std::cout << "계산 완료: " << value << std::endl;
});
std::cout << "값: " << value << std::endl;
}
예외 처리
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag flag;
void init() {
throw std::runtime_error("초기화 실패");
}
void func() {
try {
std::call_once(flag, init);
} catch (...) {
// 예외 발생 시 flag 리셋
// 다음 call_once에서 재시도
}
}
자주 발생하는 문제
문제 1: 여러 once_flag
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::once_flag flag1, flag2;
void init1() { std::cout << "Init 1" << std::endl; }
void init2() { std::cout << "Init 2" << std::endl; }
void func() {
std::call_once(flag1, init1);
std::call_once(flag2, init2);
}
문제 2: 인자 전달
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::once_flag flag;
void init(int x, const std::string& s) {
std::cout << x << ", " << s << std::endl;
}
void func() {
std::call_once(flag, init, 42, "Hello");
}
문제 3: 멤버 함수
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class MyClass {
std::once_flag flag;
void init() {
std::cout << "초기화" << std::endl;
}
public:
void process() {
std::call_once(flag, &MyClass::init, this);
}
};
문제 4: 예외 재시도
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag flag;
int attempt = 0;
void init() {
attempt++;
if (attempt < 3) {
throw std::runtime_error("재시도");
}
std::cout << "성공" << std::endl;
}
void func() {
try {
std::call_once(flag, init);
} catch (...) {
// 다음 호출에서 재시도
}
}
정적 지역 변수 대안
다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// call_once
std::once_flag flag;
Resource* resource = nullptr;
Resource& getResource() {
std::call_once(flag, {
resource = new Resource();
});
return *resource;
}
// 정적 지역 변수 (C++11, 더 간단)
Resource& getResource() {
static Resource resource; // 스레드 안전
return resource;
}
실무 패턴
패턴 1: 지연 초기화 래퍼
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
template<typename T>
class LazyInit {
std::once_flag flag_;
std::unique_ptr<T> instance_;
public:
template<typename....Args>
T& get(Args&&....args) {
std::call_once(flag_, [this, &args...]() {
instance_ = std::make_unique<T>(std::forward<Args>(args)...);
});
return *instance_;
}
};
// 사용
LazyInit<Database> db;
db.get("localhost", 5432).query("SELECT * FROM users");
패턴 2: 초기화 체인
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Application {
std::once_flag configFlag_;
std::once_flag dbFlag_;
std::once_flag cacheFlag_;
void initConfig() {
std::cout << "설정 로드\n";
// 설정 초기화
}
void initDatabase() {
std::call_once(configFlag_, [this]() { initConfig(); });
std::cout << "DB 연결\n";
// DB 초기화
}
void initCache() {
std::call_once(dbFlag_, [this]() { initDatabase(); });
std::cout << "캐시 초기화\n";
// 캐시 초기화
}
public:
void start() {
std::call_once(cacheFlag_, [this]() { initCache(); });
std::cout << "애플리케이션 시작\n";
}
};
패턴 3: 재시도 가능한 초기화
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class RetryableInit {
std::once_flag flag_;
int maxRetries_ = 3;
int attempts_ = 0;
void tryInit() {
attempts_++;
if (attempts_ < maxRetries_) {
throw std::runtime_error("초기화 실패, 재시도");
}
std::cout << "초기화 성공\n";
}
public:
bool initialize() {
try {
std::call_once(flag_, [this]() { tryInit(); });
return true;
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
return false;
}
}
};
// 사용
RetryableInit init;
while (!init.initialize()) {
std::this_thread::sleep_for(std::chrono::seconds(1));
}
싱글톤 초기화에 call_once를 쓰는 이유
전역·함수 수준에서 한 번만 비용 큰 초기화를 하고 싶을 때, 직접 mutex + bool 플래그를 쓰면 이중 확인 잠금(double-checked locking) 을 손으로 맞추기 어렵고 컴파일러 최적화에 취약했습니다. std::call_once는 표준이 보장하는 한 번만 실행을 제공하므로, 싱글톤 지연 초기화의 고전적인 구현에 자주 쓰입니다.
다만 C++11 이후에는 매이어스 싱글톤처럼 static 지역 객체를 쓰는 편이 더 단순한 경우가 많습니다(다음 절 참고). call_once는 여러 단계 초기화 순서, 인자를 넘긴 일회성 호출, 실패 시 재시도처럼 static만으로 애매한 때에 빛납니다.
멀티스레드
일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다.
안전성이 표준에 어떻게 적용되나
std::call_once(flag, f, args...)는 모든 스레드에서 동시에 호출해도 f는 성공적으로 완료된 경우 한 번만 실행됩니다. 한 스레드가 f를 실행하는 동안 다른 스레드는 완료까지 블로킵니다.
초기화 중 예외가 나면 표준에 따라 다음 call_once에서 다시 시도할 수 있습니다(구현은 once_flag를 실패 상태로 되돌리는 방식). 그래서 네트워크·파일처럼 실패할 수 있는 초기화를 감쌀 때 유용합니다.
static 지역 변수와의 비교 (실무 선택)
C++11 이후, 블록 스코프 static 지역 변수의 초기화는 데이터 레이스 없이 한 번만 일어나도록 보장됩니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
Foo& instance() {
static Foo f; // 첫 호출 시 한 번만 초기화(스레드 안전)
return f;
}
| 상황 | 추천 |
|---|---|
타입 T를 그 자리에서 만들 수 있고, 기본/인자 있는 생성만 있으면 됨 | static 지역 변수가 짧고 읽기 쉬움 |
| 초기화에 복잡한 단계, 여러 함수 호출, 예외 후 재시도 | call_once |
동적 라이브러리 로드, 플러그인 등록처럼 “한 번만”이지만 static으로 표현하기 어색함 | call_once 또는 해당 프레임워크의 초기화 API |
실전 패턴 보강
- 라이브러리 초기화:
register_codec(),load_config()를 앱 전역에서 한 번만 호출할 때once_flag를 네임스페이스 수준에 두고call_once로 감쌉니다. - 지연 로딩된 DLL/so: 핸들 획득이 실패할 수 있으면 예외 처리 루프와 함께
call_once로 재시도 정책을 구현합니다. - 테스트:
once_flag는 리셋할 수 없으므로 단위 테스트에서 “매 테스트마다 다시 초기화”가 필요하면 별도 픽스처나 정적이 아닌 객체로 옮기는 편이 낫습니다.
성능에 대한 현실적인 기대
성공한 뒤의 call_once 호출은 매우 가벼운 원자적 경로로 처리되는 것이 일반적입니다. 정확한 수치는 플랫폼·컴파일러마다 다르지만, 핫 루프 안에서 매 반복 call_once를 호출하는 것은 여전히 피하는 것이 좋습니다. “한 번만”이면 호출 지점을 루프 밖이나 초기화 단계로 옮기세요.
첫 호출 시에만 무거운 작업이 있고 이후에는 동일 플래그로 빠르게 빠져 나오는 구조가 이상적입니다.
FAQ
Q1: call_once는 무엇인가요?
A: 여러 스레드에서 동시에 호출되어도 함수를 정확히 한 번만 실행하도록 보장하는 C++11 함수입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::once_flag flag;
void init() {
std::cout << "초기화\n";
}
// 여러 스레드에서 호출해도 init()은 한 번만 실행됨
std::thread t1( { std::call_once(flag, init); });
std::thread t2( { std::call_once(flag, init); });
Q2: 언제 사용해야 하나요?
A:
- 싱글톤 패턴: 인스턴스를 한 번만 생성
- 자원 초기화: DB 연결, 파일 열기 등
- 설정 로드: 설정 파일을 한 번만 읽기
- 지연 초기화: 필요할 때 한 번만 초기화 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 싱글톤
static Logger& getLogger() {
static std::once_flag flag;
static Logger* instance = nullptr;
std::call_once(flag, {
instance = new Logger();
});
return *instance;
}
Q3: 예외 처리는 어떻게 되나요?
A: 예외가 발생하면 once_flag가 리셋되어 다음 호출에서 재시도할 수 있습니다.
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::once_flag flag;
int attempt = 0;
void init() {
attempt++;
if (attempt < 3) {
throw std::runtime_error("재시도");
}
std::cout << "성공\n";
}
// 여러 번 호출하면 재시도됨
for (int i = 0; i < 5; ++i) {
try {
std::call_once(flag, init);
} catch (...) {
std::cout << "실패, 재시도\n";
}
}
Q4: 정적 지역 변수와 어떤 차이가 있나요?
A: C++11 이후 정적 지역 변수가 스레드 안전하므로, 대부분의 경우 더 간단합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// call_once: 명시적
std::once_flag flag;
Resource* resource = nullptr;
Resource& getResource() {
std::call_once(flag, {
resource = new Resource();
});
return *resource;
}
// 정적 지역 변수: 더 간단 (C++11 이후 스레드 안전)
Resource& getResource() {
static Resource resource; // 자동으로 한 번만 초기화
return resource;
}
call_once를 사용하는 경우:
- 초기화 로직이 복잡할 때
- 예외 재시도가 필요할 때
- 초기화 시점을 명시적으로 제어하고 싶을 때
Q5: 성능은 어떤가요?
A: 첫 호출 후 매우 빠릅니다. 내부적으로 원자적 연산을 사용하여 빠른 체크를 수행합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 첫 호출: 초기화 실행 (느림)
std::call_once(flag, expensiveInit);
// 이후 호출: 원자적 체크만 (매우 빠름, ~1-2ns)
std::call_once(flag, expensiveInit);
Q6: 멤버 함수를 호출할 수 있나요?
A: 가능합니다. 멤버 함수 포인터와 this를 전달하면 됩니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class MyClass {
std::once_flag flag_;
void init() {
std::cout << "초기화\n";
}
public:
void process() {
std::call_once(flag_, &MyClass::init, this);
}
};
Q7: 인자를 전달할 수 있나요?
A: 가능합니다. call_once는 가변 인자를 지원합니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::once_flag flag;
void init(int x, const std::string& s) {
std::cout << x << ", " << s << '\n';
}
void func() {
std::call_once(flag, init, 42, "Hello");
}
Q8: call_once 학습 리소스는?
A:
- “C++ Concurrency in Action” (2nd Edition) by Anthony Williams
- “Effective Modern C++” by Scott Meyers
- cppreference.com - std::call_once
관련 글: Singleton Pattern, Thread Basics, Mutex.
한 줄 요약:
std::call_once는 여러 스레드에서 함수를 정확히 한 번만 실행하도록 보장하는 스레드 안전한 초기화 메커니즘입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 균일 초기화 | “Uniform Initialization” 가이드
- C++ Dynamic Initialization | “동적 초기화” 가이드
- C++ async & launch | “비동기 실행” 가이드