개발이 취미인 사람

[Java] Call by Value와 Call by Reference 완벽 이해 본문

언어(Programming Language)/Java

[Java] Call by Value와 Call by Reference 완벽 이해

RyanSin 2026. 1. 28. 10:57
반응형

개요

안녕하세요. 이번 시간에는 Java의 인자 전달 방식인 Call by Value와 Call by Reference에 대해 알아보겠습니다.

Java를 공부하다 보면 "함수에 객체를 넘겼는데 왜 원본이 바뀌지?", "근데 왜 재할당은 안 되는 거야?"라는 의문이 생기곤 합니다.

 

특히 "Java는 Call by Value다" vs "Java는 객체는 Call by Reference다"라는 논쟁은 개발자 면접에서도 자주 등장하는 단골 질문입니다.

 

오늘은 이 혼란스러운 개념을 명확하게 정리하고, 실제 메모리 관점에서 어떻게 동작하는지 깊이 있게 다뤄보겠습니다!

개념

Call by Value란?

함수에 값의 복사본을 전달하는 방식입니다.

함수 내에서 매개변수를 변경해도 원본 변수는 영향을 받지 않습니다.

void change(int x) {
    x = 100;  // 복사본만 변경
}

int num = 5;
change(num);
System.out.println(num);  // 여전히 5

Call by Reference란?

함수에 **변수의 메모리 주소(참조)**를 전달하는 방식입니다.

함수 내에서 매개변수를 변경하면 원본 변수도 함께 변경됩니다.

// C++ 예시
void change(int &x) {  // &는 참조
    x = 100;  // 원본 변수가 100으로 변경됨!
}

int num = 5;
change(num);
cout << num;  // 100 출력

 

Java는 항상 Call by Value!

핵심: Java의 모든 인자 전달은 Call by Value

많은 사람들이 혼란스러워하는 이유는 **Java가 객체를 다룰 때 "참조값 자체를 복사해서 전달"**하기 때문입니다.

기본 타입: 값 자체를 복사해서 전달
참조 타입: 참조값(주소)을 복사해서 전달

중요한 것은 "복사"라는 점입니다! 원본이 아닌 복사본을 전달하므로 Call by Value입니다.

 

1. 기본 타입(Primitive Type)의 전달

코드 예시

public class PrimitiveExample {
    static void changeValue(int x) {
        System.out.println("함수 내부 (변경 전): " + x);
        x = 100;
        System.out.println("함수 내부 (변경 후): " + x);
    }
    
    public static void main(String[] args) {
        int num = 5;
        System.out.println("호출 전: " + num);
        changeValue(num);
        System.out.println("호출 후: " + num);
    }
}

 

실행 결과

호출 전: 5
함수 내부 (변경 전): 5
함수 내부 (변경 후): 100
호출 후: 5

 

메모리 상태

[호출 전]
Stack (main)
└─ num = 5

[changeValue 호출]
Stack (main)
└─ num = 5

Stack (changeValue)
└─ x = 5 (복사된 값)

[x = 100 실행]
Stack (main)
└─ num = 5 (변경 안됨!)

Stack (changeValue)
└─ x = 100 (복사본만 변경)

결론: 기본 타입은 값 자체가 복사되므로 원본에 영향을 주지 않습니다.

 

2. 참조 타입(Reference Type)의 전달

여기서부터 헷갈립니다! 차근차근 알아보겠습니다.

2-1. 객체 내부 속성 변경

class Person {
    String name;
    
    Person(String name) {
        this.name = name;
    }
}

public class ReferenceExample {
    static void changeName(Person p) {
        System.out.println("함수 내부 (변경 전): " + p.name);
        p.name = "김철수";
        System.out.println("함수 내부 (변경 후): " + p.name);
    }
    
    public static void main(String[] args) {
        Person person = new Person("홍길동");
        System.out.println("호출 전: " + person.name);
        changeName(person);
        System.out.println("호출 후: " + person.name);
    }
}

 

실행 결과

호출 전: 홍길동
함수 내부 (변경 전): 홍길동
함수 내부 (변경 후): 김철수
호출 후: 김철수  ← 원본이 변경됨!

 

메모리 상태

Heap
└─ Person 객체 (주소: 0x1234)
   └─ name = "홍길동"

Stack (main)
└─ person = 0x1234 (참조값)

Stack (changeName)
└─ p = 0x1234 (참조값 복사!)

핵심: 참조값(0x1234)이 복사되어 전달됩니다. 그래서 p와 person은 같은 객체를 가리킵니다!

따라서 p.name을 변경하면 같은 객체를 가리키는 person.name도 변경됩니다.

하지만 이건 Call by Reference가 아닙니다! 왜일까요?

 

2-2. 객체 재할당 (핵심!)

class Person {
    String name;
    
    Person(String name) {
        this.name = name;
    }
}

public class ReassignExample {
    static void reassignObject(Person p) {
        System.out.println("함수 내부 (재할당 전): " + p.name);
        p = new Person("이영희");  // 새 객체 생성 후 재할당
        System.out.println("함수 내부 (재할당 후): " + p.name);
    }
    
    public static void main(String[] args) {
        Person person = new Person("홍길동");
        System.out.println("호출 전: " + person.name);
        reassignObject(person);
        System.out.println("호출 후: " + person.name);
    }
}

 

