본문 바로가기
C,C++/C++

[C++] 스택과 힙 메모리, "RAII"라는 패턴 및 기법에 대해 알아보자

by woohyeon 2020. 2. 23.
반응형

RAII가 스마트 포인터와 연관이 있어 해당 카테고리에 작성한다.

 

출처: TCP School



먼저 지역 변수와 매개변수가 저장되는 스택 메모리에 대해 알아보자.


스택 메모리
 (Stack memory)

스택은 지역 변수와 매개변수가 저장되는 공간이며 예약된 지역 메모리 공간이다. 예약된 메모리라는 말은 동적 할당처럼 사용자의 입력 N에 따라 할당하려는 크기가 변하는 것이 아니라 int arr[10]과 같이 컴파일 타임에 그 크기가 미리 정해지는 메모리란 뜻이다. 예약된 메모리 공간이기 때문에 동적으로 할당하는 힙 메모리와 달리 메모리 할당과 해제에 대한 오버헤드가 없다.

스택은 데이터를 저장할 때 스택의 경계를 넘어 다른 영역이 침범되는 것을 막기 위해 한계치(가장 높은 주소)부터 거꾸로 저장한다. 즉 스택의 top은 스택 메모리 중 가장 낮은 주소이다.


스택의 전반적인 특징을 알아보기전에 스택 프레임에 대해 먼저 살펴보자. 특징만 보려면 [스택이 힙보다 빠른 이유 1] 부터 보면된다.


[스택 프레임]

함수 호출과 반환이 이 스택 메모리에서 일어나며 함수를 호출할 때마다 아래 사진 처럼 스택에 "스택 프레임" 이라는 것이 쌓인다. 이 스택 프레임은 하나의 호출된 함수가 사용하는 영역이다. 모든 프로그램에 존재하는 main도 함수이며, entry point이기 때문에 항상 스택의 가장 아래에 main 함수의 스택 프레임이 쌓여있다.

스택과 스택 프레임

 


스택 프레임에 저장되는 것들은 생각보다 여러가지가 있다. 사용하려는 지역 변수, 매개 변수뿐만 아니라,  함수 동작을 마치고 호출한 영역으로 복귀할 주소가 필요하며, 자신을 호출한 함수를 계속해서 실행하기 위하여 그 함수의 프레임 주소도 필요하다. 따라서 재귀 함수와 같이 함수의 호출이 연속으로 많이 발생할 경우 스택 영역은 크기가 작기 때문에 overflow가 일어날 수 있으며  속도가 매우 느려질 수도 있다. 아래는 두 함수의 스택 프레임을 보여준다.

 

 

스택은 데이터를 push & pop 할 때 스택 포인터(SP)를 단순히 위아래로 옮김으로써 스택을 동작 시킨다.
스택 포인터(SP)는 항상 스택 프레임의 top을 가리킨다. 그리고 베이스 포인터(BP)라는 것은 현재 실행되고 있는 함수의 스택 프레임의 첫 위치를 저장한다. 따라서 BP는 새로운 함수가 호출될 때마다 변경되므로 이전 BP를 어디엔가 임시로 저장시켜놓는다. 그 값이 위의 sub 스택 프레임에 저장된 "caller base"이며 main 함수의 첫 주소를 가리키고 있다.

그 위의 "return adderess"는 함수의 동작을 다한 후 돌아갈 위치로 복귀 주소라고도 한다. 이 주소를 PC(Program Counter)에 넣어주면 해당 위치부터 다시 코드를 실행한다.


[스택이 힙보다 빠른 이유 1]
처음에 말했듯이 스택은 미리 예약된 메모리이기 때문에 메모리 할당이 따로 필요없으며, 사용한 메모리를 굳이 반납하지 않고 단순히 스택 포인터를 감소시킴으로써 스택 공간을 제한시킨다. 스택 포인터를 감소시키면서 남아있던 데이터는 스택 포인터가 다시 증가할 때 사용했던 공간을 단순히 덮어 쓰는 식으로 사용된다.

때문에 객체는 속한 영역(scope)을 벗어나면 스택 포인터의 위치가 바뀜에따라 알아서 사라진다. 따라서 함수를 통해 메모리가 할당되고 해제되는 힙 메모리에 비해 매우 빠르다는 이점을 가지고 있다.


[스택이 힙보다 빠른 이유 2]
스택은 모든 것이 컴파일 타임과 관련이 있다.
스택에 저장되는 데이터들은 그 크기가 컴파일 타임에 정해지기 때문에 스택은 미리 정해진 크기를 따라 움직이게 된다. 따라서 실행 중에 메모리를 계산하거나 판단할 일이 없기에 힙 메모리 할당보다 훨씬 빠른 이유기도 하다.

[스택의 단점]
힙에 비해 메모리의 크기가 매우 적다는 단점을 가지고 있다. 
그리고 메모리 주소 범위를 벗어나게 되면 overflow가 발생할 수 있다. 특히 재귀 함수를 잘못 사용하면 무수히 중첩되는 스택 프레임(Stack frame) 때문에 발생할 수 있다.



 

