0. 글을 시작하며
C++ , JAVA 모두 객체 지향 언어인데 하나만 쓸까?
에 대한 이유를 막연히 메모리 관리, JAVA가 더 편하고 여러 환경에서 사용이 가능해서?
그렇다면 좀 더 구체적으로 이게 왜 저런 이유가 나오는지
그리고 두 언어는 단점 혹은 장점을 그대로 두거나 살리거나 어떻게 변했는지에 대해서 글을 적어보려고 합니다.
목차
1. 두 언어는 어떻게 빌드 되는가
2.C++의 메모리 영역
3. C++의 특징
4. C++ 속도의 함정
5. C++의 해결책 그리고 vs Java의 해결책
6. 도메인의 차이
1. 두 언어는 어떻게 빌드 되는가
1-1. c++의 빌드 과정
컴파일 단계에서 특정 운영체제(OS)와 하드웨어 아키텍처가 바로 알아들을 수 있는 기계어로 직접
번역되어서 AOT(Ahead-of-Time) 컴파일을 통해 컴파일 합니다.
- AOT 란 프로그램을 실행하기 이전, 즉 개발자가 배포할 때 이미 기계어로 번역된 상태
장점 : 실행하면 번역 과정 없이 바로 실행, 빠른 속도 적은 메모리 사용
단점 : 빌드 한 운영체제나 CPU 구조가 다르면 실행 불가
1-2.java의 빌드
기계어 대신 바이트코드(Bytecode)라는 중간 언어로 번역 os에 설치된 JVM이 실시간으로 읽어 기계어로 변환
장점 : JVM이 있으면 어느 환경에서든 똑같이 돌아감
단점 : 실행 시 JVM을 거치기 때문에 약간의 성능 오버헤드 발생
단점에 대한 해결 책 JIT 컴파일러
JVM은 프로그램을 실행하면서 자주 반복해서 실행되는 코드 구간(Hotspot)을 추적합니다. 그리고 이 자주 쓰이는 바이트코드를 아예 기계어로 직접 컴파일하여 캐싱(Caching)해 둡니다. 다음번에 같은 코드를 실행할 때는 번역 과정을 생략하고 캐싱 된 기계어를 바로 실행하기 때문에, 실행 속도가 비약적으로 향상됩니다.
2.C++의 메모리 영역
프로그램의 5대 메모리 영역
1. Code(Text): 컴파일된 기계어 코드가 저장되는 읽기 전용 구역
특징
- 읽기 전용 구역, 동작 변조를 방지하기 위해 운영체제(OS)가 쓰기를 금지
예시
void cal(int *a, int *b) { p->hp -= 10; }함수의 작동 지시 내용 자체가 Code 영역에 속함
특징
- 프로그램 시작부터 끝까지 메모리에 남아있다
예시
- int max_players = 100; // main 밖의 전역변수라 가정
- static int server_port = 8080; // 초기화된 정적 변수 -> Data 영역
3.Stack 영역 : 지역 변수, 매개 변수
특징
- 함수의 호출과 함께 할당, 함수가 완료되면 함께 소멸
- 컴파일 시 이미 크기가 정해짐
- 메모리 주소가 높은 곳에서 낮은 곳으로
4.heap 영역 : 사용자에 의해 동적 할당
특징 : 사용자가 다 쓰면 delete 혹은 free로 반환
- 메모리의 주소의 낮은 곳에서 높은 곳으로 증식
- 런타임에 메모리를 찾아서 할당해야 해서 Stack에 비해 속도가 느리다
Player* newPlayer = new Player();
5. BSS(Block Started by Symbol) 영역 초기화되지 않은 전역 변수와 정적 변수
특징
- 프로그래머가 아직 값을 넣지 않은 변수들을 0으로 초기화

