엔지니어링 101: 포인터 이해하기

엔지니어링 101: 포인터 이해하기

Python이나 JavaScript로 코딩 여정을 시작했다면, "포인터"라는 단어는 마치 흰 수염을 기른 시스템 엔지니어들만을 위한 신비로운 흑마법처럼 들릴지도 모릅니다. 대학에서 C++를 배웠다 하더라도, 세그멘테이션 폴트(segmentation fault)와 메모리 누수로 고생했던 기억만 남아있을 수도 있죠.

하지만 비밀을 알려드리자면: 포인터는 그저 변수일 뿐입니다. 마법이 아닙니다. 숫자입니다. 단지 그 숫자가 다른 무언가의 주소일 뿐입니다.

포인터를 제대로 이해하려면, 먼저 데이터가 실제로 어디에 사는지 이야기해야 합니다.

두 왕국: 스택(Stack) vs. 힙(Heap)

코드가 실행될 때, 데이터는 어딘가에 있어야 합니다. 일반적으로 데이터는 스택 또는 이라는 두 곳 중 하나에 자리 잡습니다.

스택(Stack): 빠르고 정리된 공간

스택을 책상 위의 포스트잇 뭉치라고 상상해 보세요.

int a = 10;과 같은 변수가 있다면, 아마 여기에 있을 겁니다. 깔끔하고, 빠르고, 안전하죠.

힙(Heap): 야생의 서부

힙은 거대한 창고입니다.

그래서, 포인터가 뭔가요?

포인터는 단순히 표지판입니다.

힙의 주소 0x1234score = 99라는 변수가 있다면, 포인터는 스택에 있는 아주 작은 변수로 "데이터는 0x1234에 있어요"라고 말해주는 것입니다.

int score = 99;  // 실제 값
int *p = &score; // 포인터 'p'는 'score'의 주소를 담고 있음

왜 이런 고통을 겪어야 하나요?

왜 그냥 값을 주고받으면서 언어가 알아서 처리하게 하지 않을까요?

1. 속도 (복사 없음)

10MB 크기의 이미지가 있다고 상상해 보세요. 만약 process(image) 함수에 이 이미지를 넘길 때 언어가 이미지를 복사한다면, 픽셀을 복사하느라 10MB의 RAM과 CPU 시간을 낭비하게 됩니다. 포인터를 사용하면 주소(8바이트)만 넘기면 됩니다. 함수에게 "이미지는 저쪽에 있으니 가서 봐"라고 말하는 것이죠.

2. 상태 공유

변수를 값으로 함수에 전달하면, 함수는 복제본을 받습니다. 복제본을 변경해도 원본은 그대로입니다. 포인터를 전달하면, 함수는 원본이 어디에 있는지 알게 됩니다. 실제 데이터를 수정할 수 있죠.

3. 동적 구조

연결 리스트, 트리, 그래프—이것들은 다른 데이터 덩어리를 가리키는 포인터를 가진 데이터 덩어리일 뿐입니다. 참조 없이는 이런 구조를 쉽게 만들 수 없습니다.

치러야 할 대가

포인터는 공짜가 아닙니다.

  1. 메모리 오버헤드: 포인터 자체도 공간을 차지합니다 (64비트 시스템에서 보통 8바이트). 아주 작은 정수들의 배열을 가리키는 포인터 배열이 있다면, 데이터 자체보다 포인터에 더 많은 메모리를 쓸 수도 있습니다!
  2. 성능 저하: "역참조"(포인터를 따라 데이터로 가는 것)는 CPU 사이클을 소모합니다. 또한 "캐시 미스"를 유발할 수 있습니다—데이터가 RAM의 먼 곳에 있다면 CPU는 데이터를 가져오기 위해 기다려야 합니다. 스택은 캐시 친화적이지만, 포인터가 많은 구조(연결 리스트 등)는 그렇지 않은 경우가 많습니다.
  3. 두뇌 오버헤드: 널 포인터 예외. 댕글링 포인터. 메모리 누수. 포인터를 수동으로 관리하는 것은 훈련이 필요합니다. 그래서 현대 언어(Rust, Go)나 런타임(Java, V8)은 포인터를 숨기거나 대신 관리해주려고 노력합니다.

결론

포인터는 여러분의 코드와 하드웨어 사이의 다리입니다. 고수준의 JavaScript나 Python을 작성하더라도, 객체가 내부적으로는 그저 참조(포인터!)라는 것을 이해하면 왜 a = bb를 복사하는 것이 아니라 a가 같은 데이터를 가리키게 되는지 이해하는 데 도움이 됩니다.

포인터를 마스터하면 코드가 어떻게 실행되는지 꿰뚫어 볼 수 있는 엑스레이 비전을 갖게 됩니다. "마법"이 아닌 "메모리"를 보게 될 것입니다.

© Melvin Laplanche - All rights reserved.