힙 메모리 (Heap memory)

힙 메모리는 동적으로 할당된 메모리가 저장되는 공간이다. 동적 메모리란 사용자가 미리 그 크기를 알 수 없어 프로그램 실행 중에 확인하여 할당하는 메모리를 말한다.
malloc 또는 new를 통해 동적으로 할당된 메모리는 Heap 영역에 저장되며 스택과 달리 scope를 벗어나도 자동으로 사라지지 않는다. 따라서 힙 메모리는 할당한 사람이 책임지고 사용이 끝나면 해제해주어야 한다.


unmanaged 언어인 C++에선 메모리 관리를 해주지 않기 때문에 할당한 메모리를 사용 후에 반환해주지 않으면 프로그램이 종료될 때까지 사용되지 않는 상태로 남아있으며, 이 사용되지 않고 메모리만 차지하는 놈이 메모리 누수(Memory leak)의 원인이다. C/C++ 을 제외한 Java, C#과 같은 언어는 관리되는 언어라고 해서 managed 언어라고 불리며, GC(Garbage Collector)가 알아서 쓰
지 않는 메모리를 해제해준다.

[메모리 단편화와 단점]
아까 힙 메모리에 메모리를 할당하는 것은 스택보다 매우 느리다고 했다. 그 이유로는 힙 메모리는 메모리를 할당할 때 운영체제가 메모리를 훑으면서 할당 가능한 연속된 메모리 공간을 찾은 뒤에 반환해주어야 하기 때문이다. 100mb가 필요하다면 연속된 100mb 공간을 찾아 반환해주어야 한다. 

또한 힙 메모리의 재할당이 자주 발생하면 메모리 단편화를 일으키키도 한다. 메모리 단편화란 남아있는 메모리는 충분하지만 연속된 메모리가 남아있지 않고 띄엄띄엄 있어 배열과 같은 연속된 메모리를 할당해줄 수 없는 현상을 말한다.
하지만 힙 메모리는 스택에 비해 저장할 수 있는 공간이 훨씬 많기 때문에 많은 데이터를 저장할 수 있다.

+ 추가 내용새롭게 알게 된 사실인데 힙 메모리의 해제 또한 속도를 저하시키는 원인 중 하나라고 한다. 메모리 해제 시 사용하던 메모리를 반환할 때 기존에 존재하던 큰 메모리 덩어리와 합치게 되는데 이 병합 작업이 속도를 저하시킨다고 한다. 병합한다는 것은 알았지만 이 것이 느린 줄은 몰랐다.woo-dev.tistory.com/187  




RAII (Resource Acquisition Is Initializaion)

C++에는 RAII라는 패턴 및 기법이 있다. Resource Acquisition Is Initializaion: "자원 획득은 초기화" 라는 의미이다.
자원은 메모리를 뜻하며, 조금 풀어서 써보면 "자원 획득은 초기화 시점에 일어나야 한다", "초기화는 객체의 올바른 자원 획득을 보장해야한다" 정도로 볼 수 있다. 

단순히 위의 의미에서 끝날 수도 있지만 이 RAII라는 기법은 좀 더 깊게 생각해주어야 한다. 이는 전체적으로 메모리 관리에 대한 기법 및 패턴이라는 것을 기억해야 한다. 메모리 중에서도 굳이 따지자면 스택보단 힙 메모리 관리에 대한 필요성이 좀 더 크지만, 원리가 스택의 특징과 비슷하므로 둘 다 해당된다. 

우리가 지역 변수 또는 객체를 생성하면(= 자원을 획득하면) 이 객체는 자기가 속한 scope를 벗어나면 자동으로 사라지는 특징(= 자원을 반환)을 가지고 있다.

이는 클래스에서 생성자와 소멸자의 관계로도 볼 수 있다.
객체는 범위(scope)를 벗어나면 자동으로 소멸자가 호출되며 객체가 파괴되는 성질을 가지고 있다. 이 성질을 이용하여 생성자에서 자원을 할당하고 소멸자에서 자원을 반환함으로써 마치 스택처럼 자원의 순환을 보장해주도록 하는 것이다.

스택 메모리의 객체는 영역을 벗어나면 자동으로 해제되지만, 힙 메모리는 자동으로 해제되지 않기 때문에 생성자와 소멸자에 힙 메모리를 할당 및 해제하는 방식으로 스택처럼 메모리가 자동으로 순환되도록 해준다. 


정리하자면, RAII라는 이름에선 자원 획득과 초기화에 관한 것이 전부이지만 이는 객체가 초기화 시 자원을 획득했다면 파괴 시엔 자원을 반환한 상태가 되어야 한다는 것이다.

RAII는 생성자와 소멸자에만 한정하는 것이 아니라, 메모리를 할당했으면 메모리 해제가 보장되도록 하는 모든 상황에서 적용될 수 있다.

C++ 11에서는 RAII의 원리가 적용된 스마트 포인터라는 것이 생겼으며, 이는 사용자의 메모리 관련 실수를 막기 위한 포인터이며 나중에 포스팅하겠다.




댓글