| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
- It
- 코틀린
- 반복문
- vue
- restful api
- kafka
- AWS
- Nest.js
- 상속
- front-end
- jpa
- 개발자
- state
- java
- node.js
- 조건문
- Sequelize
- back-end
- props
- swagger
- SWIFT
- component
- spring boot
- react
- Kotlin
- 개발이취미인사람
- javascript
- Producer
- 개발이 취미인 사람
- file upload
- Today
- Total
개발이 취미인 사람
[Java] 참조 타입의 메모리 저장 방식 - Stack과 Heap의 관계 본문
개요
안녕하세요. 이번 시간에는 Java의 **참조 타입(Reference Type)**이 메모리에 어떻게 저장되는지 알아보겠습니다.
이전 글에서 리터럴에 대해 배웠는데, "기본 타입은 Stack에 값이 저장되고, 문자열은 Heap에 저장된다"는 말을 했었죠? 오늘은 이 개념을 확실하게 이해해볼 거예요!
"Stack에 주소를 저장한다"는 말이 정확히 무슨 의미인지, 왜 ==과 equals()가 다른 결과를 내는지, null은 무엇을 의미하는지 등을 메모리 관점에서 깊이 있게 다뤄보겠습니다.
이 개념을 이해하면 Call by Value, String Pool, Garbage Collection 등의 고급 주제들이 훨씬 쉬워집니다!
개념
기본 타입 vs 참조 타입
Java의 데이터 타입은 크게 2가지로 나뉩니다.
기본 타입 (Primitive Type): 8가지
byte, short, int, long // 정수
float, double // 실수
char // 문자
boolean // 불린
참조 타입 (Reference Type): 기본 타입을 제외한 모든 것
String, Integer, Double // 클래스
int[], String[] // 배열
List, Map, Set // 컬렉션
Person, Car, Animal // 사용자 정의 클래스
핵심 차이
구분 기본 타입 참조 타입
| 저장 위치 | Stack에 값 직접 저장 | Stack에 주소, Heap에 객체 |
| 기본값 | 타입별 다름 (0, false 등) | null |
| 크기 | 고정 | 가변 |
| 비교 | ==로 값 비교 | ==로 주소 비교 |
1. 기본 타입의 메모리 저장
값이 직접 저장됨
int age = 25;
double height = 175.5;
boolean isStudent = true;
메모리 상태:
Stack (main 스레드)
├─ age = 25 ← 값 25가 직접 저장
├─ height = 175.5 ← 값 175.5가 직접 저장
└─ isStudent = true ← 값 true가 직접 저장
특징:
- 값 자체가 Stack에 저장됩니다
- 변수 간 복사 시 값이 복사됩니다
기본 타입 복사 예제
public class PrimitiveExample {
public static void main(String[] args) {
int a = 10;
int b = a; // 값 10이 복사됨
System.out.println("a = " + a); // 10
System.out.println("b = " + b); // 10
b = 20; // b만 변경
System.out.println("a = " + a); // 10 (변경 안됨!)
System.out.println("b = " + b); // 20
}
}
메모리 변화:
[초기 상태]
Stack
├─ a = 10
└─ b = 10 (a의 값이 복사됨)
[b = 20 실행 후]
Stack
├─ a = 10 ← 그대로!
└─ b = 20 ← b만 변경
결론: 기본 타입은 값이 독립적으로 저장되므로 서로 영향을 주지 않습니다.
2. 참조 타입의 메모리 저장
주소를 저장함!
String name = "홍길동";
메모리 상태:
Heap (String Pool)
└─ "홍길동" 객체 (주소: 0x1234)
Stack
└─ name = 0x1234 ← 주소를 저장!
핵심 개념: 참조값(Reference)
참조값: Heap에 있는 객체의 메모리 주소
String str = "Hello";
// ^^^ ^^^^^^^
// │ └─ 리터럴 "Hello"는 Heap에 저장
// └─────── 변수 str은 주소를 저장
비유:
택배 상자(객체) → 창고(Heap)에 보관
송장 번호(주소) → 내 수첩(Stack)에 기록
3. 참조 타입 변수의 동작
예제 1: 객체 생성
public class Person {
String name;
int age;
}
public class ReferenceExample {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "홍길동";
p1.age = 30;
}
}
메모리 상태:
Heap
└─ Person 객체 (주소: 0x1234)
├─ name → "홍길동" (String 객체도 Heap)
└─ age = 30
Stack
└─ p1 = 0x1234 ← 주소만 저장!
그림으로 보면:
Stack Heap
┌─────┐ ┌────────────┐
│ p1 │──────→│Person 객체│
└─────┘ │ name: 홍길동│
│ age: 30 │
└────────────┘
예제 2: 참조 복사 (중요!)
public class ReferenceExample {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "홍길동";
p1.age = 30;
Person p2 = p1; // 주소가 복사됨!
System.out.println("p1.name: " + p1.name); // 홍길동
System.out.println("p2.name: " + p2.name); // 홍길동
p2.name = "김철수"; // p2를 통해 변경
System.out.println("p1.name: " + p1.name); // 김철수 (영향받음!)
System.out.println("p2.name: " + p2.name); // 김철수
}
}
메모리 상태:
Heap
└─ Person 객체 (주소: 0x1234)
├─ name → "김철수"
└─ age = 30
Stack
├─ p1 = 0x1234 ┐
└─ p2 = 0x1234 ┘ 같은 주소!
그림으로 보면:
Stack Heap
┌─────┐ ┌────────────┐
│ p1 │──┐ │Person 객체│
└─────┘ │ │ name: 김철수│
└──→│ age: 30 │
┌─────┐ │ └────────────┘
│ p2 │──┘
└─────┘
핵심:
- p2 = p1은 주소를 복사합니다
- p1과 p2는 같은 객체를 가리킵니다
- 둘 중 하나로 수정하면 둘 다 영향받습니다!
예제 3: 새 객체 생성
public class ReferenceExample {
public static void main(String[] args) {
Person p1 = new Person();
p1.name = "홍길동";
p1.age = 30;
Person p2 = new Person(); // 새 객체!
p2.name = "김철수";
p2.age = 25;
System.out.println("p1.name: " + p1.name); // 홍길동
System.out.println("p2.name: " + p2.name); // 김철수
p2.name = "이영희";
System.out.println("p1.name: " + p1.name); // 홍길동 (영향 없음!)
System.out.println("p2.name: " + p2.name); // 이영희
}
}
메모리 상태:
Heap
├─ Person 객체 (주소: 0x1234)
│ ├─ name → "홍길동"
│ └─ age = 30
│
└─ Person 객체 (주소: 0x5678)
├─ name → "이영희"
└─ age = 25
Stack
├─ p1 = 0x1234 ← 첫 번째 객체
└─ p2 = 0x5678 ← 두 번째 객체 (다른 주소!)
핵심: new를 사용하면 새 객체가 생성되어 독립적입니다!
4. == vs equals() 완벽 이해
== 연산자: 주소 비교
public class ComparisonExample {
public static void main(String[] args) {
String s1 = "Hello"; // 리터럴
String s2 = "Hello"; // 같은 리터럴
String s3 = new String("Hello"); // new 객체
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
}
}
메모리 상태:
Heap
├─ String Pool
│ └─ "Hello" (0x1234)
│
└─ new String 객체 (0x5678)
Stack
├─ s1 = 0x1234 ┐
├─ s2 = 0x1234 ┘ 같은 주소!
└─ s3 = 0x5678 다른 주소!
== 비교:
s1 == s2 → 0x1234 == 0x1234 → true
s1 == s3 → 0x1234 == 0x5678 → false
equals() 메서드: 내용 비교
System.out.println(s1.equals(s2)); // true
System.out.println(s1.equals(s3)); // true (내용이 같음!)
equals() 비교:
s1.equals(s2) → "Hello".equals("Hello") → true
s1.equals(s3) → "Hello".equals("Hello") → true
비교 정리
연산자 비교 대상 결과
| == | 주소 (참조값) | 같은 객체인가? |
| equals() | 내용 (값) | 값이 같은가? |
실전 예제
public class PersonComparison {
public static void main(String[] args) {
Person p1 = new Person("홍길동", 30);
Person p2 = new Person("홍길동", 30);
Person p3 = p1;
// == 비교 (주소)
System.out.println(p1 == p2); // false (다른 객체)
System.out.println(p1 == p3); // true (같은 객체)
// equals 비교 (내용) - equals 오버라이드 필요
System.out.println(p1.equals(p2)); // false (오버라이드 안 함)
}
}
메모리:
Heap
├─ Person("홍길동", 30) - 0x1234
└─ Person("홍길동", 30) - 0x5678 ← 다른 객체!
Stack
├─ p1 = 0x1234 ┐
├─ p2 = 0x5678 │ 다른 주소!
└─ p3 = 0x1234 ┘ p1과 같은 주소!
5. null의 의미
null = 참조가 없음
String str = null;
메모리 상태:
Heap
(비어있음)
Stack
└─ str = null ← 아무것도 가리키지 않음
그림으로:
Stack
┌─────┐
│ str │──X (가리키는 것 없음)
└─────┘
null 예제
public class NullExample {
public static void main(String[] args) {
String str = null;
// NullPointerException 발생!
// System.out.println(str.length());
// null 체크 필수
if (str != null) {
System.out.println(str.length());
} else {
System.out.println("str은 null입니다");
}
// null 할당
Person p = new Person();
p = null; // 더 이상 객체를 참조하지 않음 → GC 대상
}
}
기본 타입은 null 불가!
int x = null; // 컴파일 에러!
boolean b = null; // 컴파일 에러!
Integer x = null; // OK! (참조 타입)
Boolean b = null; // OK! (참조 타입)
6. 배열의 메모리 저장
배열도 참조 타입!
int[] arr = {10, 20, 30};
메모리 상태:
Heap
└─ int[] 배열 객체 (0x1234)
├─ [0] = 10
├─ [1] = 20
└─ [2] = 30
Stack
└─ arr = 0x1234
배열 복사 예제
public class ArrayExample {
public static void main(String[] args) {
int[] arr1 = {10, 20, 30};
int[] arr2 = arr1; // 주소 복사!
System.out.println("arr1[0]: " + arr1[0]); // 10
System.out.println("arr2[0]: " + arr2[0]); // 10
arr2[0] = 100; // arr2를 통해 변경
System.out.println("arr1[0]: " + arr1[0]); // 100 (영향받음!)
System.out.println("arr2[0]: " + arr2[0]); // 100
}
}
메모리:
Heap
└─ int[] 배열 (0x1234)
├─ [0] = 100
├─ [1] = 20
└─ [2] = 30
Stack
├─ arr1 = 0x1234 ┐
└─ arr2 = 0x1234 ┘ 같은 배열!
7. 메서드 파라미터 전달
기본 타입 전달
public class ParamExample {
static void change(int x) {
x = 100;
System.out.println("메서드 내부: " + x); // 100
}
public static void main(String[] args) {
int num = 10;
change(num);
System.out.println("메서드 외부: " + num); // 10 (변경 안됨!)
}
}
메모리:
[change 호출 전]
Stack (main)
└─ num = 10
[change 호출]
Stack (main)
└─ num = 10 ← 그대로!
Stack (change)
└─ x = 10 ← 복사된 값
참조 타입 전달
public class ParamExample {
static void change(Person p) {
p.name = "김철수";
System.out.println("메서드 내부: " + p.name); // 김철수
}
public static void main(String[] args) {
Person person = new Person();
person.name = "홍길동";
change(person);
System.out.println("메서드 외부: " + person.name); // 김철수 (변경됨!)
}
}
메모리:
Heap
└─ Person 객체 (0x1234)
└─ name = "김철수"
Stack (main)
└─ person = 0x1234
Stack (change)
└─ p = 0x1234 ← 같은 주소 복사!
핵심: 주소가 복사되므로 같은 객체를 가리킴 → 객체 내부 수정 가능!
자세한 내용은 [Call by Value vs Call by Reference] 글을 참고하세요!
8. 실전 예제
예제 1: 학생 정보 관리
class Student {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
}
public class StudentExample {
public static void main(String[] args) {
// 학생 2명 생성
Student s1 = new Student("홍길동", 90);
Student s2 = new Student("김철수", 85);
// s3는 s1과 같은 학생
Student s3 = s1;
// s1의 점수 변경
s1.score = 95;
System.out.println("s1 점수: " + s1.score); // 95
System.out.println("s2 점수: " + s2.score); // 85
System.out.println("s3 점수: " + s3.score); // 95 (s1과 같음!)
}
}
메모리:
Heap
├─ Student("홍길동", 95) - 0x1234
└─ Student("김철수", 85) - 0x5678
Stack
├─ s1 = 0x1234 ┐
├─ s2 = 0x5678 │
└─ s3 = 0x1234 ┘ s1과 같은 객체!
예제 2: null 체크의 중요성
public class NullCheckExample {
static void printLength(String str) {
// 나쁜 예
// System.out.println(str.length()); // NullPointerException 위험!
// 좋은 예
if (str != null) {
System.out.println("길이: " + str.length());
} else {
System.out.println("str이 null입니다");
}
}
public static void main(String[] args) {
String s1 = "Hello";
String s2 = null;
printLength(s1); // 길이: 5
printLength(s2); // str이 null입니다
}
}
예제 3: 배열과 참조
public class ArrayReferenceExample {
public static void main(String[] args) {
int[] original = {1, 2, 3, 4, 5};
int[] copy = original; // 주소 복사
// 진짜 복사 (새 배열 생성)
int[] realCopy = new int[original.length];
for (int i = 0; i < original.length; i++) {
realCopy[i] = original[i];
}
// 또는 Arrays.copyOf() 사용
// int[] realCopy = Arrays.copyOf(original, original.length);
original[0] = 100;
System.out.println("original[0]: " + original[0]); // 100
System.out.println("copy[0]: " + copy[0]); // 100 (영향받음)
System.out.println("realCopy[0]: " + realCopy[0]); // 1 (영향 없음!)
}
}
핵심 정리
1. 기본 타입 vs 참조 타입
기본 타입: Stack에 값 직접 저장
참조 타입: Stack에 주소, Heap에 객체
2. 참조 복사의 의미
Person p1 = new Person();
Person p2 = p1; // 주소 복사!
p1과 p2는 같은 객체를 가리킴
→ 하나를 수정하면 둘 다 영향받음
3. == vs equals()
연산자 기본 타입 참조 타입
| == | 값 비교 | 주소 비교 |
| equals() | - | 내용 비교 |
4. null의 의미
String str = null; // 아무것도 가리키지 않음
참조가 없다 = null
기본 타입은 null 불가!
5. 메모리 구조
Stack (Thread별 독립)
├─ 지역 변수
│ ├─ 기본 타입: 값 직접
│ └─ 참조 타입: 주소만
└─ 메서드 호출 정보
Heap (모든 Thread 공유)
├─ 객체 (new로 생성)
├─ 배열
└─ String Pool
마무리
이번 시간에는 참조 타입의 메모리 저장 방식에 대해 알아봤습니다.
특히 "Stack에 주소를 저장하고 Heap에 객체를 저장한다"는 개념을 정확히 이해하는 것이 중요합니다. 이 개념을 알면:
- ==과 equals()의 차이를 이해할 수 있습니다
- 왜 참조 복사가 위험한지 알 수 있습니다
- null이 무엇을 의미하는지 알 수 있습니다
- Call by Value의 원리를 이해할 수 있습니다
다음 시간에는 이 개념을 바탕으로 String & String Pool에 대해 깊이 있게 알아보겠습니다!
꼭 실습을 통해 학습하신 걸 추천드리겠습니다!!
참고 자료
'언어(Programming Language) > Java' 카테고리의 다른 글
| [Java] 리터럴(Literal)이란? - 변수와 값의 차이 완벽 이해 (0) | 2026.01.29 |
|---|---|
| [Java] Call by Value와 Call by Reference 완벽 이해 (0) | 2026.01.28 |
| [Java] JVM 메모리 구조 - Static, Heap, Stack 완벽 정리 (0) | 2025.11.05 |
| [Java] fork/join framework 란? (1) | 2023.12.09 |
| [Java] Callable과 Runnable 및 ExecutorService 그리고 Executors, Executor (4) | 2023.12.06 |