안동민 개발노트

본문 시작

기본형과 참조형 복사

자바의 대입은 항상 변수 값을 복사한다는 원칙으로 독립 변경과 공유 변경을 설명합니다.

int b = a 뒤에 b를 바꾸면 a는 그대로인데, Member second = first 뒤에 second.age를 바꾸면 first.age도 바뀝니다.

이 차이만 보고 “기본형은 복사되고 객체는 복사되지 않는다”라고 외우면 메서드 호출과 배열에서 다시 혼란이 생깁니다.

자바의 대입 규칙은 하나입니다.

변수에 들어 있는 값을 복사해 다른 변수에 넣는다.

기본형 변수에는 실제 계산 값이 들어 있고 참조형 변수에는 객체에 도달하는 참조값이 들어 있습니다.

대입은 둘 다 그대로 복사하지만, 참조값을 복사하면 두 변수가 같은 객체에 도달한다는 결과가 생깁니다.


기본형 값 복사

먼저 예상하기 쉬운 코드부터 실행합니다.

src/members/ch04/v6/PrimitiveCopy.java
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=70

copiedAge = originalAge 시점에 숫자 40이 복사됩니다.

그 뒤 두 변수에 새 숫자를 대입하면 각 변수의 칸만 바뀝니다.

두 변수 사이에 연결이 남아 있지 않습니다.

대입문 오른쪽을 “변수 자체”가 아니라 “그 변수에서 지금 읽은 값”으로 바꿔 읽어도 좋습니다.

int copiedAge = 40;

대입 순간의 값이 복사될 뿐, 원본 변수를 계속 지켜보는 연결을 만드는 문법은 아닙니다.

기본형 대입 뒤 두 변수의 독립된 값 변화

PrimitiveCopy에서 originalAge의 40을 copiedAge에 복사한 뒤 originalAge는 50, copiedAge는 70으로 각각 대입됩니다. 복사 후 두 변수를 연결하는 관계는 남지 않습니다.

PrimitiveCopy에서 각 문장을 실행한 직후의 값
방금 실행한 문장originalAgecopiedAge
int originalAge = 40;40아직 선언하지 않음
int copiedAge = originalAge;4040
originalAge = 50;5040
copiedAge = 70;5070

PrimitiveCopy · 각 문장 실행 직후

  1. int originalAge = 40;

    originalAge = 40, copiedAge는 아직 선언하지 않았습니다.

  2. int copiedAge = originalAge;

    originalAge = 40, copiedAge = 40.

  3. originalAge = 50;

    originalAge = 50, copiedAge = 40.

  4. copiedAge = 70;

    originalAge = 50, copiedAge = 70.

복사되는 것은 대입 순간에 읽은 숫자 40입니다. 이후 대입은 각 변수의 값만 바꾸며, 실행 결과는 original=50, copied=70입니다.


참조값 복사

이번에는 같은 구조를 Member로 바꿉니다.

src/members/ch04/v6/ReferenceCopy.java
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=true

first에 x101이라는 참조값이 들어 있다고 가정하면 second = first는 second = x101처럼 동작합니다.

인스턴스의 세 필드가 새 공간에 복제되지 않습니다.

두 변수가 x101을 통해 같은 객체에 도달합니다.

second.age = 70을 실행할 때 바뀌는 것은 second 변수에 든 참조값이 아닙니다.

그 참조를 따라간 객체의 age 필드입니다.

first로 같은 객체를 읽으므로 70이 보입니다.

같은 Member를 가리키는 두 참조와 하나의 필드 변경

ReferenceCopy에서 second에 first의 참조값을 복사한 뒤 second.age를 70으로 바꿉니다. 객체는 하나이므로 first.age와 second.age 모두 70이고 first == second는 true입니다.

ReferenceCopy · Member second = first; 다음 second.age = 70; 실행 직후

first와 second가 함께 참조하는 하나의 Member first와 second에는 같은 참조값이 있습니다. second를 통해 객체의 age 필드를 40에서 70으로 변경해도 두 변수의 참조값은 바뀌지 않고, 어느 변수로 읽어도 같은 필드 값 70을 얻습니다. first 변수의 참조값 second 복사한 같은 참조값 Member 한 개 email: [email protected] age: 40 → 70 바뀐 것은 이 객체의 필드
first와 second
second = first로 같은 참조값을 가지며, 하나의 Member 객체에 도달합니다.
공유하는 Member 객체
email = "[email protected]". 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을 가리킵니다.

