기본형과 참조형 복사
자바의 대입은 항상 변수 값을 복사한다는 원칙으로 독립 변경과 공유 변경을 설명합니다.
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;대입 순간의 값이 복사될 뿐, 원본 변수를 계속 지켜보는 연결을 만드는 문법은 아닙니다.
기본형 대입 뒤 두 변수의 독립된 값 변화
40의 값 복사 뒤 originalAge=50과 copiedAge=70은 각 변수만 바꿉니다. 대입 순간의 값과 이후 독립 변경을 추적하는 전환입니다.
PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다.
기본형 대입은 그 순간의 값을 복사합니다
40을 copiedAge에 복사한 뒤 두 변수는 독립적으로 바뀝니다. 각 문장 실행 직후의 값을 비교합니다.
| 방금 실행한 문장 | 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입니다.
이 예의 조건
- 기본형 복사는 숫자 값이며 영구 공유 참조가 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | originalAge 40을 copiedAge에 숫자로 복사한 뒤 original 50/copied 70을 각각 독립적으로 대입한다. 점선은 값 복사이고 실선은 각 변수 갱신이며 영구 공유 참조가 아니다. |
| 구분 | 내용 |
|---|---|
| 설명 | originalAge 40을 copiedAge에 숫자로 복사한 뒤 original 50/copied 70을 각각 독립적으로 대입한다. 점선은 값 복사이고 실선은 각 변수 갱신이며 영구 공유 참조가 아니다. |
| 전체 읽기 정보 | 기본형 대입 뒤 두 변수의 독립된 값 변화 기본형 대입 뒤 두 변수의 독립된 값 변화 PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다. 기본형 대입은 그 순간의 값을 복사합니다 40을 copiedAge에 복사한 뒤 두 변수는 독립적으로 바뀝니다. 각 문장 실행 직후의 값을 비교합니다. PrimitiveCopy 에서 각 문장을 실행한 직후의 값 방금 실행한 문장 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 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 복사 시점 originalAge 40을 copiedAge에 대입합니다. |
|
|
| 원본 변수 변경 originalAge에 50을 대입합니다. |
|
|
| 복사 변수 변경 copiedAge에 70을 대입합니다. |
|
|
기본형 대입 뒤 두 변수의 독립된 값 변화
40의 값 복사 뒤 originalAge=50과 copiedAge=70은 각 변수만 바꿉니다. 대입 순간의 값과 이후 독립 변경을 추적하는 전환입니다.
PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다.
기본형 대입은 그 순간의 값을 복사합니다
40을 copiedAge에 복사한 뒤 두 변수는 독립적으로 바뀝니다. 각 문장 실행 직후의 값을 비교합니다.
| 방금 실행한 문장 | 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입니다.
이 예의 조건
- 기본형 복사는 숫자 값이며 영구 공유 참조가 아닙니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 2 | originalAge 40을 copiedAge에 숫자로 복사한 뒤 original 50/copied 70을 각각 독립적으로 대입한다. 점선은 값 복사이고 실선은 각 변수 갱신이며 영구 공유 참조가 아니다. |
| 구분 | 내용 |
|---|---|
| 설명 | originalAge 40을 copiedAge에 숫자로 복사한 뒤 original 50/copied 70을 각각 독립적으로 대입한다. 점선은 값 복사이고 실선은 각 변수 갱신이며 영구 공유 참조가 아니다. |
| 전체 읽기 정보 | 기본형 대입 뒤 두 변수의 독립된 값 변화 기본형 대입 뒤 두 변수의 독립된 값 변화 PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다. 기본형 대입은 그 순간의 값을 복사합니다 40을 copiedAge에 복사한 뒤 두 변수는 독립적으로 바뀝니다. 각 문장 실행 직후의 값을 비교합니다. PrimitiveCopy 에서 각 문장을 실행한 직후의 값 방금 실행한 문장 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 입니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 복사 시점 originalAge 40을 copiedAge에 대입합니다. |
|
|
| 원본 변수 변경 originalAge에 50을 대입합니다. |
|
|
| 복사 변수 변경 copiedAge에 70을 대입합니다. |
|
|
참조값 복사
이번에는 같은 구조를 Member로 바꿉니다.
public class ReferenceCopy {
public static void main(String[] args) {
Member first = new Member();
first.email = "[email protected]";
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이 보입니다.
같은 Member를 가리키는 두 참조와 하나의 필드 변경
second에 같은 참조를 복사한 뒤 second.age=70은 객체 필드를 바꾸어 first에서도 70이 보입니다. 참조값은 그대로이고 필드만 40→70으로 변하는 실제 상태입니다.
ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다.
참조값을 복사하면 같은 필드를 읽고 씁니다
first와 second는 하나의 객체에 도달합니다. second.age를 70으로 바꾸면 first.age에서도 같은 70을 읽습니다.
ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후
- first와 second · 같은 참조값
second = first로 하나의 Member 객체에 도달합니다.
읽은 값은 first.age와 second.age 모두 70이고, first == second는 true입니다. 두 변수의 참조값을 바꾸거나 객체를 복제한 것이 아닙니다.
이 예의 조건
- Member 객체 하나의 age 필드만 변경합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 같은 Member를 가리키는 두 참조와 하나의 필드 변경 같은 Member를 가리키는 두 참조와 하나의 필드 변경 ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다. 참조값을 복사하면 같은 필드를 읽고 씁니다 first와 second는 하나의 객체에 도달합니다. second.age를 70으로 바꾸면 first.age에서도 같은 70을 읽습니다. ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후 first와 second · 같은 참조값 second = first 로 하나의 Member 객체에 도달합니다. 공유 객체의 age 변경 email = "[email protected]" second.age = 70 은 이 객체의 age를 40에서 70으로 바꿉니다. 읽은 값은 first.age 와 second.age 모두 70이고, first == second 는 true 입니다. 두 변수의 참조값을 바꾸거나 객체를 복제한 것이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 참조값 first와 second가 Kim 객체를 가리킵니다. |
|
|
| 필드 변경 second를 통해 age를 70으로 씁니다. |
|
|
| 공통 읽기 어느 변수로 읽어도 같은 age를 얻습니다. |
|
|
같은 Member를 가리키는 두 참조와 하나의 필드 변경
second에 같은 참조를 복사한 뒤 second.age=70은 객체 필드를 바꾸어 first에서도 70이 보입니다. 참조값은 그대로이고 필드만 40→70으로 변하는 실제 상태입니다.
ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다.
참조값을 복사하면 같은 필드를 읽고 씁니다
first와 second는 하나의 객체에 도달합니다. second.age를 70으로 바꾸면 first.age에서도 같은 70을 읽습니다.
ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후
- first와 second · 같은 참조값
second = first로 하나의 Member 객체에 도달합니다.
읽은 값은 first.age와 second.age 모두 70이고, first == second는 true입니다. 두 변수의 참조값을 바꾸거나 객체를 복제한 것이 아닙니다.
이 예의 조건
- Member 객체 하나의 age 필드만 변경합니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 읽기 정보 | 같은 Member를 가리키는 두 참조와 하나의 필드 변경 같은 Member를 가리키는 두 참조와 하나의 필드 변경 ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다. 참조값을 복사하면 같은 필드를 읽고 씁니다 first와 second는 하나의 객체에 도달합니다. second.age를 70으로 바꾸면 first.age에서도 같은 70을 읽습니다. ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후 first와 second · 같은 참조값 second = first 로 하나의 Member 객체에 도달합니다. 공유 객체의 age 변경 email = "[email protected]" second.age = 70 은 이 객체의 age를 40에서 70으로 바꿉니다. 읽은 값은 first.age 와 second.age 모두 70이고, first == second 는 true 입니다. 두 변수의 참조값을 바꾸거나 객체를 복제한 것이 아닙니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 같은 참조값 first와 second가 Kim 객체를 가리킵니다. |
|
|
| 필드 변경 second를 통해 age를 70으로 씁니다. |
|
|
| 공통 읽기 어느 변수로 읽어도 같은 age를 얻습니다. |
|
|
재대입과 필드 변경
다음 두 줄은 비슷해 보이지만 전혀 다른 대상을 바꿉니다.
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 = "[email protected]";
first.age = 40;
Member second = first;
second = new Member();
second.email = "[email protected]";
second.age = 10;
System.out.println(first.email + "=" + first.age);
System.out.println(second.email + "=" + second.age);
System.out.println("same=" + (first == second));
}
}[email protected]=40
[email protected]=10
same=false공유 상태 문제를 조사할 때는 “어떤 변수를 바꿨나?”보다 “어떤 참조를 따라 어느 객체의 필드를 바꿨나?”를 물어야 합니다.
참조 동일성 비교
참조형에 ==를 사용하면 두 변수가 같은 참조값을 갖는지 비교합니다.
필드 내용이 같은지를 비교하는 연산은 아닙니다.
public class IdentityCheck {
public static void main(String[] args) {
Member first = new Member();
first.email = "[email protected]";
first.age = 30;
Member alias = first;
Member copy = new Member();
copy.email = "[email protected]";
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입니다.
필드 값이 같아도 같은 객체는 아닙니다
first와 alias는 객체 A를 공유하고 copy는 별도 객체 B를 참조합니다. 참조의 ==는 필드 내용이 아닌 객체 동일성을 비교합니다.
IdentityCheck · 비교 직전의 참조 관계
- 객체 A · first와 alias
alias = first로 참조값을 복사하여 같은 객체에 도달합니다.- 객체 B · copy
- 별도
new Member()로 만든 다른 객체입니다. - 두 객체의 같은 필드 값
- 각각
email = "[email protected]",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 = "[email protected]";
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회원을 이메일"[email protected]", 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 = "[email protected]";
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 = "[email protected]"처럼 대입하면 edited의 필드가 다른 문자열 참조를 가질 뿐 기존 문자열 내용을 수정하지 않습니다.
그래서 이번 얕은 복사에서 이메일의 공유는 변경 가능한 내부 상태 공유와 같은 문제를 만들지 않습니다.
기본형과 참조형 모두 변수 값을 복사합니다.
숫자 값 복사는 독립된 계산 값을 만들고 참조값 복사는 같은 객체로 가는 경로를 늘립니다.
다음 절에서는 이 대입 규칙이 메서드 호출의 인자와 매개변수 사이에서도 그대로 적용되는지 확인합니다.