[Java] Pass by Value vs. Pass by Reference (Call by Value vs. Call by Reference)
메소드에서 객체를 수정하면 원본도 바뀌는데, 왜 자바는 Pass by Reference가 아닐까요? 많은 개발자가 헷갈리는 '참조값의 복사' 개념에 대해 알아봅시다.
Pass by Value vs. Pass by Reference
프로그래밍에서 메서드에 변수를 넘길 때 값만 복사해서 주는지, 아니면 메모리 주소 자체를 주는지는 매우 중요합니다. 이 전달 방식에 따라 메서드 내부의 변경 사항이 원본 변수에도 반영되는지가 결정되기 때문입니다.
이 두 가지 방식을 각각 Pass by Value(값에 의한 전달)와 Pass by Reference(참조에 의한 전달)라고 부릅니다. 그렇다면 자바는 어느 쪽일까요?
결론부터 말하자면 Java는 오직 ‘값만 복사해서 전달하는’ 방식만 사용합니다.(Pass by Value)
“어? 저는 메소드에서 객체 수정하니까 원본도 바뀌던데요?”라고 반문하실 수 있습니다. 객체를 전달할 때는, 마치 주소를 직접 전달받은 것처럼 원본 데이터가 수정되는 현상이 발생하기 때문입니다.
바로 이 지점이 Pass by Value와 Pass by Reference를 가장 헷갈리게 만드는 부분입니다. 겉보기엔 비슷해 보이지만 내부 원리는 완전히 다른 이 두 개념을 명확히 정리해 보겠습니다.
1. 개념 정리 : 값 복사 vs 주소 전달
- Pass by Value (값에 의한 전달)
- 메서드를 호출할 때 매개변수의 ‘값(Value)’을 복사하여 전달합니다.
- 복사본을 전달하기 때문에 메서드 내부에서 매개변수를 변경하더라도 원본 변수에는 영향을 주지 않습니다.
- 쉽게 말해, 친구에게 내 노트를 빌려주는 대신 복사본을 만들어서 주는 것과 같습니다. 친구가 복사본에 낙서를 해도 내 원본 노트는 깨끗하죠.
- Pass by Reference (참조에 의한 전달)
- 메서드를 호출할 때 매개변수의 ‘메모리 주소(Reference)’ 자체를 전달합니다.
- 메서드 내부에서 이 주소를 통해 원본 메모리에 접근할 수 있으므로, 원본 변수의 값을 직접 변경할 수 있습니다.
- 이는 친구에게 내 노트 원본을 잠시 맡기는 것과 같습니다. 친구가 낙서를 하면 내 노트도 더러워집니다.
2. Java에서의 전달 방식 : 항상 값(Value)만 전달
자바는 매개변수 타입과 무관하게 항상 Pass by Value 방식을 따릅니다. 하지만 매개변수 타입에 따라 ‘실제로 복사되는 값’이 달라집니다. 원시 타입은 ‘실제 값의 복사본’이, 객체 타입은 ‘주소 값의 복사본’이 전달된다는 차이가 있습니다.
원시 타입 (Primitive Type)
int, double, boolean 같은 원시 타입은 변수 안에 실제 값이 들어있습니다.
- 무엇을 넘기나? : 변수가 가지고 있는 ‘실제 값의 복사본’을 넘깁니다.
- 결과 : 메서드 안에서 이 복사된 값을 바꿔도, 원본 변수는 그대로 유지 (전혀 영향 X)
객체 타입 (Reference Type)
Class, Array, String 같은 객체 타입은 변수 안에 객체가 위치한 메모리 주소값(참조값)이 들어있습니다.
- 무엇을 넘기나? : 객체 자체가 아니라, ‘주소값의 복사본’을 넘깁니다.
- 결과 : 주소값을 복사했기 때문에 사용 방식에 따라 결과가 나뉩니다.
- 객체 내부 수정 (
p.set..): 복사된 주소를 통해 힙 영역에 접근하여 값을 바꾸면, 원본 객체의 내용도 함께 변경됩니다. (같은 힙 메모리 공유) - 새로운 객체 할당 (
p = new..): 매개변수에 아예 새로운 객체(주소)를 할당하면, 원본 변수에는 아무런 영향이 없습니다. (연결 끊김)
- 객체 내부 수정 (
(모두 주소값 복사이지만, 사용 방식에 따라 결과가 다름)
"같은 주소를 보고 내용을 바꿈"
"주소를 새로 바꿈 -> 연결 끊김"
위 그림은 객체를 전달했을 때 발생할 수 있는 두 가지 상황을 보여줍니다.
1. Case 1: 객체 내용 변경 (p.setName("B"))
- “리모컨(주소값)을 복사해 줬더니, 그 리모컨으로 채널을 돌려버린 상황”입니다.
- 복사된 참조값(
p)도 원본과 같은 0x100 번지의 객체를 가리키고 있습니다. 따라서p를 통해 객체의 내부 데이터를 수정하면 원본 객체도 영향을 받습니다.
2. Case 2: 새 객체 할당 (p = new Member("C"))
- “복사해 준 리모컨(주소값)을 버리고, 새로 산 TV의 리모컨을 쥔 상황”입니다.
- 매개변수
p에 새로운 객체(0x200)의 주소를 덮어씌웠습니다. 이제p와 원본m은 서로 다른 객체를 가리킵니다. 따라서p가 무슨 짓을 해도 원본m(0x100)은 안전합니다.
3. 원본이 바뀌었는데 왜 Pass by Reference가 아닐까?
: 변수가 가리키는 곳의 차이
“어쨌든 결과적으로, 복사본이 아니라 원본 데이터의 내용이 바뀌었으니 Pass by Reference 아닌가요?”라고 생각할 수 있습니다. 하지만 메모리 관점에서 보면 명확한 차이가 있습니다. 아래 그림에서 “메모리 상에서 화살표가 어디를 향하는지”에 주목해 주세요.
변수가 가리키는 곳(Heap)을 건드리느냐, 변수 그 자체(Stack)를 건드리느냐
"객체의 주소(0x100)만 복사"
"변수 자체의 주소(&m)를 전달"
이 비교의 핵심은 “메소드가 Main 스택의 변수 m을 건드릴 수 있는가?” 입니다.
- Java : 메소드는 변수
m이 어디 있는지 모릅니다.- 단지
m이 가리키는 ‘힙 영역의 객체 주소(0x100)’만 복사 받았습니다. - 따라서 힙에 있는 객체 데이터는 수정할 수 있지만, Main 스택에 있는 변수
m자체(0x100이라는 값)는 절대 건드릴 수 없습니다. (원본 변수 보호)
- 단지
- C언어 : 메소드는 변수
m의 위치를 압니다.- 메소드는 변수
m의 ‘스택 메모리 주소(&m)’를 직접 전달받아,m의 위치(&m)를 알게 되었습니다. - 직접 접근 (* 연산자): 그런데 C언어에는 “그 변수으로 들어가라(*)”는 특수 명령어가 있습니다.
- 따라서 포인터를 통해 Main 스택에 있는
m에 직접 접근(*)해서,m이 가리키는 대상을 아예 다른 것으로 바꿔버릴 수 있습니다. (원본 변수 변경 가능)
- 메소드는 변수
요약하자면, 자바는 원본 변수를 보호하기 위해 항상 값을 복사해서 전달합니다. 객체의 경우 그 ‘값’이 주소값일 뿐입니다. 참조값 자체가 바뀐 것이 아니라, 참조값의 복사본을 통해 가리키는 객체의 ‘내용’이 바뀐 것입니다.
댓글남기기