본문 바로가기
Backend/Java

[Java] Pass by Value vs. Pass by Reference

by unknownomad 2023. 12. 5.

메모리 할당

  • 어떤 변수 선언 = 메모리 할당
  • 변수를 선언하기 위해 할당되는 메모리로는 크게 스택과 힙이 있음

 

스택(Stack) 영역

  • 함수의 호출과 함께 지역 변수 / 매개 변수 등이 할당됨
  • 정렬된 방식으로 메모리 할당 및 해제됨

 

힙(Heap) 영역

  • 클래스 변수나 인스턴스 변수 / 객체 등이 할당됨
  • 우연하고 무질서하게 메모리 할당됨
  • ➡️ JVM 은 무질서하게 관리되는 힙 영역을 위주로, GC 통해 메모리 해제 관리함
  • (인스턴스 변수로 존재하는 원시 변수는 힙 영역에서 관리됨)

원시 변수(Primitive Value)의 메모리 할당

  • 자바에서 변수는 객체가 아닌 실제 값들인 int, double, float boolean 등과 같은 원시 값(Primitive Value)들이 존재
public void test() {
    // Primitive Value
    int x = 3;
    float y = 10.012f;
    boolean isTrue = true;
}
  • 메모리에서는 아래처럼 스택 영역에 할당됨
  • 스택 영역에 실제 값들이 저장되는 것

https://mangkyu.tistory.com/106


객체(Object)의 메모리 할당

  • 원시 변수는 스택 영역에 실제 값들이 할당됨
  • 그러나 객체는 원시 변수와 다른 방식으로 값이 할당됨
public void test() {
    // Primitive Value
    int x = 3;
    float y = 101.012f;
    boolean isTrue = true;
    
    // Object
    String name = "MangKyu";
    
    String[] names = new String[3];
    names[0] = "I";
    names[1] = "am";
    names[2] = new String("MangKyu");   
}
  • 먼저 객체의 경우 힙 영역에 실제 값이 할당됨
  • 그리고 이에 접근하기 위해 스택 영역에는 힙 영역에 존재하는 값의 실제 주소가 저장됨
  • (C/C++에서는 이를 포인터(pointer)라 부름)
  • 즉, 스택 영역에는 실제 값이 존재하는 힙 영역의 주소가 저장됨

https://mangkyu.tistory.com/106

  • 힙 영역은 스택 영역과 다르게, 무질서하게 메모리 공간을 활용함
  • 객체의 메모리 할당인 경우, 스택 영역에 실제 값을 참조하기 위한 Reference(참조값)이 저장됨 ➡️ 이를 참조하여 실제 값에 접근
  • 배열의 경우, 그 배열의 인덱스마다 참조값이 할당되며, 이를 통해 실제 값에 접근

테스트 코드

class Dog {

    private String name;

