🎯1주차 Unit 4.2 — Pass by Value (자바의 진실)

Psj·2026년 5월 7일

F-lab

목록 보기
36/240

🎯 Unit 4.2 — Pass by Value (자바의 진실) ★★★

F-lab Java 1주차 / Phase 4 / Unit 4.2 본격 학습 자료
9-섹션 마스터 프롬프트 형식으로 깊이 파헤친다.

선수 지식: Unit 4.1 (JVM 런타임 데이터 영역)
다음 Phase: Phase 5 — GC 깊이 파기

이 Unit의 의미: 자바 면접의 가장 헷갈리는 영역.
"자바는 Pass by Reference 가 아닌가요?" 라는 흔한 오해의 진실.
메모리 구조 위에서 값/참조 전달의 본질을 이해.


🌍 1. 세상 속 비유

Pass by Value = "주소가 적힌 종이의 복사본"

당신이 친구에게 집 주소 를 알려주려 합니다.

Case 1 — 주소를 종이에 적어줌 (Pass by Value):

  • 당신: 종이에 "서울시 강남구 ..." 적음
  • 친구에게 종이 복사본
  • 친구가 그 종이를 찢어도 → 당신의 종이는 그대로
  • 친구가 종이에 새 주소 써도 → 당신과 무관

Case 2 — 친구가 그 주소로 가서 집을 칠함 (참조의 활용):

  • 친구가 종이에 적힌 주소로 감
  • 집의 외벽을 빨갛게 칠함
  • 당신이 집에 가보면 → 빨간 외벽 ⚠️
  • 같은 집을 가리키는 주소이기 때문

핵심:

  • 종이(주소) 자체는 복사본 → 친구가 종이에 뭐라 써도 무관
  • 그러나 그 주소가 가리키는 실제 집 은 변경 가능 → 영향 있음

이게 자바의 Pass by Value 의 진실.


더 직관적인 비유 — "리모컨 복사"

당신이 TV 리모컨을 친구에게 빌려준다고 합시다.

진짜 리모컨을 빌려줌 = Pass by Reference (자바에 없음):

  • 친구가 리모컨 들고 다른 방으로
  • 당신은 TV 못 켬

리모컨 복사본을 만들어 줌 = Pass by Value (자바 방식):

  • 2개의 같은 리모컨
  • 친구가 자기 리모컨으로 TV 끔
  • → TV는 꺼짐 (영향 있음)
  • 친구가 자기 리모컨을 부숨 → 당신 리모컨은 멀쩡

핵심:

  • 두 리모컨 모두 같은 TV 를 가리킴
  • 리모컨(참조) 은 별개
  • 그러나 TV(객체)는 하나 → 영향 공유

자바의 메서드 호출이 정확히 이 방식.


핵심 한 문장

"자바는 항상 Pass by Value 다. 단, 객체의 경우 '참조의 값(주소)' 이 복사된다."

용어 정리:

  • Pass by Value (값에 의한 전달) ← 자바 ✅
  • Pass by Reference (참조에 의한 전달) ← 자바에는 없음 ❌

흔한 오해:

  • "객체는 참조로 전달되니 Pass by Reference 아닌가요?"
  • NO. 참조(주소) 의 값 이 복사되어 전달.

비유 정리:

비유 요소자바 적용
종이 (주소)참조 변수
종이의 복사본참조의 값 복사
실제 집Heap 의 객체
집 칠하기객체 변경
종이 찢기참조 변경 (외부 무관)

🔥 2. 탄생 배경

프로그래밍 언어의 인자 전달 방식

언어마다 메서드 호출 시 인자를 전달하는 방식이 다릅니다:

방식 1: Pass by Value (값에 의한 전달)

  • 인자의 값을 복사 해서 전달
  • 함수 내부에서 변경해도 외부 영향 X
  • 예: C 의 기본 인자 전달
