포스팅 계기
C++에서 멀티스레딩을 구현할 때, 스마트 포인터를 활용하면 자원 관리가 편리해질 것 같지만, 오히려 더 복잡한 문제를 초래할 수도 있다. 특히, 직접 스마트 포인터를 구현해서 사용하면서 데드락(Deadlock), 레이스 컨디션(Race Condition), Dangling Reference, Reference Count 문제, 성능 이슈 등을 모두 경험하면서 많은 시행착오를 겪었다.
이 글에서는 직접 스마트 포인터를 구현하면서 발생한 문제와 해결 방법을 공유하고, 실무에서 멀티스레딩을 안전하게 구현하는 팁을 정리해 보겠다.

직접 스마트 포인터를 구현한 이유
일반적으로 std::shared_ptr이나 std::unique_ptr을 사용하면 안전한 메모리 관리를 할 수 있다. 하지만 특정한 환경에서는 커스텀 스마트 포인터가 필요할 때가 있다.
제가 직접 스마트 포인터를 구현한 이유는 다음과 같다:
- 특정한 메모리 관리 전략을 적용하기 위해 (usedCount == 1일 때만 삭제)
- 프로그램 종료 시 자동으로 해제되도록 설계하기 위해
- 멀티스레드 환경에서 동작하도록 최적화하기 위해
- 내가 필요한 기능만 직접 구현하여 가볍게 라이브러리화 하기 위해
이러한 목표를 가지고 스마트 포인터를 직접 설계했지만, 여러 가지 문제점에 직면했었다. 그 직면한 문제를 공유하고 어떻게 해결했는지 밑에 정리해 보겠다.
발생한 문제들
- 레이스 컨디션 (Race Condition)
- 스레드 간에 공유되는 스마트 포인터의 참조 카운트가 동시에 변경되면서 예기치 않은 동작이 발생했다
- 여러 스레드가 동시에 참조 카운트를 변경하면서 객체가 중복 삭제되거나, 삭제되지 않는 문제가 발생했다.
if (refCount == 0) {
delete ptr; // 다른 스레드에서 동시에 접근하면 문제가 발생할 수 있음
}
- 데드락 (Deadlock)
- 스마트 포인터를 참조하는 중 한 스레드가 락을 걸고 다른 스레드가 같은 락을 기다리는 문제가 발생했다.
- 특히, 스마트 포인터 내부에서 mutex를 걸어놓고, 다른 스레드에서 같은 객체를 접근하면 교착 상태가 발생했다.
- Dangling Reference (소멸된 객체 참조)
- shared_ptr을 구현할 때, 참조 카운트가 남아있다고 생각했지만, 실제 객체는 이미 삭제된 경우가 있었다..
- 특정한 스레드에서 스마트 포인터를 사용 중인데, 다른 스레드에서 이미 삭제해 버려유효하지 않은 메모리를 참조하는 문제가 발생했다.
- Reference Count 동기화 문제
- 스마트 포인터의 참조 카운트를 관리하는 과정에서 증가/감소 연산이 원자적이지 않아 비정상적인 동작이 발생했다.
- std::atomic<int>을 사용하지 않고 단순한 int 카운터를 사용하면서 동기화 문제 발생.
- 성능 문제
- mutex를 사용하면서 참조 카운트 증감 연산이 많아지면 **락 경합(Lock Contention)**이 발생하여 성능이 저하되었다.
해결 방법
- Mutex를 활용한 Reference Count 관리
- 참조 카운트를 변경할 때 mutex를 사용하여 안전하게 동기화
- 카운트가 0이 되었을 때만 안전하게 객체 삭제
class CustomSharedPtr {
private:
T* ptr;
int refCount;
std::mutex mtx;
public:
void addRef() {
std::lock_guard<std::mutex> lock(mtx);
refCount++;
}
void release() {
std::lock_guard<std::mutex> lock(mtx);
refCount--;
if (refCount == 0) {
delete ptr;
}
}
};
- 객체 소멸 시점 제어 (메모리 부족 방지)
- 프로그램이 종료될 때 자동으로 객체가 해제되도록 하고, 메모리가 부족할 때만 즉시 삭제
if (usedCount == 1) {
delete ptr; // 관리하는 스레드만 접근할 때 삭제하여 메모리 확보
}
- std::atomic을 활용하여 Reference Count 원자화
- mutex 대신 std::atomic을 활용하여 락 없이 카운트 증가/감소 연산을 수행
- 성능 개선 및 동기화 문제 해결
std::atomic<int> refCount;
- weak_ptr 개념 도입하여 Dangling Reference 방지
- weak_ptr 개념을 활용하여 객체가 삭제되었는지 확인한 후 접근
- Dangling Reference 문제 해결
std::weak_ptr<T> weakRef = sharedPtr;
if (auto validRef = weakRef.lock()) {
// 안전하게 사용 가능
}
실무에서 배운 교훈
🔥 1. 스마트 포인터를 잘못 구현하면 디버깅이 극도로 어려워진다.
- 죽는 곳이 한 곳이 아니라 여러 곳이라 문제를 찾기가 어려웠다.
- 스마트 포인터 내부에서 메모리 관리가 꼬이면 예상하지 못한 곳에서 크래시가 발생한다.
🔥 2. 같은 포인터를 공유하는 경우 상호배제(Mutex)가 필수
- 멀티스레드 환경에서 동기화 없이 스마트 포인터를 사용하면 위험하다.
- "Reference Count" 관리에 mutex 또는 std::atomic을 사용해야 한다.
🔥 3. 멀티스레드 구현 시 메모리 커럽션을 방지해야 한다.
- 멀티스레드 시작 시점과 종료 시점을 명확하게 해야 한다.
- 예외 상황을 고려하여 중단할 수 있도록 예외 처리를 신경 써야 한다.
마무리
C++에서 스마트 포인터는 강력한 도구이지만, 멀티스레드 환경에서 직접 구현할 경우 예상치 못한 문제가 많이 발생할 수 있다. 직접 스마트 포인터를 구현하면서 다양한 문제를 겪고 해결하는 과정에서 많은 교훈을 얻었다.
C++ 멀티스레딩 환경에서 스마트 포인터를 사용할 때는 반드시 상호배제를 명확히 하고, Reference Count를 안전하게 관리해야 한다. 이 글이 같은 문제를 고민하는 개발자들에게 도움이 되었으면 좋겠다.
'C++ 개발이야기' 카테고리의 다른 글
C++ 개발자의 반성 (2) | 2025.04.16 |
---|---|
C++ 개발자로서 성장하는 법: 나의 경험과 팁 (0) | 2025.03.05 |
C++ 나만의 코딩 습관 (2) | 2024.10.07 |
멀티 쓰레드(Multi Thread) (0) | 2024.01.17 |
Thread Lib(Windows) - C ++ (0) | 2024.01.05 |