3주차 Unit 1.1-1.2 — Pass by Value vs Pass by Reference

Psj·2026년 5월 18일

F-lab

목록 보기
76/230

Unit 1.2 — Pass by Value vs Pass by Reference

F-LAB JAVA · 3주차 · Phase 1 · Pass by Value의 진짜 이해


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • Pass by Value 의 정확한 메모리 동작은?
  • Pass by Reference 의 정확한 메모리 동작은?
  • C++의 & 매개변수 는 어떻게 동작하나?
  • C의 포인터 가 Pass by Reference 와 어떻게 다른가?
  • 진짜 Pass by Reference포인터 전달 을 시뮬레이션해서 같아 보이는 것의 결정적 차이는?
  • 자바는 두 방식 중 어느 쪽 인가?
  • "객체가 변경되는데 Pass by Reference 아닌가?" 라는 흔한 오해를 어떻게 해소하나?
  • Python, Go, Swift, Rust, Kotlin 은 각각 어느 방식인가?

🎯 핵심 한 문장

Pass by Value 와 Pass by Reference 는 "매개변수가 호출자 변수와 어떤 관계인가" 의 두 가지 정답이다.
Pass by Value: 매개변수는 호출자 변수의 값을 복사한 새 변수.
Pass by Reference: 매개변수는 호출자 변수 자체의 다른 이름 (별칭).
이 차이가 함수 안에서 매개변수에 재할당 했을 때 호출자에 반영되는지 여부를 가른다.
자바는 오직 Pass by Value 만 존재한다 — 박승제씨가 1·2주차 면접 단골 질문의 정답.

비유 — 문서의 복사본 vs 별칭

시스템비유
Pass by Value원본 문서를 복사해서 줌. 복사본을 찢어도 원본은 멀쩡
Pass by Reference같은 문서에 다른 이름 라벨을 붙임. 어느 라벨로 접근해도 같은 문서. 한 쪽이 찢으면 다 사라짐
C 포인터 전달원본 문서의 위치 메모 를 복사해서 줌. 메모는 별개지만 가리키는 곳은 같음
재할당 차이복사본을 새 문서로 바꿔도 원본 무관 vs 별칭을 새 문서로 바꾸면 모두 새 문서

핵심: "값을 바꾸는 것" 과 "변수를 재할당하는 것" 은 다르다.
→ 둘의 차이를 메모리 레벨에서 본다.


🧭 9개 섹션 로드맵

1. 두 방식의 기본 정의 — 호출자 변수와의 관계
2. Pass by Value — 메모리 추적
3. Pass by Reference — C++의 & 매개변수
4. C 포인터 — Reference 시뮬레이션의 한계
5. 결정적 차이 — 매개변수 재할당의 시점
6. 자바는 어디인가 — Pass by Value 뿐
7. 다른 언어들의 선택 (Python/Go/Swift/Rust/Kotlin)
8. ILIC 실무 — 매개변수 설계 가이드
9. 면접 질문 + 자기 점검

1️⃣ 두 방식의 기본 정의 — 호출자 변수와의 관계

1.1 함수 호출의 메모리 메커니즘 복습

박승제씨가 2주차 Unit 1.4, 2.2 에서 본 것:

함수 호출 시 JVM (또는 C/C++ 런타임) 동작:
  1. 새 Stack Frame 생성
  2. 매개변수 슬롯 할당 (LVA)
  3. 호출자가 전달한 값을 슬롯에 저장
  4. 함수 본체 실행
  5. 함수 종료 시 Frame 제거

핵심 질문: 3번 단계에서 "호출자가 전달한 값" 이 무엇인가?

이 질문의 답이 두 가지로 갈린다.

1.2 Pass by Value의 정의

Pass by Value:
  호출자 변수의 "값" 을 새 변수에 복사
  
결과:
  매개변수 = 새 변수 (별개의 메모리)
  호출자 변수와 무관하게 독립
  
효과:
  - 매개변수에 무엇을 하든 호출자에 영향 X
  - 매개변수 재할당해도 호출자 무관

1.3 Pass by Reference의 정의

Pass by Reference:
  호출자 변수 "자체" 를 매개변수로 사용
  매개변수 = 호출자 변수의 다른 이름 (alias)
  
결과:
  매개변수와 호출자 변수가 같은 메모리
  
효과:
  - 매개변수 변경 = 호출자 변수 변경
  - 매개변수 재할당 = 호출자 변수 재할당

1.4 결정적 시각화

함수 호출 전:
  호출자 스택:
    ┌───────────┐
    │ a = 10    │  주소: 0x1000
    └───────────┘

Pass by Value 호출 후 함수 안:
  호출자 스택:                함수 스택:
    ┌───────────┐              ┌───────────┐
    │ a = 10    │ 0x1000       │ x = 10    │  주소: 0x2000  ← 새 메모리!
    └───────────┘              └───────────┘

Pass by Reference 호출 후 함수 안:
  호출자 스택:
    ┌───────────┐
    │ a = 10    │  주소: 0x1000  ← x도 같은 곳을 가리킴
    └───────────┘
         ▲ ▲
         │ │
         a x  ← x 는 a 의 별칭 (같은 메모리)

메모리 레벨에서 완전히 다름.

1.5 비유로 다시 보기

