F-lab Java 1주차 / Phase 4 / Unit 4.2 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.선수 지식: Unit 4.1 (JVM 런타임 데이터 영역)
다음 Phase: Phase 5 — GC 깊이 파기이 Unit의 의미: 자바 면접의 가장 헷갈리는 영역.
"자바는 Pass by Reference 가 아닌가요?" 라는 흔한 오해의 진실.
메모리 구조 위에서 값/참조 전달의 본질을 이해.
당신이 친구에게 집 주소 를 알려주려 합니다.
Case 1 — 주소를 종이에 적어줌 (Pass by Value):
Case 2 — 친구가 그 주소로 가서 집을 칠함 (참조의 활용):
핵심:
→ 이게 자바의 Pass by Value 의 진실.
당신이 TV 리모컨을 친구에게 빌려준다고 합시다.
진짜 리모컨을 빌려줌 = Pass by Reference (자바에 없음):
리모컨 복사본을 만들어 줌 = Pass by Value (자바 방식):
핵심:
→ 자바의 메서드 호출이 정확히 이 방식.
"자바는 항상 Pass by Value 다. 단, 객체의 경우 '참조의 값(주소)' 이 복사된다."
용어 정리:
흔한 오해:
비유 정리:
| 비유 요소 | 자바 적용 |
|---|---|
| 종이 (주소) | 참조 변수 |
| 종이의 복사본 | 참조의 값 복사 |
| 실제 집 | Heap 의 객체 |
| 집 칠하기 | 객체 변경 |
| 종이 찢기 | 참조 변경 (외부 무관) |
언어마다 메서드 호출 시 인자를 전달하는 방식이 다릅니다:
void modify(int x) {
x = 100; // 복사된 값만 변경
}
int n = 10;
modify(n);
printf("%d", n); // 10 (변경 안 됨)
& 참조 매개변수void modify(int& x) { // & 참조 매개변수
x = 100;
}
int n = 10;
modify(n);
cout << n; // 100 ⚠️ (변경됨)
void modify(int* x) {
*x = 100;
}
int n = 10;
modify(&n);
printf("%d", n); // 100 (포인터로 변경)
자바 설계자들의 결정 (1995):
"자바는 Pass by Value 만 지원한다. 단, 객체는 참조로 다룬다."
왜?:
그러나 많은 개발자가 헷갈립니다:
public void modify(Customer c) {
c.setName("Bob"); // ⚠️ 변경됨!
}
Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName()); // "Bob" — 변경됨!
오해:
진실:
→ 이 함정을 정확히 이해하는 게 시니어 자바 개발자의 차별화.
자바 창시자 James Gosling은 명시적으로 말함:
"Java has no pass by reference. Everything is pass by value."
("자바는 Pass by Reference가 없다. 모든 것이 Pass by Value 다.")
→ 자바 공식 입장.
면접 단골 질문:
"자바는 Pass by Value 인가 Pass by Reference 인가?"
잘못된 답:
올바른 답:
→ 이 답으로 시니어 후보 차별화.
"Pass by Value 의 진실은 'JVM 메모리 구조' 위에서 명확해진다."
Stack에는 변수의 값 (기본형은 값 자체, 객체는 참조 = 주소), Heap에는 객체. 메서드 호출 시 Stack의 값 이 복사되어 새 Stack Frame 으로. 그래서 객체의 경우 두 참조가 같은 Heap 객체를 가리킴.
메모리 구조 (Unit 4.1) 를 모르면 Pass by Value 도 진짜 이해 못 함.
Pass by Value의 진실을 모르면 다양한 버그와 혼란이 발생합니다.
public class FareService {
public void applyDiscount(Fare fare, int rate) {
int discount = fare.getAmount() * rate / 100;
fare.setAmount(fare.getAmount() - discount);
}
}
Fare myFare = new Fare(50000);
fareService.applyDiscount(myFare, 20);
System.out.println(myFare.getAmount()); // 40000? 50000?
Pass by Value 진실 모르면:
Pass by Value 진실 알면:
myFare 의 참조 값 (주소) 이 복사되어 전달myFare 와 fare) 가 같은 Heap 객체 가리킴public class FareService {
public void replace(Fare fare) {
fare = new Fare(10000); // 새 객체로 교체 시도
}
}
Fare myFare = new Fare(50000);
fareService.replace(myFare);
System.out.println(myFare.getAmount()); // 50000? 10000?
Pass by Reference 라고 오해하면:
진실 (Pass by Value):
fare = myFare 의 참조 복사본fare = new Fare(10000) → 메서드 안의 fare 만 새 객체 가리킴myFare 는 여전히 50000 객체 가리킴메모리 변화:
호출 직후:
[Stack — main]
myFare ─────┐
│
[Stack — replace]
fare ───────┤
│
[Heap] ▼
Fare(50000)
fare = new Fare(10000) 후:
[Stack — main]
myFare ─────┐
│
[Stack — replace]
fare ───────┐
│
[Heap] ▼ ▼
Fare(50000) Fare(10000) ← 새 객체
메서드 종료 후:
[Stack — main]
myFare ─────┐
│
[Heap] ▼
Fare(50000) Fare(10000)
← GC 대상 (참조 X)
→ 객체 교체는 Pass by Value 로 안 됨.
// ❌ 잘못된 코드
public void increment(Integer count) {
count = count + 1; // 외부 영향 없음
}
Integer myCount = 10;
increment(myCount);
System.out.println(myCount); // 10 (변경 안 됨)
왜?:
Integer 는 객체이지만 불변 (immutable)count = count + 1 → 새 Integer 객체 만들어 메서드 안의 count에 할당myCount 무관해결 — 반환 사용:
public Integer increment(Integer count) {
return count + 1;
}
Integer myCount = 10;
myCount = increment(myCount); // ← 반환받아서
System.out.println(myCount); // 11 ✅
public void sort(List<Integer> list) {
list.sort(Comparator.naturalOrder()); // 정렬
}
List<Integer> myList = new ArrayList<>(List.of(3, 1, 2));
sort(myList);
System.out.println(myList); // [3, 1, 2]? [1, 2, 3]?
진실: [1, 2, 3] ⚠️
왜?:
list 는 myList 의 참조 복사list.sort() → 객체 내부 변경→ 객체의 메서드로 변경하면 외부 영향.
public void process(List<Integer> list) {
list.add(100); // 외부 영향 ✅
list = new ArrayList<>(); // 재할당
list.add(200); // 외부 영향 X
}
List<Integer> myList = new ArrayList<>(List.of(1, 2, 3));
process(myList);
System.out.println(myList); // ?
답: [1, 2, 3, 100]
왜?:
1. list.add(100) — 같은 객체 변경 → myList 에도 100 추가
2. list = new ArrayList<>() — 메서드 안의 list만 새 객체 가리킴, myList 무관
3. list.add(200) — 새 객체에만 추가, myList 무관
→ 재할당 전후의 동작이 완전히 다름.
"자바는 Pass by Value 인가요 Pass by Reference 인가요?"
답 못함 또는 헷갈림:
잘 답함:
자바의 모든 메서드 호출 은 다음 규칙을 따릅니다:
public void modify(int x) {
x = 100;
}
int n = 10;
modify(n);
System.out.println(n); // 10
메모리:
호출 직후:
[Stack — main]
n = 10
[Stack — modify]
x = 10 ← 값 복사
x = 100 후:
[Stack — main]
n = 10 ← 그대로
[Stack — modify]
x = 100 ← 메서드 안만 변경
→ 값 복사. 외부 무관.
public void modify(Customer c) {
c.setName("Bob"); // 객체 내부 변경
}
Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName()); // "Bob"
메모리:
호출 직후:
[Stack — main]
alice (참조 = 0x1234)
│
[Stack — modify] │
c (참조 = 0x1234) ─┤ ← 같은 주소
│
[Heap] ▼
Customer ("Alice") 주소 0x1234
c.setName("Bob") 후:
[Stack — main]
alice (참조 = 0x1234)
│
[Stack — modify] │
c (참조 = 0x1234) ─┤
│
[Heap] ▼
Customer ("Bob") ← 객체 자체 변경
→ 참조의 값(주소) 이 복사. 같은 객체를 가리키므로 변경 공유.
public void modify(Customer c) {
c = new Customer("Bob"); // 재할당
}
Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName()); // "Alice" (변경 안 됨)
메모리:
호출 직후:
[Stack — main]
alice (참조 = 0x1234)
│
[Stack — modify] │
c (참조 = 0x1234) ─┤
│
[Heap] ▼
Customer ("Alice") 주소 0x1234
c = new Customer("Bob") 후:
[Stack — main]
alice (참조 = 0x1234)
│
[Stack — modify]
c (참조 = 0x5678) ← 새 주소
│
[Heap] ▼ ▼
Customer ("Alice") Customer ("Bob") 주소 0x5678
← c만 가리킴
메서드 종료 후:
[Stack — main]
alice (참조 = 0x1234)
│
[Heap] ▼ ▼
Customer ("Alice") Customer ("Bob")
← GC 대상 (참조 X)
→ 재할당 = 메서드 안의 변수만 변경. 외부 무관.
| 케이스 | 메서드 안 변경 | 외부 영향 |
|---|---|---|
| 기본형 변경 | 메서드 안만 변경 | X |
객체 메서드 호출 (c.setName()) | 객체 내부 변경 | O ⚠️ |
객체 재할당 (c = new ...) | 메서드 안만 새 객체 가리킴 | X |
| 불변 객체 변경 시도 (Integer + 1) | 새 객체 만들어 재할당 | X |
→ 이 표를 그릴 수 있으면 자바 면접 OK.
Java 8 람다도 같은 규칙:
public void process(Function<Integer, Integer> func) {
int result = func.apply(10);
}
process(x -> x * 2); // 람다 자체가 객체로 전달
→ 람다도 객체 → 참조의 값 복사.
기본형을 메서드에서 변경하려면:
// ❌ 안 됨
public void increment(int x) {
x++;
}
// ✅ Wrapper 사용
public void increment(int[] arr) {
arr[0]++; // 배열은 객체
}
int[] count = {0};
increment(count);
System.out.println(count[0]); // 1
// ✅ 또는 AtomicInteger
public void increment(AtomicInteger count) {
count.incrementAndGet();
}
AtomicInteger count = new AtomicInteger(0);
increment(count);
System.out.println(count.get()); // 1
→ 객체로 감싸서 참조의 값 복사 활용.
그러나 — 보통 반환 사용 이 더 깔끔:
public int increment(int x) {
return x + 1;
}
int n = 10;
n = increment(n);
질문: "자바는 Pass by Value 인가 Pass by Reference 인가?"
답변 구조:
결론 (10초): "자바는 항상 Pass by Value 입니다."
기본형 설명 (30초):
객체 설명 (1분):
메모리 그림 (1분):
흔한 오해 (30초):
실용적 함의 (1분):
→ 이 구조면 시니어 답변.
public class Demo {
public static void main(String[] args) {
int x = 10;
Customer c = new Customer("Alice");
process(x, c);
System.out.println(x); // ?
System.out.println(c.getName()); // ?
}
public static void process(int n, Customer cust) {
n = 100;
cust.setName("Bob");
}
}
Step 1 — main 시작:
[Method Area]
Demo 클래스 정보, Customer 클래스 정보
[Heap]
(아직 비어있음)
[Stack — main]
args (참조)
x (아직 미할당)
c (아직 미할당)
Step 2 — int x = 10:
[Stack — main]
args
x = 10 ← Stack 에 직접 값 저장
c
Step 3 — Customer c = new Customer("Alice"):
[Heap]
Customer 인스턴스 ("Alice") 주소 0x1234
↑
[Stack — main]
args
x = 10
c = 0x1234 ← 참조 (주소) 저장
Step 4 — process(x, c) 호출:
[Heap]
Customer ("Alice") 0x1234
↑
│
[Stack — main]
args
x = 10
c = 0x1234 ──┐
│
[Stack — process] (새 Frame)
n = 10 ← x의 값 복사
cust = 0x1234 ──┘ ← c의 값(주소) 복사
Step 5 — n = 100:
[Stack — main]
x = 10 ← 그대로
[Stack — process]
n = 100 ← 메서드 안만 변경
Step 6 — cust.setName("Bob"):
[Heap]
Customer ("Bob") ← 객체 자체 변경
↑ ↑
│ │
[Stack — main]
c = 0x1234 ──┘
│
[Stack — process]
n = 100
cust = 0x1234 ──┘
↑ 같은 주소 가리킴
Step 7 — process 종료:
[Stack — main]
c = 0x1234 ──┐
│
▼
[Heap]
Customer ("Bob") ← 변경됨
(process Frame 제거)
최종 출력:
System.out.println(x) → 10 (메서드 안 변경 무관)System.out.println(c.getName()) → "Bob" (객체 변경됨)자바의 참조와 C의 포인터의 차이:
| C 포인터 | Java 참조 | |
|---|---|---|
| 명시적 | int* p | 모든 객체 변수 |
| 산술 연산 | 가능 (p++) | 불가 |
| null 체크 | 자동 X | 자동 (NPE) |
| 메모리 안전 | 위험 | 안전 |
| 직접 주소 보기 | printf("%p", p) | 불가 |
→ 자바 참조 = "안전한 포인터".
== vs .equals() 의 진짜 의미 ⭐String a = new String("hello");
String b = new String("hello");
a == b; // false
a.equals(b); // true
왜?:
== 는 참조의 값(주소) 비교.equals() 는 객체 내용 비교메모리:
[Heap]
String ("hello") 주소 0x1111
String ("hello") 주소 0x2222
[Stack]
a = 0x1111
b = 0x2222
a == b → 0x1111 == 0x2222 → falsea.equals(b) → 내용 "hello" == "hello" → true기본형 비교:
int x = 10;
int y = 10;
x == y; // true (값 비교)
→ 기본형의 == 는 값, 객체의 == 는 참조.
Integer a = 100;
Integer b = 100;
a == b; // true ⚠️
Integer c = 200;
Integer d = 200;
c == d; // false ⚠️
왜?:
-128 ~ 127 범위 Integer 를 캐시 함해결: 항상 equals() 사용:
a.equals(b); // true (안전)
c.equals(d); // true
→ 자바 면접 함정 질문.
String a = "hello";
String b = "hello";
a == b; // true!
String c = new String("hello");
a == c; // false
왜?:
"hello" 는 String Pool (Method Area 또는 Heap의 특별 영역) 에 저장new String() → Heap 에 새 객체 → 다른 참조→ Unit 6에서 자세히.
@Service
public class FareUpdateService {
public void applyDiscount(Fare fare, int rate) {
int discount = fare.getAmount() * rate / 100;
fare.setAmount(fare.getAmount() - discount);
// ⚠️ 외부 fare 도 변경됨
}
}
@Service
public class CheckoutService {
private final FareUpdateService updateService;
public Fare checkout(Long fareId) {
Fare fare = fareRepository.findById(fareId).orElseThrow();
// 의도: 임시 할인 적용 후 영수증 출력
updateService.applyDiscount(fare, 20);
return fare;
// ⚠️ DB 의 fare 도 영향?
// → JPA 영속성 컨텍스트에 의해 변경 감지 → DB UPDATE
}
}
부수효과 위험:
fare 는 영속성 컨텍스트에 있음applyDiscount 가 객체 변경해결:
public Fare calculateDiscountedFare(Fare fare, int rate) {
int discountedAmount = fare.getAmount() * (100 - rate) / 100;
return new Fare(discountedAmount); // 새 객체 반환
}
→ 불변성 + 반환 을 활용.
public class ArrayDemo {
public void modify(int[] arr) {
arr[0] = 100; // 외부 영향 ✅
arr = new int[]{1, 2}; // 외부 영향 X
}
public static void main(String[] args) {
int[] myArr = {10, 20, 30};
new ArrayDemo().modify(myArr);
System.out.println(Arrays.toString(myArr));
// [100, 20, 30] ⚠️
}
}
왜?:
arr[0] = 100 → Heap 의 배열 객체 내부 변경 → 외부에서 보임arr = new int[]{...} → 메서드 안만 새 배열 가리킴메모리:
호출 직후:
[Heap]
int[] {10, 20, 30} 주소 0x1234
↑
[Stack — main]
myArr = 0x1234 ──┐
│
[Stack — modify] │
arr = 0x1234 ─────┘
arr[0] = 100 후:
[Heap]
int[] {100, 20, 30} ← 변경
arr = new int[]{1, 2} 후:
[Heap]
int[] {100, 20, 30} ← myArr 가 가리킴
int[] {1, 2} 주소 0x5678 ← arr 만 가리킴
public void process(List<Integer> list) {
list.add(100); // 외부 영향 ✅
list.remove(0); // 외부 영향 ✅
list.set(0, 999); // 외부 영향 ✅
list = new ArrayList<>(); // 외부 영향 X
list.add(200); // 외부 영향 X
}
List<Integer> myList = new ArrayList<>(List.of(1, 2, 3));
process(myList);
System.out.println(myList); // ?
진행 단계:
1. 시작: [1, 2, 3]
2. add(100) → [1, 2, 3, 100] (외부 영향)
3. remove(0) → [2, 3, 100] (외부 영향)
4. set(0, 999) → [999, 3, 100] (외부 영향)
5. list = new ArrayList<>() → 메서드 안의 list만, myList 그대로
6. list.add(200) → 새 객체에만, myList 무관
최종: [999, 3, 100]
public void process(String s) {
s = s + "!"; // 새 String 객체 생성
}
String myStr = "hello";
process(myStr);
System.out.println(myStr); // "hello"
왜 변경 안 됨?:
s + "!" → 새 String 객체 생성, s 에 재할당불변 객체의 장점:
ILIC 적용:
public class FareSnapshot { // 불변
private final Long id;
private final int amount;
private final FareStatus status;
// setter 없음
// 변경 시 새 객체 반환
public FareSnapshot withAmount(int newAmount) {
return new FareSnapshot(id, newAmount, status);
}
}
→ 불변 객체로 부수효과 방지.
public class Counter {
private int value;
public Counter(int value) { this.value = value; }
public void increment() { value++; }
public int getValue() { return value; }
}
public void process(Counter counter) {
counter.increment();
}
Counter myCounter = new Counter(0);
process(myCounter);
process(myCounter);
process(myCounter);
System.out.println(myCounter.getValue()); // 3
핵심:
increment() 호출 → 객체 내부 변경 → 외부에서 보임→ Wrapper 패턴.
더 나은 방법 — AtomicInteger (스레드 안전):
AtomicInteger counter = new AtomicInteger(0);
public void process(AtomicInteger counter) {
counter.incrementAndGet();
}
public class Order {
private final List<Item> items;
public Order(List<Item> items) {
// 방어적 복사 — 외부 변경으로부터 보호
this.items = new ArrayList<>(items);
}
public List<Item> getItems() {
// 방어적 복사 — 외부에서 변경 못하게
return new ArrayList<>(items);
// 또는 Collections.unmodifiableList(items);
}
}
List<Item> myItems = new ArrayList<>();
myItems.add(new Item("A"));
Order order = new Order(myItems);
myItems.add(new Item("B")); // 외부에서 추가
order.getItems(); // [Item("A")] — 영향 없음 ✅
왜?:
this.items = items 하면 외부 변경에 영향 받음new ArrayList<>(items) 로 새 리스트 생성→ Effective Java 의 권장 패턴.
Q: "객체를 메서드에 넘기면?"
A: "Pass by Reference 입니다" ❌
정답: Pass by Value — 참조의 값 복사.
== 로 객체 비교String a = new String("hello");
String b = new String("hello");
if (a == b) { ... } // ❌ false 나옴
해결: .equals() 사용:
if (a.equals(b)) { ... } // ✅
→ 객체 비교는 항상 equals().
@Service
public class FareService {
public void process(Fare fare) {
fare.setStatus(FareStatus.PROCESSING);
// ⚠️ 호출자의 fare 도 영향
}
}
Fare myFare = repository.findById(1L).orElseThrow();
service.process(myFare);
// myFare 의 status 도 변경됨
// 트랜잭션 안이면 DB UPDATE까지!
해결: 의도를 명확히 또는 불변 객체:
public Fare process(Fare fare) {
return new Fare(fare.getId(), fare.getAmount(), FareStatus.PROCESSING);
}
Integer a = 200;
Integer b = 200;
if (a == b) { ... } // ⚠️ false (캐시 범위 밖)
해결: 항상 equals():
if (a.equals(b)) { ... } // ✅
또는 기본형 사용:
int a = 200;
int b = 200;
if (a == b) { ... } // ✅ true
public void process(int count) {
count = count + 1; // ❌ 의미 X
// count 변경해도 외부 영향 없음
}
해결: 반환:
public int process(int count) {
return count + 1;
}
public void clear(List<Integer> list) {
list = new ArrayList<>(); // ❌ 외부 list 안 비워짐
}
해결:
public void clear(List<Integer> list) {
list.clear(); // ✅ 객체 내부 메서드 호출
}
public class Order {
private List<Item> items;
public Order(List<Item> items) {
this.items = items; // ❌ 외부 list 와 공유
}
public List<Item> getItems() {
return items; // ❌ 외부에서 변경 가능
}
}
해결: 방어적 복사:
this.items = new ArrayList<>(items);
return Collections.unmodifiableList(items);
[Unit 4.1: JVM 런타임 데이터 영역]
↓
[Unit 4.2: Pass by Value] ← 지금 여기 ★
↓
[Phase 4 완료]
→ Phase 4 완료. 다음은 Phase 5 GC.
| 학습 | Pass by Value 관점 |
|---|---|
| Unit 1.1 (객체지향) | 객체는 참조로 다룸 (Pass by Value) |
| Unit 2.1 (메서드) | 메서드 호출 시 인자 복사 |
| Unit 2.4 (다형성) | 참조의 타입과 실제 객체 구분 |
| Unit 4.1 (JVM 메모리) | Stack/Heap 위에서 동작 |
→ Pass by Value 는 메모리 모델 위에서 이해.
3주차 (제네릭/람다):
4주차 (동시성):
5주차 (Spring DI):
11-12주차 (JPA):
Effective Java 의 권장:
| 질문 | 이 Unit에서의 답 |
|---|---|
| "자바는 Pass by Value? Reference?" | 항상 Pass by Value, 객체는 참조의 값 복사 |
| "객체를 메서드에서 변경 시 외부 영향?" | 객체 내부 변경 시 영향, 재할당은 X |
| "== 와 equals() 차이?" | == 는 참조 (객체) 또는 값 (기본형) 비교, equals() 는 내용 |
| "Integer 비교 함정?" | -128~127 캐시, 그 외 다른 객체 |
| "방어적 복사가 뭔가요?" | Pass by Value 의 부수효과 방지 |
1️⃣ 자바는 항상 Pass by Value 다 — 객체도 마찬가지.
James Gosling 도 명시: "Java has no pass by reference." 기본형은 값 자체 복사, 객체는 참조의 값(주소) 복사. 둘 다 Stack 의 값을 새 Stack Frame 에 복사하는 방식. 이 사실은 JVM 메모리 모델 (Unit 4.1) 위에서 명확해진다.
2️⃣ 객체 메서드 호출 vs 재할당 — 결과가 완전히 다르다.
메서드에서
c.setName("Bob")같은 객체 내부 변경 은 외부에 영향 ⭐ (같은 Heap 객체를 가리키는 두 참조이기 때문). 반면c = new Customer()같은 재할당 은 메서드 안의 변수만 새 객체를 가리켜 외부 무관. 이 구분이 자바 면접의 가장 헷갈리는 영역.3️⃣ 불변 객체와 방어적 복사로 부수효과를 통제하라.
Pass by Value 의 객체 변경 영향을 방지하려면: ① 불변 객체 사용 (String, Integer 처럼 변경 시 새 객체), ② 방어적 복사 (생성자/getter 에서 새 컬렉션 생성), ③ 반환 값 활용 (변경 결과를 반환해서 명시적 할당). Effective Java 가 권장하는 패턴들 — 모두 Pass by Value 의 함정 회피.
Q1: 자바에는 정말 Pass by Reference가 없는가?
한 줄 답: YES, 절대 없다. James Gosling 도 명시.
상세 설명:
자바는 모든 인자를 값으로 전달:
Pass by Reference 라면 (자바에 없는 것):
// 가상 — 자바에 이런 문법 없음
public void modify(Customer& c) { // & 참조 매개변수 (C++)
c = new Customer("Bob"); // 외부도 새 객체 가리킴
}
Customer alice = new Customer("Alice");
modify(alice);
// alice 가 새 Customer("Bob") 를 가리켜야 Pass by Reference
// → 자바에서는 불가능
자바의 실제 (Pass by Value):
public void modify(Customer c) {
c = new Customer("Bob"); // 메서드 안만
}
Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName()); // "Alice" — 변경 안 됨
→ Pass by Reference 라면 "Bob" 이 나와야 함. 그런데 안 나옴 → Pass by Reference 가 아님.
왜 헷갈리나?:
c.setName("Bob")) 은 외부에 영향비유:
Q2: 기본 자료형과 객체 자료형의 메서드 인자 전달 차이는?
한 줄 답: 둘 다 Pass by Value, 단 복사되는 대상 이 다름.
상세 설명:
public void modify(int x) {
x = 100;
}
int n = 10;
modify(n);
System.out.println(n); // 10
복사 대상: 값 자체 (10)
메모리:
[Stack — main]
n = 10
[Stack — modify]
x = 10 ← 값 복사
효과: 메서드 안 변경 → 외부 무관.
public void modify(Customer c) {
c.setName("Bob");
}
Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName()); // "Bob"
복사 대상: 참조의 값(주소)
메모리:
[Heap]
Customer ("Alice") 주소 0x1234
↑ ↑
│ │
[Stack — main] [Stack — modify]
alice = 0x1234 c = 0x1234 ← 같은 주소
효과: 두 참조가 같은 객체 → 객체 변경 시 양쪽 영향.
| 측면 | 기본형 | 객체 |
|---|---|---|
| 복사 대상 | 값 자체 | 참조의 값 (주소) |
| 메서드 안 변경 시 | 메서드 안만 | 객체 내부 변경 → 외부 영향 |
| 재할당 시 | 메서드 안만 | 메서드 안만 |
| 메모리 위치 | Stack 에 직접 | 참조는 Stack, 객체는 Heap |
핵심 통찰 ⭐ :
"둘 다 Pass by Value 인데, '값' 의 의미가 다르다.
기본형의 값 = 실제 데이터, 객체의 값 = 참조(주소)"
시각화:
기본형:
[복사되는 것]: ●●●●● (실제 데이터)
객체:
[복사되는 것]: → → → → → (주소만)
↓
실제 객체 (Heap, 1개)
→ 메모리 구조 (Unit 4.1) 위에서 명확해짐.
| Unit | 주제 | 핵심 |
|---|---|---|
| 4.1 | JVM 런타임 데이터 영역 ★★★ | 5가지 메모리 영역 |
| 4.2 | Pass by Value ★★★ | 자바의 인자 전달 진실 |
"메모리 구조를 알아야 자바를 진짜 이해한 것이다."
Pass by Value, 동시성, GC, JPA 영속성 — 모든 자바의 깊은 주제가 이 메모리 모델 위에 서 있다. 추상의 세계 (Phase 1-3) 와 물리의 세계 (Phase 4) 가 만나는 지점.
이제 Heap 의 객체가 어떻게 수거되는지:
→ Heap 이해 (Unit 4.1) 가 결정적 토대.