2010년 1월 6일 수요일

C++ 에서 힙메모리를 자동으로 관리하게 코드를 작성하자


아직도  C++은 동적메모리를  new delete로 관리 하여만 한다고 생각하는 사람이 많은것 같다

지금도 남이 짜 놓은 수 많은 코드속에서 new로 생성한 객체를  더미(raw)  포인터에 그냥 담고 ,  
"언제가 delete가 필요할때  완벽하게 delete 해줄수 있을거야" 라는 식의 코드를  보고 있는중이다
(오해는 없길 바란다 누굴 비난하기 위한 내용은 아니다)

이런 설계는 언제나 그렇듯 처음에는 무난한 ??? 설계가 될수 있으나
코드는 항상 수정되어야 하는 경우가 대부분이기때문에 수정할때마다
이 수많은 경우의 수를 다 파악해 delete 코드를 넣어주는 지옥과 같은 경험을 하게 된다

그래서 이러한 코드를 보게되면 ,코드 분석보단   " 아마 메모리 관리가 이렇게 힘들어서야... "
같은  반복된 생각만 든다

"이 중구 난방식의 new속에 , 도대체 delete를 어떻게 완벽히 기억해내어  처리해주지?"

 실용주의 프로그래머 책의 저자가  그러지 않았던가 ,  
'문제는  어떻게  완벽히 기억할수 있는게 아니라, 언제 잊어버릴것인가' 이다 [물론 정확한 인용은 아니다]

알고리즘의 핵심이 아닌상  , 잘잘하고 구차한 문법문제는  
잊어버려도 문제가 없는 코드가 구성되어야 좋은 코드라 할수 있는것이다.

자바가 C++ 프로그래머를 대량으로 흡수할때 1순위로 강조한것이 무엇인가?
가비지 컬랙션이 아니었던가?
쓸때없는 것에 머리 쓰지 않아도 되는건 대단한 강점인것이다

소실적에는 c++에 가비지 콜랙션을 만들수 있지 않을까하는  생각도 해보았으나
이런 당연하고도 고전적인 문제를 C++이 해결을 못했을리는 없다

믿을지 모르겠지만  나는 약 6년전 대학교때 STL과 BOOST를 공부한 이후 단한번도 직접 delete를 작성한적이
없다. 혹!? 모르나 아마 없을것이다
[물론 crt 디버그에서 메모리릭이라 투털거린적 역시 없다]

믿을수 없다고 ??

지금 말한 3가지만 사용한다면  앞으로 99.9%  delete를 직접 할 필요가 없다
1. 스트링
2. 자료구조
3. 스마트 포인터


1.  스트링
c언어 스트링은 가장 고전적이고 다루기 어려운 구조이다
스트링 특성상 가변적으로 크기가 변하는게 대부분이기때문에
동적 메모리를 활용하는 경우가 많고 , 경우에 따라서 관리가 복잡하다

하지만 스트링 객체를 쓰면 대부분 해소 되는데
표준 std::string이 아니더라도  스트링 객체를 활용한다면 구질 구질한 c언어 스트링으로 인한
new char[XX]같은  동적 메모리 사용을 피할수 있다

아직도 문자를 생성할때  new char[XX] 쓰고 있다면 반드시 표준std::string 를 이용해보자
다른 세상을 경험할수 있을것이다

그런데도 굳이 스트링 객체 자체를 new로 생성하는 몰지각스러운 코드를 만나면 답이 안나온다
스트링 객체를 대부분  그렇게 생성되어야 할 이유가 99%이상  없기때문이다

2. 자료구조
C++의 대표적인 자료구조로는 STL이 존재한다
STL이 객체를 다루는 방법이   복사를 이용하기때문에 주로 가벼운 객체를 주로 담게되나
어지간히 무겁지 않는한  복사가  new 로 할당해 담는 비용보다  훨씬 싼편에 속한다
만약 정말로 무겁거나 객체의 다형성을 위해 new를 이용해야 한다면 스마트포인터를 담으면 그만이다
 스캇마이어(Effective STL)가 그리 강조하지 않던가?

컴퓨터 공학생이라면 필수로 읽어봤을 자료구조책에   당골로 등장하는
vector list hash map deque 등 [이름은 조금 다를수도 있다] 을
STL를 이용하면 공짜로 이용할수 있으며
이것은 곧  가면적으로 크기가 변하는 컨테이너를 공짜로 사용할수 있다는 말이다

new를 이용하는 가장 핵심적인 이유중 한가지가 가변 크기때문 아니던가? 그것을 자동으로 STL
컨테이너가 해주기때문에 new를 쓸이 없어진다
[물론 hash나 set, map은 그뿐이 아니라 , 키로 데이터 관리까지 해주니까 두말할 나위가 없다]

그중 new를 이용한 배열의 경우

 vector를 이용하면 C의 배열과 100% 호환되기때문에 그대로 C API와 더불어 운영될수 있으며
컴파일러의 최적화 능력 [STL의 성능은 다음에 다루기로 하겠다]
까지 더해지면 vector의 편리함과 C배열과 같은 성능을  동시에 누릴수도 있는것이다
게다가 수백만명이 검증한 STL 자료구조의 안정성도 더불어 누릴것이다