Pass by Value:
  영수증 발급
  - 원본은 가게에 있음
  - 손님은 복사본 받음
  - 손님이 영수증에 낙서해도 가게의 원본은 그대로
  
Pass by Reference:
  공동 명의 통장
  - 두 사람 이름으로 등록
  - 한 사람이 입금/출금하면 다른 사람도 영향
  - 통장이 사라지면 둘 다 끝

1.6 자기 점검 답변

Pass by Value 와 Pass by Reference 의 결정적 차이는?

:
1. 메모리 관점:

  • Value: 매개변수가 새 메모리 (값 복사)
  • Reference: 매개변수와 호출자 변수가 같은 메모리
  1. 재할당 영향:

    • Value: 매개변수 재할당해도 호출자 무관
    • Reference: 매개변수 재할당 = 호출자 재할당
  2. 변수 자체의 정체:

    • Value: 매개변수는 호출자 변수의 복사본
    • Reference: 매개변수는 호출자 변수의 별칭

2️⃣ Pass by Value — 메모리 추적

2.1 C 코드로 보기

void modify(int x) {
    x = 100;       // 매개변수에 새 값 대입
}

int main() {
    int a = 10;
    modify(a);     // a 의 값(10)을 복사해서 전달
    printf("%d\n", a);   // 10 (변경 안 됨)
}

2.2 메모리 6단계 추적

Step 1: int a = 10;
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014
    └───────────┘

Step 2: modify(a) 호출 직전
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014
    └───────────┘
  
  호출 준비:
    a 의 값 (10) 을 읽어서 새 메모리에 복사 준비

Step 3: modify 함수 진입 직후
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014  ← main 의 a (그대로)
    ├───────────┤
    │ x = 10    │ 0x7FFE_0004  ← modify 의 x (복사된 값)
    └───────────┘

Step 4: x = 100 실행
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014  ← 변하지 않음
    ├───────────┤
    │ x = 100   │ 0x7FFE_0004  ← x 의 메모리만 변경
    └───────────┘

Step 5: modify 종료
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014  ← main 의 a
    └───────────┘
  (x 의 메모리는 frame 과 함께 사라짐)

Step 6: printf("%d", a)
  출력: 10

a 와 x 는 완전히 별개 메모리.
→ x 에 무엇을 하든 a 와 무관.

2.3 객체 (구조체) 도 Pass by Value 시

C에서 구조체를 값으로 전달:

typedef struct {
    int id;
    char name[32];
} Person;

void modify(Person p) {
    p.id = 999;
    strcpy(p.name, "Modified");
}

int main() {
    Person bob = {1, "Bob"};
    modify(bob);
    printf("%d %s\n", bob.id, bob.name);   // 1 Bob (변경 안 됨)
}

→ 구조체 전체가 복사 됨.
→ 큰 구조체면 비효율적 (그래서 보통 포인터로 전달).

2.4 자바의 primitive

void modify(int x) {
    x = 100;
}

int a = 10;
modify(a);
System.out.println(a);   // 10 (변경 안 됨)

→ 자바도 primitive 는 Pass by Value.
→ C와 똑같은 동작.

2.5 자기 점검 답변

매개변수에 다른 값을 대입해도 호출자가 영향 안 받는 이유는?

:
1. 매개변수는 새 메모리 (호출자 변수의 복사본)
2. 매개변수에 대입 = 새 메모리의 값 변경
3. 호출자 변수의 메모리는 건드리지 않음
4. 그래서 호출자엔 영향 없음

메모리가 다르다는 점이 핵심.


3️⃣ Pass by Reference — C++의 & 매개변수

3.1 C++의 Reference 문법

C++는 자바와 다르게 진짜 Pass by Reference 가 있음.

void modify(int& x) {    // 매개변수에 & 붙임
    x = 100;
}

int main() {
    int a = 10;
    modify(a);           // 그냥 a 전달
    cout << a;           // 100! 변경됨!
}

int& x 는 "x 는 int 의 reference (별칭)" 라는 선언.
→ a 와 x 가 같은 메모리.

3.2 메모리 시각화

Step 1: int a = 10;
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014
    └───────────┘

Step 2: modify(a) 호출 시
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014  ← x 도 이 메모리를 가리킴
    └───────────┘
              ▲
              │
              x (별칭, 새 메모리 없음)

Step 3: x = 100 실행
  Stack:
    ┌───────────┐
    │ a = 100   │ 0x7FFE_0014  ← 변경! (x 와 a 가 같은 메모리)
    └───────────┘

Step 4: modify 종료 후 cout << a
  출력: 100

x 는 별도 메모리가 없음. a 의 다른 이름일 뿐.

3.3 C++ Reference 의 특성

int a = 10;
int& r = a;        // r 은 a 의 reference (별칭)

r = 20;            // a 도 20 이 됨
a = 30;            // r 도 30 이 됨

cout << &a << endl;  // 0x7FFE_0014
cout << &r << endl;  // 0x7FFE_0014  같은 주소!

특징:

  • 선언 시 반드시 초기화
  • 한 번 정해진 reference 는 다른 변수를 가리키게 못 함
  • 진짜 별칭

3.4 C++ Reference 매개변수의 강력함

// 큰 객체 효율적 전달
void process(LargeStruct& obj) {
    obj.modify();
}

// vs Pass by Value (느림)
void process(LargeStruct obj) {   // 전체 복사
    obj.modify();
}

