본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
4장 : 클래스와 참조

기본형과 참조형 복사

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

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;

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


참조값 복사

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

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

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

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

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

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

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

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


재대입과 필드 변경

다음 두 줄은 겉보기보다 전혀 다른 대상을 바꿉니다.

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 = "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

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


참조 동일성 비교

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

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

src/members/ch04/v6/IdentityCheck.java
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 계약을 정의해 판단합니다.

지금 단계에서는 ==를 객체 식별자 검사로만 사용하세요.


독립 객체 복사

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

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

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

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

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

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

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

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


연습 문제

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

  1. original 회원을 이메일 "kim@example.com", 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 = "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
복사 동일=false

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

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

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


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

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

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