nathan_H

[Java] Call By Value와 Call By Reference 본문

Programming Laguage/Java

[Java] Call By Value와 Call By Reference

nathan_H 2020. 4. 27. 18:35

C, C++을 배우지 않고 Java를 배우는 경우 참조 타입에 대해서 낯선 느낌이 들 수 있다. 그리고 실제 참조 타입에 대한 이해가 부족해 프로그램을 만드는 과정에서 참조 타입에 대한 것을 고려하지 않고 짠 프로그램은 나중에 어떠한 큰 문제들이 발생할 위험이 커지게 된다. 좋은 프로그램이라 함은 데이터를 얼마나 안전하게 가져오고 사용하는지에 따라 결정이 되는데, 참조 타입이 거기서 핵심적인 역할을 한다고 해도 과언이 아니다. 그래서 이제 참조 타입, 그리고 이 글의 제목인 Call By Value, Call By Reference가 무엇인지 알아보자.

참조 변수


자바의 타입


본격적으로 참조 타입을 알아보기 전에 자바의 타입에 대해 소개하자면 자바는 크게 2가지의 타입이 존재를 한다.

image


https://www.geeksforgeeks.org/data-types-in-java/

  1. 기본 타입
    • 정수, 실수, 문자, 논리 리터럴을 저장하는 타입
    • 변수에 실제 값을 저장함
    • byte, char, short, int, long, float, double, boolean ...
  2. 참조 타입
    • 객체의 번지를 참조하는 타입
    • 변수에 메모리의 번지를 저장함
    • 배열, 열거, 클래스, 인터페이스
  • 즉 기본 타입과 참조 타입의 차이점은 변수에 무엇을 저장하냐의 차이이다.
    • 기본 타입 : 값, 참조 타입 : 메모리 주소

메모리상에서의 변수


  • 메모리상에서 변수를 갖는 값은 아래 그림과 같은데, 간단히 설명하면 변수는 스택 영역에서 생성이 되고, 객체는 힙 영역에서 생성이 된다고 볼 수 있다.

  • 그리고 여기서 기본 타입의 경우는 변수에 실제 값을 저장에 스택에서만 존재하지만, 참조 타입의 경우 힙 영역에 생성되고, 변수에는 힙 영역에 있는 참조 타입의 주소가 저장이 되는 것이다.

image


https://samdo0812.tistory.com/1

메모리 사용 영역


image


https://programmer-seva.tistory.com/72

메소드 영역

  • JVM이 시작시 생성되고 모든 스레드가 공유하는 영역
  • 메소드 영역에는 코드에서 사용되는 클래스들을 클래스 로더로 읽어 클래스별로 정적 필드와 상수, 메소드 코드, 생성자 코드 등을 분류해 저장

힙 영역

  • 객체와 배열이 생성되는 영역으로 생성된 객체와 배열은 JVM 스택의 변수나 다른 객체의 필드에서 "참조"함
  • 만일 참조하는 변수나 필드가 없다면 의미 없는 객체가 되기 때문에 JVM이 이것을 쓰레기라고 하고 Garbage Collector를 실행시켜 자동으로 제거함

JVM 스택 영역

  • 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 할당됨
  • 자바 프로그램에서 추가적으로 스레드를 생성하지 않았으면 main 스레드만 존재하므로 JVM 스택도 하나임
  • JVM 스택은 메소드를 호출할 때마다 프레임을 추가하고 메소드가 종료되면 해당 프레임을 제거하는 동작을 수행
    • 선입후출(FILO, First In Last Out) 구조로 push와 pop 기능 사용
  • 메소드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame을 저장
  • 메소드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장
  • 스택영역에서 변수가 생성되는 시점은 초기화 될 때, 즉 최초로 변수에 값이 저장될 때이다. 그리고 변수는 선언된 블록 안에서만 스택이 존재하고 블록을 벗어나면 스택에서 사라짐
    • 기본(원시)타입 변수 : 스택 영역에 직접 값을 가짐
    • 참조 타입 변수 : 힙 영역이나 메소드 영역의 객체 주소를 가짐
char a = 'a'; // 1

if (a == 'a') {   // 2
    int b = 10;
    char c = 'c';
}

boolean d = true; // 3

위 코드에서 주석처리한 구간 별로 스택을 살펴보면

  1. 스택 영역에 a = 'a' 저장되어 있음
  2. a = 'a', b=10, c='c' 3개의 변수가 저장되어 있음
  3. 2번 블록에 있는 b,c 변수는 사라지고 a, d 변수만 저장되어있음
  • 추가로 만약 배열과 같은 변수가 스택에 저장될 경우 해당 변수에는 힙 영역에 저장되어 있는 배열의 주소를 참조한 값이 저장되어 있음

참조 변수의 비교 연산


  • 그렇다면 이제 참조 변수와 기본 변수의 차이점은 이해했을 것이다. 그렇다면 참조 변수에 대해서 ==,!= 와 같은 비교 연산 수행 시 어떻게 동작하는지 알아보자.
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};

a == b
a != b
  • 위 예시의 경우 a == b false, a!= b는 true가 나오게 되는데, 그 이유는 간단하다 앞서 말한 대로 배열은 참조 타입이기 때문에 실제 변수에 저장된 값은 배열 객체의 주소 값이기 때문에 a와 b는 실제 다른 객체가 생성이 된 주소의 값들이 저장되기 때문이다.

String

  • 하지만 참조 타입중에 주의해야할 타입이 있는데 그건 바로 String 문자열 타입이다.
  • 문자열 타입의 경우 문자열 리터럴(값)이 동일하다면 String 객체가 공유된다.