// vs Pass by Pointer (가능하지만 문법 더 복잡)
void process(LargeStruct* obj) {
    obj->modify();
}

→ Reference 는 포인터의 안전한 버전.

3.5 자바에는 없다

void modify(int& x) {   // ❌ 자바 문법 아님
    x = 100;
}

자바는 Reference 매개변수가 없음.
→ 모든 매개변수는 Pass by Value.

3.6 자기 점검 답변

C++의 int& x 매개변수가 자바와 어떻게 다른가?

:
1. C++ Reference: 매개변수가 호출자 변수의 별칭

  • 같은 메모리 공유
  • 매개변수 변경 = 호출자 변경
  • x = 100 으로 호출자의 a 변경
  1. 자바: Reference 매개변수 없음
    • 모든 매개변수가 새 메모리
    • 객체도 "주소를 값으로" 복사
    • 매개변수 재할당 = 호출자 무관

4️⃣ C 포인터 — Reference 시뮬레이션의 한계

4.1 C 포인터로 비슷한 효과

C 는 Reference 매개변수가 없지만, 포인터 로 비슷한 효과:

void modify(int *p) {
    *p = 100;          // p 가 가리키는 곳에 100 저장
}

int main() {
    int a = 10;
    modify(&a);        // a 의 주소 전달
    printf("%d\n", a); // 100! 변경됨
}

겉으로는 C++ Reference 와 비슷.

4.2 메모리 시각화 — C 포인터

Step 1: int a = 10;
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014
    └───────────┘

Step 2: modify(&a) 호출
  Stack:
    ┌───────────┐
    │ a = 10    │ 0x7FFE_0014
    ├───────────┤
    │ p =       │ 0x7FFE_0008  ← p 는 새 메모리!
    │ 0x7FFE_   │                p 의 값은 a 의 주소 (0x7FFE_0014)
    │   0014    │
    └───────────┘

Step 3: *p = 100 실행
  내부:
    1. p 의 값 읽음: 0x7FFE_0014
    2. 그 주소에 100 저장
    3. → a 의 메모리에 100
  
  Stack:
    ┌───────────┐
    │ a = 100   │ 0x7FFE_0014  ← 변경!
    ├───────────┤
    │ p =       │ 0x7FFE_0008  ← p 자체는 그대로 (주소 그대로)
    │ 0x7FFE_   │
    │   0014    │
    └───────────┘

→ a 가 변경되지만, p 자체는 새 메모리.

4.3 C++ Reference 와 C 포인터의 결정적 차이

// C++ Reference
void modify(int& x) {
    x = 100;
}
// C 포인터
void modify(int *p) {
    *p = 100;
}

비슷한 점:

  • 둘 다 호출자의 a 변경 가능
  • 호출자 코드 차이만 있음: modify(a) vs modify(&a)

다른 점:

  • C++ Reference: x 자체가 a (같은 메모리)
  • C 포인터: p 는 새 메모리 (a 의 주소를 담음)

4.4 결정적 차이 — 매개변수 재할당

// C++ Reference
void modify(int& x) {
    int y = 999;
    x = y;          // x 를 y 로 "교체" 할 수 있나?
                    // → 아니. 단지 a 에 y 의 값 (999) 저장
}
// 호출 후 a = 999 (a 의 값이 변경)
// C 포인터
void modify(int *p) {
    int y = 999;
    p = &y;         // p 가 y 의 주소 가리키게 함
    *p = 0;         // y 에 0 저장 (a 와 무관!)
}
// 호출 후 a = 10 (변경 안 됨!)

C++ Reference 는 별칭 이라 다른 변수의 별칭으로 만들 수 없음.
C 포인터는 변수 이므로 재할당 가능.

4.5 본질적 차이 정리

C++ Reference:
  호출자 변수 = 매개변수 (같은 메모리)
  진짜 Pass by Reference

C 포인터:
  호출자 변수 ← 매개변수 (포인터, 새 메모리)
  주소를 값으로 전달 = Pass by Value (of an address)

C 의 포인터 전달은 사실 Pass by Value 의 한 형태.
→ 단지 값이 "주소" 일 뿐.

4.6 자바와의 비교

자바의 객체 전달:

void modify(Shipment s) {
    s.setBlNo("New");   // 객체 변경 → 반영
    s = new Shipment(); // 재할당 → 호출자 무관
}

이게 정확히 C 의 포인터 전달 과 같음.

  • 객체 변경: s.setBlNo()*p->blNo = "New" (객체 내용 변경)
  • 재할당: s = new ...p = &newObj (포인터 자체 변경)

자바는 C 포인터 전달 패턴.
→ C++ Reference 와 다름.

4.7 자기 점검 답변

C 포인터 전달은 진짜 Pass by Reference 인가?

:

  • 아니오. C 포인터 전달은 Pass by Value 의 특수 케이스.
  • 단지 값이 "주소" 일 뿐.
  • 매개변수 p 는 호출자 변수와 별개 메모리.
  • p 를 재할당해도 호출자 변수 무관.

진짜 Pass by Reference 의 조건:

  • 매개변수와 호출자 변수가 같은 메모리
  • 매개변수 재할당이 호출자 재할당

C++ Reference 만 이 조건 충족.
C 포인터는 비슷한 효과지만 메커니즘 다름.