void modify(int x) {
    x = 100;  // 복사된 값만 변경
}
int n = 10;
modify(n);
printf("%d", n);  // 10 (변경 안 됨)

방식 2: Pass by Reference (참조에 의한 전달)

  • 변수 자체 를 전달 (별칭)
  • 함수 내부에서 변경하면 외부도 변경
  • 예: C++ 의 & 참조 매개변수
void modify(int& x) {  // & 참조 매개변수
    x = 100;
}
int n = 10;
modify(n);
cout << n;  // 100 ⚠️ (변경됨)

방식 3: Pass by Pointer (포인터에 의한 전달)

  • 변수의 주소를 전달
  • C 의 명시적 포인터
void modify(int* x) {
    *x = 100;
}
int n = 10;
modify(&n);
printf("%d", n);  // 100 (포인터로 변경)

자바의 결정 — Pass by Value 만 ⭐

자바 설계자들의 결정 (1995):

"자바는 Pass by Value 만 지원한다. 단, 객체는 참조로 다룬다."

왜?:

  • 단순함 (한 가지 방식만)
  • 안전 (예측 가능)
  • 객체는 어차피 참조로 처리되니 별도 메커니즘 불필요

"자바는 Pass by Reference" 오해의 등장

그러나 많은 개발자가 헷갈립니다:

public void modify(Customer c) {
    c.setName("Bob");  // ⚠️ 변경됨!
}

Customer alice = new Customer("Alice");
modify(alice);
System.out.println(alice.getName());  // "Bob" — 변경됨!

오해:

  • "어? 변경되네? 그럼 Pass by Reference 아닌가?"

진실:

  • 자바는 여전히 Pass by Value
  • 참조의 값(주소) 이 복사되어 전달
  • 두 참조가 같은 객체를 가리킴
  • 객체 변경 → 양쪽 모두 영향

이 함정을 정확히 이해하는 게 시니어 자바 개발자의 차별화.


James Gosling (자바 창시자) 의 인용

자바 창시자 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 인가?"

잘못된 답:

  • "기본형은 Value, 객체는 Reference 입니다" ❌

올바른 답:

  • "자바는 항상 Pass by Value 입니다. 객체의 경우 참조의 값(주소) 이 복사되어 전달됩니다." ✅

이 답으로 시니어 후보 차별화.


핵심 통찰

"Pass by Value 의 진실은 'JVM 메모리 구조' 위에서 명확해진다."

Stack에는 변수의 값 (기본형은 값 자체, 객체는 참조 = 주소), Heap에는 객체. 메서드 호출 시 Stack의 값 이 복사되어 새 Stack Frame 으로. 그래서 객체의 경우 두 참조가 같은 Heap 객체를 가리킴.

메모리 구조 (Unit 4.1) 를 모르면 Pass by Value 도 진짜 이해 못 함.


💣 3. 없으면 생기는 문제

Pass by Value의 진실을 모르면 다양한 버그와 혼란이 발생합니다.

시나리오 1: 객체 변경의 의외성

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 진실 모르면:

  • "값으로 전달됐으니 50000 그대로 아닌가?"
  • 답은 40000 ⚠️

Pass by Value 진실 알면:

  • myFare 의 참조 값 (주소) 이 복사되어 전달
  • 두 참조 (myFarefare) 가 같은 Heap 객체 가리킴
  • 객체 변경 → 양쪽 모두 보임
  • 40000

시나리오 2: 객체 교체 시도

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 라고 오해하면:

  • "10000으로 교체됐겠지"

진실 (Pass by Value):

  • 메서드 안의 fare = myFare 의 참조 복사본
  • fare = new Fare(10000)메서드 안의 fare 만 새 객체 가리킴
  • myFare여전히 50000 객체 가리킴
  • 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 로 안 됨.


시나리오 3: 흔한 버그 — 반환을 깜빡

// ❌ 잘못된 코드
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 ✅

시나리오 4: List 정렬의 의외성

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] ⚠️

