Java 에서 함수에 인자를 전달하는 방법

2022년 12월 2일 작성

Call by ValueCall by Reference

현재 많은 프로그래밍 언어에서는, 함수에 인자를 전달하는 방법으로

  • 값 자체를 전달 (passing by value)
  • 참조를 전달 (passing by reference)

이 둘을 가장 많이 채택하고 있다.

언어마다 이런 개념을 다르게 사용하고 있는데, 자바에서는 엄격히, 모든 인자 전달이 Pass-by-Value로 이루어진다.

값 전달 vs 참조 전달

일단 하나의 변수를 두 개의 다른 함수에서 어떻게 다룰 수 있는지 얘기해보자. 함수 호출하는 함수(Caller)와 호출되는 함수(Callee)를 구분해서 비교해 볼 것이다.

위처럼, 호출 되는 함수를 callee라고 칭한다

값 전달 (Pass-by-Value)

함수의 파라미터가 값으로 전달되면, Caller와 Callee는 같은 변수라 하더라도 서로 다른 복사본으로 두고 연산한다. 각자가 다루는 변수는, 서로의 값에 영향을 미치지 않는다.

이는 곧, 함수의 인자로 들어가는 값은 원본의 클론이라는 말이다. Callee 함수 안에서 일어나는 어떠한 변경사항도 caller 함수 안의 원본에 영향을 끼지 않는다는 뜻이다.

참조 전달 (Pass-by-Reference)

함수의 매개변수로 참조를 전달할 경우, caller 함수와 callee 함수는 같은 객체를 다루게 된다.

참조를 전달한다는 것은 객체의 식별자를 함수에게 전달한다는 뜻이고, 참조를 통해 원본 변수를 직접 다루게 되니 인자(참조)가 가리키는 객체의 변경은 곧 원본 객체의 변경이라고 할 수 있다.

Java 에서의 인자 전달

자바에서는 원시타입 변수의 경우 실제 값을 저장하는 반면에, 참조타입 변수들은 객체의 주소를 가리키는 참조 변수를 저장한다. 이때, 실제 값과 참조 변수는 모두 스택 메모리에 저장된다.

결론부터 말하자면, 자바에서의 인자는 항상 값으로 전달된다.

메소드 호출 시, 매개변수마다 복사본이 생성된다. 매개변수가 원시타입이든, 아니든지 상관없이 모두 복사본이 생성되어 스택 메모리에 새로 쌓인다. 원시타입의 경우, 스택 메모리에 값 자체를 복사하여 쌓는다. 참조타입 매개변수는 힙메모리에 있는 원본 객체의 주소를 값으로 가진 변수일텐데, 이 변수도 복사본이 새로 생성되어 스택메모리에 쌓인다.

호출된 메소드로는 이렇게 새로 생긴 복사본이 전달된다.

원시타입 전달하기

원시 타입 변수는 스택메모리에 직접 값을 저장한다. 하지만 어떤 원시타입 변수를 매개변수에 넣어도 실제 인자로 넘어가는 것은 그것들의 복사본이고, 그 복사본들은 스택메모리에서, 호출되는 함수의 영역에 쌓이게 된다.

그래서 인자들의 생명주기는 그 인자를 사용하는 함수가 동작하는 동안이라고 할 수 있다. 함수가 반환되고 나면, 이 모든 인자는 스택 메모리에서 사라진다.

public class PrimitivesTest {
 
    @Test
    public void whenModifyingPrimitives_thenOriginalValuesNotModified() {
 
        int x = 1;
        int y = 2;
 
        // 수정 전
        assertEquals(x, 1);
        assertEquals(y, 2);
 
        modify(x, y);
 
        // 수정 후
        assertEquals(x, 1);
        assertEquals(y, 2);
    }
 
    public static void modify(int x1, int y1) {
        x1 = 5;
        y1 = 10;
    }
}

테스트 메서드를 Caller, modify()를 Callee라고 보면 된다. x, y 변수는 처음에 스택의 테스트 메서드 영역에 저장돼있다. 이 두 변수를 modify()의 매개변수로 넘겼을 때, x1과 y1이 스택의 modify() 영역에 새로 쌓이게 된다.

객체의 참조를 전달하기

자바에서는 모든 객체가 힙 영역에 동적으로 저장되고 이 객체들은 참조 변수들을 통해 접근할 수 있다.

원시 값들과 달리, 자바의 객체는 두 단계를 거쳐 저장된다. 참조 변수는 스택메모리에 저장되고, 그들이 가리키는 객체는 힙메모리에 저장된다.

한 객체가 함수의 매개변수로 전달되면, 정확히, 참조 변수의 복사본이 만들어지고 그것이 인자로 함수에서 쓰인다.

이때, 복사본이 가지는 값은 원래 참조변수의 값(힙메모리 주소)과 같고, 가리키는 객체는 변하지 않는 것이 되는 것이다. 따라서 새로운 메서드 안에서 객체를 변경했을 때, 힙 메모리에 있는 원본 객체에 변경을 주는 것이 되고 함수가 반환되고 나서도 그 변경사항은 유지된다.

하지만 우리가 인자로 넣어준 새로운 참조변수에 새로운 객체를 할당하면 이야기가 달라진다

예제 코드를 보자.

public class NonPrimitiveTest {
 
    @Test
    @DisplayName("객체를 수정하면 원본도 바뀐다.")
    public void whenModifyingObjects_thenOriginalObjectChanged() {
        Clazz a = new Clazz(1);
        Clazz b = new Clazz(1);
 
        // 수정 전
        assertEquals(a.member, 1);
        assertEquals(b.member, 1);
 
        modify(a, b);
 
        // 수정 후
        assertEquals(a.member, 2);
        assertEquals(b.member, 1);
    }
 
    public static void modify(Clazz a1, Clazz b1) {
        a1.member++;
 
        b1 = new Clazz(1);
        b1.member++;
    }
 
}
 
class Clazz {
    public int member;
 
    public Clazz(int num) {
        this.member = num;
    }
}

modify() 메서드에서, a와 b의 멤버변수를 변경하는데 추가로, b의 경우에는 새로 생성한 객체의 주소를 가지도록 변경한다.

따라서 b1은 새로 생성된 객체를 가리키게 되고, 해당 객체의 멤버변수가 수정된다.

변수 b가 가리키는 원본 객체에 변화가 없는 것은 당연해진다.

이렇게 modify()가 종료되면 스택에 쌓여있던 b1 변수는 사라질 것이다. 그럼 우리가 생성했던 객체는 GC에 의해 사라지게 된다.

결론

아무튼, 자바에서는 모든 매개변수의 인자가 값으로 전달된다. 객체의 경우 헷갈릴 수 있지만,

우리가 참조변수에 다른 객체를 할당한다고 해서 원래 참조 변수가 가리키는 주소의 데이터가 변하는 Pass-by-Reference 가 아니다.

참조 변수는 변수이다, 말 그대로 값이 변할 수 있는 아이고 새로운 객체를 할당하면 다른 주소를 담게 되는 그릇이다

인자로써의 참조변수는 값(주소)를 전달하는 복사된 변수라는 것을 기억하자


Reference

Pass-By-Value as a Parameter Passing Mechanism in Java Java 의 Call by Value, Call by Reference