3. 스마트 포인터
new 직접 사용에 대해 계속해서 부정적인 이야기를 했지만
반드시 직접 힙메모리를 이용해야 하는 경우가 존재한다
다형성이 존재하는 객체를 컨테이너에 담을때 부모형태로 포인터를 저장해 관리 할때는
대부분 new를 피하기 어렵다
그래서 생겨난것이 스마트 포인터이다
[ 물론 객체 크기가 커서 new로 생성해 스마트 포인터에 담는 경우는 예외이다 ]

스마트 포인터의 기본 이념은  new생성된 포인터를  객체가
참조 카운팅(std::auto_ptr 같은것은 제외) 관리해서 유지하는것이다

참조하는 객체가 전부 사라지면 그때에 delete 된다

가장 대표적인게 표준으로도 포함된 boost::shared_ptr이 될것이다 [boost.org의 doc를 참조하라]

기본적으로 가비지 컬랙션을 사용하는 언어들도 내부적으로는 참조 카운팅을 유지하고 있다
[원리는 같다는 것! ,다만 C++은 참조하는 객체가 사라질때 바로 제거 된고 , 다른 언어들은
언어의 가비지 컬랙션 정책에 따라 다를것이다 ]


물론 만능은 아니다
스마트 포인터를 이용할때   내부의 스마트포인가 서로를 포함하는 구조로 객체가 구성될 경우
참조 카운팅이 꼬여 메모리가 해제가 안될수 있는것이다

그것을 위해 weaken 어쩌구 방식의 스마트 포인터도 boost에서 지원하는데
그러한 꼬임은
그것은 C++의 스마트 포인터가 아니라 가비질 컬랙션을 지원하는
어떤 언어에서도  정상적인 방법으로 메모리를 해제할수가 없다

다른 언어에서도 boost의  weaken 어쩌고 같은
방식을 이용해야 한다 , 한마디로  상호 포함관계의 스마트 포인터는  정상인 논리 구조라 보기 힘들다고 생각한다

그것을 이용할 정도면  설계 자체가 이해하기 어려운 구조기때문에
설계를 바꾸는게 정답일것이다

---------------------------------------------------------------------------------------------------
참고로

boost의 shared_ptr은  delete 뿐 아니라 직접 제거자를 지정해 주어
delete 대신  다른 해제 방법을 이용할수 있게 해놓았다
new로 생성하지 않고 create나 destroy를 이용해야 하는 리소스도
대응해서 사용할수 있는것이다

혹 STL에 스마트 포인터를 담으면  참조할때  이터레이터 참조뿐 아니라 스마트 포인터를 참조해야하기에 **iterator를 쓰게되어 가독성이 떨어지 거슬리는분들이 존재할거라 생각된다
나 역시 그랬으니까

그건 boost의     boost/ptr_container/ptr_list.hpp 같은걸 이용하면 된다
부스트의 ptr_containter에는  new로 생성된 포인터를 직접 넣어주면
컨테이너에서 제거 될때 자동으로 delete를 해준다
 
----------------------------------------------------------------------------------------------------


STL 스마트 포인터 알게된지 이후 6년동안 delete 코드를 작성해본적이 없지만
힙메모리를 이용하고도  , delete 코드가 없다하면 아직도 믿지 못하는 사람이 많다

gpg study에서도 비슷한 발언을 했을적에  믿기 힘들다는 사람을 대다수인걸보면
얼마나 많은 사람들이  아직도  힙메모리를 직접 다루고 있는지 느껴지는듯했다


프로그래머라면 정말 중요한 알고리즘을 기억하는데만해도 머리에 담을 용량이 부족하지
않았던가?


다음 편엔 STL 자료구조에 대해 이야기 해보겠다
덤으로 fix_vector도 ~ ^^




댓글 4개:

  1. 레퍼런스 카운팅 기반의 가비지 컬렉터는 순환참조에 취약하지만, mark and sweep 방식의 가비지 컬렉터는 순환참조된 오브젝트들을 다루는데 문제가 없지 않나요?



    http://en.wikipedia.org/wiki/Garbage_collection_(computer_science)

    답글삭제
  2. mark and sweep 이것을 간과 했었네요



    대부분 가비지 컬랙션이 시간이 많이 걸리는 작업이므로

    mark and sweep보단 참조 카운팅을 주로 사용한다고 가정해 버렸습니다



    아무래도 mark and sweep는 재귀적으로 다 체크를 해봐야 하는것이니까요

    물론 이것도 나눠서 처리할순 있겠습니다만...

    답글삭제
  3. 니가 알려줘서 첨에는 사용하는데 꽤 불편했는데.. 무작정 믿고 개발했는데 특별히 문제가 안생겨서 좋았다. 앞으로 더 좋은글 많이 올려줘~

    ㅅㄱ

    답글삭제
  4. @leaf - 2010/01/11 17:35
    야 답글이 초큼 이상하군 , 5년전 내가 STL과 스마트 포인터를 전수해줬을 적에 장족의 발전을 했던넘의 말투치고 좀 이상하군



    "특별히 문제가 안생겨서 좋았다" <- 오해의 여지가 있는 말투넹 -_-;



    모범 답안을 꼭 내가 알려줘야돼?

    정답 --- "참 좋았던거 같아" ----



    이런거 -_-;

    답글삭제