본문 바로가기
C++

[C++ Core Guidelines] P.8 자원이 새지 않도록 하자

by 코드쉼터 2024. 4. 17.

P.8: Don’t leak any resources

 

이유

자원 사용량이 천천히 증가하더라도 시간이 지나면 언젠가는 시스템의 모든 자원은 고갈됩니다.

메모리 뿐만 아니라 파일 핸들과 소켓 등도 OS 가 마음대로 제공하는게 불가능한 한정된 자원입니다.

자원이 새지 않도록 하는 것은 오랜 기간 실행되는 프로그램에서 상당히 중요할 뿐만 아니라 프로그래머의 책임감을 보여주는 요소이기도 합니다.

 

 

예시 (나쁜 예)

void f(char* name)
{
    FILE* input = fopen(name, "r");
    // ...
    if (something) return;   // 나쁨 : something == true 일 때, 파일 핸들이 누수됩니다
    // ...
    fclose(input);
}

위처럼 코드를 작성한다면 fclose() 는 상황에 따라 호출되지 않습니다.

이처럼 자원의 할당과 소멸을 각기 다른 곳에서 수행하는 것은 가독성을 저하시키고 실수를 유발하기 쉽습니다.

 

 

예시 (좋은 예, RAII 준수)

void f(char* name)
{
    ifstream input {name};
    // ...
    if (something) return;   // 좋음 : 아무렇게나 return 해도 자원 누수가 없습니다
    // ...
}

C++ 의 경우 사용자 정의 개체를 중괄호 { } 안에 지역적으로 생성하면, 중괄호를 벗어나면서 개체가 자동 소멸되며 소멸자 또한 자동으로 호출됩니다.

std::ifstream 개체는 이러한 원리를 이용해 생성자를 통해 메모리 자원을 획득하고, 소멸자를 통해 메모리 자원을 알아서 해제하도록 만들어져 있습니다.이처럼 자원을 얻은 곳에서 초기화와 소멸까지 책임지는 것을 RAII (Resource acquisition is initialization) 라고 합니다.

락 객체가 뮤텍스를 잠그고 풀거나 스마트 포인터가 메모리를 관리하고 STL의 컨테이너가 자신의 요소들을 관리할 때 모두 이러한 방식으로 RAII 를 준수합니다.

 

 

알아두기

자원의 누수는 다른말로 "더 이상 정리할 수 없는 상태가 아닌 것"입니다. 사용하지 않을 자원이라면 정리를 늦추지 마세요.

힙 메모리를 잡아두고 그 포인터를 잃어버리지 마세요.

코드를 단순화한다면서 프로그램이 종료될때 OS 가 알아서 자원을 회수해가길 기대하지 마세요.

차라리 개체를 만들면서 항상 암시적으로 정리되도록 해 두는게 단순하고 더 안전할 수 있습니다. (예: 스마트 포인터 사용)

또한 Lifetime safety profile 기법과 RAII 를 결합하면 GC (쓰레기 수집) 가 필요하지 않습니다. Type and bounds profiles 라는 도구를 통해 타입과 완전한 자원 안전성을 보장할 수도 있습니다.

 

 

정리

1. 포인터를 조심하세요 : 유효한 메모리를 들고 있거나 그렇지 않은 포인터를 분류하고, 가급적 표준 라이브러리 핸들을 사용해서 관리하세요. (예: 위 std::ifstream 개체 참고) 그게 불가능하면 GSL 의 owner 를 사용해서 표시하세요.
2. 중요한 코드의 흐름에 자랑스럽게 new 와 delete 를 그대로 사용하지 마세요. 안전하게 감추는게 좋습니다.
3. 완전 날것으로 포인터를 반환하는 C스타일 리소스 할당 함수를 사용할 때 특히 주의하세요. (예: fopen, malloc, strdup) 

 

 

참고자료

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#r1-manage-resources-automatically-using-resource-handles-and-raii-resource-acquisition-is-initialization

https://www.modernescpp.com/index.php/c-core-guidelines-lifetime-safety/