스파르타 내일배움캠프/Today I Learned

Today I Learned - Day 21 [메모리 구조에 대하여]

불면증 도사 2024. 10. 14. 20:31

금일은 특강으로 메모리 구조에 대해서 공부했었다.

이전에도 배운적이 있어서 알고 있던 내용이었는데, 막상 보니까 했깔렸었다.

그래서 한번 더 정리해볼 생각이다.

 

Intro. Struct와 Class의 차이점?

Struct와 Class는 메모리 구조의 차이가 가장 두드러지게 나타나는 요소들이다.

일반적으로 차이점을 서술할 때에는 상속이 가능하느냐, Call by Value | Call by Referrence 등등 사용 상의 차이점이 나오겠지만, 오늘은 하술할 메모리 구조를 살펴본 뒤에, '최적화 측면'에서 살펴보도록 하자.

[즉, 마지막에 마무리로 정리할 예정인 셈이다.]

 

Heap? Stack?

프로그램이 메모리를 사용할 때, 메모리의 영역은 다음 4개로 나뉜다.

  • CODE: 프로그램의 소스코드가 할당되는 영역.
  • DATA: 프로그램의 작동에 필요한 정보가 저장되는 영역. static [전역]변수나 const [불변]변수도 여기에 저장된다.
  • HEAP: '동적' 데이터가 할당되는 곳.
  • STACK: '정적' 데이터가 할당되는 곳.

위의 두 줄은 어떤 요소가 할당되는 지 명확하게 나오지만, Heap과 Stack은 '객체'와 '변수'라고 애매하게 서술되어 있다.

오늘은 이 둘의 차이에 대해서 서술하겠다.

 

Heap에 대하여

Heap은 객체 데이터, 정확하게는 '동적으로 할당되는 데이터'를 저장하는 메모리 공간으로, 동적으로 할당된다는 특성 상, 함수에 종속되지 않기 때문에 함수 안에서 직접 접근하기가 어렵고 주소(포인터) 변수라는 요소를 이용해서 접근해야 하는 영역이다.

 

이 영역의 특징은 '함수에 종속되지 않는다'는 점으로, 전역 변수(static)를 제외하면 함수와 함수 사이를 연결할 수 있는 몇 안되는 방법 중 하나이기 때문에 객체지향 프로그래밍과 절차적 프로그래밍, '정보 지향 프로그래밍' 셋 모두에게 중요한 개념이다.

[다른 하나는 신호(signal). 다만, 이쪽은 멀티 프로세싱이 기반 조건으로 깔려있어야 한다.]

[정보 지향 프로그래밍은 MonoBehavior를 메인 쓰레드에서만 동작 가능한 Unity에서 멀티 프로세싱을 구현하기 위해서 발안한 개념이다. 유니티의 특색인 '편의성'이 퇴색되기에, 일반적으로는 잘 안쓰이는 것으로 알고 있다.]

 

다만, 반대로 함수에 종속되지 않기 때문에, 사용하지 않을 때에 메모리를 해제해 주지 않으면, 메모리에 항상 상주해 있게 되는 '가비지'로 전락해, 가비지 컬렉터로 해제하기 전 까지는 항상 메모리를 잡아먹게 된다.

 

이러한 가비지가 쌓여서 프로그램의 작동에 장해를 주는 것을 메모리 누수라고 말한다.

특히 일반적인 프로그래밍 언어들은 수동으로 가비지 컬렉션을 해야되는 구조라서 코딩 실수로 메모리 누수가 나게 되면 걷잡을 수 없이 일이 커지기 때문에 매우 중요하게 살펴봐야 하는 요소.

 

다행히, Unity의 경우에는 C#을 사용하기 때문에 가비지 컬렉션션이 자동으로 실행된다.

다만, 가비지가 많으면 성능이 떨어지는 건 동일하기 때문에 가비지가 늘어나지 않도록 신경써야 하는 것은 동일하다.

 

Stack에 대하여

Stack은 변수 데이터, 정확하게는 '정적으로 할당되는 데이터'를 저장하는 메모리 공간으로, 함수에 종속되어 '쌓아올리는' 구조이기 때문에 Stack[(책 등의) 더미]로 불린다.

[이름 때문에 자료 구조 상의 Stack과 유사하거나 같다고 볼 수도 있으나, 엄연히 다른 개념이다.]

 

'함수에 종속된다'는 점 덕분에 이점이 꽤 많은데, 대표적으로 '주소를 지정하는 변수 없이도 함수가 직접 접근이 가능하다'는 점이 있다. 이 때문에 Stack 영역에 있는 자료들은 접근 속도가 빠르고 안정적이다.

 

