개발자로 후회없는 삶 살기

[문법] 자바는 Call By Reference를 지원하지 않는다. 본문

[백엔드]/[이슈해결]

[문법] 자바는 Call By Reference를 지원하지 않는다.

몽이장쥰 2025. 8. 17. 19:43

🚨 서론 (문제 상황)

우아코스 다리 건너기 게임을 연습하던 도중 의도한 것과 다른 동작이 일어나서 해결해 보고자 한다.

 

@Test
@DisplayName("O / X 표시를 추가하고 결과를 출력한다.")
void setUP() {

    // given
    BridgeLine bridgeLine = BridgeLine.setUpBridgeStatusMarkLine();
    Integer count = 0;

    // when // then
    String successMark1 = " O ";
    bridgeLine.addMark(successMark1);
    String expected = makeResult(count, successMark1);
    System.out.println("expected = " + expected);
    assertThat(bridgeLine.toString()).isEqualTo(expected);

    String successMark2 = " X ";
    bridgeLine.addMark(successMark2);
    String expected2 = makeResult(count, successMark2);
    System.out.println("expected = " + expected2);
    assertThat(bridgeLine.toString()).isEqualTo(expected2);
}

private String makeResult(Integer count, String mark) {

    StringBuilder expected = new StringBuilder("[ ");
    String suffix = " ]";
    count ++;
    System.out.println("count = " + count);

    if (count.equals(1)) {
        return expected.append(mark)
                .append(suffix)
                .toString();
    }

    for (int i = 0; i < count; i++) {
        expected.append(mark)
                .append("|");
    }

    return expected.append(suffix)
            .toString();
}

다리에 O/X가 정상적으로 추가되는지 확인하기 위해서 count만큼 mark를 추가하는 makeResult 함수를 만들었다.

 

위 코드에서 의도한 바는 makeResult의 인자로 넣은 count 값이 ++ 연산자로 증가하면서 expected의 길이가 길어지는 것이었지만, count ++을 해도 원본 count에 변화가 없어서 makeResult의 count 파라미터가 [1]로 유지되는 문제가 발생했다.

 

본론

- 메서드 호출 시 파라미터 전달 방법

메서드를 호출할 때, 파라미터를 전달하는 방법은 Call by Value와 Call by Reference 2가지가 있다.

 

1. Call by Value

int argsNumber = 1;

void method (int paramNumber) {}

method(argsNumber);

파라미터에 인자의 값을 넘겨주는 방식으로, 원본 값을 복사해서 전달한다. 원본 데이터가 가지고 있는 실제 데이터를 전달하는 방식이다. 위 예제에서 argsNumber와 paramNumber는 서로 다른 변수이며, paramNumber에 args의 값이 복사되어 전달된다.

 

2. Call by Reference

실제 참조를 넘겨서, 대상 변수와 동일시한다. 파라미터 변수와 인자 변수가 이름만 다를 뿐 같은 하나의 변수이다.

C++에서 지원하는 방식으로, 따라서 메서드 내에서 파라미터 변수를 수정하면 함수 밖, 원본 변수에도 반영된다.

 

- Java에서 파라미터 전달 방법

Java는 오직 Call by Value 방식만 제공한다. C언어도 자바와 동일하게 동작한다.

 

-> JVM의 변수 저장 방식

스택
primitive type = 값과 변수를 함께 저장  
참조 타입 = 변수에 실제 객체의 주소를 저장 참조 타입 = 실제 객체

참조 타입은 Stack에 변수를 생성하고 변수에 실제 객체의 주소를 저장하여, 힙의 객체를 바라보고 있다.

 

-> Primitive Type 전달

@Test
@DisplayName("O / X 표시를 추가하고 결과를 출력한다.")
void setUP() {

    // given
    BridgeLine bridgeLine = BridgeLine.setUpBridgeStatusMarkLine();
    Integer count = 0;

    // when // then
    String successMark1 = " O ";
    bridgeLine.addMark(successMark1);
    String expected = makeResult(count, successMark1);
    System.out.println("expected = " + expected);
    assertThat(bridgeLine.toString()).isEqualTo(expected);

    String successMark2 = " X ";
    bridgeLine.addMark(successMark2);
    String expected2 = makeResult(count, successMark2);
    System.out.println("expected = " + expected2);
    assertThat(bridgeLine.toString()).isEqualTo(expected2);
}

private String makeResult(Integer count, String mark) {

    StringBuilder expected = new StringBuilder("[ ");
    String suffix = " ]";
    count ++;
    System.out.println("count = " + count);

    if (count.equals(1)) {
        return expected.append(mark)
                .append(suffix)
                .toString();
    }

    for (int i = 0; i < count; i++) {
        expected.append(mark)
                .append("|");
    }

    return expected.append(suffix)
            .toString();
}

Call by Value 방식으로 전달되면 파라미터는 새로운 변수로 Stack에 새롭게 생성된다. 즉 setUp 함수의 count와 makeResult 함수의 count는 아예 다르다.

 

1) 스택 : setUp 함수의 count 변수와 값 함께 저장
2) 스택 : makeResult 함수의 count 변수에 setUp 함수의 값을 복사하여 함께 저장 <- Stack에 총 2개의 변수와 2개의 값
3) makeResult의 count ++ : setUP 함수의 count에는 영향 X