5️⃣ 결정적 차이 — 매개변수 재할당의 시점

5.1 두 가지 "변경" 의 구분

매개변수에서 "변경" 은 두 가지:

변경 1: 매개변수가 가리키는 객체 변경 (내용 수정)
  - C++ Reference: x = 100 (x 가 별칭인 변수의 값 변경)
  - C 포인터: *p = 100 (p 가 가리키는 곳 변경)
  - Java 객체: s.setBlNo("New") (객체 내용 변경)
  
변경 2: 매개변수 자체를 재할당 (다른 것을 가리키게)
  - C++ Reference: 불가능 (별칭은 재할당 X)
  - C 포인터: p = &other (다른 변수 가리키게)
  - Java 객체: s = new Shipment() (다른 객체 가리키게)

5.2 자바에서의 두 변경

public class ShipmentService {
    public void process(Shipment s) {
        // 변경 1: 객체 내용 수정 → 호출자에 반영
        s.setStatus(ShipmentStatus.PROCESSED);
        s.addCargo(new Cargo());
        
        // 변경 2: 매개변수 재할당 → 호출자 무관
        s = new Shipment("NEW-BL");
        s.setStatus(ShipmentStatus.NEW);   // 새 객체에만 영향
    }
}

// 호출
Shipment original = new Shipment("BL-001");
service.process(original);

System.out.println(original.getStatus());  // PROCESSED (변경 1 반영)
System.out.println(original.getBlNo());    // "BL-001" (재할당 영향 X)

5.3 자바 메모리 시각화

Step 1: Shipment original = new Shipment("BL-001");
  Stack:                            Heap:
    ┌───────────┐                    ┌──────────────────┐
    │ original  │ ─────────────────► │ Shipment obj 1   │
    │ (ref)     │                    │  blNo: "BL-001"  │
    └───────────┘                    │  status: NEW      │
                                      └──────────────────┘

Step 2: service.process(original) 호출
  s 매개변수에 original 의 참조 (주소) 복사
  
  Stack:                            Heap:
    ┌───────────┐                    ┌──────────────────┐
    │ original  │ ─────────────────► │ Shipment obj 1   │
    │ (ref)     │                    │  blNo: "BL-001"  │
    ├───────────┤                    │  status: NEW      │
    │ s (ref)   │ ─────────────────► └──────────────────┘
    └───────────┘                       ▲
                                         │
                                  s 도 같은 객체 가리킴

Step 3: s.setStatus(PROCESSED) 실행
  Stack:                            Heap:
    ┌───────────┐                    ┌──────────────────┐
    │ original  │ ─────────────────► │ Shipment obj 1   │
    │ (ref)     │                    │  blNo: "BL-001"  │
    ├───────────┤                    │  status: PROCESSED│ ← 변경!
    │ s (ref)   │ ─────────────────► └──────────────────┘
    └───────────┘

Step 4: s = new Shipment("NEW-BL") 실행
  Stack:                            Heap:
    ┌───────────┐                    ┌──────────────────┐
    │ original  │ ─────────────────► │ Shipment obj 1   │
    │ (ref)     │                    │  blNo: "BL-001"  │
    ├───────────┤                    │  status: PROCESSED│
    │ s (ref)   │ ─┐                  └──────────────────┘
    └───────────┘  │
                   │                  ┌──────────────────┐
                   └─────────────────► │ Shipment obj 2   │ ← 새 객체!
                                      │  blNo: "NEW-BL"  │
                                      │  status: NEW      │
                                      └──────────────────┘

Step 5: s.setStatus(NEW) 실행 (s 는 obj 2 가리킴)
  Heap:
    ┌──────────────────┐
    │ Shipment obj 1   │  ← original 만 가리킴
    │  blNo: "BL-001"  │
    │  status: PROCESSED│
    └──────────────────┘
    
    ┌──────────────────┐
    │ Shipment obj 2   │  ← s 만 가리킴
    │  blNo: "NEW-BL"  │
    │  status: NEW      │
    └──────────────────┘

Step 6: 메서드 종료, s 사라짐
  Heap:
    ┌──────────────────┐
    │ Shipment obj 1   │  ← original 이 가리킴 (살아있음)
    │  status: PROCESSED│
    └──────────────────┘
    
    ┌──────────────────┐
    │ Shipment obj 2   │  ← 아무도 안 가리킴 (GC 대상)
    └──────────────────┘

5.4 핵심 통찰

"s 와 original 은 같은 객체를 가리킨다" 까지는 사실
"s 와 original 은 같은 메모리이다" 는 거짓
  • s 는 Stack 의 매개변수 슬롯
  • original 은 Stack 의 다른 변수 슬롯
  • 둘 다 Heap 의 같은 객체를 가리킬 수 있지만
  • 변수 자체는 별개 메모리

5.5 C++ Reference 와 비교

만약 자바가 C++ 처럼 Reference 매개변수를 지원했다면:

// 가상의 자바 Reference (실제로는 없음)
void process(Shipment& s) {
    s = new Shipment("NEW-BL");   // 원본 변수도 새 객체 가리키게 됨!
}

// 호출
Shipment original = new Shipment("BL-001");
service.process(original);
original.getBlNo();   // "NEW-BL"   ← 변경됨!