동시에 함수에 종속되기에 '함수가 종료되면 같이 제거된다.' 이것이 정적 변수의 가장 큰 장점으로, 메모리 할당 빈도나 가비지를 걱정할 필요 없이 사용이 가능하다. 바로 이 점 때문에 Struct와 Class가 서로 다른 개념으로 분리된 것이라고 봐도 무방할 정도로 우수한 특성.

 

대신 이쪽도 문제는 있는데, 바로 '함수 안의 함수'같은 방식, 특히 재귀 함수를 사용하는 경우에는 변수의 양이 폭증한다는 것. 상술한 메모리 누수 보다 더 심각한 문제로, 자칫 잘못하면 코드 몇 줄으로 시스템 자체가 터져버릴 수 있다.

이 점 때문에 Unity에서는 Struct나 재귀함수를 너무 자주 쓰지는 않는 것을 권장하고 있다.

 

 

자, 여기까지 Heap과 Stack의 차이점에 대해서 알아보았다.

그럼, Class와 Struct로 다시 돌아가 보자.

 

Outro. Struct와 Class의 차이점?

다시금 Class와 Struct다. 이제 무엇이 다른지를 정리해 보자.

 

Struct는 Stack 영역에 저장되는 데이터다.

  • 때문에 함수에 종속되어 있고,
  • 때문에 Call by Value로 작동하며,
  • 함수가 끝나면 이 역시 같이 제거된다.
  • 대신 모종의 이유로 함수가 과포화 상태가 되면 메모리 이슈의 주 원인이 된다.

Class는 Heap영역에 저장되는 데이터다.

  • 때문에 독립적인 객체로서 활용되고,
  • 때문에 Call by Referrence로 작동하며,
  • 함수 외부에서 편리하게 접근할 수 있다.
  • 대신 함수가 끝나도 남아 있어 가비지가 될 가능성이 있다.

이렇게, 저장되는 영역 만 알아도 다양한 내용을 추측할 수 있다.

사실상 Class와 Struct의 가장 두드러지는 차이점이며, 다른 차이점이 이에 파생되었다고도 볼 수 있을 정도.

 

 

번외. static에 대하여.

static은 전역 범위를 선언하는 키워드로, 이를 사용하여 생성된 변수 혹은 함수를 프로젝트 안의 어떤 함수더라도 간단하게 사용할 수 있다.

이 변수나 함수들은 앱을 시작할 때 Data 영역에 저장되며, 앱을 종료할 때에 제거된다.

 

번외 2. 생성되는 시점과 제거되는 시점.

간단하게 정리한다.

  • Stack
    • 생성 시점: 해당 변수를 선언했을 때.
    • 제거 시점: 해당 함수가 종료되었을 때.
  • Heap
    • 생성 시점: C#은 new Class() 로 생성. Unity에서는 Instantiate가 이 역할을 한다.
                      [C에서는 malloc을 통해서 생성한다.]
    • 제거 시점: C#은 가비지 컬렉터가 안쓰는 데이터를 자동으로 제거.
                      [Unity에서도 Destroy()는 객체의 연결 만 끊을 뿐, 제거 자체는 가비지 컬렉터가 수행한다,]
                      [C는 free를 통해서 제거 가능. 만약 못했다면 가비지 컬렉터를 수동으로 작동해야 한다.]
  • Static
    • 생성 시점: 프로그램 시작 시 생성.
      ※ 생성 시점이 '가장 앞'이기 때문에, 같은 static이 아니라면 static에서 활용할 수가 없다.
    • 제거 시점: 프로그램 종료 시 제거.

 

번외 3. Stack와 Heap은 메모리 공간을 공유한다.

정확히는 특정 기준으로 나누고 있기는 하나, 기본적으로는 메모리 공간을 공유하며, 부족하면 동적으로 변화한다.

다만, Stack이 Heap의 범위를 넘어버리거나, Heap이 Stack의 범위를 넘어버리는, Overflow 문제가 발생하기도 한다.

물론, C#은 가비지 컬렉터가 자동으로 수거해 버리기 때문에 Overflow 문제 보다는 가비지 컬렉터 효율성 문제가 훨씬 더 잦지만, 그래도 메모리 최적화 문제는 지속적으로 유념해야 하는 개념이다.

 

 

오늘은 메모리 구조에 대해서 알아보았습니다.

솔직히 말해서 글로만 설명하기에는 좀 복잡할 수도 있는 내용인지라, 최대한 이해하기 쉽도록 정리했는데, 잘 됬는지는 잘 모르겠네요.

솔직히 저도 원래 알고 있는 내용인데도 Stack과 Heap을 한번 씩 헷깔리는 지라, 저도 틈틈히 찾아볼 거 같습니다...;;

[솔직히 헷깔릴 만 한 건 아닌데, 이상하게 아리쏭하게 됩니다...;;]