F-LAB JAVA · 3주차 · Phase 1 · Pass by Value의 진짜 이해
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
& 매개변수 는 어떻게 동작하나?Pass by Value 와 Pass by Reference 는 "매개변수가 호출자 변수와 어떤 관계인가" 의 두 가지 정답이다.
Pass by Value: 매개변수는 호출자 변수의 값을 복사한 새 변수.
Pass by Reference: 매개변수는 호출자 변수 자체의 다른 이름 (별칭).
이 차이가 함수 안에서 매개변수에 재할당 했을 때 호출자에 반영되는지 여부를 가른다.
자바는 오직 Pass by Value 만 존재한다 — 박승제씨가 1·2주차 면접 단골 질문의 정답.
| 시스템 | 비유 |
|---|---|
| Pass by Value | 원본 문서를 복사해서 줌. 복사본을 찢어도 원본은 멀쩡 |
| Pass by Reference | 같은 문서에 다른 이름 라벨을 붙임. 어느 라벨로 접근해도 같은 문서. 한 쪽이 찢으면 다 사라짐 |
| C 포인터 전달 | 원본 문서의 위치 메모 를 복사해서 줌. 메모는 별개지만 가리키는 곳은 같음 |
| 재할당 차이 | 복사본을 새 문서로 바꿔도 원본 무관 vs 별칭을 새 문서로 바꾸면 모두 새 문서 |
→ 핵심: "값을 바꾸는 것" 과 "변수를 재할당하는 것" 은 다르다.
→ 둘의 차이를 메모리 레벨에서 본다.
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. 면접 질문 + 자기 점검
박승제씨가 2주차 Unit 1.4, 2.2 에서 본 것:
함수 호출 시 JVM (또는 C/C++ 런타임) 동작:
1. 새 Stack Frame 생성
2. 매개변수 슬롯 할당 (LVA)
3. 호출자가 전달한 값을 슬롯에 저장
4. 함수 본체 실행
5. 함수 종료 시 Frame 제거
핵심 질문: 3번 단계에서 "호출자가 전달한 값" 이 무엇인가?
이 질문의 답이 두 가지로 갈린다.
Pass by Value:
호출자 변수의 "값" 을 새 변수에 복사
결과:
매개변수 = 새 변수 (별개의 메모리)
호출자 변수와 무관하게 독립
효과:
- 매개변수에 무엇을 하든 호출자에 영향 X
- 매개변수 재할당해도 호출자 무관
Pass by Reference:
호출자 변수 "자체" 를 매개변수로 사용
매개변수 = 호출자 변수의 다른 이름 (alias)
결과:
매개변수와 호출자 변수가 같은 메모리
효과:
- 매개변수 변경 = 호출자 변수 변경
- 매개변수 재할당 = 호출자 변수 재할당
함수 호출 전:
호출자 스택:
┌───────────┐
│ a = 10 │ 주소: 0x1000
└───────────┘
Pass by Value 호출 후 함수 안:
호출자 스택: 함수 스택:
┌───────────┐ ┌───────────┐
│ a = 10 │ 0x1000 │ x = 10 │ 주소: 0x2000 ← 새 메모리!
└───────────┘ └───────────┘
Pass by Reference 호출 후 함수 안:
호출자 스택:
┌───────────┐
│ a = 10 │ 주소: 0x1000 ← x도 같은 곳을 가리킴
└───────────┘
▲ ▲
│ │
a x ← x 는 a 의 별칭 (같은 메모리)
→ 메모리 레벨에서 완전히 다름.
Pass by Value:
영수증 발급
- 원본은 가게에 있음
- 손님은 복사본 받음
- 손님이 영수증에 낙서해도 가게의 원본은 그대로
Pass by Reference:
공동 명의 통장
- 두 사람 이름으로 등록
- 한 사람이 입금/출금하면 다른 사람도 영향
- 통장이 사라지면 둘 다 끝
Pass by Value 와 Pass by Reference 의 결정적 차이는?
답:
1. 메모리 관점:
재할당 영향:
변수 자체의 정체:
void modify(int x) {
x = 100; // 매개변수에 새 값 대입
}
int main() {
int a = 10;
modify(a); // a 의 값(10)을 복사해서 전달
printf("%d\n", a); // 10 (변경 안 됨)
}
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 와 무관.
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 (변경 안 됨)
}
→ 구조체 전체가 복사 됨.
→ 큰 구조체면 비효율적 (그래서 보통 포인터로 전달).
void modify(int x) {
x = 100;
}
int a = 10;
modify(a);
System.out.println(a); // 10 (변경 안 됨)
→ 자바도 primitive 는 Pass by Value.
→ C와 똑같은 동작.
매개변수에 다른 값을 대입해도 호출자가 영향 안 받는 이유는?
답:
1. 매개변수는 새 메모리 (호출자 변수의 복사본)
2. 매개변수에 대입 = 새 메모리의 값 변경
3. 호출자 변수의 메모리는 건드리지 않음
4. 그래서 호출자엔 영향 없음
→ 메모리가 다르다는 점이 핵심.
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 가 같은 메모리.
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 의 다른 이름일 뿐.
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 같은 주소!
특징:
// 큰 객체 효율적 전달
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 는 포인터의 안전한 버전.
void modify(int& x) { // ❌ 자바 문법 아님
x = 100;
}
자바는 Reference 매개변수가 없음.
→ 모든 매개변수는 Pass by Value.
C++의
int& x매개변수가 자바와 어떻게 다른가?
답:
1. C++ Reference: 매개변수가 호출자 변수의 별칭
x = 100 으로 호출자의 a 변경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 와 비슷.
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 자체는 새 메모리.
// C++ Reference
void modify(int& x) {
x = 100;
}
// C 포인터
void modify(int *p) {
*p = 100;
}
비슷한 점:
modify(a) vs modify(&a)다른 점:
// 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 포인터는 변수 이므로 재할당 가능.
C++ Reference:
호출자 변수 = 매개변수 (같은 메모리)
진짜 Pass by Reference
C 포인터:
호출자 변수 ← 매개변수 (포인터, 새 메모리)
주소를 값으로 전달 = Pass by Value (of an address)
→ C 의 포인터 전달은 사실 Pass by Value 의 한 형태.
→ 단지 값이 "주소" 일 뿐.
자바의 객체 전달:
void modify(Shipment s) {
s.setBlNo("New"); // 객체 변경 → 반영
s = new Shipment(); // 재할당 → 호출자 무관
}
이게 정확히 C 의 포인터 전달 과 같음.
s.setBlNo() ≈ *p->blNo = "New" (객체 내용 변경)s = new ... ≈ p = &newObj (포인터 자체 변경)→ 자바는 C 포인터 전달 패턴.
→ C++ Reference 와 다름.
C 포인터 전달은 진짜 Pass by Reference 인가?
답:
진짜 Pass by Reference 의 조건:
C++ Reference 만 이 조건 충족.
C 포인터는 비슷한 효과지만 메커니즘 다름.
매개변수에서 "변경" 은 두 가지:
변경 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() (다른 객체 가리키게)
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)
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 대상)
└──────────────────┘
"s 와 original 은 같은 객체를 가리킨다" 까지는 사실
"s 와 original 은 같은 메모리이다" 는 거짓
만약 자바가 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" ← 변경됨!
→ 자바는 이런 방식을 금지.
→ 매개변수 재할당이 호출자에 영향을 안 줌 → 코드 예측 가능성 ↑.
자바의 객체 매개변수에서 "변경" 의 두 가지 의미는?
답:
1. 객체 내용 변경 (메서드 호출, 필드 변경):
→ 흔한 오해: "객체 변경이 반영되니 Pass by Reference 다"
→ 정답: "참조의 값이 복사된 것이지, Pass by Reference 가 아니다"
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."
번역:
"메서드/생성자 호출 시, 실인자 표현식의 값들이
새로 생성된 매개변수 변수들을 초기화한다."
핵심:
모든 매개변수 전달:
1. 실인자(argument) 값 평가
2. 매개변수 슬롯에 그 값 저장
3. = 값 복사
primitive (int, long, double, ...):
- 값 자체가 복사
- 호출자와 매개변수가 다른 메모리
객체 참조 (Shipment, String, ...):
- 참조(주소) 가 값으로 복사
- 두 변수가 같은 객체를 가리킴
- 그러나 변수 자체는 다른 메모리
public void modify(Shipment s) {
s.setStatus(ACTIVE); // 호출자에 반영
}
오해: "s 가 원본을 가리키니 Pass by Reference"
정답:
오해: "Java 는 primitive 는 Value, 객체는 Reference"
정답:
public void modify(Shipment s) {
s = new Shipment(); // 호출자 무관
}
이게 호출자에 반영 안 되는 게 Pass by Value 의 증거.
Q: "Java 는 Pass by Value 인가 Pass by Reference 인가?"
모범 답안:
"Java 는 오직 Pass by Value 입니다."
- primitive 는 값 자체가 복사됩니다.
- 객체 변수도 사실은 참조(주소)를 값으로 보유하는데,
이 참조가 복사됩니다.- 그래서 호출자 변수와 매개변수가 같은 객체를 가리키게 되지만,
변수 자체는 별개 메모리입니다.- 매개변수에 객체 메서드를 호출하면 호출자에 반영되지만,
매개변수에 새 객체를 재할당하면 호출자엔 무관합니다.- C++ 처럼 진짜 Pass by Reference 였다면 재할당도 반영되어야 하는데,
그렇지 않으므로 Pass by Value 입니다.
→ 박승제씨의 면접 답안.
James Gosling 등 자바 설계자의 선택:
- Pass by Value 단일화
→ 코드 예측 가능성 ↑
→ 매개변수 재할당이 호출자에 영향 X
→ 디버깅 쉬움
- 객체 참조라는 안전한 추상화
→ 큰 객체도 효율적 전달 (참조 8 bytes)
→ 그러면서도 메모리 안전
- C++ Reference 의 함정 회피
→ 함수 안에서 매개변수 재할당이
→ 호출자에 영향 주면 코드 추적 어려움
→ 자바의 단순함과 안전성을 위한 선택.
"자바는 객체를 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).
func modify(p *Person) {
p.Name = "Modified" // 포인터를 통한 변경 → 원본 반영
}
// 호출
p := &Person{Name: "Bob"}
modify(p)
→ C 처럼 명시적 포인터.
→ 기본은 Pass by Value (struct 도 복사).
→ 효율적 전달은 포인터 명시.
func modify(value: inout Int) {
value = 100 // 원본 변경
}
var a = 10
modify(value: &a)
print(a) // 100
→ inout 키워드로 진짜 Pass by Reference 비슷한 효과.
→ 그러나 내부적으로는 "copy in, copy out" 패턴.
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 지만 규칙이 매우 엄격.
fun modify(s: Shipment) {
s.status = ACTIVE // 원본 반영
// s = Shipment() // ❌ 컴파일 에러! Kotlin 매개변수는 val
}
→ Kotlin 도 Pass by Value.
→ 추가로 매개변수는 자동 val (재할당 불가).
→ "재할당으로 호출자 헷갈리게 만드는 코드" 자체를 막음.
void modify(ref int x) {
x = 100; // 원본 변경
}
int a = 10;
modify(ref a); // 호출 시도 ref 명시
Console.WriteLine(a); // 100
→ ref 키워드로 진짜 Pass by Reference.
→ 호출 시점에도 명시 → 명확함.
| 언어 | 기본 방식 | 명시적 Reference |
|---|---|---|
| Java | Pass by Value | ❌ 없음 |
| Python | Pass by Object Reference (= Value of ref) | ❌ |
| Kotlin | Pass by Value (재할당도 금지) | ❌ |
| C | Pass by Value | 포인터로 시뮬레이션 |
| C++ | Pass by Value | int& x 로 Reference |
| C# | Pass by Value | ref int x |
| Go | Pass by Value (struct 복사) | 포인터로 시뮬레이션 |
| Swift | Pass by Value | inout 키워드 |
| Rust | Move 기본 | &mut borrow |
→ 현대 언어들은 Pass by Value 기본 + 명시적 Reference.
→ 자바는 명시적 Reference 도 없는 가장 단순한 모델.
자바 → Kotlin 마이그레이션 시:
매개변수 동작 동일
단, Kotlin 은 매개변수 자동 val → 재할당 불가
더 안전한 코드
자바 → C++ 전환 시:
Reference 개념 추가 학습 필요
& 매개변수와 포인터 구분
자바 → Go 전환 시:
포인터 다시 등장
단, GC 있어서 안전
자바 → Python 마이그레이션 시:
거의 동일 (Pass by Object Reference)
primitive vs 객체 구분 안 됨 (모든 것이 객체)
원칙 1: 매개변수는 가능하면 final
매개변수 재할당은 코드 가독성 ↓
원칙 2: 메서드 안에서 매개변수 객체를 변경하지 말 것 (함수형 권장)
새 객체 반환이 더 명확
원칙 3: null 가능성을 명확히
@Nullable, Optional, NonNull 어노테이션 활용
원칙 4: 큰 객체는 객체 참조로 전달 (자동)
자바에선 자동, 신경 안 써도 됨
@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.
→ 자바도 명시 권장.
// ❌ 매개변수 객체를 변경 (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();
}
함수형 스타일의 이점:
→ ILIC 같은 시스템에서 권장.
@Value // Lombok
public final class ShipmentEvent {
private final Long id;
private final ShipmentStatus status;
private final LocalDateTime timestamp;
// 모든 필드 final + setter 없음
}
장점:
ILIC 의 도메인 이벤트, DTO 등에 적극 활용.
가끔 매개변수 객체 변경이 필요하면 명시적으로:
// 명시적 변경 메서드명
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();
}
→ 메서드명으로 의도 명확히.
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();
}
박승제씨가 ILIC 코드 리뷰 시 체크:
매개변수 자체:
☐ final 표시?
☐ 재할당 시도?
☐ null 가능성 명시?
매개변수 객체:
☐ 변경 (side effect) 발생?
☐ 컬렉션을 변경하는가?
☐ 불변 객체로 받을 수 있나?
메서드 시그니처:
☐ void 인가 반환값 있나?
☐ 함수형 스타일 가능?
☐ 이름이 의도를 표현하나? (normalize vs mutate)
면접관: "자바의 매개변수 전달은?"
박승제씨:
"자바는 오직 Pass by Value 입니다.
primitive 는 값이, 객체는 참조(주소)가 값으로 복사됩니다.
그래서 호출자와 매개변수가 같은 객체를 가리키게 되어
객체 내용 변경은 반영되지만,
매개변수에 새 객체를 재할당해도 호출자엔 무관합니다.
C++ Reference 처럼 진짜 별칭이 아니라
단지 참조의 값이 복사된 것입니다.
매개변수 동작을 예측 가능하게 만든 자바 설계자의 선택입니다."
→ 이 답변이면 완벽.
| 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 |
int& x 동작을 메모리 레벨에서 설명할 수 있다public void modify(String s) {
s = s + " World"; // 새 String 객체 (s 가 가리키는 곳 변경 X)
}
String hello = "Hello";
modify(hello);
System.out.println(hello); // "Hello" (변경 안 됨)
답:
s + " World" 는 새 String 객체 생성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)
답:
public void modify(Integer i) {
i = i + 100; // Integer 는 불변 → 새 객체
}
Integer x = 10;
modify(x);
System.out.println(x); // 10 (변경 안 됨)
답:
i + 100 은 새 객체 (auto-boxing)// 방법 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 효과는 우회적.
→ 보통 반환값 사용이 정답.
1. Pass by Value vs Pass by Reference
2. 자바는 오직 Pass by Value
3. ILIC 실무 권장
이번 Unit에서 Pass by Value 와 Pass by Reference 의 메모리 차이를 봤다면, 다음은 자바에 깊이 들어간 정점.
→ Phase 1 의 마지막 Unit. 면접 단골 질문 완벽 정복.
🚀 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 — 함수형 프로그래밍
작성한 자료:
Unit 1.1 C 포인터 기초
Unit 1.2 Pass by Value vs Pass by Reference ← 여기
총: 2/43 Unit 작성 (약 5%)
다음 Unit 에서 다룰 핵심:
박승제씨가 Unit 1.3 을 마스터하면, 자바 매개변수 전달에 대한 어떤 질문도 자신감 있게 답변 가능.
F-LAB JAVA · 3주차 · Phase 1 · Unit 1.2 · 끝