→ 자바는 이런 방식을 금지.
→ 매개변수 재할당이 호출자에 영향을 안 줌 → 코드 예측 가능성 ↑.

5.6 자기 점검 답변

자바의 객체 매개변수에서 "변경" 의 두 가지 의미는?

:
1. 객체 내용 변경 (메서드 호출, 필드 변경):

  • s.setBlNo("New")
  • 호출자에 반영됨
  • 같은 객체를 가리키므로 같은 효과
  1. 변수 재할당 (= 으로 새 객체 지정):
    • s = new Shipment()
    • 호출자에 반영 안 됨
    • 매개변수는 새 메모리 (별도 슬롯)

→ 흔한 오해: "객체 변경이 반영되니 Pass by Reference 다"
→ 정답: "참조의 값이 복사된 것이지, Pass by Reference 가 아니다"


6️⃣ 자바는 어디인가 — Pass by Value 뿐

6.1 자바의 공식 입장

Java Language Specification (JLS):
  
  "When a method or constructor is invoked, the values 
   of the actual argument expressions initialize newly 
   created parameter variables, each of the declared type, 
   before execution of the body of the method or constructor."
   
  번역:
  "메서드/생성자 호출 시, 실인자 표현식의 값들이
   새로 생성된 매개변수 변수들을 초기화한다."

핵심:

  • "newly created parameter variables" → 새 변수
  • "values ... initialize" → 값으로 초기화
  • Pass by Value

6.2 자바에서 일어나는 일

모든 매개변수 전달:
  1. 실인자(argument) 값 평가
  2. 매개변수 슬롯에 그 값 저장
  3. = 값 복사

primitive (int, long, double, ...):
  - 값 자체가 복사
  - 호출자와 매개변수가 다른 메모리

객체 참조 (Shipment, String, ...):
  - 참조(주소) 가 값으로 복사
  - 두 변수가 같은 객체를 가리킴
  - 그러나 변수 자체는 다른 메모리

6.3 흔한 오해 분석

오해 1 — "객체 변경이 반영되니 Reference 다"

public void modify(Shipment s) {
    s.setStatus(ACTIVE);   // 호출자에 반영
}

오해: "s 가 원본을 가리키니 Pass by Reference"

정답:

  • 참조의 이 복사되어 같은 객체를 가리킴
  • 객체 자체는 Heap 의 같은 것
  • 그러나 s 와 호출자 변수는 별개 메모리
  • → Pass by Value (of a reference)

오해 2 — "primitive 만 Pass by Value"

오해: "Java 는 primitive 는 Value, 객체는 Reference"

정답:

  • 모든 매개변수가 Pass by Value
  • primitive: 값 자체가 복사
  • 객체: 참조 (주소) 가 값으로 복사
  • 둘 다 매개변수가 새 메모리

오해 3 — "재할당이 반영되면 Reference"

public void modify(Shipment s) {
    s = new Shipment();    // 호출자 무관
}

이게 호출자에 반영 안 되는 게 Pass by Value 의 증거.

  • C++ Reference 라면 호출자도 새 객체 가리킴
  • 자바는 그렇지 않음 → 매개변수가 별개 메모리

6.4 면접 정답 5단계

Q: "Java 는 Pass by Value 인가 Pass by Reference 인가?"

모범 답안:

"Java 는 오직 Pass by Value 입니다."

  1. primitive 는 값 자체가 복사됩니다.
  2. 객체 변수도 사실은 참조(주소)를 값으로 보유하는데,
    이 참조가 복사됩니다.
  3. 그래서 호출자 변수와 매개변수가 같은 객체를 가리키게 되지만,
    변수 자체는 별개 메모리입니다.
  4. 매개변수에 객체 메서드를 호출하면 호출자에 반영되지만,
    매개변수에 새 객체를 재할당하면 호출자엔 무관합니다.
  5. C++ 처럼 진짜 Pass by Reference 였다면 재할당도 반영되어야 하는데,
    그렇지 않으므로 Pass by Value 입니다.

→ 박승제씨의 면접 답안.

6.5 자바 설계자의 의도

James Gosling 등 자바 설계자의 선택:
  
- Pass by Value 단일화
  → 코드 예측 가능성 ↑
  → 매개변수 재할당이 호출자에 영향 X
  → 디버깅 쉬움

- 객체 참조라는 안전한 추상화
  → 큰 객체도 효율적 전달 (참조 8 bytes)
  → 그러면서도 메모리 안전

- C++ Reference 의 함정 회피
  → 함수 안에서 매개변수 재할당이
  → 호출자에 영향 주면 코드 추적 어려움

→ 자바의 단순함과 안전성을 위한 선택.

6.6 자기 점검 답변

"자바는 객체를 reference 로 전달한다" 가 왜 부정확한가?

:

  • "reference 로 전달" 이 의미하는 게 "Pass by Reference" 인지 헷갈림
  • 정확한 표현: "reference 의 값을 전달" (Pass by Value of a reference)
  • 또는: "참조의 복사본을 전달"
  • "Reference 자체를 공유하는 게 아니라, 참조의 값(주소) 만 복사"

→ 용어를 정확히.


7️⃣ 다른 언어들의 선택

7.1 Python — "Pass by Object Reference"

def modify(lst):
    lst.append(99)         # 원본에 반영
    lst = [1, 2, 3]        # 재할당 → 원본 무관