왜?:

  • listmyList 의 참조 복사
  • 같은 ArrayList 객체 가리킴
  • list.sort()객체 내부 변경
  • → 양쪽에서 보임

객체의 메서드로 변경하면 외부 영향.


시나리오 5: 매개변수 재할당 시 헷갈림

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 무관

재할당 전후의 동작이 완전히 다름.


시나리오 6: 면접 탈락

"자바는 Pass by Value 인가요 Pass by Reference 인가요?"

답 못함 또는 헷갈림:

  • "음... 객체는 reference 같은데..."
  • 시니어 자격 노출

잘 답함:

  • "자바는 항상 Pass by Value 입니다. 객체도 마찬가지로 참조의 값 이 복사되어 전달됩니다. 다만 두 참조가 같은 객체를 가리키므로 객체 내부 변경은 양쪽에 영향을 미칩니다."
  • → 정확한 이해 표현

✅ 4. 해결책 — Pass by Value 의 정확한 이해

핵심 규칙 ⭐

자바의 모든 메서드 호출 은 다음 규칙을 따릅니다:

규칙 1: 기본형 (primitive) — 값 자체가 복사

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  ← 메서드 안만 변경

값 복사. 외부 무관.


규칙 2: 객체 — 참조의 값(주소) 이 복사

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")  ← 객체 자체 변경

참조의 값(주소) 이 복사. 같은 객체를 가리키므로 변경 공유.


규칙 3: 객체 재할당 — 메서드 안만 영향

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+)

Java 8 람다도 같은 규칙:

public void process(Function<Integer, Integer> func) {
    int result = func.apply(10);
}

process(x -> x * 2);  // 람다 자체가 객체로 전달

람다도 객체 → 참조의 값 복사.


의도적으로 외부 변경 — Wrapper 패턴

기본형을 메서드에서 변경하려면:

// ❌ 안 됨
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);

핵심 정리 — 면접용 5분 답변 구조

질문: "자바는 Pass by Value 인가 Pass by Reference 인가?"

답변 구조:

  1. 결론 (10초): "자바는 항상 Pass by Value 입니다."

  2. 기본형 설명 (30초):

    • "기본형은 값 자체가 복사됩니다."
    • 예시 코드 또는 메모리 그림.
  3. 객체 설명 (1분):

    • "객체도 Pass by Value 인데, 참조의 값(주소) 이 복사 됩니다."
    • "그래서 메서드 안에서 객체 내부 변경 시 외부에 영향이 있습니다."
    • "그러나 객체를 재할당하면 메서드 안만 변경됩니다."
  4. 메모리 그림 (1분):

    • Stack 의 변수, Heap 의 객체, 참조 관계 설명.
  5. 흔한 오해 (30초):

    • "Pass by Reference 와 헷갈리기 쉬운데, James Gosling 도 명시적으로 'Java has no pass by reference' 라고 말했습니다."
  6. 실용적 함의 (1분):

    • "그래서 객체를 메서드에 넘길 때 부수효과 에 주의해야 하고, 변경 결과를 외부에 반영하려면 반환 을 활용합니다."

이 구조면 시니어 답변.


🏗️ 5. 내부 동작 원리

메서드 호출의 메모리 변화 (자세히)

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" (객체 변경됨)

참조 (Reference) vs 포인터 (Pointer)

자바의 참조와 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 → false
  • a.equals(b) → 내용 "hello" == "hello" → true

기본형 비교:

int x = 10;
int y = 10;
x == y;  // true (값 비교)

기본형의 == 는 값, 객체의 == 는 참조.


Integer Cache 의 함정 ⚠️

Integer a = 100;
Integer b = 100;
a == b;  // true ⚠️

Integer c = 200;
Integer d = 200;
c == d;  // false ⚠️

왜?:

  • Java는 -128 ~ 127 범위 Integer 를 캐시
  • 100, 100 → 캐시에서 같은 객체 → 같은 참조
  • 200, 200 → 새 객체 → 다른 참조

