Загружаем каталог…
Загружаем каталог…
개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 Java, Python, JavaScript처럼 메모리를 직접 관리하지 않는 언어를 사용하더라도 프로그램이 실행되는 아래쪽에서는 결국 메모리에 값을 저장하고, 주소를 따라 객체에 접근하고, 필요 없는 메모리를 회수하는 일이 반복된다. 이번 글에서는 C를 중심으로 저수준 메모리와 시스템 언어(Systems Language)의 공통 개념을 살펴본다. 고수준 언어를 사용하면 new , 객체, 배열 같은 추상화 뒤에서 메모리가 자동으로 관리된다. 하지만 메모리 누수(Memory Leak), 복사 비용, 동시성 문제, 가비지 컬렉션(Garbage Collection), 네이티브 라이브러리 연동, 성능 문제를 이해하려면 그 아래에서 데이터가 어떻게 저장되고 이동하는지 알아야 한다. 이번 글의 흐름은 값의 표현 → 주소와 포인터 → 메모리 영역 → 동적 메모리 → 사용자 정의 자료형 → 빌드 과정 → 소유권 → 동시성 모델 순서로 이어진다. 1️⃣ 값은 메모리에 어떻게 저장될까 1-1. 기본 타입(Primitive Type) ⭐️⭐️⭐️ 컴퓨터의 메모리는 결국 바이트(Byte)의 연속된 공간이다. 프로그래밍 언어의 타입은 이 바이트들을 얼마나 읽을 것인지, 어떤 의미로 해석할 것인지 결정한다. C에서는 char , int , float , double 처럼 타입마다 일반적으로 서로 다른 크기와 표현 방법을 사용한다. 예를 들어 같은 비트 패턴이라도 정수(Integer)로 읽는지, 문자(Character)로 읽는지에 따라 의미가 달라진다. Memory ┌────────┬────────┬────────┬────────┐ │ Byte │ Byte │ Byte │ Byte │ └────────┴────────┴────────┴────────┘ ↑ 어떤 타입으로 해석할 것인가? 고수준 언어에서는 이런 세부사항을 직접 다루는 일이 적지만 데이터베이스 타입, 네트워크 패킷, 파일 형식, 직렬화, GPU 메모리처럼 시스템 경계에 가까워질수록 다시 중요해진다. 1-2. 부호 있는 정수와 부호 없는 정수(Signed & Unsigned) ⭐️⭐️ 정수는 음수를 표현할 수 있는 부호 있는 정수(Signed Integer)와 0 이상의 값만 표현하는 부호 없는 정수(Unsigned Integer)로 나눌 수 있다. 같은 비트 수를 사용하면 부호 없는 정수는 음수를 표현하지 않는 대신 양수 영역을 더 넓게 사용할 수 있다. 문제는 두 타입을 섞어서 계산하거나 범위를 벗어나는 값을 다룰 때 예상하지 못한 변환이나 오버플로(Overflow)가 발생할 수 있다는 점이다. 따라서 숫자 타입을 선택할 때는 단순히 int면 충분하다 가 아니라 값의 범위, 음수 가능성, 다른 타입과의 연산, 직렬화 형식 까지 함께 고려해야 한다. 2️⃣ 포인터(Pointer) 2-1. 주소와 포인터(Address & Pointer) ⭐️⭐️⭐️ 메모리의 각 위치에는 주소(Address)가 있다. 포인터(Pointer)는 다른 데이터가 저장된 메모리 주소를 값으로 가지는 변수 다. 변수 p ┌──────────┐ │ 0x1000 │ └──────────┘ │ ▼ 주소 0x1000 ┌──────────┐ │ 42 │ └──────────┘ C에서 & 는 변수의 주소를 가져오고 * 는 포인터가 가리키는 위치의 값에 접근하는 데 사용한다. int value = 42; int *p = &value; printf("%d", *p); 여기에서 p 가 저장하는 것은 42 자체가 아니라 value 가 존재하는 주소다. *p 를 사용하면 그 주소로 이동해 실제 값을 읽는다. 포인터를 이해하면 참조(Reference), 객체 주소, 배열, 동적 메모리, 함수 포인터(Function Pointer), 네이티브 인터페이스를 이해하기 훨씬 쉬워진다. 2-2. 배열과 문자열(Array & String) ⭐️⭐️⭐️ C의 배열(Array)은 같은 타입의 값들이 메모리에 연속적으로 배치되는 구조다. int arr[4] 주소 값 1000 → [10] 1004 → [20] 1008 → [30] 1012 → [40] 따라서 배열의 시작 주소를 알고 각 요소의 크기를 알면 다음 요소의 주소를 계산할 수 있다. C에서 문자열(String) 역시 문자 배열을 기반으로 표현하며 마지막에 문자열의 끝을 나타내는 널 문자(Null Character) \0 를 사용한다. "H i ! \0" 고수준 언어에서는 문자열이 별도의 객체처럼 보이지만 저수준에서는 결국 메모리에 연속적으로 놓인 데이터와 그 데이터를 해석하는 규칙 이 중요하다. 2-3. 포인터 연산(Pointer Arithmetic) ⭐️⭐️ 포인터에 1 을 더한다고 주소 숫자가 항상 정확히 1 증가하는 것은 아니다. 포인터의 타입에 따라 해당 타입의 크기만큼 이동한다. 예를 들어 4바이트 int 를 가리키는 포인터라면 p + 1 은 다음 int 가 있는 위치를 가리킨다. p → arr[0] p + 1 → arr[1] p + 2 → arr[2] 배열 순회가 포인터와 자연스럽게 연결되는 이유다. 반대로 잘못된 주소까지 이동하면 프로그램이 소유하지 않은 메모리에 접근할 수 있다. 이런 자유로움 때문에 C는 강력하지만 메모리 안전성(Memory Safety)을 개발자가 직접 책임져야 하는 부분도 많다. 3️⃣ 프로그램의 메모리 영역 3-1. 스택과 힙(Stack & Heap) ⭐️⭐️⭐️ 프로그램의 메모리는 역할에 따라 여러 영역으로 나누어 생각할 수 있다. 대표적으로 코드 영역, 전역·정적 영역, 스택(Stack), 힙(Heap)이 있다. 스택은 주로 함수 호출 과정에서 만들어지는 지역 변수, 매개변수, 함수 실행 정보 등을 관리한다. 함수 호출과 반환에 따라 자연스럽게 생성되고 제거되므로 관리 비용이 비교적 작다. 힙은 실행 중 필요한 크기의 메모리를 동적으로 할당하는 공간이다. 객체의 크기나 생명주기를 컴파일 시점에 알기 어려운 경우 사용된다. Process Memory ┌────────────────────┐ │ Code │ ├────────────────────┤ │ Global / Static │ ├────────────────────┤ │ Heap │ │ ↓ │ │ │ │ ↑ │ │ Stack │ └────────────────────┘ 언어와 운영체제에 따라 실제 구현은 더 복잡하지만 Stack = 함수 호출과 밀접 , Heap = 동적으로 관리되는 메모리 라는 기본적인 구분은 여러 시스템을 이해할 때 반복해서 사용된다. 3-2. 전역 영역과 정적 영역(Global & Static) ⭐️⭐️ 전역 변수(Global Variable)와 정적 변수(Static Variable)는 일반적인 지역 변수보다 훨씬 긴 생명주기를 가진다. 프로그램 실행 동안 계속 존재할 수 있기 때문에 여러 코드에서 공유되는 상태를 만들기 쉽다. 공유 상태는 편리하지만 의존성 증가, 테스트 어려움, 동시성 문제, 변경 위치 추적의 어려움으로 이어질 수 있다. 따라서 static 이나 전역 상태를 볼 때는 단순히 메모리에 어디에 저장되는지만 보는 것이 아니라 누가 상태를 읽고 변경하는지, 생명주기가 얼마나 긴지 까지 함께 보는 것이 중요하다. 4️⃣ 동적 메모리 관리 4-1. malloc/free ⭐️⭐️⭐️ C에서는 힙 메모리를 직접 할당하고 해제할 수 있다. 대표적인 함수가 malloc , calloc , realloc , free 다. malloc 은 필요한 크기의 메모리를 할당하고, calloc 은 여러 요소를 위한 메모리를 할당하면서 초기화한다. realloc 은 기존에 할당된 메모리의 크기를 변경하고, free 는 더 이상 필요하지 않은 메모리를 해제한다. int *numbers = malloc(sizeof(int) * 100); /* 사용 */ free(numbers); 핵심은 malloc 보다 free 다. 메모리를 할당했다면 누가, 언제 해제할 책임이 있는가 가 반드시 결정되어야 한다. 이 질문은 이후 C++의 RAII, Rust의 소유권(Ownership), Java·Python·JavaScript의 가비지 컬렉션으로 이어진다. 4-2. 메모리 누수(Memory Leak) ⭐️⭐️⭐️ 필요한 메모리를 계속 할당하면서 더 이상 사용하지 않는 메모리를 해제하지 않으면 메모리 누수가 발생한다. malloc() ↓ 사용 ↓ 참조를 잃음 ↓ free() 불가능 ↓ Memory Leak 짧게 실행되는 프로그램에서는 눈에 띄지 않을 수 있지만 서버처럼 며칠 또는 몇 달 동안 계속 실행되는 프로세스에서는 작은 누수도 누적되어 장애로 이어질 수 있다. 가비지 컬렉션을 사용하는 언어에서도 메모리 누수는 사라지지 않는다. 사용하지 않는 객체가 다른 객체에서 계속 참조되고 있다면 가비지 컬렉터가 해당 객체를 제거할 수 없기 때문이다. 4-3. 댕글링 포인터(Dangling Pointer) ⭐️⭐️⭐️ 댕글링 포인터는 이미 생명주기가 끝난 메모리를 계속 가리키는 포인터 다. p ───▶ Object ↓ free() p ───▶ ??? 메모리를 해제했다고 포인터 변수 자체가 자동으로 사라지는 것은 아니다. 그 주소를 다시 사용하면 이미 다른 데이터가 들어갔거나 접근할 수 없는 영역일 수 있다. 메모리 누수가 사용하지 않는 메모리를 계속 가지고 있는 문제 라면 댕글링 포인터는 이미 사용할 수 없는 메모리를 다시 사용하는 문제 에 가깝다. 5️⃣ 구조체, 공용체, 열거형, 함수 포인터 5-1. 구조체(Struct) ⭐️⭐️ 구조체(Struct)는 서로 다른 타입의 값을 하나의 논리적인 데이터로 묶을 수 있다. struct User { int id; char *name; }; 고수준 언어의 객체나 데이터 클래스와 비슷하게 보이지만 구조체는 실제 메모리 배치와 훨씬 직접적으로 연결된다. 필드 사이에는 정렬(Alignment)을 맞추기 위한 패딩(Padding)이 들어갈 수 있기 때문에 선언된 필드의 크기를 단순히 더한 값과 구조체 전체 크기가 다를 수도 있다. 5-2. 공용체(Union) ⭐️ 공용체(Union)는 여러 필드가 같은 메모리 공간을 공유 한다. Union ┌─────────────┐ │ int │ │ float │ ← 같은 메모리 │ char[...] │ └─────────────┘ 한 시점에 여러 값을 동시에 별도로 저장하는 구조체와 달리 공용체는 같은 메모리를 여러 타입으로 해석할 수 있다. 메모리가 제한적인 시스템, 네트워크 프로토콜, 하드웨어와 가까운 코드에서 볼 수 있다. 5-3. 열거형(Enum) ⭐️ 열거형(Enum)은 가능한 값의 범위를 의미 있는 이름으로 제한할 때 사용한다. Status ├─ READY ├─ RUNNING └─ DONE 단순한 숫자 0 , 1 , 2 보다 코드의 의미를 명확하게 표현할 수 있으며 상태 머신(State Machine), 프로토콜 상태, 결과 타입 등을 표현할 때 자주 사용된다. 5-4. 함수 포인터(Function Pointer) ⭐️ 함수 역시 메모리의 특정 위치에 존재하는 코드이므로 함수의 주소를 저장하고 전달할 수 있다. 이를 함수 포인터(Function Pointer)라고 한다. 함수 포인터는 콜백(Callback), 이벤트 처리, 동작 교체, 플러그인 구조 같은 패턴의 저수준 기반이 된다. 고수준 언어에서 함수를 변수에 저장하거나 콜백으로 넘기는 코드 역시 추상화 수준은 다르지만 비슷한 아이디어와 연결된다. 6️⃣ 소스 코드는 어떻게 실행 파일이 될까 6-1. 전처리기, 헤더, 번역 단위(Preprocessor, Header, Translation Unit) C/C++ 코드가 바로 CPU에서 실행되는 것은 아니다. 일반적으로 소스 코드는 전처리, 컴파일, 링크 등의 과정을 거친다. Source Code ↓ Preprocessor ↓ Translation Unit ↓ Compiler ↓ Object File ↓ Linker ↓ Executable 전처리기(Preprocessor)는 #include , #define 같은 지시문을 처리한다. 헤더 파일(Header File)은 여러 소스 파일이 공유하는 선언을 제공하며, 전처리가 끝난 하나의 컴파일 단위를 번역 단위(Translation Unit)라고 한다. 각 번역 단위는 별도로 컴파일될 수 있고 최종 단계에서 링커(Linker)가 결과들을 연결해 실행 파일을 만든다. 이 과정을 이해하면 왜 헤더가 필요한가 , 컴파일 오류와 링크 오류가 왜 다른가 , 라이브러리를 연결한다는 것이 무엇인가 를 구분할 수 있다. 6-2. 정적 라이브러리와 동적 라이브러리(Static & Dynamic Library) 라이브러리를 프로그램에 포함하는 방식도 크게 정적 연결(Static Linking)과 동적 연결(Dynamic Linking)로 나눌 수 있다. 정적 연결은 필요한 라이브러리 코드를 최종 실행 파일에 포함하는 방식이고, 동적 연결은 실행 시점에 별도의 공유 라이브러리(Shared Library)를 사용하는 방식이다. Static Application + Library ↓ Executable Dynamic Application ──▶ Shared Library 정적 연결은 배포가 단순해질 수 있지만 실행 파일 크기가 커질 수 있고, 동적 연결은 여러 프로그램이 라이브러리를 공유할 수 있지만 라이브러리 버전과 실행환경의 영향을 받을 수 있다. 7️⃣ 프로그래밍 패러다임(Programming Paradigm) 프로그래밍 언어는 단순히 문법만 다른 것이 아니라 상태와 동작을 조직하는 방식 도 다르다. 대표적인 패러다임에는 절차지향(Procedural), 객체지향(Object-Oriented), 함수형(Functional), 선언형(Declarative), 논리형(Logic), 데이터 지향(Data-Oriented) 등이 있다. 절차지향은 수행할 명령의 순서를 중심으로 구성하고, 객체지향은 상태와 동작을 객체 단위로 묶으며, 함수형은 상태 변경을 줄이고 함수 조합을 강조한다. 선언형은 어떻게 수행할 것인가 보다 어떤 결과가 필요한가 를 표현하는 방식에 가깝다. 실제 애플리케이션은 하나의 패러다임만 사용하는 경우보다 여러 방식을 혼합하는 경우가 많다. JavaScript나 Python에서도 객체지향 코드와 함수형 코드가 함께 사용된다. 중요한 것은 패러다임을 서로 경쟁하는 방식으로 보는 것이 아니라 문제를 어떤 형태로 모델링하는 것이 가장 적절한지를 판단하는 도구 로 보는 것이다. 8️⃣ 소유권과 빌림(Ownership & Borrowing) Rust에서 대표적으로 사용되는 소유권(Ownership) 모델은 메모리와 자원의 생명주기를 컴파일 단계에서 관리하려는 접근이다. 하나의 값에는 해당 값을 정리할 책임을 가진 소유자(Owner)가 존재하고, 값의 소유권을 다른 곳으로 이동(Move)하거나 일정 기간 빌려서(Borrow) 사용할 수 있다. Owner A │ ├─ Borrow ──▶ Function │ └─ Move ────▶ Owner B 전통적인 C에서는 개발자가 malloc/free 의 책임을 직접 관리한다. 가비지 컬렉션 기반 언어에서는 런타임이 사용하지 않는 객체를 찾아 정리한다. Rust는 이 사이에서 컴파일 시점의 규칙으로 잘못된 메모리 사용을 줄이는 방식 을 선택한다. 소유권을 반드시 Rust 문법 수준까지 깊게 알 필요가 없더라도 서버 개발자에게 중요한 질문은 같다. 이 자원의 소유자는 누구인가? 누가 변경할 수 있는가? 언제까지 사용할 수 있는가? 누가 정리해야 하는가? 여러 실행 흐름에서 동시에 접근해도 되는가? 이 질문은 메모리뿐 아니라 파일, 데이터베이스 연결, Lock, Transaction, Socket과 같은 자원에도 그대로 적용된다. 9️⃣ 동시성 모델(Concurrency Model) 여러 작업을 동시에 처리하는 방법도 언어와 런타임에 따라 다르다. 9-1. 스레드와 공유 메모리(Thread & Shared Memory) 여러 스레드(Thread)가 같은 메모리에 접근하는 방식이다. 공유 데이터에 동시에 접근하면 경쟁 상태(Race Condition)가 발생할 수 있기 때문에 Lock, Mutex 같은 동기화 도구가 필요하다. Thread A ─┐ ├──▶ Shared State Thread B ─┘ 공유 메모리는 직접적이고 빠르지만 상태를 안전하게 관리하기 어려워질 수 있다. 9-2. 액터 모델(Actor Model) 액터(Actor)는 각자 자신의 상태를 가지고 다른 액터와 메시지를 주고받는다. 상태를 직접 공유하기보다 메시지 전달(Message Passing)을 중심으로 동시성을 구성한다. Actor A ──message──▶ Actor B 공유 상태를 줄일 수 있지만 메시지 순서, 장애 처리, 상태 분산 같은 새로운 문제를 고려해야 한다. 9-3. CSP와 채널(CSP & Channel) CSP(Communicating Sequential Processes)는 독립적인 실행 흐름이 채널(Channel)을 통해 통신하는 방식이다. Go의 Goroutine과 Channel이 대표적으로 이 모델의 영향을 받았다. Worker A ──▶ Channel ──▶ Worker B 상태를 직접 공유하는 대신 데이터 전달 경계를 명확하게 만들 수 있다. 9-4. 비동기와 코루틴(Async & Coroutine) 비동기(Async) 모델은 하나의 작업이 I/O 결과를 기다리는 동안 다른 작업을 처리할 수 있도록 한다. Node.js의 Event Loop, Python의 asyncio , Kotlin Coroutine 등은 세부 구현은 다르지만 I/O 대기 시간을 효율적으로 활용한다 는 공통 목적을 가진다. 스레드, 액터, 채널, 비동기는 서로 완전히 배타적인 모델이 아니다. 실제 시스템에서는 여러 방식을 함께 사용할 수도 있다. 🔟 핵심 개념 연결 Value Representation ↓ Address ↓ Pointer ↓ Memory Layout ↓ Stack / Heap / Global / Static ↓ malloc / free ↓ Resource Lifetime ↓ Ownership ↓ Concurrency 고수준 언어를 사용할 때도 아래 질문으로 연결해서 생각할 수 있다. 이 데이터는 메모리에 어떤 형태로 존재하는가? 이 변수는 값 자체를 가지는가, 다른 객체를 가리키는가? 이 객체는 스택과 힙 중 어디에서 어떤 생명주기로 관리되는가? 메모리나 자원의 소유자는 누구인가? 누가 자원을 정리할 책임을 가지는가? 같은 상태에 여러 실행 흐름이 동시에 접근할 수 있는가? 공유 상태 대신 메시지나 비동기 작업으로 분리할 수 있는가? 1️⃣1️⃣ 더 알아보기 RAII(Resource Acquisition Is Initialization) C++에서 자원의 생명주기를 객체의 생명주기와 연결하는 방식이다. 객체가 생성될 때 자원을 확보하고 객체가 소멸될 때 자원을 해제하도록 만들어 malloc/free , 파일, Lock 같은 자원 관리 실수를 줄일 수 있다. 스마트 포인터(Smart Pointer) C++에서는 unique_ptr , shared_ptr , weak_ptr 같은 스마트 포인터를 통해 객체 소유권을 표현할 수 있다. unique_ptr 는 단독 소유, shared_ptr 는 공유 소유, weak_ptr 는 객체의 생명주기를 연장하지 않는 참조를 표현한다. 가상 함수와 동적 디스패치(Virtual Function & Dynamic Dispatch) C++의 가상 함수(Virtual Function)는 실제 객체의 타입에 따라 실행할 메서드를 런타임에 선택한다. 내부적으로 가상 함수 테이블(vtable) 같은 구현과 연결될 수 있으며 객체지향 다형성(Polymorphism)의 저수준 구현을 이해하는 데 도움이 된다. STL 자료구조 C++ 표준 템플릿 라이브러리(STL)에는 vector , list , deque , map , unordered_map , set 등의 자료구조가 있다. 고수준 언어의 Collection을 이해할 때도 메모리 연속성, 탐색 방식, 삽입·삭제 비용을 비교하는 기준이 된다. 평가 전략과 의미론(Evaluation & Semantics) 즉시 평가와 지연 평가(Eager/Lazy Evaluation), 값에 의한 호출(Call-by-Value), 참조에 의한 호출(Call-by-Reference), 조작적 의미론(Operational S
То, что RADAR обнаружил и классифицировал для этой возможности. Это опубликованный источником текст, а не подтверждение, что предложение ещё действует.
[넓얕컴지 #2] 메모리는 어떻게 다뤄질까: 포인터, malloc/free, 소유권과 동시성. 개발자를 위한 넓고 얕은 컴퓨터 지식, 넓얕컴지 Java, Python, JavaScript처럼 메모리를 직접 관리하지 않는 언어를 사용하더라도 프로그램이 실행되는 아래쪽에서는 결국 메모리에 값을 저장하고, 주소를 따라 객체에 접근하고, 필요 없는 메모리를 회수하는 일이 반복된다. 이번 글에서는 C를 중심으로 저수준 메모리와 시스템 언어(Systems Language)의 공통 개념을 살펴본다. 고수준 언어를 사용하면 new , 객체, 배열 같은 추상화 뒤에서 메모리가 자동으로 관리된다. 하지만 메모리 누수(Memory Leak), 복사 비용, 동시성 문제, 가비지 컬렉션(Garbage Collection),…
Открыть источник