메서드 호출과 값 전달
메서드를 호출하면 인수의 값이 매개변수로 복사된다는 규칙으로, 메서드 안의 필드 변경은 호출자에게 보이고 매개변수 재대입은 보이지 않는 이유를 이해합니다.
회원 객체를 메서드에 넘기고 그 안에서 target.active = true를 실행하면, 메서드가 끝난 뒤 호출한 쪽에서도 active가 true로 바뀌어 있습니다. 그런데 같은 메서드 안에서 target = new Member()로 새 회원을 대입하면, 호출한 쪽의 변수는 여전히 원래 회원을 가리킵니다. 둘 다 '메서드 안에서 객체를 바꾼' 것처럼 보이는데 결과는 정반대입니다.
흔히 듣는 "기본형은 값 전달, 객체는 참조 전달"이라는 요약으로는 두 번째 결과를 설명할 수 없습니다. 메서드가 호출자의 변수를 직접 다룬다면 재대입도 호출자에게 보여야 하기 때문입니다. 실제 규칙은 앞 글에서 본 대입 규칙과 같습니다. 메서드를 호출하면 인수(argument)의 값이 매개변수(parameter)라는 새 지역 변수에 복사됩니다. 기본형이면 숫자나 true·false 같은 값 자체가 복사되고, 참조형이면 객체에 도달하는 참조값이 복사됩니다.
3장의 메서드와 매개변수에서 배열을 인수로 넘겼을 때 원소 변경이 호출자에게 보였던 것도 같은 이유였습니다. 이 글에서는 이 규칙 하나로 앞의 두 결과를 모두 설명하고, 같은 규칙을 이용해 객체를 채우는 메서드와 객체를 만들어 돌려주는 메서드를 작성합니다.
기본형 인수: 값이 복사된다
다음 프로그램은 나이에 10을 더하는 메서드를 호출합니다. 실행하기 전에 두 줄이 각각 무엇을 출력할지 먼저 예측해 보세요.
public class PrimitiveParameter {
public static void main(String[] args) {
int age = 30;
increase(age);
System.out.println("main=" + age);
}
static void increase(int value) {
value += 10;
System.out.println("method=" + value);
}
}method=40
main=30increase(age)를 실행하는 순간 increase를 위한 새 지역 변수 value가 만들어지고, 그 안에 age에서 읽은 값 30이 복사됩니다. 그래서 메서드 안의 value += 10은 value만 40으로 바꾸고, 첫 줄에 method=40이 출력됩니다. 메서드가 끝나면 value는 사라집니다. main의 age에는 그동안 아무것도 대입되지 않았으므로 둘째 줄은 main=30입니다.
| 시점 | main의 age | increase의 value |
|---|---|---|
| 호출 직전 | 30 | 아직 없음 |
| 호출 직후 | 30 | 30 (복사됨) |
value += 10 뒤 | 30 | 40 |
| 메서드가 끝난 뒤 | 30 | 사라짐 |
매개변수 이름을 age로 바꿔도 결과는 같습니다. increase 안의 age는 main의 age와 이름만 같은 별개의 지역 변수이기 때문입니다. 두 변수를 이어 주는 것은 호출하는 순간 한 번 일어나는 값 복사뿐입니다.
호출한 쪽의 값을 바꾸고 싶다면 메서드가 새 값을 돌려주고, 호출한 쪽이 그 값을 직접 대입해야 합니다. 다음은 increase를 값을 돌려주는 메서드로 바꾼 버전입니다.
public class ReturnParameter {
public static void main(String[] args) {
int age = 30;
age = increased(age);
System.out.println("main=" + age);
}
static int increased(int value) {
return value + 10;
}
}main=40이번에도 value에는 30이 복사됩니다. increased는 value를 바꾸지 않고 value + 10을 계산해 40을 돌려주는데, 반환값 역시 호출한 자리로 복사되는 값입니다. 따라서 age가 40이 되는 순간은 메서드 안이 아니라 main의 age = increased(age) 대입문이 실행될 때입니다. 아래 도표는 두 방식을 같은 출발점에서 나란히 비교합니다.
increase의 지역 값 변경과 increased의 반환값 대입
void 호출은 복사한 value만 30→40으로 바꾸고 반환값 재대입 버전은 호출자 age를 40으로 만듭니다. 두 독립 호출의 지역값·반환·호출자 상태를 대응 단계로 보여 줄 수 있습니다.
두 경우 모두 호출자의 age가 30인 상태에서 독립적으로 시작합니다. void increase는 지역 변수 value만 40으로 바꾸고 호출자의 age는 30을 유지합니다. int increased는 value + 10의 계산 결과 40을 반환하고, age = increased(age)의 대입이 호출자의 age를 40으로 바꿉니다.
호출자의 값은 반환 뒤 재대입해야 바뀝니다
같은 숫자 30을 매개변수에 복사합니다. void 메서드의 지역 변경과 int 메서드의 반환값 대입을 비교합니다.
두 경우 모두 age = 30에서 독립적으로 시작합니다.
| 비교 지점 | void increase(int value) | int increased(int value) |
|---|---|---|
| 호출문 | increase(age); | age = increased(age); |
| 호출 시 복사 | age = 30에서 value = 30 | age = 30에서 value = 30 |
| 메서드 안 | value += 10;으로 지역 값이 40 | return value + 10;의 계산 결과가 40 |
| 반환값 | 없음 · void | 40 · int |
| 호출문 실행 후 | age = 30 유지 | 반환값을 다시 대입해 age = 40 |
- PrimitiveParameter · void increase
increase(age);- 호출 시 숫자
30을 지역 변수value에 복사합니다. value += 10;은value만40으로 바꿉니다.- 반환값은 없고 호출자의
age는30입니다. - 본문 대안 조각 · int increased
age = increased(age);- 호출 시 숫자
30을 지역 변수value에 복사합니다. return value + 10;이 계산한40을 반환합니다.- 호출문이 그 반환값을 다시 대입하므로
age는40입니다.
increased는 value 자체를 재대입하지 않습니다. 호출자의 age가 바뀌는 지점은 반환값을 받는 age = … 대입입니다.
이 예의 조건
- 두 사례는 age 30에서 독립적으로 시작합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 두 사례는 독립적으로 age 30에서 시작한다. increase는 매개변수만 40으로 바꾸고 호출자는 30을 유지한다. increased의 value는 30 그대로이며 value+10의 결과 40을 반환한 후 호출자 대입으로 age 40이 된다. 점선은 값 복사이고 실선은 호출 복귀이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 두 사례는 독립적으로 age 30에서 시작한다. increase는 매개변수만 40으로 바꾸고 호출자는 30을 유지한다. increased의 value는 30 그대로이며 value+10의 결과 40을 반환한 후 호출자 대입으로 age 40이 된다. 점선은 값 복사이고 실선은 호출 복귀이다. |
| 전체 읽기 정보 | increase의 지역 값 변경과 increased의 반환값 대입 increase의 지역 값 변경과 increased의 반환값 대입 두 경우 모두 호출자의 age가 30인 상태에서 독립적으로 시작합니다. void increase는 지역 변수 value만 40으로 바꾸고 호출자의 age는 30을 유지합니다. int increased는 value + 10의 계산 결과 40을 반환하고, age = increased(age)의 대입이 호출자의 age를 40으로 바꿉니다. 호출자의 값은 반환 뒤 재대입해야 바뀝니다 같은 숫자 30을 매개변수에 복사합니다. void 메서드의 지역 변경과 int 메서드의 반환값 대입을 비교합니다. 두 경우 모두 age = 30 에서 독립적으로 시작합니다. 왼쪽은 PrimitiveParameter 원문, 오른쪽은 본문의 반환값 대안 조각 비교 지점 void increase(int value) int increased(int value) 호출문 increase(age); age = increased(age); 호출 시 복사 age = 30 에서 value = 30 age = 30 에서 value = 30 메서드 안 value += 10; 으로 지역 값이 40 return value + 10; 의 계산 결과가 40 반환값 없음 · void 40 · int 호출문 실행 후 age = 30 유지 반환값을 다시 대입해 age = 40 PrimitiveParameter · void increase increase(age); 호출 시 숫자 30 을 지역 변수 value 에 복사합니다. value += 10; 은 value 만 40 으로 바꿉니다. 반환값은 없고 호출자의 age 는 30 입니다. 본문 대안 조각 · int increased age = increased(age); 호출 시 숫자 30 을 지역 변수 value 에 복사합니다. return value + 10; 이 계산한 40 을 반환합니다. 호출문이 그 반환값을 다시 대입하므로 age 는 40 입니다. increased 는 value 자체를 재대입하지 않습니다. 호출자의 age 가 바뀌는 지점은 반환값을 받는 age = … 대입입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 인자 값 복사 두 메서드의 value에 숫자 30을 복사합니다. |
|
|
| 메서드 내부 increase는 value를 40으로 바꾸고, increased는 value 30에서 40을 계산합니다. |
|
|
| 복귀와 채택 void 호출자의 age는 30, 반환값 대입 호출자는 40입니다. |
|
|
increase의 지역 값 변경과 increased의 반환값 대입
void 호출은 복사한 value만 30→40으로 바꾸고 반환값 재대입 버전은 호출자 age를 40으로 만듭니다. 두 독립 호출의 지역값·반환·호출자 상태를 대응 단계로 보여 줄 수 있습니다.
두 경우 모두 호출자의 age가 30인 상태에서 독립적으로 시작합니다. void increase는 지역 변수 value만 40으로 바꾸고 호출자의 age는 30을 유지합니다. int increased는 value + 10의 계산 결과 40을 반환하고, age = increased(age)의 대입이 호출자의 age를 40으로 바꿉니다.
호출자의 값은 반환 뒤 재대입해야 바뀝니다
같은 숫자 30을 매개변수에 복사합니다. void 메서드의 지역 변경과 int 메서드의 반환값 대입을 비교합니다.
두 경우 모두 age = 30에서 독립적으로 시작합니다.
| 비교 지점 | void increase(int value) | int increased(int value) |
|---|---|---|
| 호출문 | increase(age); | age = increased(age); |
| 호출 시 복사 | age = 30에서 value = 30 | age = 30에서 value = 30 |
| 메서드 안 | value += 10;으로 지역 값이 40 | return value + 10;의 계산 결과가 40 |
| 반환값 | 없음 · void | 40 · int |
| 호출문 실행 후 | age = 30 유지 | 반환값을 다시 대입해 age = 40 |
- PrimitiveParameter · void increase
increase(age);- 호출 시 숫자
30을 지역 변수value에 복사합니다. value += 10;은value만40으로 바꿉니다.- 반환값은 없고 호출자의
age는30입니다. - 본문 대안 조각 · int increased
age = increased(age);- 호출 시 숫자
30을 지역 변수value에 복사합니다. return value + 10;이 계산한40을 반환합니다.- 호출문이 그 반환값을 다시 대입하므로
age는40입니다.
increased는 value 자체를 재대입하지 않습니다. 호출자의 age가 바뀌는 지점은 반환값을 받는 age = … 대입입니다.
이 예의 조건
- 두 사례는 age 30에서 독립적으로 시작합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | 두 사례는 독립적으로 age 30에서 시작한다. increase는 매개변수만 40으로 바꾸고 호출자는 30을 유지한다. increased의 value는 30 그대로이며 value+10의 결과 40을 반환한 후 호출자 대입으로 age 40이 된다. 점선은 값 복사이고 실선은 호출 복귀이다. |
| 구분 | 내용 |
|---|---|
| 설명 | 두 사례는 독립적으로 age 30에서 시작한다. increase는 매개변수만 40으로 바꾸고 호출자는 30을 유지한다. increased의 value는 30 그대로이며 value+10의 결과 40을 반환한 후 호출자 대입으로 age 40이 된다. 점선은 값 복사이고 실선은 호출 복귀이다. |
| 전체 읽기 정보 | increase의 지역 값 변경과 increased의 반환값 대입 increase의 지역 값 변경과 increased의 반환값 대입 두 경우 모두 호출자의 age가 30인 상태에서 독립적으로 시작합니다. void increase는 지역 변수 value만 40으로 바꾸고 호출자의 age는 30을 유지합니다. int increased는 value + 10의 계산 결과 40을 반환하고, age = increased(age)의 대입이 호출자의 age를 40으로 바꿉니다. 호출자의 값은 반환 뒤 재대입해야 바뀝니다 같은 숫자 30을 매개변수에 복사합니다. void 메서드의 지역 변경과 int 메서드의 반환값 대입을 비교합니다. 두 경우 모두 age = 30 에서 독립적으로 시작합니다. 왼쪽은 PrimitiveParameter 원문, 오른쪽은 본문의 반환값 대안 조각 비교 지점 void increase(int value) int increased(int value) 호출문 increase(age); age = increased(age); 호출 시 복사 age = 30 에서 value = 30 age = 30 에서 value = 30 메서드 안 value += 10; 으로 지역 값이 40 return value + 10; 의 계산 결과가 40 반환값 없음 · void 40 · int 호출문 실행 후 age = 30 유지 반환값을 다시 대입해 age = 40 PrimitiveParameter · void increase increase(age); 호출 시 숫자 30 을 지역 변수 value 에 복사합니다. value += 10; 은 value 만 40 으로 바꿉니다. 반환값은 없고 호출자의 age 는 30 입니다. 본문 대안 조각 · int increased age = increased(age); 호출 시 숫자 30 을 지역 변수 value 에 복사합니다. return value + 10; 이 계산한 40 을 반환합니다. 호출문이 그 반환값을 다시 대입하므로 age 는 40 입니다. increased 는 value 자체를 재대입하지 않습니다. 호출자의 age 가 바뀌는 지점은 반환값을 받는 age = … 대입입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 인자 값 복사 두 메서드의 value에 숫자 30을 복사합니다. |
|
|
| 메서드 내부 increase는 value를 40으로 바꾸고, increased는 value 30에서 40을 계산합니다. |
|
|
| 복귀와 채택 void 호출자의 age는 30, 반환값 대입 호출자는 40입니다. |
|
|
참조형 인수: 참조값이 복사된다
이번에는 Member를 넘깁니다. 이 글의 참조형 예제는 클래스와 인스턴스에서 만든 필드 세 개(email, age, active)짜리 Member를 사용합니다. 그 글에서 한 것처럼 예제 디렉터리에 Member.java를 함께 두고, 실행할 클래스와 같이 컴파일하세요.
javac -d out src/members/ch04/v7/Member.java src/members/ch04/v7/ReferenceParameter.java
java -cp out ReferenceParameterpublic class ReferenceParameter {
public static void main(String[] args) {
Member member = new Member();
member.email = "[email protected]";
member.age = 30;
member.active = false;
activate(member);
System.out.println(member.email);
System.out.println(member.active);
}
static void activate(Member target) {
target.active = true;
}
}[email protected]
true전달 규칙은 기본형과 똑같고, member 변수에 들어 있는 값이 객체 자체가 아니라 참조값이라는 점만 다릅니다. 앞에서처럼 참조값을 x101 같은 기호로 나타내겠습니다. member에 x101이 들어 있다면 activate(member)를 호출할 때 x101이 매개변수 target에 복사됩니다. 이때 Member 객체가 하나 더 생기지는 않습니다. 변수는 두 개지만 둘 다 같은 x101 객체에 도달합니다.
target.active = true는 target 변수를 바꾸는 문장이 아닙니다. target에 든 참조를 따라가 x101 객체의 active 필드에 true를 씁니다. 메서드가 끝나 target이 사라져도 객체의 필드에 쓴 값은 남아 있으므로, main이 같은 객체를 member.active로 읽으면 true가 나옵니다. email은 아무도 바꾸지 않았으니 첫 줄에 그대로 출력됩니다.
| 시점 | member | target | x101 객체의 active |
|---|---|---|---|
| 호출 직전 | x101 | 아직 없음 | false |
| 호출 직후 | x101 | x101 (복사됨) | false |
target.active = true 뒤 | x101 | x101 | true |
| 메서드가 끝난 뒤 | x101 | 사라짐 | true |
매개변수에 새 객체를 대입하면
참조값이 복사된다는 사실은 메서드 안에서 매개변수에 다른 객체를 대입해 보면 분명하게 드러납니다. 다음 replace는 넘겨받은 매개변수에 새 회원 객체를 대입하고 그 객체의 이메일을 바꿉니다.
public class ReassignParameter {
public static void main(String[] args) {
Member member = new Member();
member.email = "[email protected]";
replace(member);
System.out.println(member.email);
}
static void replace(Member target) {
target = new Member();
target.email = "[email protected]";
}
}replace가 시작될 때는 member와 target이 모두 x101을 가리킵니다. 그다음 target = new Member()가 새 객체 x202를 만들고 그 참조값을 지역 변수 target에 넣습니다. 바뀐 것은 target이라는 변수의 값뿐이고, main의 member에는 아무것도 대입하지 않았습니다. 그래서 이어지는 target.email = ...은 x202의 필드를 바꾸고, x101은 손대지 않은 채로 남습니다.
| 시점 | member | target | 객체 상태 |
|---|---|---|---|
| 호출 직후 | x101 | x101 | x101의 email은 [email protected] |
target = new Member() 뒤 | x101 | x202 | x202가 새로 생김 |
target.email = ... 뒤 | x101 | x202 | x202의 email은 [email protected] |
| 메서드가 끝난 뒤 | x101 | 사라짐 | x202를 가리키는 변수가 없음 |
메서드가 끝나면 target이 사라지므로 x202를 가리키는 변수는 하나도 남지 않습니다. 이렇게 어떤 경로로도 도달할 수 없게 된 객체는 가비지 컬렉터(garbage collector)가 메모리를 회수할 수 있는 대상이 되는데, 이 과정은 다음 글에서 자세히 다룹니다.
정리하면 메서드는 넘겨받은 참조로 공유하는 객체의 필드를 바꿀 수는 있지만, 호출자의 변수에 다른 참조를 넣을 수는 없습니다. 아래 도표에서 두 프로그램의 차이를 한 번 더 비교해 보세요. 도표의 A는 필드를 바꾼 ReferenceParameter이고, B는 매개변수를 재대입한 ReassignParameter입니다.
공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계
activate는 같은 객체의 필드를 바꾸고 replace는 매개변수 target만 새 객체로 바꿉니다. 호출자 참조가 유지되는 두 변경 대상의 전후를 비교합니다.
ReferenceParameter와 ReassignParameter는 별도 프로그램입니다. activate의 target은 호출자의 member와 같은 객체를 참조해 active를 true로 바꿉니다. replace에서는 target만 새 객체를 가리키며, 호출자의 member는 기존 객체를 계속 가리킵니다.
공유 객체 변경과 매개변수 재대입을 구별합니다
둘 다 참조값을 지역 매개변수로 복사합니다. 필드 변경은 공유 객체에 보이고, 매개변수 재대입은 호출자의 변수에 대입하지 않습니다.
서로 다른 두 프로그램입니다. 둘 다 호출 시 member의 참조값을 지역 매개변수 target에 복사합니다.
ReferenceParameter · target.active = true; 직후, 반환 전
- member와 target의 도착지는 같은 객체
email = "[email protected]",age = 30target.active = true;가 그 객체의 필드를 바꿉니다.
반환 뒤 member.active로 읽어도 true입니다. target의 참조값은 바꾸지 않았습니다.
ReassignParameter · target.email = "[email protected]"; 직후, 반환 전
- target · replace가 만든 새 객체
target = new Member();로 매개변수만 재대입합니다.- 그 새 객체의
email은"[email protected]"입니다.
두 객체의 이메일 필드만 표시했습니다. 반환 뒤 호출자 출력은 [email protected]입니다. target의 수명이 끝나고 다른 참조가 없다면 새 객체에는 도달할 수 없으며, 이것이 즉시 회수를 뜻하지는 않습니다.
이 예의 조건
- activate 매개변수는 객체 참조값 복사입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계 공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계 ReferenceParameter와 ReassignParameter는 별도 프로그램입니다. activate의 target은 호출자의 member와 같은 객체를 참조해 active를 true로 바꿉니다. replace에서는 target만 새 객체를 가리키며, 호출자의 member는 기존 객체를 계속 가리킵니다. 공유 객체 변경과 매개변수 재대입을 구별합니다 둘 다 참조값을 지역 매개변수로 복사합니다. 필드 변경은 공유 객체에 보이고, 매개변수 재대입은 호출자의 변수에 대입하지 않습니다. 서로 다른 두 프로그램입니다. 둘 다 호출 시 member 의 참조값을 지역 매개변수 target 에 복사합니다. ReferenceParameter · target.active = true; 직후, 반환 전 member와 target의 도착지는 같은 객체 email = "[email protected]" , age = 30 target.active = true; 가 그 객체의 필드를 바꿉니다. 반환 뒤 member.active 로 읽어도 true 입니다. target 의 참조값은 바꾸지 않았습니다. ReassignParameter · target.email = "[email protected]"; 직후, 반환 전 member · 호출자가 만든 객체 email = "[email protected]" 호출자의 변수에는 새 참조를 대입하지 않았습니다. target · replace가 만든 새 객체 target = new Member(); 로 매개변수만 재대입합니다. 그 새 객체의 email 은 "[email protected]" 입니다. 두 객체의 이메일 필드만 표시했습니다. 반환 뒤 호출자 출력은 [email protected] 입니다. target 의 수명이 끝나고 다른 참조가 없다면 새 객체에는 도달할 수 없으며, 이것이 즉시 회수를 뜻하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호출자 객체 member가 [email protected], age 30 객체를 참조합니다. |
|
|
| 매개변수 경로 target이 호출자와 같은 객체를 가리킵니다. |
|
|
| 필드 쓰기 target.active=true를 저장합니다. |
|
|
공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계
activate는 같은 객체의 필드를 바꾸고 replace는 매개변수 target만 새 객체로 바꿉니다. 호출자 참조가 유지되는 두 변경 대상의 전후를 비교합니다.
ReferenceParameter와 ReassignParameter는 별도 프로그램입니다. activate의 target은 호출자의 member와 같은 객체를 참조해 active를 true로 바꿉니다. replace에서는 target만 새 객체를 가리키며, 호출자의 member는 기존 객체를 계속 가리킵니다.
공유 객체 변경과 매개변수 재대입을 구별합니다
둘 다 참조값을 지역 매개변수로 복사합니다. 필드 변경은 공유 객체에 보이고, 매개변수 재대입은 호출자의 변수에 대입하지 않습니다.
서로 다른 두 프로그램입니다. 둘 다 호출 시 member의 참조값을 지역 매개변수 target에 복사합니다.
ReferenceParameter · target.active = true; 직후, 반환 전
- member와 target의 도착지는 같은 객체
email = "[email protected]",age = 30target.active = true;가 그 객체의 필드를 바꿉니다.
반환 뒤 member.active로 읽어도 true입니다. target의 참조값은 바꾸지 않았습니다.
ReassignParameter · target.email = "[email protected]"; 직후, 반환 전
- target · replace가 만든 새 객체
target = new Member();로 매개변수만 재대입합니다.- 그 새 객체의
email은"[email protected]"입니다.
두 객체의 이메일 필드만 표시했습니다. 반환 뒤 호출자 출력은 [email protected]입니다. target의 수명이 끝나고 다른 참조가 없다면 새 객체에는 도달할 수 없으며, 이것이 즉시 회수를 뜻하지는 않습니다.
이 예의 조건
- activate 매개변수는 객체 참조값 복사입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계 공유 필드 수정과 지역 매개변수 재대입의 다른 참조 관계 ReferenceParameter와 ReassignParameter는 별도 프로그램입니다. activate의 target은 호출자의 member와 같은 객체를 참조해 active를 true로 바꿉니다. replace에서는 target만 새 객체를 가리키며, 호출자의 member는 기존 객체를 계속 가리킵니다. 공유 객체 변경과 매개변수 재대입을 구별합니다 둘 다 참조값을 지역 매개변수로 복사합니다. 필드 변경은 공유 객체에 보이고, 매개변수 재대입은 호출자의 변수에 대입하지 않습니다. 서로 다른 두 프로그램입니다. 둘 다 호출 시 member 의 참조값을 지역 매개변수 target 에 복사합니다. ReferenceParameter · target.active = true; 직후, 반환 전 member와 target의 도착지는 같은 객체 email = "[email protected]" , age = 30 target.active = true; 가 그 객체의 필드를 바꿉니다. 반환 뒤 member.active 로 읽어도 true 입니다. target 의 참조값은 바꾸지 않았습니다. ReassignParameter · target.email = "[email protected]"; 직후, 반환 전 member · 호출자가 만든 객체 email = "[email protected]" 호출자의 변수에는 새 참조를 대입하지 않았습니다. target · replace가 만든 새 객체 target = new Member(); 로 매개변수만 재대입합니다. 그 새 객체의 email 은 "[email protected]" 입니다. 두 객체의 이메일 필드만 표시했습니다. 반환 뒤 호출자 출력은 [email protected] 입니다. target 의 수명이 끝나고 다른 참조가 없다면 새 객체에는 도달할 수 없으며, 이것이 즉시 회수를 뜻하지는 않습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 호출자 객체 member가 [email protected], age 30 객체를 참조합니다. |
|
|
| 매개변수 경로 target이 호출자와 같은 객체를 가리킵니다. |
|
|
| 필드 쓰기 target.active=true를 저장합니다. |
|
|
swap 메서드가 두 변수를 맞바꾸지 못하는 이유
"객체는 참조로 전달된다"고 외워 두면 다음 메서드가 호출자의 두 변수를 맞바꿀 것이라고 기대하기 쉽습니다. swap은 매개변수 a와 b의 참조값을 임시 변수 temp를 거쳐 맞바꿉니다. 기대대로라면 main이 출력하는 두 이메일의 순서가 뒤집혀야 합니다.
public class SwapAttempt {
public static void main(String[] args) {
Member first = new Member();
first.email = "[email protected]";
Member second = new Member();
second.email = "[email protected]";
swap(first, second);
System.out.println(first.email);
System.out.println(second.email);
}
static void swap(Member a, Member b) {
Member temp = a;
a = b;
b = temp;
}
}swap 안에서 a와 b의 참조값은 실제로 맞바뀝니다. 하지만 a와 b는 first와 second의 값을 복사해 받은 별개의 지역 변수이므로, 메서드가 끝나면 맞바꾼 결과도 함께 사라집니다. 호출자의 first와 second는 처음 그대로입니다. 자바에는 메서드가 호출자의 지역 변수를 대신 바꿔 주는 방법이 없으므로, 이런 일이 필요하면 결과를 반환해 호출자가 대입하게 하거나 무엇을 맞바꿀지를 다시 정해야 합니다.
예를 들어 목적이 두 회원의 이메일을 맞바꾸는 것이라면, 변수 대신 두 객체의 필드를 맞바꾸면 됩니다.
static void swapEmails(Member a, Member b) {
String temp = a.email;
a.email = b.email;
b.email = temp;
}swapEmails(first, second) 뒤에 두 이메일을 출력하면 [email protected], [email protected] 순서로 바뀌어 나옵니다. a와 b는 여전히 복사된 지역 변수지만, 이번에 바뀐 것은 변수가 아니라 그 변수들이 가리키는 두 객체의 필드이기 때문입니다.
그래서 정확한 표현은 "자바는 모든 인수를 값으로 전달하고, 참조형 인수의 값은 참조값이다"입니다. 줄여서 '참조를 값으로 전달한다'고 말하기도 합니다.
더 깊이: 참조에 의한 전달과 무엇이 다른가
C++처럼 참조에 의한 전달(call by reference)을 지원하는 언어에서는 매개변수가 호출자의 변수에 붙는 또 다른 이름이 됩니다. 아래 C++ 코드에서 int& value는 main의 age를 가리키는 별명입니다. 그래서 value += 10이 곧 age를 바꾸고, 프로그램은 main=40을 출력합니다.
#include <iostream>
void increase(int& value) { value += 10; }
int main() {
int age = 30;
increase(age);
std::cout << "main=" << age << "\n";
}자바에는 이런 매개변수가 없습니다. Java 언어 명세(JLS §8.4.1)에 따르면 메서드가 호출될 때마다 매개변수 변수가 새로 만들어지고, 인수 식을 계산한 값으로 초기화됩니다. 그래서 swap이나 replace처럼 매개변수에 대입해도 호출자의 변수에는 영향이 없습니다. 참조형 인수가 참조로 전달되는 것처럼 보이는 이유는 복사된 값이 객체를 가리키는 참조값이기 때문일 뿐이고, 전달 방식 자체는 기본형과 같습니다.
매개변수 재대입이 혼란을 부른다면 매개변수를 final로 선언할 수 있습니다. static void activate(final Member target)에서 target = new Member()를 쓰면 컴파일러가 final parameter target may not be assigned 오류를 냅니다. 반면 target.active = true는 그대로 허용됩니다. final은 변수에 다른 값을 넣지 못하게 할 뿐, 그 변수가 가리키는 객체의 필드까지 고정하지는 않기 때문입니다. 이 차이는 final 참조와 객체 변경에서 자세히 다룹니다.
초기화 메서드로 반복되는 대입 줄이기
회원 객체를 여러 개 만들다 보면 같은 모양의 필드 대입이 되풀이됩니다. 다음 코드는 회원 두 명을 만드는데, 두 덩어리는 변수 이름과 넣는 값만 다르고 줄의 모양은 똑같습니다.
Member first = new Member();
first.email = "[email protected]";
first.age = 40;
first.active = false;
Member second = new Member();
second.email = "[email protected]";
second.age = 35;
second.active = false;필드가 세 개뿐인데도 객체마다 네 줄씩 필요하고, 그중 한 줄을 빠뜨려도 컴파일러는 알려 주지 않습니다. 앞에서 본 것처럼 메서드는 넘겨받은 참조로 호출자의 객체를 바꿀 수 있으므로, 이 대입을 메서드 하나로 모을 수 있습니다.
public class MemberInitializer {
public static void main(String[] args) {
Member first = new Member();
init(first, "[email protected]", 40, false);
Member second = new Member();
init(second, "[email protected]", 35, false);
print(first);
print(second);
}
static void init(
Member target,
String email,
int age,
boolean active
) {
target.email = email;
target.age = age;
target.active = active;
}
static void print(Member member) {
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}[email protected] / 40세 / false
[email protected] / 35세 / falseinit(first, ...)를 호출하면 first의 참조값이 target에 복사되고, init은 그 참조를 따라가 호출자가 만든 객체의 필드를 채웁니다. init이 새 객체를 만들지 않으므로 main의 first와 second로 읽으면 채워진 값이 그대로 보이고, print가 두 줄을 출력합니다. print에도 참조값이 복사되지만 print는 필드를 읽기만 합니다.
이 방식은 중복을 줄여 주지만 약점이 있습니다. 호출하는 쪽이 먼저 빈 객체를 new로 만들어 넘겨야 하고, init 호출을 잊으면 email이 null이고 age가 0인 객체가 그대로 남습니다. 객체를 만드는 일과 채우는 일이 서로 다른 곳에서 일어나기 때문입니다.
객체를 만들어 돌려주는 메서드
만들기와 채우기를 한 메서드에 모으면 이 약점을 줄일 수 있습니다. 다음 create는 객체를 직접 만들고 필드를 채운 뒤 그 참조를 반환합니다. 이렇게 객체를 만들어 돌려주는 메서드를 흔히 팩터리 메서드(factory method)라고 부릅니다.
public class MemberFactory {
public static void main(String[] args) {
Member member = create("[email protected]", 45, false);
print(member);
}
static Member create(
String email,
int age,
boolean active
) {
Member created = new Member();
created.email = email;
created.age = age;
created.active = active;
return created;
}
static void print(Member member) {
System.out.printf(
"%s / %d세 / %s%n",
member.email, member.age, member.active
);
}
}[email protected] / 45세 / falsecreated는 create 안의 지역 변수이므로 create가 끝나면 사라집니다. 그런데도 main이 객체를 쓸 수 있는 이유는 return created가 created에 든 참조값을 호출한 자리로 복사하고, main이 그 값을 member에 대입했기 때문입니다. 객체는 new로 한 번만 만들어졌고 반환할 때 필드가 복제되지도 않습니다. 반환 전에는 created가, 반환 뒤에는 member가 같은 객체를 가리킬 뿐입니다.
create의 지역 변수 종료와 반환된 객체의 도달 가능성
create가 지역 변수 created의 참조값을 반환하면 main의 member가 이를 받아 같은 객체를 계속 가리킵니다. 프레임 종료 전후에서 지역 변수 수명과 객체 도달 가능성을 구분할 수 있습니다.
MemberFactory의 create가 만든 객체 하나를 두 시점에서 표시합니다. 반환 전에는 created가, 반환값 대입 후에는 main의 member가 같은 객체를 참조합니다. 두 시점 모두 email은 [email protected], age는 45, active는 false입니다. 지역 변수 created의 수명이 끝나도 그 객체의 소멸을 뜻하지는 않습니다.
반환된 참조로 같은 객체에 계속 도달합니다
create의 지역 변수는 호출이 끝나면 종료됩니다. 반환값을 받은 main의 member가 같은 객체를 계속 참조합니다.
MemberFactory · create("[email protected]", 45, false)
아래 두 시점은 new 한 번으로 만든 같은 객체를 다시 표시한 것입니다.
- ① return created; 직전
create의 지역 변수created가 새로 만든 객체를 참조합니다.email = "[email protected]",age = 45,active = false- ② main의 반환값 대입 직후 · print 호출 전
created의 참조값을 반환받아member에 복사했습니다.member는 새로 복제한 객체가 아니라 위와 같은 객체를 가리킵니다.- 필드는 그대로
"[email protected]",45,false입니다.
create가 끝나면 지역 변수 created의 수명은 끝납니다. 호출자의 member가 같은 객체를 참조하므로 그 객체에는 계속 도달할 수 있습니다.
이 예의 조건
- 반환 전후 객체는 같고 main의 print 호출 전 상태입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | create의 지역 변수 종료와 반환된 객체의 도달 가능성 create의 지역 변수 종료와 반환된 객체의 도달 가능성 MemberFactory의 create가 만든 객체 하나를 두 시점에서 표시합니다. 반환 전에는 created가, 반환값 대입 후에는 main의 member가 같은 객체를 참조합니다. 두 시점 모두 email은 [email protected], age는 45, active는 false입니다. 지역 변수 created의 수명이 끝나도 그 객체의 소멸을 뜻하지는 않습니다. 반환된 참조로 같은 객체에 계속 도달합니다 create의 지역 변수는 호출이 끝나면 종료됩니다. 반환값을 받은 main의 member가 같은 객체를 계속 참조합니다. MemberFactory · create("[email protected]", 45, false) 아래 두 시점은 new 한 번으로 만든 같은 객체를 다시 표시한 것입니다. ① return created; 직전 create 의 지역 변수 created 가 새로 만든 객체를 참조합니다. email = "[email protected]" , age = 45 , active = false ② main의 반환값 대입 직후 · print 호출 전 created 의 참조값을 반환받아 member 에 복사했습니다. member 는 새로 복제한 객체가 아니라 위와 같은 객체를 가리킵니다. 필드는 그대로 "[email protected]" , 45 , false 입니다. create 가 끝나면 지역 변수 created 의 수명은 끝납니다. 호출자의 member 가 같은 객체를 참조하므로 그 객체에는 계속 도달할 수 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 팩토리 지역 create의 created가 새 객체를 참조합니다. |
|
|
| 참조 반환 return created가 현재 참조값을 반환합니다. |
|
|
| 호출자 보관 main의 member가 반환 참조를 받습니다. |
|
|
create의 지역 변수 종료와 반환된 객체의 도달 가능성
create가 지역 변수 created의 참조값을 반환하면 main의 member가 이를 받아 같은 객체를 계속 가리킵니다. 프레임 종료 전후에서 지역 변수 수명과 객체 도달 가능성을 구분할 수 있습니다.
MemberFactory의 create가 만든 객체 하나를 두 시점에서 표시합니다. 반환 전에는 created가, 반환값 대입 후에는 main의 member가 같은 객체를 참조합니다. 두 시점 모두 email은 [email protected], age는 45, active는 false입니다. 지역 변수 created의 수명이 끝나도 그 객체의 소멸을 뜻하지는 않습니다.
반환된 참조로 같은 객체에 계속 도달합니다
create의 지역 변수는 호출이 끝나면 종료됩니다. 반환값을 받은 main의 member가 같은 객체를 계속 참조합니다.
MemberFactory · create("[email protected]", 45, false)
아래 두 시점은 new 한 번으로 만든 같은 객체를 다시 표시한 것입니다.
- ① return created; 직전
create의 지역 변수created가 새로 만든 객체를 참조합니다.email = "[email protected]",age = 45,active = false- ② main의 반환값 대입 직후 · print 호출 전
created의 참조값을 반환받아member에 복사했습니다.member는 새로 복제한 객체가 아니라 위와 같은 객체를 가리킵니다.- 필드는 그대로
"[email protected]",45,false입니다.
create가 끝나면 지역 변수 created의 수명은 끝납니다. 호출자의 member가 같은 객체를 참조하므로 그 객체에는 계속 도달할 수 있습니다.
이 예의 조건
- 반환 전후 객체는 같고 main의 print 호출 전 상태입니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | create의 지역 변수 종료와 반환된 객체의 도달 가능성 create의 지역 변수 종료와 반환된 객체의 도달 가능성 MemberFactory의 create가 만든 객체 하나를 두 시점에서 표시합니다. 반환 전에는 created가, 반환값 대입 후에는 main의 member가 같은 객체를 참조합니다. 두 시점 모두 email은 [email protected], age는 45, active는 false입니다. 지역 변수 created의 수명이 끝나도 그 객체의 소멸을 뜻하지는 않습니다. 반환된 참조로 같은 객체에 계속 도달합니다 create의 지역 변수는 호출이 끝나면 종료됩니다. 반환값을 받은 main의 member가 같은 객체를 계속 참조합니다. MemberFactory · create("[email protected]", 45, false) 아래 두 시점은 new 한 번으로 만든 같은 객체를 다시 표시한 것입니다. ① return created; 직전 create 의 지역 변수 created 가 새로 만든 객체를 참조합니다. email = "[email protected]" , age = 45 , active = false ② main의 반환값 대입 직후 · print 호출 전 created 의 참조값을 반환받아 member 에 복사했습니다. member 는 새로 복제한 객체가 아니라 위와 같은 객체를 가리킵니다. 필드는 그대로 "[email protected]" , 45 , false 입니다. create 가 끝나면 지역 변수 created 의 수명은 끝납니다. 호출자의 member 가 같은 객체를 참조하므로 그 객체에는 계속 도달할 수 있습니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 팩토리 지역 create의 created가 새 객체를 참조합니다. |
|
|
| 참조 반환 return created가 현재 참조값을 반환합니다. |
|
|
| 호출자 보관 main의 member가 반환 참조를 받습니다. |
|
|
여기서 지역 변수의 수명과 객체의 수명이 다르다는 것을 알 수 있습니다. 지역 변수는 그 변수를 선언한 메서드가 끝나면 사라지지만, 객체는 특정 변수에 속해 있지 않습니다. 객체는 변수나 배열 칸 같은 경로로 도달할 수 있는 동안 계속 쓸 수 있습니다.
팩터리 메서드를 쓰면 호출자는 완성된 객체만 받으므로 초기화를 빠뜨릴 일이 줄어듭니다. 하지만 다른 코드가 new Member()를 직접 호출해 빈 객체를 만드는 것까지 막지는 못합니다. 객체가 만들어지는 순간 필수 값을 받도록 하는 방법은 5장의 생성자와 this에서 배웁니다.
메서드 이름으로 변경 여부 알리기
참조형 매개변수를 받는 메서드는 넘겨받은 객체를 읽기만 할 수도 있고, 필드를 바꿀 수도 있습니다. 다음 두 선언은 본문을 생략하고 머리만 적은 것이며, 각 메서드가 하는 일은 주석에 적었습니다.
static void print(Member member) { ... } // 읽기만 한다
static void activate(Member member) { ... } // 필드를 바꾼다두 선언은 이름 말고는 차이가 없습니다. 컴파일러는 메서드가 받은 객체를 바꾸는지를 선언만 보고 구분하지 않으며, 바꾸지 못하게 막아 주지도 않습니다. 그래서 변경 여부는 메서드 이름과 주석으로 드러내야 합니다. 만약 print가 출력하는 김에 active를 true로 바꾼다면, 출력만 부탁한 호출자는 나중에 원인을 알 수 없는 상태 변화를 만나게 됩니다.
지금의 Member는 필드가 모두 열려 있어서 어느 메서드든 값을 바꿀 수 있는 단순한 데이터 묶음입니다. 5장에서 접근 제어와 캡슐화를 배우면 필드를 바꾸는 일을 객체 자신의 메서드로 옮겨, 어떤 변경이 허용되는지를 객체가 직접 정하게 할 수 있습니다. 지금은 메서드를 호출할 때 무엇이 복사되는지를 정확히 읽는 데 집중합니다.
정리
- 메서드를 호출하면 인수의 값이 매개변수라는 새 지역 변수에 복사됩니다. 대입과 같은 규칙입니다.
- 기본형 인수는 값 자체가 복사되므로 메서드 안에서 매개변수를 바꿔도 호출자의 변수는 그대로입니다. 호출자의 값을 바꾸려면 새 값을 반환하고 호출자가 대입합니다.
- 참조형 인수는 참조값이 복사됩니다. 그 참조로 객체의 필드를 바꾸면 같은 객체를 가리키는 호출자에게도 변경이 보입니다.
- 매개변수에 새 객체를 대입하면 매개변수만 바뀝니다. 메서드는 호출자의 변수를 다른 객체로 바꿀 수 없습니다.
- 반환값도 복사됩니다. 메서드의 지역 변수가 사라져도 반환된 참조로 객체에 계속 도달할 수 있습니다.
연습 문제
- 다음 프로그램이 출력하는 값을 먼저 예측한 뒤 실행해 확인하세요. 메서드 안의 세 줄이 각각 어느 변수나 객체를 바꾸는지 적어 보면 쉽게 풀립니다.
public class UpdateQuiz {
public static void main(String[] args) {
Member member = new Member();
member.age = 30;
update(member);
System.out.println(member.age);
}
static void update(Member target) {
target.age = 50;
target = new Member();
target.age = 99;
}
}해설 보기
50호출하는 순간 member의 참조값(x101이라고 하겠습니다)이 target에 복사됩니다. 첫 줄 target.age = 50은 x101 객체의 필드를 바꾸므로 호출자에게도 보입니다. 둘째 줄 target = new Member()부터 target은 새 객체 x202를 가리키고, 셋째 줄의 99는 x202에만 들어갑니다. 메서드가 끝난 뒤 member는 여전히 x101을 가리키므로 50이 출력됩니다.
- 다음 메서드를 구현하고 차이를 설명하세요. 나이는 14세 이상 120세 이하이고,
celebrateBirthday와replace에는null이 아닌 실제Member를 넘긴다고 가정합니다. 문자열 입력을 숫자로 바꾸는 일이나null처리는 이 메서드들이 하지 않습니다.int nextAge(int current)는 다음 나이를 반환합니다.void celebrateBirthday(Member member)는 전달된 회원의 나이를 1 증가시킵니다.- 현재 나이가 120이면 두 메서드 모두
IllegalStateException을 던집니다. replace(Member member)에서 매개변수에 새 객체를 대입해도 호출자의 변수가 바뀌지 않는 실행 사례를 추가합니다.
해설 보기
public class BirthdayParameter {
public static void main(String[] args) {
int age = 30;
age = nextAge(age);
System.out.println("number=" + age);
Member member = new Member();
member.email = "[email protected]";
member.age = 30;
celebrateBirthday(member);
System.out.println("member=" + member.age);
replace(member);
System.out.println("after replace=" + member.email);
}
static int nextAge(int current) {
requireBirthdayAllowed(current);
return current + 1;
}
static void celebrateBirthday(Member member) {
requireBirthdayAllowed(member.age);
member.age++;
}
static void replace(Member member) {
member = new Member();
member.email = "[email protected]";
}
static void requireBirthdayAllowed(int currentAge) {
if (currentAge >= 120) {
throw new IllegalStateException("maximum age");
}
}
}number=31
member=31
after [email protected]숫자 버전 nextAge는 계산한 결과를 반환할 뿐이므로 호출자가 age = nextAge(age)로 다시 대입해야 값이 바뀝니다. 객체 버전 celebrateBirthday는 복사된 참조로 공유 객체의 age 필드를 바꾸므로 대입 없이도 member.age가 31이 됩니다. replace는 지역 매개변수 member만 새 객체로 바꾸기 때문에 호출자의 member.email은 그대로입니다. 메서드의 매개변수 이름이 main의 변수 이름과 같아도 서로 다른 변수라는 점을 확인할 수 있습니다.
119세와 120세처럼 허용 범위의 끝에 걸친 값도 따로 확인해 보세요. 119세이면 두 버전 모두 120세가 되지만, 120세이면 maximum age 예외가 먼저 발생합니다. 객체 버전에서는 예외 때문에 member.age++까지 가지 못하므로 나이가 120으로 남습니다. 예외를 잡지 않으면 main이 그 자리에서 끝나므로, 120세 실패는 숫자 버전과 객체 버전을 한 번에 하나씩 바꿔 가며 실행해야 둘 다 볼 수 있습니다.
다음 글
지금까지는 참조 변수가 언제나 어떤 객체를 가리킨다고 가정했습니다. 다음 글에서는 참조 변수에 아무 객체도 없음을 뜻하는 null이 들어 있을 때 점(.)으로 필드에 접근하면 왜 프로그램이 멈추는지 살펴봅니다.