실행 결과

호출 전: 홍길동
함수 내부 (재할당 전): 홍길동
함수 내부 (재할당 후): 이영희
호출 후: 홍길동  ← 원본은 그대로!

 

메모리 상태 (단계별)

[초기 상태]
Heap
└─ Person 객체 A (주소: 0x1234)
   └─ name = "홍길동"

Stack (main)
└─ person = 0x1234

[reassignObject 호출]
Stack (main)
└─ person = 0x1234

Stack (reassignObject)
└─ p = 0x1234 (복사된 참조값)

[p = new Person("이영희") 실행]
Heap
├─ Person 객체 A (주소: 0x1234)
│  └─ name = "홍길동"
└─ Person 객체 B (주소: 0x5678)  ← 새로 생성!
   └─ name = "이영희"

Stack (main)
└─ person = 0x1234 (변경 안됨!)

Stack (reassignObject)
└─ p = 0x5678 (새 객체를 가리킴)

핵심:

  • p는 새로운 객체(0x5678)를 가리키게 되었지만
  • person은 여전히 원래 객체(0x1234)를 가리킵니다
  • 왜? p는 참조값의 복사본이기 때문입니다!

만약 진짜 Call by Reference였다면, person도 새 객체를 가리켜야 합니다.

왜 Java는 Call by Value만 사용할까?

1. 안정성

void dangerousFunction(Person p) {
    p = null;  // 만약 Call by Reference라면?
}

Person person = new Person("홍길동");
dangerousFunction(person);
// Java: person은 여전히 유효 (안전)
// C++: person이 null이 되어 크래시 위험

2. 단순성

// 모든 전달이 같은 방식
void func(int x);        // Call by Value
void func(Person p);     // Call by Value (참조값의)
// C++은 여러 방식을 구분해야 함
void func(int x);        // Call by Value
void func(int &x);       // Call by Reference
void func(int *x);       // 포인터
void func(const int &x); // Const Reference

3. 포인터 제거

C++의 가장 큰 버그 원인:

  • 널 포인터 에러
  • 댕글링 포인터 (해제된 메모리 접근)
  • 메모리 누수

Java는 이런 문제를 원천적으로 차단했습니다.

실전 예제: swap 함수

C++ (Call by Reference 가능)

void swap(int &a, int &b) {
    int temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
swap(x, y);
// x = 10, y = 5 (성공!)

Java (Call by Value만 가능)

// 시도 1: 실패
static void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}

int x = 5, y = 10;
swap(x, y);
// x = 5, y = 10 (실패!)

// 시도 2: 객체로 감싸기
class IntWrapper {
    int value;
    IntWrapper(int value) { this.value = value; }
}

static void swap(IntWrapper a, IntWrapper b) {
    int temp = a.value;
    a.value = b.value;
    b.value = temp;
}

IntWrapper x = new IntWrapper(5);
IntWrapper y = new IntWrapper(10);
swap(x, y);
// x.value = 10, y.value = 5 (성공!)

언어별 비교

언어 Call by Value Call by Reference 특징

Java ✅ 항상 ❌ 불가능 단순하고 안전
C++ ✅ 가능 ✅ 가능 (& 사용) 유연하지만 복잡
C# ✅ 가능 ✅ 가능 (ref 사용) Java + C++ 절충
Python - - 독특한 방식
JavaScript ✅ 항상 ❌ 불가능 Java와 유사

핵심 정리

Java의 인자 전달 규칙

1. 기본 타입 (int, boolean 등)
   → 값 자체를 복사해서 전달
   → 원본 변경 불가능

2. 참조 타입 (객체, 배열 등)
   → 참조값(주소)을 복사해서 전달
   → 객체 내부는 변경 가능
   → 변수 재할당은 불가능

3줄 요약

  1. Java는 모든 경우에 Call by Value다
  2. 객체의 경우, "참조값"을 값으로 전달한다
  3. 객체 내부 수정 가능 ≠ Call by Reference

면접 질문 답변 예시

질문: "Java는 Call by Value인가요, Call by Reference인가요?"

정답:

"Java는 항상 Call by Value입니다. 기본 타입은 값 자체를 복사하고, 참조 타입은 참조값을 복사해서 전달합니다. 따라서 객체 내부를 수정할 수는 있지만, 변수 자체를 다른 객체로 재할당하는 것은 불가능합니다. 이것이 진짜 Call by Reference와의 결정적 차이입니다."

마무리

이번 시간에는 Java의 인자 전달 방식에 대해 알아봤습니다.

처음에는 "객체는 참조 전달 아닌가?"라고 생각하기 쉽지만, 정확히는 **"참조값의 복사"**라는 점을 기억하세요!

특히 면접에서 이 질문이 나오면:

  • "Java는 항상 Call by Value다"
  • "객체 재할당이 안 되는 이유"

이 두 가지를 명확히 설명할 수 있다면 완벽합니다!

실제 코드를 작성하면서 "이 변수는 왜 안 바뀌지?", "저 객체는 왜 바뀌지?"를 생각하다 보면 자연스럽게 이해가 됩니다.

꼭 실습을 통해 학습하신 걸 추천드리겠습니다!!

참고 자료