    public Dog (String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

public class Test {

    public static void main(String[] args) {
        int x = 10;
        int[] y = {2, 3, 4};
        Dog dog1 = new Dog("강아지1");
        Dog dog2 = new Dog("강아지2");
        
        // 함수 실행
        foo(x, y, dog1, dog2);
        
        System.out.println("x = " + x);
        System.out.println("y = " + y[0]);
        System.out.println("dog1.name = " + dog1.getName());
        System.out.println("dog2.name = " + dog2.getName());
    }

    public static void foo(int x, int[] y, Dog dog1, Dog dog2) {
        x++;
        y[0]++;
        dog1 = new Dog("이름 바뀐 강아지1");
        dog2.setName("이름 바뀐 강아지2");
    }

}

 

테스트 결과

x = 10
y = 3
dog1.name = 강아지1
dog2.name = 이름 바뀐 강아지2
  • 위 결과의 근본적인 이유 : 자바는 Pass By Value 로 변수를 전달하기 때문

foo() ➡️ int x

  • 스택에 파라미터가 할당됨 = 즉, 새로운 x 가 스택에 10의 값으로 생성됨
  • foo 는 x 값 증가시킴 = 기존의 x 가 아닌, 새로운 x 값을 증가시킴
  • ➡️ 기존의 x 는 10 으로 값 유지
  • ➡️ 새로운 x 에 10 할당된 후, 11 로 증가
  • ➡️ foo 함수 종료 시, 새로운 x 는 스택에서 해제됨
  • 그러므로 foo 호출한 후 x 출력 시, 기존의 x 출력됨

https://mangkyu.tistory.com/106


foo() ➡️ int y

  • 스택에 새로운 파라미터인 int[] y 할당

https://mangkyu.tistory.com/106

  • y 의 0 번째 인덱스에 존재하는 값을 1 증가
  • 새로운 y 가 할당되었으나, x 와 달리 배열의 주소를 가리킴
  • 해당 y 가 기존의 y 이든 새로운 y 이든, 실제 배열의 주소로 접근하여 값을 1 증가시키기에, foo가 종료된 후에도 배열의 값은 변화하게 됨

https://mangkyu.tistory.com/106


foo() ➡️ Dog dog1

  • 스택에 새로운 파라미터인 dog1 생성
  • y 와 마찬가지로 dog1 은 객체이므로 dog1 의 주소를 값으로 가짐

https://mangkyu.tistory.com/106

  • foo 함수는 dog1 을 새로 생성
  • ➡️ 힙에는 새로운 dog1 이 생성됨
  • ➡️ 스택에서 가리키는 주소값은 새로운 dog1 을 가리키게 됨
  • ➡️ foo 함수 종료 시, 새로운 dog1 은 소멸 및 기존의 dog1 에는 변화 X

https://mangkyu.tistory.com/106


foo() ➡️ Dog dog2

  • 네 번째 파라미터로 Dog 클래스의 인스턴스인 dog2 를 받아옴
  • dog1 때와 마찬가지로 스택에는 새로운 dog2 할당됨 ➡️ 실제 dog2 의 주소값 가리킴

https://mangkyu.tistory.com/106

  • 그러나 dog2 가 수행하는 연산 != dog1 연산
  • dog2 는 새로운 값 할당 X, 주소값 통해 힙에 존재하는 객체에 접근 ➡️ set() 으로 값 변화시킴

https://mangkyu.tistory.com/106

  • foo() 함수 종료 시, 스택에 할당된 새로운 dog2 소멸됨
  • 그러나 힙에 존재하는 값은 변하였기에 foo 함수가 종료된 후 dog2 의 name 출력 시 "이름 바뀐 강아지2"임을 확인

foo() 종료 결과

https://mangkyu.tistory.com/106


Pass by Value (값에 의한 전달)

  • 복사된 데이터를 전달하여 구성
  • 값을 수정해도 원본 데이터에는 영향 주지 않음
  • process() 내에서 해당 값을 수정해도, 해당 해당 함수가 종료되면 원본 값은 기존 상태 유지

https://mangkyu.tistory.com/107

  1. 먼저 main() 호출 ➡️ 스택에 main() 과 그 지역 변수로 선언한 someValue 존재
  2. 그 위로 process() 의 Return Address 가 스택에 쌓임
  3. 그 위로 process() 의 파라미터인 int 변수 value 가 스택에 쌓임

 

  • 메모리에 할당된 main 함수의 someValue는 그대로 유지된 상태
  • 7이라는 값만 복사하여 새롭게 메모리를 할당
  • main() 의 someValue 주소값 = 0x07040
  • process() 의 value 주소값 = 0x07000
  • 두 변수는 값만 같을뿐 다른 존재

 

Q. process() 로 전달받은 파라미터 value 에 어떤 연산이 수행된 후 process() 가 종료되면?

  • process() 위해 할당된 Call Stack 이 pop 되고 ➡️ 해당 메모리는 모두 소멸
  • 그렇기에 process() 종료된 후 main() 에서 someValue 를 다시 출력해도 7이라는 기존 값 유지
    https://mangkyu.tistory.com/107

Pass by Reference (참조에 의한 전달)

  • Pass by Reference 가 어떤 Alias 를 구성하여 실제 값에 접근한다는 관점에서
  • Pass by Pointer 는 Alias 로 주소값을 넘겨주었을 뿐이기 때문에 동일한 기술이라고 볼 수 있음
  • Pass By Reference 는 주소값을 전달해 실제 값에 대한 Alias 를 구성
  • ➡️ 값 수정 시, 원본 데이터가 수정되게 함
  • Pass by Value 에서는 실제 값이 쌓인 것과 다르게, Pass by Reference 에서는 실제 값의 주소가 쌓임

https://mangkyu.tistory.com/107

 

  • process() 에서 파라미터로 전달받은 value 는 someValue 가 저장된 값을 참조하는 Alias
  • value 를 10 으로 변경 = value가 가리키는 메모리(실제 someValue 가 저장된 메모리)의 값을 7에서 10으로 변경
  • 그래서 process() 종료 후 someValue 출력 시, 10 으로 변경되어 있음

자바에서 Pass by Value

  • 이론적으로 원시 값은 스택 메모리에 실제 값이 저장되고, 객체는 실제 메모리를 참고하기 위한 값이 저장됨
  • 그렇기에 자바에서는 객체에 한해 확장된 규칙이 적용됨
  • 객체 복사 후 전달되는 값 = 실제 메모리를 가리키는 Reference(참조값)인 포인터

 

자바에서 객체 생성 시

Dog dog = new Dog();
  • dog 은 실제로 생성된 Dog 클래스의 객체를 저장하는 것이 아니고,
  • Dog 클래스의 객체가 저장된 주소값(포인터값)을 가지고 있는 것
  • 자바에서의 객체 전달 = 이런 dog 변수가 복제되어 전달
  • 자바에서 어떤 객체가 파라미터로 전달되면, 필드값에 접근해 해당 값 수정은 가능하나, 그 객체 자체는 변경 불가능

 

예시

class Foo {

    private int value;

    public Foo(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

}

public class Main {

    public static void main(String[] args) {
        Foo someFoo = new Foo(7);
        process(someFoo);
        System.out.println(someFoo.getValue());
    }

    public static void process(Foo foo) {
        // 주소 값을 통해 필드 값을 변경
        foo.setValue(10);

        // 객체 자체를 변경하는 것은 영향 X
        foo = new Foo(15);
    }
}

https://mangkyu.tistory.com/107

  • 이번에는 main() 에서 Foo 클래스에 대한 객체 생성 후 이를 process() 의 파라미터로 넘김
  • 자바에서는 Pass by Value 에 따라 Foo 객체가 존재하는 주소(0x07070)를 갖는 someFoo 변수를 복사하여 전달
  • ➡️ process() 가 주소값을 통해 필드값에 접근하게 해줌
  • 그러나 process() 종료 시, copied pointer(0x07000) 는 소멸됨
  • Foo 객체 자체에 변경사항이 있었다면 이는 반영되지 않음

 

Pass by Reference(참조에 의한 전달)

  • 주소값을 전달하여 실제 값에 대한 Alias 를 구성 = 값 수정 시 원본의 데이터가 수정되는 방식
  • 자바에서 객체를 전달하는 방식에는 분명 주소값이 전달되고 있지만, 이는 someFoo 에 대한 복사본일 뿐
  • process() 의 파라미터로 전달받은 foo 역시 실제 주소값을 참조하고 있기에, foo 의 setValue 를 통해 실제 객체의 필드값 수정 시 결과에 반영됨
  • 그러나 이는 객체의 주소값으로 객체의 필드값에 접근하여 값을 변경하는 것일 뿐, 실제 객체 자체에 변화 X

정리

전달 방식 Pass by Value Pass by Reference
의미 복사된 데이터를 전달해 구성하는 방식 주소값을 전달해 실제 값에 대한 Alias 구성하는 방식
원본 데이터 변화 값을 수정해도 원본 데이터는 변화 X 값 수정 시, 원본 데이터 수정됨

출처

https://mangkyu.tistory.com/105

https://mangkyu.tistory.com/106

https://mangkyu.tistory.com/107

'Backend > Java' 카테고리의 다른 글

[Java] Exception  (0) 2023.12.05
[Java] Enum  (0) 2023.12.05
[Java] 기본 자료형 & 참조 자료형  (0) 2023.12.05
[자바의 신] 29장 (ongoing)  (0) 2022.08.31
[자바의 신] 28장 (ongoing)  (0) 2022.08.31

댓글