해결: 항상 equals() 사용:

a.equals(b);  // true (안전)
c.equals(d);  // true

자바 면접 함정 질문.


String Pool 과 Pass by Value

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에서 자세히.


💻 6. 실전 코드 예시

예시 1: ILIC 운임 변경의 부수효과

@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 가 객체 변경
  • 트랜잭션 종료 시 자동 UPDATE → 의도하지 않은 DB 변경

해결:

public Fare calculateDiscountedFare(Fare fare, int rate) {
    int discountedAmount = fare.getAmount() * (100 - rate) / 100;
    return new Fare(discountedAmount);  // 새 객체 반환
}

불변성 + 반환 을 활용.


예시 2: 배열의 Pass by Value

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 만 가리킴

예시 3: List 변경

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]


예시 4: 불변 객체의 안전성

public void process(String s) {
    s = s + "!";  // 새 String 객체 생성
}

String myStr = "hello";
process(myStr);
System.out.println(myStr);  // "hello"

왜 변경 안 됨?:

  • String 은 불변 (immutable)
  • s + "!" → 새 String 객체 생성, s 에 재할당
  • 메서드 안의 s 만 새 객체 가리킴
  • myStr 무관

불변 객체의 장점:

  • Pass by Value 의 부수효과 없음
  • 안전
  • 멀티스레드에 안전

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);
    }
}

불변 객체로 부수효과 방지.


예시 5: 의도적 변경 — Wrapper 활용

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

핵심:

  • Counter 는 가변 객체
  • 메서드에서 increment() 호출 → 객체 내부 변경 → 외부에서 보임
  • 의도적으로 외부 상태 변경 가능

Wrapper 패턴.

더 나은 방법 — AtomicInteger (스레드 안전):

AtomicInteger counter = new AtomicInteger(0);

public void process(AtomicInteger counter) {
    counter.incrementAndGet();
}

예시 6: 복사가 필요한 경우 — Defensive Copy

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 의 권장 패턴.


⚠️ 7. 주의사항 & 흔한 실수

실수 1: "객체는 Pass by Reference" 라고 함

Q: "객체를 메서드에 넘기면?"
A: "Pass by Reference 입니다" ❌

정답: Pass by Value — 참조의 값 복사.


실수 2: == 로 객체 비교

String a = new String("hello");
String b = new String("hello");
if (a == b) { ... }  // ❌ false 나옴

해결: .equals() 사용:

if (a.equals(b)) { ... }  // ✅

객체 비교는 항상 equals().


실수 3: 의도하지 않은 객체 변경

@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);
}

실수 4: Integer 비교

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

실수 5: 매개변수 변경

public void process(int count) {
    count = count + 1;  // ❌ 의미 X
    // count 변경해도 외부 영향 없음
}

해결: 반환:

public int process(int count) {
    return count + 1;
}

실수 6: List 매개변수에 새 List 할당

public void clear(List<Integer> list) {
    list = new ArrayList<>();  // ❌ 외부 list 안 비워짐
}

해결:

public void clear(List<Integer> list) {
    list.clear();  // ✅ 객체 내부 메서드 호출
}

실수 7: 복사 없이 외부 변경 허용

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);

🔗 8. 연관 개념 맵

Phase 4 (JVM 메모리) 완성

[Unit 4.1: JVM 런타임 데이터 영역]
        ↓
[Unit 4.2: Pass by Value] ← 지금 여기 ★
        ↓
[Phase 4 완료]

Phase 4 완료. 다음은 Phase 5 GC.


Phase 1-3과의 통합

학습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주차 (제네릭/람다):

  • 람다도 객체 → Pass by Value
  • 함수형 프로그래밍의 불변성 강조

4주차 (동시성):

  • 공유 객체의 위험 (Heap 의 객체)
  • volatile, synchronized

5주차 (Spring DI):

  • Bean 들의 참조 주입