프로세스를 따르면 makeResult의 count 변수의 값이 증가하며, 원본 setUp의 변수는 증가하지 않는다.

 

-> Reference Type 전달

참조 타입은 원시 타입과는 조금 다르다. 변수는 stack에 생성되지만 힙의 실제 객체를 바라보고 있다. 하지만, Primitive 타입과 동일하게 Stack에 있는 변수와 값을 똑같이 복사하여 새로 만드는 것은 같다.

 

@Test
    @DisplayName("O / X 표시를 추가하고 결과를 출력한다.")
    void setUP() {
        // given
        BridgeLine bridgeLine = BridgeLine.setUpBridgeStatusMarkLine();
        int[] count = { 0 };
        List<String> marks = new ArrayList<>();

        // when // then
        marks.add("O");
        bridgeLine.addMark(marks.get(0));
        String expected = makeResult(count, marks);
        System.out.println("expected = " + expected);
        assertThat(bridgeLine.toString()).isEqualTo(expected);

        marks.add("X");
        bridgeLine.addMark(marks.get(1));
        String expected2 = makeResult(count, marks);
        System.out.println("expected = " + expected2);
        assertThat(bridgeLine.toString()).isEqualTo(expected2);
    }

    private String makeResult(int[] count, List<String> mark) {
        StringBuilder expected = new StringBuilder("[ ");
        String suffix = " ]";
        count[0] += 1;

        System.out.println("count = " + count[0]);

        if (count[0] == 1) {
            return expected.append(mark.get(0))
                    .append(suffix)
                    .toString();
        }

        for (int i = 0; i < count[0]; i++) {
            if(i == count[0] - 1) {
                expected.append(mark.get(i));
            }
            else {
                expected.append(mark.get(i))
                        .append(" | ");
            }
        }
        return expected.append(suffix)
                .toString();
    }

참조 타입으로 알아보기 위해서 count 배열을 선언했다.

 

1) 스택 : setUp의 count 배열 변수 생성 / 힙 : count 배열 객체 생성
2) 스택 : makeResult count 변수 생성 후 setUp count의 값 복사 <- 이때 값이 count 객체 주소라서 2개의 변수가 1개의 객체를 주시

3) makeResult의 count ++ : count가 바라보는 객체의 값 증가 <- setUp count와 동일한 객체를 주시하므로 영향 O

참조 타입을 인자로 전달할 경우, stack에 변수가 생성되는 것과 값이 복사되는 것은 동일 하지만, 변수의 값이 실제 주소값이라서 동일한 객체를 바라보게 된다. 참조를 직접 전달한 것이 아니라, Call by Value 방식으로 값을 전달한 것인데 그 값이 주소값이었을 뿐인 것이다.

 

- Call by Reference와의 차이

주소값을 주었고, 원본에 영향을 주니 Call by Reference와 같아서 혼란이 있을 수 있다. 하지만, 앞서 말한 것 처럼 값을 전달한 것인데 그 값이 주소값인 것이다. 참조를 직접 전달하게 되면 원본 변수와 완벽히 같은 것을 바라보지만 이름만 다른 변수가 생성되고, 따라서 파라미터 변수에 새로운 객체를 대입했을 때 원본 변수가 바라보는 객체도 변경된다.

 

1) 자바 참조 타입 전달 (c언어도 동일)

int main() {
	
    Person p0 = new Person();
    test(p0);
}

void test(Person p1) {
	p1 = new Person();
}
  • 인자로 참조 타입을 넣으면, 스택에 새로운 변수 생성되며, 생성된 변수에 원본 변수의 값인 참조 타입의 주소 값을 대입
  • 변수는 2개이고 참조 타입은 1개
  • 위 예시에서 p0와 p1이 다른 변수이며, p1에 새로운 객체를 대입하면 2개의 변수, 2개의 객체로 둘은 다른 객체를 바라봄

 

2) 실제 Call by Reference

  • 파라미터에 참조를 넣어도 새로운 변수 생성 X
  • 대신 원본 변수 자체에 파라미터 변수로 추가 이름을 붙임
  • 1개의 변수에 이름표가 2개인 상태로 2개의 이름 중 어떤 이름을 부르든 같은 변수를 바라보는 상태라서 모든 변경이 원본에 직접 적용
  • 위 예시에서 p0, p1은 같은데 이름이 다른 거라서, 최종적으로 p0도 새로운 Person이 할당

장 중요한 것은 인자로 대입했을 때, 인자와 파라미터 변수는 스택에 따로 생성되는 다른 변수인 것이다.

 

- static과 연결

만약 count를 static 변수로 하면 될까? 이것은 Call by Value를 생각하면 쉽게 안 된다는 것을 알 수 있다. static은 JVM 메모리 할당 관점에서 메서드 영역에 1개 생기고 여러 객체가 공유해서 사용한다고 보면 이해가 쉽다. 즉 static을 붙이더라도 파라미터로 받은 변수는 stack에 새롭게 생성되는 변수라서 원본 static 변수에 영향이 없다.

Comments