[프로그래머가 되자] 소프트웨어 개발자의 메모리 관리

bae.200.ok
7 min readJun 26, 2022

안녕하세요. 배민혁 입니다.

지난 글을 통해서 “소프트웨어 개발자는 컴퓨터 동작에서 오는 병목현상을 이해해야 한다.”고 결론 내렸습니다. 그 중에서도 메모리 관리에 대한 이야기를 조금 했었습니다. 오늘은 그 이야기를 자세히 해보려 합니다.

1 메모리 관리

  • 폰노이만 병목현상: CPU의 데이터 처리 능력과 다른 장치들의 데이터 처리 능력이 매우 차이가 많이 난다. 이것이 곧 병목을 만든다.
  • 하드웨어를 통한 병목 현상 해결: CPU와 메모리 사이에 캐시 메모리를 두어 속도 차를 해결하려한다.
  • 병목현상 해결의 핵심은 ‘제한된 저장공간인 메모리에 필요한 데이터가 존재하도록 하자.’이다. 즉, Hit Rate를 올려야한다.

메모리 관리는 운영체제의 영역과 프로그래머의 영역으로 구분할 수 있습니다. 해당 글은 운영체제의 메모리 관리에 대해서 이야기하고자 하는 것은 아니기에 키워드만 가볍게 남기도록 하겠습니다.

  • 운영체제의 메모리 관리와 관련된 키워드: 가상 메모리, 페이징, LRU 알고리즘

2 메모리 구조

글의 뒤쪽에 있는 내용을 이해하기 위해서 메모리의 구조부터 알아보겠습니다.

2.1 메모리 부가 설명

  • 메모리: 전기신호를 저장할 수 있는 트랜지스터라는 작은 반도체 소재로 이루어져 있다. 이를 이용해서 0,1 (bit, binary Digit)을 표현할 수 있다.
  • 1MB 메모리가 있다고 가정하면, 1KB는 1Byte가 2¹⁰개, 1KB가 2¹⁰개 있어야 1MB가 될 수 있다.
  • 주소값: 메모리 공간 속에서 데이터를 찾기 위해서는 바이트마다 지표가 필요한데, 그 지표가 주소값이 된다.(0x00xFFFFF)
  • 자료구조는 데이터를 저장하고 관리하는 방식을 말하는데, 이를 잘 사용하면 메모리를 효율적으로 쓸 수 있다.

2.2 메모리 구조

2.2.1 런타임에 할당

  • Stack: First In Last Out, 먼저 들어온 데이터가 나중에 나가는 구조를 가진 공간
    > 함수나 메서드가 호출 될 때 사용되는 지역변수, 매개변수, 반환 값이 차지하는 공간이다. 위와 같은 호출 정보를 스택 프레임이라고 한다.
  • Heap: 프로그래머가 직접 할당/해제하는 메모리 영역.
    > 자바, 코틀린의 경우 객체를 새롭게 생성하면 메모리를 할당하게 된다. 해제는 GC(Garbage Collector)가 메모리를 수거한다.
    > 프로세스가 종료되면 힙 메모리는 OS에 반납되어 해제된다.
    > 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 영역을 공유한다.

동적 메모리 할당(Dynamic Memory Allocation)

:런타임 동안 사용할 힙 메모리 공간을 프로그래머가 할당하는 것을 이야기한다. 프로그램 실행 시 크기가 결정되는 정적 메모리할당과 대조된다.

2.2.2 컴파일타임 할당

  • Code(Text): 컴파일 된 명령어의 집합으로, 해당 명령어는 CPU가 한 라인씩 수행한다.
  • Data: 전역변수, Static변수(이하 정적변수)가 저장되는 영역이다. 여기서 정적변수는 말그대로 정적으로 할당되는 변수로, 힙 메모리에 동적 할당 되는 객체의 반의어이다.
    > 정적변수를 프로그래밍에 더 가깝게 풀어서 이야기해보면, 정적변수에 할당된 값은 프로그램이 종료되지 않는 이상 메모리상에 남아있다. 그렇기 때문에 더욱 주의해서 사용해야한다.
    > ex. java: static, kotlin: companion object

3 우리가 작성한 코드가 실행될 때, 데이터가 메모리 빈 공간에 어떻게 할당되는가?

소프트웨어 개발자의 메모리 관리에 대해 이해도를 높이기 위해서, 우리가 작성한 코드가 실행될 때 데이터가 메모리 빈 공간에 할당되는 프로세스를 이해하는 것이 중요합니다.