original = [10, 20]
modify(original)
print(original)   # [10, 20, 99]

자바와 정확히 같은 방식.
→ 공식 용어: "Pass by Object Reference" 또는 "Call by Sharing".
→ 사실 Pass by Value (of reference).

7.2 Go — Pass by Value

func modify(p *Person) {
    p.Name = "Modified"    // 포인터를 통한 변경 → 원본 반영
}

// 호출
p := &Person{Name: "Bob"}
modify(p)

→ C 처럼 명시적 포인터.
→ 기본은 Pass by Value (struct 도 복사).
→ 효율적 전달은 포인터 명시.

7.3 Swift — Inout 매개변수

func modify(value: inout Int) {
    value = 100            // 원본 변경
}

var a = 10
modify(value: &a)
print(a)   // 100

inout 키워드로 진짜 Pass by Reference 비슷한 효과.
→ 그러나 내부적으로는 "copy in, copy out" 패턴.

7.4 Rust — Borrow 시스템

fn modify(s: &mut String) {
    s.push_str(" world");
}

let mut greeting = String::from("hello");
modify(&mut greeting);
println!("{}", greeting);   // "hello world"

→ ownership + borrow 라는 독특한 모델.
→ 컴파일 타임에 메모리 안전성 보장.
→ 진짜 reference 지만 규칙이 매우 엄격.

7.5 Kotlin — 자바와 동일

fun modify(s: Shipment) {
    s.status = ACTIVE      // 원본 반영
    // s = Shipment()      // ❌ 컴파일 에러! Kotlin 매개변수는 val
}

→ Kotlin 도 Pass by Value.
→ 추가로 매개변수는 자동 val (재할당 불가).
→ "재할당으로 호출자 헷갈리게 만드는 코드" 자체를 막음.

7.6 C# — ref / out 매개변수

void modify(ref int x) {
    x = 100;               // 원본 변경
}

int a = 10;
modify(ref a);             // 호출 시도 ref 명시
Console.WriteLine(a);      // 100

ref 키워드로 진짜 Pass by Reference.
→ 호출 시점에도 명시 → 명확함.

7.7 비교 표

언어기본 방식명시적 Reference
JavaPass by Value❌ 없음
PythonPass by Object Reference (= Value of ref)
KotlinPass by Value (재할당도 금지)
CPass by Value포인터로 시뮬레이션
C++Pass by Valueint& x 로 Reference
C#Pass by Valueref int x
GoPass by Value (struct 복사)포인터로 시뮬레이션
SwiftPass by Valueinout 키워드
RustMove 기본&mut borrow

현대 언어들은 Pass by Value 기본 + 명시적 Reference.
→ 자바는 명시적 Reference 도 없는 가장 단순한 모델.

7.8 박승제씨가 알아둘 것

자바 → Kotlin 마이그레이션 시:
  매개변수 동작 동일
  단, Kotlin 은 매개변수 자동 val → 재할당 불가
  더 안전한 코드

자바 → C++ 전환 시:
  Reference 개념 추가 학습 필요
  & 매개변수와 포인터 구분

자바 → Go 전환 시:
  포인터 다시 등장
  단, GC 있어서 안전

자바 → Python 마이그레이션 시:
  거의 동일 (Pass by Object Reference)
  primitive vs 객체 구분 안 됨 (모든 것이 객체)

8️⃣ ILIC 실무 — 매개변수 설계 가이드

8.1 좋은 매개변수 설계 원칙

원칙 1: 매개변수는 가능하면 final
  매개변수 재할당은 코드 가독성 ↓
  
원칙 2: 메서드 안에서 매개변수 객체를 변경하지 말 것 (함수형 권장)
  새 객체 반환이 더 명확
  
원칙 3: null 가능성을 명확히
  @Nullable, Optional, NonNull 어노테이션 활용
  
원칙 4: 큰 객체는 객체 참조로 전달 (자동)
  자바에선 자동, 신경 안 써도 됨

8.2 매개변수 final 패턴

@Service
public class ShipmentService {
    
    // ✓ 권장: 매개변수 final
    public ShipmentResponse process(final ShipmentRequest req) {
        // req 재할당 불가 → 안전
        return ShipmentResponse.from(processInternal(req));
    }
    
    // ❌ 매개변수 재할당 시도 (가능하지만 권장 X)
    public ShipmentResponse process(ShipmentRequest req) {
        req = req.normalize();   // 가능하지만 가독성 ↓
        // 호출자는 req 가 바뀌었는지 알기 어려움
        return process(req);
    }
}

→ 코틀린은 자동 final.
→ 자바도 명시 권장.

8.3 메서드 안에서 객체 변경 vs 새 객체 반환

// ❌ 매개변수 객체를 변경 (side effect)
public void normalize(Shipment s) {
    s.setBlNo(s.getBlNo().trim());
    s.setStatus(ShipmentStatus.NORMALIZED);
}

// ✓ 새 객체 반환 (함수형)
public Shipment normalize(Shipment s) {
    return s.toBuilder()
        .blNo(s.getBlNo().trim())
        .status(ShipmentStatus.NORMALIZED)
        .build();
}

함수형 스타일의 이점:

  • 호출자가 원본 그대로 사용 가능
  • 동시성 안전 (객체 변경 X)
  • 테스트 쉬움
  • 디버깅 쉬움