String a = "nathan";
String b = "nathan";
String c = new String("nathan");

a == b // true
a == c // false
a.equals(c) // true
  • 가령 위 a, c에 같은 값을 저장하고 비교 연산을 수행하면 기본 타입에 의한 결과가 나오게 된다.
    • 하지만 new 연산자로 새로운 String 객체를 생성해 저장을 한다면, 참조 타입에 의한 결과가 나오게 된다.
  • 추가로 참조 타입에 대해서 비교 연산을 수행할때는 equals 메소드를 통해 수행하는 것이 좋다.

Call by Value, Call of Reference


  • 이제 드디어 오늘 포스팅의 주제인 Call by Value, Call of Reference에 대해 소개하고자 한다.

Call By Value


  • 변수의 값을 복사해 함수의 매개변수로 전달하는 것
  • 즉 값에 의한 호출

Call by Value

class CallByValue {

    public static int subtract(int x, int y) {
        swap(x, y);
        return x - y;
    }

    public static void swap(int x, int y) {
        int temp = x;
        x = y;
        y = temp; // 복사된 값만 바뀌고 사라짐
    }

    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        System.out.printf("%d - %d = %d\n", x, y, subtract(x, y));
    }
}

결과

10 - 20 = -10
  • 가령 위와 같이 int 기본 타입의 값을 매개변수로 받아 두 매개 변수의 값을 swap 한 후 출력하는 코드를 보면 결과 값은 swap 되기 전의 값으로 그대로 출력이 된다.

  • 즉, Call of Value는 값을 직접 전달 받기 때문에 실제 swap을 해 값을 변경했다 하더라도 원본 데이터는 바뀌지 않고, 단순히 복사된 값을 바꾸는 것에 불과하기 때문이다.

    • 또한 자바 기본형 타입은 데이터를 처리할 때 Call by Value 형식으로 처리된다는 것을 알 수 있음

Call By Reference


  • 객체를 참조하는 주소를 함수의 매개변수로 전달하는 것
  • 즉 참조에 의한 호출

Call By Reference

class Number {
    int value;

    public Number(int value) {
        this.value = value;
    }
}
public class CallByReference {

    private static int subtract(Number x, Number y) {
        swap(x, y);
        return x.value - y.value;
    }

    private static void swap(Number x, Number y) {
        int z = x.value;
        x.value = y.value;
        y.value = z;
    }

    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        Number numberX = new Number(x);
        Number numberY = new Number(y);
        int result = subtract(numberX, numberY);
        System.out.printf("%d - %d = %d", numberX.value, numberY.value, result);
    }
}

결과

20 - 10 = 10
  • 반면 객체를 참조하는 주소를 매개변수로 넘겨줘 수행한 결과 값은 실제 swap이 이루어짐을 볼 수 있는데, Call By Referenc의 경우 객체의 참조 값을 직접 바꾼 것이 아니라 객체의 참조를 통해서 해당 객체의 멤버 변수에 접근하여 값을 바꿔 연산이 수행됨을 알 수 있다.
  • 그러나 여기서 나는 위의 결과를 보고 자바의 참조형도 Call By Reference처럼 실행이 되겠구나 라고 당연하게 생각을 했는데 실제 참조형을 함수의 매개변수로 전달해 바꿔보니 Call By Value 형식으로 작동 되었다.

참조형 타입 Call By Value

class Node {
    int value;

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

public class ReferenceTypeCallByValue {

    private static void swap(Node node1, Node node2) {
        Node temp = node1;
        node1 = node2;
        node2 = temp;
    }

    public static void main(String[] args) {
        Node node1 = new Node(1);
        Node node2 = new Node(2);
        System.out.println("====Before===");
        System.out.printf("node1 value : %d\n", node1.value);
        System.out.printf("node2 value : %d\n", node2.value);
        swap(node1, node2);
        System.out.println("====After===");
        System.out.printf("node1 value : %d\n", node1.value);
        System.out.printf("node2 value : %d\n", node2.value);
    }
}

결과

====Before===
node1 value : 1
node2 value : 2
====After===
node1 value : 1
node2 value : 2
  • 가령 참조형 타입이 Call By Reference 처럼 동작을 한다면 매개변수로 전달된 node1, node2의 필드 값이 바뀌고 각 node1, node2의 값이 2, 1로 찍혀야 하는데 바뀌지 않고 그대로 1, 2로 출력되었다.
  • 이렇듯 자바의 참조형은 Call By Reference가 아니며 기본 Call By Value 형식으로 동작함을 알 수 있다.

Call By Value, Call By Reference을 통한 중요한 자바의 특징


image

  • 자바는 객체(참조 타입)을 메서드로 넘길 때 참조하는 지역변수의 실제 주소를 넘기는 것이 아니라 그 지역변수가 가리키고 있는 힙 영역의 객체를 가리키는 새로운 지역변수를 생성하여 그것을 통하여 같은 객체를 가리키도록 하는 방식임을 알 수 있다.
    • 또한 새로운 지역변수를 생성해 이루어지기 때문에 기본 타입을 매개변수로 받아 값을 바꾼 경우와 참조타입을 매개변수로 받아 값을 변경한 경우 모두 스택에서 변경이 이루어지기 때문에 아무런 의미가 없게 된다.

마무리


  • 기본적으로 자바에서의 메소드 호출은 참조, 기본형 모두 Call By Value을 따르고 있고, 객체의 경우 객체를 가리키는 주소 값을 넘겨 새로운 지역변수를 통해 Call By Value로 객체를 참조해 모든 메소드와 필드를 호출해 실행이 된다.

자바의 메소드(함수) 호출 방식 - Call by Value vs Call by Reference

Comments