기본형과 참조형 복사
자바의 대입은 항상 변수 값을 복사한다는 원칙으로 독립 변경과 공유 변경을 설명합니다.
int b = a 뒤에 b를 바꾸면 a는 그대로인데, Member second = first 뒤에 second.age를 바꾸면 first.age도 바뀝니다.
이 차이만 보고 “기본형은 복사되고 객체는 복사되지 않는다”라고 외우면 메서드 호출과 배열에서 다시 혼란이 생깁니다.
자바의 대입 규칙은 하나입니다.
변수에 들어 있는 값을 복사해 다른 변수에 넣는다.
기본형 변수에는 실제 계산 값이 들어 있고 참조형 변수에는 객체에 도달하는 참조값이 들어 있습니다.
대입은 둘 다 그대로 복사하지만, 참조값을 복사하면 두 변수가 같은 객체에 도달한다는 결과가 생깁니다.
기본형 값 복사
먼저 예상하기 쉬운 코드부터 실행합니다.
public class PrimitiveCopy {
public static void main(String[] args) {
int originalAge = 40;
int copiedAge = originalAge;
originalAge = 50;
copiedAge = 70;
System.out.println("original=" + originalAge);
System.out.println("copied=" + copiedAge);
}
}original=50
copied=70copiedAge = originalAge 시점에 숫자 40이 복사됩니다.
그 뒤 두 변수에 새 숫자를 대입하면 각 변수의 칸만 바뀝니다.
두 변수 사이에 연결이 남아 있지 않습니다.
대입문 오른쪽을 “변수 자체”가 아니라 “그 변수에서 지금 읽은 값”으로 바꿔 읽어도 좋습니다.
int copiedAge = 40;대입 순간의 값이 복사될 뿐, 원본 변수를 계속 지켜보는 연결을 만드는 문법은 아닙니다.
PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다.
| 방금 실행한 문장 | originalAge | copiedAge |
|---|---|---|
int originalAge = 40; | 40 | 아직 선언하지 않음 |
int copiedAge = originalAge; | 40 | 40 |
originalAge = 50; | 50 | 40 |
copiedAge = 70; | 50 | 70 |
PrimitiveCopy · 각 문장 실행 직후
int originalAge = 40;originalAge = 40,copiedAge는 아직 선언하지 않았습니다.int copiedAge = originalAge;originalAge = 40,copiedAge = 40.originalAge = 50;originalAge = 50,copiedAge = 40.copiedAge = 70;originalAge = 50,copiedAge = 70.
복사되는 것은 대입 순간에 읽은 숫자 40입니다. 이후 대입은 각 변수의 값만 바꾸며, 실행 결과는 original=50, copied=70입니다.
참조값 복사
이번에는 같은 구조를 Member로 바꿉니다.
public class ReferenceCopy {
public static void main(String[] args) {
Member first = new Member();
first.email = "kim@example.com";
first.age = 40;
Member second = first;
second.age = 70;
System.out.println("first=" + first.age);
System.out.println("second=" + second.age);
System.out.println("same=" + (first == second));
}
}first=70
second=70
same=truefirst에 x101이라는 참조값이 들어 있다고 가정하면 second = first는 second = x101처럼 동작합니다.
인스턴스의 세 필드가 새 공간에 복제되지 않습니다.
두 변수가 x101을 통해 같은 객체에 도달합니다.
second.age = 70을 실행할 때 바뀌는 것은 second 변수에 든 참조값이 아닙니다.
그 참조를 따라간 객체의 age 필드입니다.
first로 같은 객체를 읽으므로 70이 보입니다.
ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다.
ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후
first와secondsecond = first로 같은 참조값을 가지며, 하나의 Member 객체에 도달합니다.- 공유하는 Member 객체
email = "kim@example.com".second.age = 70은 이 객체의age를 40에서 70으로 바꿉니다.
읽은 값은 first.age와 second.age 모두 70이고, first == second는 true입니다. 두 변수의 참조값을 바꾸거나 객체를 복제한 것이 아닙니다.
재대입과 필드 변경
다음 두 줄은 겉보기보다 전혀 다른 대상을 바꿉니다.
second.age = 70; // x101 객체의 필드를 변경
second = new Member(); // second 변수의 참조값을 변경두 번째 줄은 새 인스턴스 x202를 만들고 second 변수에 새 참조를 넣습니다.
first는 여전히 x101을 가리킵니다.
public class ReassignReference {
public static void main(String[] args) {
Member first = new Member();
first.email = "original@example.com";
first.age = 40;
Member second = first;
second = new Member();
second.email = "new@example.com";
second.age = 10;
System.out.println(first.email + "=" + first.age);
System.out.println(second.email + "=" + second.age);
System.out.println("same=" + (first == second));
}
}original@example.com=40
new@example.com=10
same=false공유 상태 문제를 조사할 때는 “어떤 변수를 바꿨나?”보다 “어떤 참조를 따라 어느 객체의 필드를 바꿨나?”를 물어야 합니다.
참조 동일성 비교
참조형에 ==를 사용하면 두 변수가 같은 참조값을 갖는지 비교합니다.
필드 내용이 같은지를 비교하는 연산은 아닙니다.
public class IdentityCheck {
public static void main(String[] args) {
Member first = new Member();
first.email = "kim@example.com";
first.age = 30;
Member alias = first;
Member copy = new Member();
copy.email = "kim@example.com";
copy.age = 30;
System.out.println(first == alias); // true
System.out.println(first == copy); // false
}
}copy의 필드 값은 first와 같지만 별도의 new로 만든 인스턴스입니다.
내용 동등성은 나중에 equals 계약을 정의해 판단합니다.
지금 단계에서는 ==를 객체 식별자 검사로만 사용하세요.
IdentityCheck의 first와 alias는 한 객체를 참조하지만 copy는 별도 new로 만든 객체를 참조합니다. 두 객체의 email과 age가 같아도 first == copy는 false입니다.
IdentityCheck · 비교 직전의 참조 관계
- 객체 A ·
first,alias alias = first로 참조값을 복사했습니다. 두 변수가 같은 Member 객체에 도달합니다.- 객체 B ·
copy - 별도의
new Member()로 만든 다른 객체입니다. - 두 객체에서 같은 필드 값
- 각각
email = "kim@example.com",age = 30입니다.
first == alias는true· 같은 객체를 참조합니다.first == copy는false· 필드 값이 같아도 별개 객체입니다.
변수는 3개, 참조하는 객체는 2개입니다. A·B는 설명용 객체 이름이며 실제 주소가 아닙니다. ==는 필드 내용 비교가 아닙니다.
독립 객체 복사
회원을 독립적으로 수정하고 싶다면 새 객체를 만들고 필요한 필드를 복사해야 합니다.
public class ManualCopy {
public static void main(String[] args) {
Member original = new Member();
original.email = "kim@example.com";
original.age = 40;
original.active = false;
Member copy = new Member();
copy.email = original.email;
copy.age = original.age;
copy.active = original.active;
copy.age = 90;
System.out.println("original=" + original.age);
System.out.println("copy=" + copy.age);
}
}original=40
copy=90여기서는 필드가 모두 기본형이거나 불변 문자열이라 얕은 필드 복사만으로 독립성이 충분합니다.
나중에 Member가 변경 가능한 다른 객체를 필드로 갖게 되면 그 내부 참조까지 공유할지 복사할지 별도로 결정해야 합니다.
“새 바깥 객체를 만들었다”는 사실만으로 모든 내부 상태가 독립적인 것은 아닙니다.
별칭과 백업의 차이
다음 코드는 수정 전에 백업을 만들려는 의도입니다.
Member backup = original;
original.age = 120;
System.out.println(backup.age); // 40을 기대했지만 120backup은 객체의 과거 상태를 저장하지 않습니다.
같은 객체로 가는 두 번째 경로를 만들 뿐입니다.
취소 기능이나 변경 전 스냅샷이 필요하다면 새 인스턴스에 값을 복사하거나, 이후 배우는 불변 객체처럼 상태 변경 방식을 다시 설계해야 합니다.
공유 참조가 항상 나쁜 것은 아닙니다.
여러 화면과 서비스가 같은 사용자나 같은 회원 정보를 함께 다뤄야 할 때 같은 객체를 참조하는 것이 자연스럽습니다.
문제는 공유 여부를 모르거나 독립 복사로 착각하는 데 있습니다.
연습 문제
다음 요구사항을 구현하세요.
original회원을 이메일"kim@example.com", 25세, 비활성으로 만듭니다.edited = original만 사용한 첫 버전에서edited.age = 60이 원본까지 바꾸는 것을 확인합니다.- 두 번째 버전에서는 새
Member를 만들고 필드를 복사해 수정본을 독립시킵니다. - 각 버전에서
original == edited결과를 출력합니다. - 왜
String email은 같은 참조를 복사해도 이번 예제에서 문제가 되지 않는지 설명합니다.
해설 보기
public class MemberDraft {
public static void main(String[] args) {
Member original = new Member();
original.email = "kim@example.com";
original.age = 25;
original.active = false;
Member alias = original;
alias.age = 60;
System.out.println("별칭 원본=" + original.age);
System.out.println("별칭 동일=" + (original == alias));
original.age = 25;
Member edited = new Member();
edited.email = original.email;
edited.age = original.age;
edited.active = original.active;
edited.age = 60;
System.out.println("복사 원본=" + original.age);
System.out.println("복사 수정본=" + edited.age);
System.out.println("복사 동일=" + (original == edited));
}
}별칭 원본=60
별칭 동일=true
복사 원본=25
복사 수정본=60
복사 동일=falseString도 참조형이지만 문자열 객체는 불변입니다.
edited.email = "changed@example.com"처럼 대입하면 edited의 필드가 다른 문자열 참조를 가질 뿐 기존 문자열 내용을 수정하지 않습니다.
그래서 이번 얕은 복사에서 이메일의 공유는 변경 가능한 내부 상태 공유와 같은 문제를 만들지 않습니다.
기본형과 참조형 모두 변수 값을 복사합니다.
숫자 값 복사는 독립된 계산 값을 만들고 참조값 복사는 같은 객체로 가는 경로를 늘립니다.
다음 절에서는 이 대입 규칙이 메서드 호출의 인자와 매개변수 사이에서도 그대로 적용되는지 확인합니다.