data와 BSS를 나눈 이유
int inventory[10000];처럼 엄청나게 큰 전역 배열을 초기화하지 않고 선언했다고 가정
이만 개의 0을 실행 파일(.exe)에 일일이 다 써놓으면 파일 용량이 뻥튀기
따라서 "실행할 때 OS가 메모리 잡아주고 0으로 채워라"라고 기록
실행 파일의 크기를 줄이기 위함
그렇다면 자바 그리고 c++의 메모리 정리에는 어떤 차이가 있을지 간단하게 비교해 보겠습니다.
| 특징 | C++ (메모리 구조) | Java (JVM 구조) |
|---|---|---|
| 지역 변수 (int a) | Stack | Stack |
| 객체 (Player p) | Stack (기본) / Heap (new 시) |
무조건 Heap (Stack은 주소만) |
| 전역/정적 변수 | Data (초기화) / BSS (미초기화) | Method Area (Static 영역) |
| 메모리 해제 | 수동 (delete 필수) |
자동 (Garbage Collector) |
| 포인터 | 실제 메모리 주소 제어 가능 | 주소 제어 불가능 (참조만 가능) |
거의 비슷하지만 new 키워드가 없는 상태라면 함수가 끝나면 사라지게 됩니다.
3. C++의 특징
3-1. 포인터 - 다른 변수의 주솟값을 담기 위해 메모리 공간을 따로 차지하는 독립적인 변수
- 독립된 메모리 : 포인터 변수 자신도 메모리 상에 자기만의 8바이트 공간을 가진다
- 실행 중에 가리키는 대상 변경 가능
- NULL 허용
- 위험성 쓰레기 주소 1 나 이미 해제된 주소를 가리키고 있을 때 접근하면 프로그램 비정상 종료 가능
int a = 10;
int b = 20;
int* ptr = &a; // ptr은 a의 주소를 담음
ptr = &b; // 언제든 b의 주소로 타겟 변경 가능
ptr = nullptr; // 아무것도 가리키지 않음 (이 상태에서 *ptr을 호출하면 프로그램 크래시!)
3-2. 참조자 (Reference, &) 이미 존재하는 메모리 공간에 새로운 이름
- 독립된 공간 없음
- 선언 즉시 초기화
- 변경 불가능
- NULL 불가
int a = 10;
int b = 20;
int& ref = a; // ref는 a의 또 다른 이름. (선언 시 반드시 초기화)
// int& ref2; // 에러! 누구를 참조할지 안 정했음.
ref = b; // 이것의 의미는?
// "ref가 b를 참조해라"가 아니라, "ref(즉, a)의 값에 b의 값(20)을 덮어써라"입니다.
// 결과: a가 20으로 바뀜.
간단하게 표로 한번더 비교 해보겠습니다.
| 특징 | 포인터 (*) | 참조자 (&) |
|---|---|---|
| 타겟 변경 | 가능 (다른 대상을 가리킬 수 있음) | 불가능 (한 번 정해지면 끝까지 고정) |
| NULL 여부 | 가능 (nullptr 할당 가능) |
불가능 (무조건 유효한 대상이 필요함) |
| 표기법 | *, -> 등 복잡한 기호 필요 |
일반 변수와 똑같이 사용 (매우 깔끔함) |
| 주 사용처 | 동적 할당, 배열 관리, NULL 처리가 필요할 때 | 함수 매개변수 전달, 객체 복사 방지 (권장) |
포인터를 이야기드린 이유는 java 그리고 C++의 가장 큰 차이는 메모리에 대한 제어이고
포인터는 그 부분에서 핵심적인 차이를 보이는 개념입니다.
포인터를 사용하면 메모리의 주소를 직접 제어를 할 수 있게 됩니다.
포인터가 가지고 있는 데이터가 간단하게
int, double 같은 간단한 수준의 데이터 라면 큰 의미가 없을 수 있겠지만
만약 좀 더 큰 크기의 데이터를 옮긴다면
데이터를 옮기는 것보단
그 데이터의 위치가 적인 주소를 옮기는 것이 압도적으로 쉽고 빠르기 때문입니다.
특히나 컴퓨터에서 주솟값의 크기는 32비트는 4바이트
64비트는 8바이트로 고정되어 있으며
데이터의 값은 얼마든지 커질 수 있습니다.
만약 이 정도 크기의 데이터를 수정하거나 혹은 정렬을 하게 된다면
이 값을 일일이 옮기게 되면 상당히 오랜 시간을 소요할 수밖에 없습니다.
하지만 이중 포인터를 사용해서 주소만 갈아 치우면 8바이트만 움직이면 됩니다.