이야기에 앞서 몇가지를 정의해보겠습니다.

  • 애플리케이션: 사용자가 사용할 기능을 제공하고 컴퓨터가 실행할 수 있는 명령어들의 집합
  • 프로그램: 디스크에 존재하는 실행파일(ex. .exe )
  • 프로세스: 프로그램이 메모리에 올라간 상태. 즉, 프로그램이 실행되고 있는 것
  • CPU: 명령어를 실행하는 주체

코드가 실행되는 큰 흐름은 아래와 같습니다.

  1. 프로그램 실행 요청에따라 프로그램의 정보가 메모리에 로드(load)된다. > 프로세스 상태
  2. 메모리에 올라온 명령어를 CPU가 실행한다.
  3. CPU에 의해 처리된 결과 값이 메모리에 저장된다.
def print_number(num):
print(num)
test = 1
print_number(test)

위의 코드가 어떻게 동작되는지는 ‘메모리의 구조’와 ‘파이썬에서 모든 것이 객체’라는 것을 알고 있다면 동작 과정을 그려볼 수 있을 것 같습니다.

  • pring_number라는 함수를 정의했다.
  • test라는 변수를 1로 초기화를 했다.
  • print_number라는 함수에 test를 매개변수로 전달했다.
  1. Heap: 1이라는 객체를 힙에 생성한다.
    Stack: 1이라는 객체의 주소값을 가리키는 test를 프레임이 스택에 할당된다.
  2. Stack: print_number를 호출하면 호출 정보인 스택 프레임이 스택에 할당된다. 이때 num으로 test를 넘기므로, num 또한 1 이라는 객체의 주소값을 가진다.

위의 과정에서 우리가 관심있는 부분은 명령어를 포함한 데이터가 메모리에 저장될 때에 주의할 점이 있는가 입니다.

4 소프트웨어 개발자가 메모리를 사용할 때에 주의할 점

4.1 객체의 상태는 그대로 유지된다.

메서드 내에서 객체인 파라미터를 변경한다면, 호출 이후에도 객체의 상태는 그대로 유지됩니다. 이는 스택 메모리에서 주소값을 참조하고 있기 때문입니다.

당연한 이야기지만, 이것을 인지하지 못한 상태로 코드 변경을 한다면 수많은 버그를 만들어 낼 것입니다.

4.2 할당된 메모리가 불필요하다면 해제해야한다.

비워주어야 할 메모리 공간을 비우지 않고 남아있도록 하면 그런 것들이 지속적으로 쌓여 메모리를 꽉채우게 됩니다. 그렇게되면 더이상 메모리를 사용할 수 없어 컴퓨터는 동작하지 못하는 상태가 되버립니다.

비워주어야할 메모리 공간을 비우지 않고 남아있도록하여 계속 점유하고 있는 현상을 memory leak(메모리 누수)라고 합니다. C , C++ 는 메모리를 할당하고 해제하는 동작을 하나하나 작성했습니다. 그렇지만 현대 언어에 가까운 언어들(java, python, kotlin …)은 가비지 컬렉션(GC, Garbage Collection 이하 GC)이 존재하여 내부적으로 알아서 쓰레기들을 처리하고 있습니다.

안타깝게도 GC가 모든 쓰레기들을 처리해주지 않습니다(예를들면, 순환참조). 그렇기 때문에 언어와 환경을 고려하여 작업을 해야합니다.

4.3 GC는 한계가 있다.

GC가 만능은 아닙니다. 어떤 데이터를 수거 해야할 지 판단하는 것도 결국은 연산에 포함되기에 리소스를 사용하게 되고, 이것 또한 프로그램의 성능에 영향을 끼치게 됩니다.

또한, 수거해야하는 데이터로 인지하지 못해서 수거하지 않는 경우도 있습니다.

이러한 GC의 특징들은 언어 별로 다릅니다. 또한 내가 사용하는 프레임워크, 라이브러리 별로 메모리를 사용하는데 여러 특징이 있습니다. 그렇기에 우리는 해당 도구에서 메모리를 효과적으로 관리할 수 있는 방법을 알아야합니다.

5 마치며

저는 이 글을 작성하면서 내가 쓰는 도구(언어를 포함한)들의 메모리 관리측면에서의 특징을 알아봐야겠다고 마음먹은 계기가 되었습니다.

최근 코틀린을 쓰고 있으니 다음에는 JVM에 대해서 글을 작성하게 될 것 같습니다.

추가로, 병목 현상의 해결을 위한 Cache hit rate를 올리고, cache를 사용하는 방식이 의미가 있기 위해선 Locality of reference가 바탕이 되어있어야한다고 합니다. 해당 내용도 확인해봐야겠습니다. 피드백은 언제나 환영입니다!

--

--