11-12주차 (JPA):

  • 영속성 컨텍스트의 객체 관리
  • Dirty Checking — 객체 변경 자동 감지

Effective Java 의 권장:

  • 불변 객체 선호
  • 방어적 복사
  • → Pass by Value 이해의 자연스러운 결과

면접 단골 질문 매핑

질문이 Unit에서의 답
"자바는 Pass by Value? Reference?"항상 Pass by Value, 객체는 참조의 값 복사
"객체를 메서드에서 변경 시 외부 영향?"객체 내부 변경 시 영향, 재할당은 X
"== 와 equals() 차이?"== 는 참조 (객체) 또는 값 (기본형) 비교, equals() 는 내용
"Integer 비교 함정?"-128~127 캐시, 그 외 다른 객체
"방어적 복사가 뭔가요?"Pass by Value 의 부수효과 방지

📝 9. 핵심 요약 — 3줄 정리

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 의 함정 회피.


🎓 학습 자기 점검

기본 이해

  • 자바가 Pass by Value 임을 한 문장으로 설명할 수 있다
  • 객체의 경우 "참조의 값" 이 복사된다는 의미를 안다
  • 객체 내부 변경 vs 재할당의 차이를 구별한다
  • == 와 equals() 의 본질적 차이를 안다

실전 적용

  • ILIC 코드의 의도하지 않은 객체 변경을 식별할 수 있다
  • 방어적 복사를 적용할 수 있다
  • 불변 객체 패턴을 작성할 수 있다
  • Integer 비교 함정을 피할 수 있다

면접 대비 (5분 답변)

  • "자바는 Pass by Value?" 답변 가능
  • 메모리 그림으로 설명 가능
  • == 와 equals() 답변 가능
  • Integer 캐시 답변 가능

자기 점검 질문 답변

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")) 은 외부에 영향
  • 이것을 "Reference로 전달돼서" 라고 오해
  • → 사실은 같은 객체를 가리키는 두 참조 때문

비유:

  • 두 사람이 같은 집 주소 를 적은 종이를 가짐
  • 한 사람이 그 집을 칠함 → 둘 다 그 집을 보면 칠해진 색
  • 그러나 종이(주소) 자체는 별개
  • → 자바 객체 전달과 정확히 동일

Q2: 기본 자료형과 객체 자료형의 메서드 인자 전달 차이는?

한 줄 답: 둘 다 Pass by Value, 단 복사되는 대상 이 다름.

상세 설명:

기본 자료형 (primitive)

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  ← 값 복사

효과: 메서드 안 변경 → 외부 무관.


객체 자료형 (Reference)

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) 위에서 명확해짐.


Phase 4 완료 — 다음으로

  • Phase 5 (GC) 학습 준비 완료
  • Heap 의 객체가 어떻게 수거되는지 궁금하다
  • 메모리 누수 방지를 만날 준비 완료

🎉 Phase 4 완료 축하!

학습한 2개 Unit

Unit주제핵심
4.1JVM 런타임 데이터 영역 ★★★5가지 메모리 영역
4.2Pass by Value ★★★자바의 인자 전달 진실

Phase 4의 핵심 통찰

"메모리 구조를 알아야 자바를 진짜 이해한 것이다."

Pass by Value, 동시성, GC, JPA 영속성 — 모든 자바의 깊은 주제가 이 메모리 모델 위에 서 있다. 추상의 세계 (Phase 1-3) 와 물리의 세계 (Phase 4) 가 만나는 지점.

Phase 5 미리보기 — GC 깊이 파기

이제 Heap 의 객체가 어떻게 수거되는지:

  • Mark-Sweep, Mark-Compact, Generational
  • Young/Old Generation
  • G1, ZGC 등 현대 GC
  • GC 튜닝과 모니터링

→ Heap 이해 (Unit 4.1) 가 결정적 토대.

profile
Software Developer

0개의 댓글