→ ILIC 같은 시스템에서 권장.

8.4 불변 객체 (Immutable Object)

@Value   // Lombok
public final class ShipmentEvent {
    private final Long id;
    private final ShipmentStatus status;
    private final LocalDateTime timestamp;
    
    // 모든 필드 final + setter 없음
}

장점:

  • 매개변수로 전달해도 절대 안 변함
  • Thread-safe 자동
  • 캐싱 가능
  • HashMap 키로 안전

ILIC 의 도메인 이벤트, DTO 등에 적극 활용.

8.5 매개변수 변경 시 명시적 방법

가끔 매개변수 객체 변경이 필요하면 명시적으로:

// 명시적 변경 메서드명
public void mutateShipment(Shipment target, Update update) {
    target.applyUpdate(update);
    // "mutate" 라는 이름으로 명확히 표시
}

// vs 함수형
public Shipment withUpdate(Shipment original, Update update) {
    return original.toBuilder()
        .apply(update)
        .build();
}

→ 메서드명으로 의도 명확히.

8.6 컬렉션 매개변수의 함정

public void process(List<Shipment> shipments) {
    shipments.add(new Shipment());   // 원본 변경!
    shipments.clear();                // 원본 비움!
}

// 호출
List<Shipment> myList = new ArrayList<>();
service.process(myList);
// myList 의 상태가 예상과 다를 수 있음

→ 컬렉션은 객체 변경의 대표적 위험 지점.

해결:

// 방법 1: 불변 컬렉션 받기
public void process(List<Shipment> shipments) {
    shipments = List.copyOf(shipments);   // 방어적 복사
    // 또는 처음부터 List.copyOf(input) 으로 받기
}

// 방법 2: 명시적 입력만
public void process(Collection<Shipment> input) {
    // input 을 변경 안 한다는 컨벤션
    for (Shipment s : input) {
        process(s);
    }
}

// 방법 3: 새 컬렉션 반환
public List<Shipment> process(List<Shipment> input) {
    return input.stream()
        .map(this::processSingle)
        .toList();
}

8.7 ILIC 매개변수 코드 리뷰 체크리스트

박승제씨가 ILIC 코드 리뷰 시 체크:

매개변수 자체:
  ☐ final 표시?
  ☐ 재할당 시도?
  ☐ null 가능성 명시?
  
매개변수 객체:
  ☐ 변경 (side effect) 발생?
  ☐ 컬렉션을 변경하는가?
  ☐ 불변 객체로 받을 수 있나?
  
메서드 시그니처:
  ☐ void 인가 반환값 있나?
  ☐ 함수형 스타일 가능?
  ☐ 이름이 의도를 표현하나? (normalize vs mutate)

8.8 박승제씨의 면접 답변 준비

면접관: "자바의 매개변수 전달은?"

박승제씨:

"자바는 오직 Pass by Value 입니다.
primitive 는 값이, 객체는 참조(주소)가 값으로 복사됩니다.
그래서 호출자와 매개변수가 같은 객체를 가리키게 되어
객체 내용 변경은 반영되지만,
매개변수에 새 객체를 재할당해도 호출자엔 무관합니다.
C++ Reference 처럼 진짜 별칭이 아니라
단지 참조의 값이 복사된 것입니다.
매개변수 동작을 예측 가능하게 만든 자바 설계자의 선택입니다."

→ 이 답변이면 완벽.


9️⃣ 면접 질문 + 자기 점검

9.1 면접 단골 질문 매핑

Q핵심 답변
Java 는 Pass by Value 인가 Reference 인가?Pass by Value 만. 참조도 값으로 복사
객체 변경이 반영되면 Reference 아닌가?아니. 참조 값이 복사되어 같은 객체 가리킴
C++의 int& x 와 Java 의 차이?진짜 별칭 vs 참조 값 복사
C 포인터 전달은 Pass by Reference?아니. Pass by Value of a pointer
매개변수 재할당 차이?C++ Ref: 호출자 변경, Java: 호출자 무관
Python 의 매개변수 방식?Java 와 동일 (Pass by Object Reference)
Kotlin 의 차이점?매개변수 자동 val (재할당 금지)
매개변수 final 권장 이유?재할당 방지, 코드 명확성
컬렉션 매개변수 위험?객체 변경 가능, 방어적 복사 권장
함수형 스타일 이점?side effect 없음, thread-safe

9.2 자기 점검 체크리스트

기본 이해

  • Pass by Value 와 Pass by Reference 의 메모리 차이를 다이어그램으로 그릴 수 있다
  • C++의 int& x 동작을 메모리 레벨에서 설명할 수 있다
  • C 포인터가 진짜 Reference 가 아닌 이유를 안다
  • 자바의 객체 매개변수에서 두 가지 변경의 차이를 안다
  • 매개변수 재할당이 호출자에 영향 안 주는 이유를 설명할 수 있다

실전 적용

  • 매개변수 final 패턴 사용
  • 객체 변경 vs 새 객체 반환 선택
  • 불변 객체 활용
  • 컬렉션 매개변수의 위험 인식
  • 코드 리뷰 시 매개변수 검토

면접 대비 — 5분 답변

  • "Java 는 Pass by Value 인가 Reference 인가?" 5문장 답변
  • C++/C/Python 등 다른 언어 비교
  • 메모리 다이어그램으로 설명
  • 매개변수 재할당 의미 명확히
  • 흔한 오해 해소 가능