즉 데이터값이 커지면 커질수록 높은 효율성을 만들고
이는 곧 속도의 빠름으로 이어집니다.
4. C++ 속도 의 함정
4-1. 업 캐스팅(Upcasting) : 부모의 껍데기를 쓰는 이유Base \*obj = new Derived();
자식 객체를 만들면서 부모젝체 담는 이유 -> 관리의 편의성
Unit*이라는 부모 포인터 배열 하나에 다 쓸어 담고 unit->attack() 한 줄로 명령을 내리기 위함
4-2 vtable과 vptr의 마법
그렇다면 컴파일러 입장에서는 부모의 attack()을 실행할지 혹은
자식의 attack()을 실행해야 할지 헷갈리는 상태
C++ 컴파일러는 두 개를 실행
vtable (가상 함수 테이블): 클래스마다 '이 클래스가 호출해야 할 진짜 함수들의 메모리 주소'를 적어둔 비밀 지도(배열)를 만듭니다.
vptr (가상 함수 포인터): 생성된 객체 내부에 몰래 포인터 변수(vptr)를 하나 쓱 집어넣습니다. 이 포인터는 자기 클래스의 vtable을 가리킵니다.

4-3 소멸자 누락 시 생기는 문제
객체를 다 썼으니 delete obj;를 호출해서 메모리를 해제
만약 Base 클래스의 소멸자(~Base())에 virtual을 안 붙였다면?
부모의 소멸 자만 실행하고 객체를 파괴 -> 자식의 소멸자가 실행되지 않아서 메모리 누락 발생
해결책
부모 클래스를 설계할 때 소멸자 앞에 virtual
class Base {
public:
virtual ~Base() {} // 황금률: 상속될 클래스의 소멸자는 무조건 virtual!
};
4-4 C++이 찾은 해결책 스마트 포인터
std::unique\_ptr 입니다. 이 외에도 몇가지가 더 있으나 너무 길어져서 간단히 하나만 소개 하겠습니다.
하나의 메모리 주소는 오직 하나의 unique\_ptr만 가질 수 있습니다. 복사(Copy)가 엄격하게 금지됩니다.
다른 곳에 넘겨주고 싶다면, 내 권리를 포기하고 '이동(std::move)'시켜야만 합니다.
#include #include // 스마트 포인터를 쓰기 위한 헤더
class Player {
public:
Player() { std::cout << "플레이어 생성\n"; }
~Player() { std::cout << "플레이어 소멸 (자동!)\n"; }
void attack() { std::cout << "공격!\n"; }
};
void play_game() {
// new 대신 std::make_unique를 사용합니다.
std::unique_ptr p1 = std::make_unique();
p1->attack();
// std::unique_ptr<Player> p2 = p1; // 에러! 복사 불가능! (컴파일 단계에서 막아줌)
std::unique_ptr<Player> p3 = std::move(p1); // 성공! p1의 소유권을 p3로 넘김. (이제 p1은 텅 빔)
} // <--- 함수가 끝나는 이 순간! p3가 소멸되면서 내부적으로 알아서 delete를 호출함.
요약하자면 delete를 통해서 메모리를 반환하는 방식이
함수가 끝나면 자동으로 반환되는 방식으로 되어있어서
더 이상 실수로 delete를 빼먹어서 메모리 누수가 발생할 일이 없습니다.
5. C++의 해결책 그리고 vs Java의 해결책
JAVA 개발자는 비즈니스 로직만
가비지 컬렉터(Garbage Collector, GC)를 사용하여
Heap 메모리가 꽉 차면, 백그라운드에 숨어있던 GC가 나타나 참조되지 않는 쓰레기 객체들을 싹 쓸어 담습니다. 개발자는 new만 delete는 안 해도 된다.
단점 : 청소를 하는 동안 프로그램이 아주 잠깐 멈추는 Stop-The-World 현상이 발생
C++의 선택 스마트 포인터를 이용한 (RAII 패턴 제시 2)
게임 렌더링처럼 1프레임의 드롭도 용납할 수 없는 분야에 사용
GC의 멈춤 현상은 용납 불가
unique\_ptr / shared\_ptr: 이들은 포인터를 객체로 감싼 형태입니다. 함수나 블록의 스코프({ })가 끝나는 **단 0.0001초의 오차도 없이, 객체의 소멸자가 호출되며 메모리를 스스로 해제(자폭)**합니다.
C++은 런타임 오버헤드(GC) 없이도 안전한 메모리 관리를 이뤄냈습니다. (물론 shared\_ptr의 순환 참조 문제3는 weak\_ptr을 통해 개발자가 직접 고리를 끊어주어야 하는 책임이 따릅니다.)
즉 스마트 포인터도 이전보다 문제는 적지만 여전히 순환 참조 등의 문제를 통해서 문제가 발생할 수 있습니다.
6. 도메인의 차이
C++, JAVA는 결국 어떤 문제를 해결할 것인가에 따라서 정답이 달라질 뿐입니다.
C++의 무대: 미세한 메모리 지연이나 GC의 개입이 치명적인 대규모 3D 게임 클라이언트, 자체 물리 엔진 개발(Netmarble, Pearl Abyss 등에서 요구하는 역량), 자율주행, 임베디드 시스템에서는 C++의 하드웨어 통제력이 필수적입니다.
Java의 무대: 안정적인 유지 보수와 높은 생산성이 요구되는 대규모 엔터프라이즈 백엔드 서버(Spring Boot 기반의 플랫폼 등)에서는 Java의 JVM과 가비지 컬렉터가 압도적인 안정성을 제공합니다.
어떤 도메인을 선택하느냐에 따라서 아예 안 쓰는 언어가 될 수도 있습니다만
왜 이 언어가 존재하고
왜 이 언어가 선택되는지를 알면
향후 기술의 발전에 따라서 사용되는 언어가 달라지거나
기술 스택이 달라질 때 능동적으로 기술을 선택할 수 있는 계기가 되지 않을까 생각합니다.
- 쓰레기 주소
메모리가 이미 해제(delete)되었음에도 불구하고 포인터가 여전히 그 주소를 가리키고 있는 상태(Dangling), 혹은 포인터를 선언만 하고 초기화하지 않아 임의의 메모리 공간을 가리키는 상태(Wild)를 말합니다. 이 주소에 접근해 값을 읽거나 쓰려 하면 치명적인 런타임 에러가 발생합니다. [본문으로] - RAII 패턴 자원의 획득은 초기화다"라는 뜻으로, 객체가 생성될 때(생성자) 자원을 할당받고, 객체가 스코프를 벗어나 소멸할 때(소멸자) 자원을 자동으로 반환하도록 설계하는 C++의 핵심 프로그래밍 기법입니다. 스마트 포인터가 이 패턴을 완벽하게 따릅니다. [본문으로]
- 순환 참조 문제 두 개 이상의 shared\_ptr 객체가 서로를 포인터로 가리키고 있는 상태입니다. 서로가 서로를 참조하고 있기 때문에 참조 카운트(Reference Count)가 절대 0이 되지 않아, 스코프를 벗어나도 영원히 메모리에서 해제되지 않는 메모리 누수(Memory Leak)가 발생합니다. 이를 해결하기 위해 참조 카운트를 올리지 않고 관찰만 하는 weak\_ptr을 사용합니다. [본문으로]
'웹' 카테고리의 다른 글
| API 응답(Response)에 관하여 (9) | 2026.04.08 |
|---|---|
| SDK는 어떤 것 일까? (2) | 2026.03.18 |
| 멀티스레딩과 멀티프로세싱의 차이 (0) | 2026.03.12 |
| 무조건 쓰던 MVC 왜 쓸까? (3) | 2026.02.18 |
| 웹 개발 공부 시작 (0) | 2026.01.27 |