src/members/ch04/v6/ReassignReference.java
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));
    }
}

공유 상태 문제를 조사할 때는 “어떤 변수를 바꿨나?”보다 “어떤 참조를 따라 어느 객체의 필드를 바꿨나?”를 물어야 합니다.


참조 동일성 비교

참조형에 ==를 사용하면 두 변수가 같은 참조값을 갖는지 비교합니다.

필드 내용이 같은지를 비교하는 연산은 아닙니다.

src/members/ch04/v6/IdentityCheck.java
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입니다.

IdentityCheck · 비교 직전의 참조 관계

first와 alias는 같은 객체, copy는 별도 객체 세 변수 first, alias, copy가 서로 다른 두 Member 객체를 참조합니다. first와 alias는 객체 A를 함께 참조하고 copy는 객체 B를 참조합니다. 두 객체 모두 email은 [email protected]이고 age는 30이지만 같은 객체는 아닙니다. first 처음 만든 객체의 참조 alias first에서 참조값 복사 copy 별도 new의 참조값 Member A email: [email protected] age: 30 first와 alias가 공유 Member B email: [email protected] age: 30
객체 A · first, alias
alias = first로 참조값을 복사했습니다. 두 변수가 같은 Member 객체에 도달합니다.
객체 B · copy
별도의 new Member()로 만든 다른 객체입니다.
두 객체에서 같은 필드 값
각각 email = "[email protected]", age = 30입니다.
  • first == alias는 true · 같은 객체를 참조합니다.
  • first == copy는 false · 필드 값이 같아도 별개 객체입니다.

변수는 3개, 참조하는 객체는 2개입니다. A·B는 설명용 객체 이름이며 실제 주소가 아닙니다. ==는 필드 내용 비교가 아닙니다.


독립 객체 복사

회원을 독립적으로 수정하고 싶다면 새 객체를 만들고 필요한 필드를 복사해야 합니다.

src/members/ch04/v6/ManualCopy.java
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을 기대했지만 120

backup은 객체의 과거 상태를 저장하지 않습니다.

같은 객체로 가는 두 번째 경로를 만들 뿐입니다.

취소 기능이나 변경 전 스냅샷이 필요하다면 새 인스턴스에 값을 복사하거나, 이후 배우는 불변 객체처럼 상태 변경 방식을 다시 설계해야 합니다.

공유 참조가 항상 나쁜 것은 아닙니다.

여러 화면과 서비스가 같은 사용자나 같은 회원 정보를 함께 다뤄야 할 때 같은 객체를 참조하는 것이 자연스럽습니다.

문제는 공유 여부를 모르거나 독립 복사로 착각하는 데 있습니다.


연습 문제

다음 요구사항을 구현하세요.

  1. original 회원을 이메일 "[email protected]", 25세, 비활성으로 만듭니다.
  2. edited = original만 사용한 첫 버전에서 edited.age = 60이 원본까지 바꾸는 것을 확인합니다.
  3. 두 번째 버전에서는 새 Member를 만들고 필드를 복사해 수정본을 독립시킵니다.
  4. 각 버전에서 original == edited 결과를 출력합니다.
  5. 왜 String email은 같은 참조를 복사해도 이번 예제에서 문제가 되지 않는지 설명합니다.
해설 보기
src/members/ch04/exercise/MemberDraft.java
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
복사 동일=false

String도 참조형이지만 문자열 객체는 불변입니다.

edited.email = "[email protected]"처럼 대입하면 edited의 필드가 다른 문자열 참조를 가질 뿐 기존 문자열 내용을 수정하지 않습니다.

그래서 이번 얕은 복사에서 이메일의 공유는 변경 가능한 내부 상태 공유와 같은 문제를 만들지 않습니다.


기본형과 참조형 모두 변수 값을 복사합니다.

숫자 값 복사는 독립된 계산 값을 만들고 참조값 복사는 같은 객체로 가는 경로를 늘립니다.

다음 절에서는 이 대입 규칙이 메서드 호출의 인자와 매개변수 사이에서도 그대로 적용되는지 확인합니다.