9.3 추가 심화 질문

Q1: String 매개변수의 경우는?

public void modify(String s) {
    s = s + " World";   // 새 String 객체 (s 가 가리키는 곳 변경 X)
}

String hello = "Hello";
modify(hello);
System.out.println(hello);   // "Hello" (변경 안 됨)

답:

  • String 은 불변 (immutable)
  • s + " World" 는 새 String 객체 생성
  • s 는 새 객체 가리키게 되지만 매개변수 재할당
  • → 호출자 무관
  • 흔한 함정.

Q2: 배열 매개변수는?

public void modify(int[] arr) {
    arr[0] = 999;       // 배열 내용 변경 → 반영
    arr = new int[10];  // 재할당 → 호출자 무관
}

int[] data = {1, 2, 3};
modify(data);
System.out.println(data[0]);   // 999
System.out.println(data.length); // 3 (재할당 영향 X)

답:

  • 배열도 객체
  • 자바의 객체 매개변수 동일 패턴
  • 내용 변경 반영, 재할당 무관

Q3: Wrapper 타입은?

public void modify(Integer i) {
    i = i + 100;        // Integer 는 불변 → 새 객체
}

Integer x = 10;
modify(x);
System.out.println(x);   // 10 (변경 안 됨)

답:

  • Integer, Long, Double 등 Wrapper 는 불변
  • i + 100 은 새 객체 (auto-boxing)
  • 매개변수 재할당
  • → 호출자 무관

Q4: 그러면 자바에서 진짜 reference 시뮬레이션은?

// 방법 1: 배열 1짜리 사용
public void modify(int[] holder) {
    holder[0] = 100;
}

int[] container = new int[]{10};
modify(container);
System.out.println(container[0]);   // 100

// 방법 2: 가변 객체 사용
public void modify(AtomicInteger holder) {
    holder.set(100);
}

// 방법 3: 반환값으로
public int modify(int x) {
    return x + 100;
}
int result = modify(10);

→ 자바에서 진짜 reference 효과는 우회적.
→ 보통 반환값 사용이 정답.


🎯 핵심 요약 — 3줄 정리

1. Pass by Value vs Pass by Reference

  • Value: 매개변수가 새 메모리 (값 복사)
  • Reference: 매개변수가 호출자 변수의 별칭 (같은 메모리)
  • 결정적 차이: 매개변수 재할당 시 호출자에 반영되는가

2. 자바는 오직 Pass by Value

  • primitive: 값 자체 복사
  • 객체: 참조 (주소) 가 값으로 복사
  • 참조의 복사이지 참조의 공유가 아님
  • C++ Reference 같은 진짜 별칭 없음

3. ILIC 실무 권장

  • 매개변수 final
  • 객체 변경 (side effect) 회피, 새 객체 반환
  • 불변 객체 적극 활용
  • 컬렉션 매개변수는 방어적 복사 검토

📚 다음으로...

Unit 1.3 — 자바의 Call by Value 한 가지 (Phase 1 정점)

이번 Unit에서 Pass by Value 와 Pass by Reference 의 메모리 차이를 봤다면, 다음은 자바에 깊이 들어간 정점.

  • 자바의 단일 방식: Call by Value
  • 박승제씨의 면접 답변 완성
  • 메모리 다이어그램 완전 정리
  • 흔한 오해 8가지 완벽 해소
  • ILIC 실무 시나리오 종합

→ Phase 1 의 마지막 Unit. 면접 단골 질문 완벽 정복.

Phase 1 진행 상황

🚀 Phase 1 — Pass by Value의 진짜 이해
  ✅ Unit 1.1 C 포인터 기초
  ✅ Unit 1.2 Pass by Value vs Pass by Reference ← 여기
  ⏭ Unit 1.3 자바의 Call by Value 한 가지 (정점)

⏭ Phase 2 — 컬렉션 프레임워크 전체 지도
⏭ Phase 3 — 해시의 원리
⏭ Phase 4 — 추상화의 두 도구
⏭ Phase 5 — 제네릭과 와일드카드
⏭ Phase 6 — 객체 비교
⏭ Phase 7 — I/O 시스템 큰 그림
⏭ Phase 8 — Stream 실전
⏭ Phase 9 — I/O 강화
⏭ Phase 10 — 함수형 프로그래밍

3주차 누적 진행

작성한 자료:
  Unit 1.1 C 포인터 기초
  Unit 1.2 Pass by Value vs Pass by Reference ← 여기

총: 2/43 Unit 작성 (약 5%)

Unit 1.3 진입 준비

다음 Unit 에서 다룰 핵심:

  • 자바 매개변수 전달의 단일 정답
  • "왜 Call by Value 만?" 의 설계 철학
  • C/C++/Python/Kotlin 와의 차이 종합
  • 박승제씨의 5문장 면접 답변 완성
  • ILIC 코드의 매개변수 패턴 가이드

박승제씨가 Unit 1.3 을 마스터하면, 자바 매개변수 전달에 대한 어떤 질문도 자신감 있게 답변 가능.


F-LAB JAVA · 3주차 · Phase 1 · Unit 1.2 · 끝

profile
Software Developer

0개의 댓글