자바에서 변수를 배우고 클래스를 사용하기 시작하면 기본형참조형이라는 말을 자주 만나게 됩니다. 처음에는 이름이 어렵지만, 핵심은 단순합니다.

변수 안에 실제 값이 바로 들어 있느냐, 아니면 객체를 찾아가기 위한 주소가 들어 있느냐의 차이입니다.

앞에서 클래스와 객체가 아직 헷갈린다면 자바 클래스와 데이터 정리 - 객체로 묶어 생각하기를 먼저 보고 오면 더 이해하기 쉽습니다.


기본형과 참조형을 나누는 이유

자바 변수는 크게 두 종류로 볼 수 있습니다.

구분예시변수 안에 들어 있는 것
기본형int, long, double, boolean실제 값
참조형클래스, 배열, String객체를 가리키는 참조값

기본형은 숫자나 참거짓처럼 값을 바로 저장합니다.

int age = 3;
double weight = 1.8;
boolean healthy = true;

반면 참조형은 객체 자체를 변수 안에 통째로 넣는 것이 아닙니다. 객체는 메모리 어딘가에 만들어지고, 변수에는 그 객체를 찾아가기 위한 참조값이 들어갑니다.

Turtle turtle = new Turtle();

이 코드는 turtle 변수 안에 거북이 객체 전체가 들어간다는 뜻이 아닙니다. new Turtle()로 만들어진 객체를 찾아갈 수 있는 값이 들어간다는 뜻입니다.


기본형은 값을 바로 가지고 있습니다

기본형 변수는 값을 직접 가지고 있습니다. 그래서 대입을 하면 값이 복사됩니다.

public class PrimitiveCopy {
    public static void main(String[] args) {
        int firstAge = 3;
        int secondAge = firstAge;

        firstAge = 5;

        System.out.println(firstAge);
        System.out.println(secondAge);
    }
}

실행 결과입니다.

5
3

secondAge = firstAge를 실행할 때 firstAge의 값인 3이 복사됩니다. 이후 firstAge5로 바꿔도 secondAge는 영향을 받지 않습니다.

상자 두 개에 숫자를 각각 넣었다고 생각하면 됩니다. 처음에는 같은 숫자가 들어 있었지만, 한쪽 상자의 숫자를 바꾼다고 다른 상자가 같이 바뀌지는 않습니다.


참조형은 객체의 위치를 가지고 있습니다

참조형은 조금 다르게 움직입니다. 먼저 간단한 클래스를 하나 만들겠습니다.

public class Turtle {
    String name;
    int age;
}

이제 Turtle 객체를 만들고, 다른 변수에 대입해보겠습니다.

public class ReferenceCopy {
    public static void main(String[] args) {
        Turtle first = new Turtle();
        first.name = "마가렛트";
        first.age = 3;

        Turtle second = first;

        second.age = 5;

        System.out.println(first.age);
        System.out.println(second.age);
    }
}

실행 결과입니다.

5
5

기본형과 다르게 firstsecond가 함께 바뀐 것처럼 보입니다. 하지만 정확히 말하면 변수 두 개가 같은 객체를 바라보고 있기 때문입니다.

first  ─┐
        ├─> Turtle 객체
second ─┘    name = "마가렛트"
             age = 5

Turtle second = first;는 객체를 하나 더 복사하는 코드가 아닙니다. first 안에 있던 참조값을 second에 복사하는 코드입니다.

그래서 firstsecond는 같은 객체를 가리키게 됩니다. 둘 중 어느 변수로 age를 바꿔도 같은 객체의 필드가 바뀝니다.


핵심 원칙은 값이 복사된다는 것입니다

기본형과 참조형을 이해할 때 가장 중요한 문장은 이것입니다.

자바에서 대입은 변수 안에 들어 있는 값을 복사합니다.

여기서 값의 종류가 다를 뿐입니다.

기본형 변수 안에는 실제 값이 들어 있습니다. 그래서 실제 값이 복사됩니다.

int a = 10;
int b = a;

참조형 변수 안에는 객체를 찾아가는 참조값이 들어 있습니다. 그래서 참조값이 복사됩니다.

Turtle a = new Turtle();
Turtle b = a;

정리하면 아래처럼 볼 수 있습니다.

기본형 대입
a 안의 숫자 10을 b에 복사

참조형 대입
a 안의 참조값을 b에 복사
두 변수는 같은 객체를 바라봄

이 원칙 하나만 잡으면 참조형이 훨씬 덜 헷갈립니다.


메서드 호출에서도 같은 원칙이 적용됩니다

메서드에 값을 넘길 때도 원칙은 같습니다. 자바는 메서드를 호출할 때 변수 안의 값을 복사해서 매개변수에 전달합니다.

기본형부터 보겠습니다.

public class PrimitiveParameter {
    public static void main(String[] args) {
        int temperature = 28;

        changeTemperature(temperature);

        System.out.println(temperature);
    }

    static void changeTemperature(int value) {
        value = 32;
    }
}

실행 결과입니다.

28

changeTemperature(temperature)를 호출하면 temperature의 값인 28value로 복사됩니다. 메서드 안에서 value32로 바꿔도 원래 변수인 temperature는 바뀌지 않습니다.


참조형을 메서드에 넘기면 객체의 필드를 바꿀 수 있습니다

이번에는 참조형을 메서드에 넘겨보겠습니다.

public class ReferenceParameter {
    public static void main(String[] args) {
        Turtle turtle = new Turtle();
        turtle.name = "마가렛트";
        turtle.age = 3;

        grow(turtle);

        System.out.println(turtle.age);
    }

    static void grow(Turtle target) {
        target.age = target.age + 1;
    }
}

실행 결과입니다.

4

이 경우에도 메서드에는 값이 복사됩니다. 다만 그 값이 실제 객체가 아니라 참조값입니다.

turtle ─┐
        ├─> Turtle 객체
target ─┘    age = 4

grow() 안의 targetturtle과 같은 객체를 바라봅니다. 그래서 target.age를 바꾸면 turtle.age로 보아도 같은 값이 바뀐 것처럼 보입니다.

중요한 점은 변수 자체가 바뀐 것이 아니라, 변수가 가리키는 객체의 필드가 바뀐 것입니다.


참조형 변수 자체를 바꾸는 것과 객체를 바꾸는 것은 다릅니다

참조형을 메서드에 넘기면 객체 필드는 바꿀 수 있습니다. 하지만 매개변수 자체에 새 객체를 넣는다고 호출한 쪽 변수가 바뀌지는 않습니다.

public class ReferenceReassign {
    public static void main(String[] args) {
        Turtle turtle = new Turtle();
        turtle.name = "마가렛트";

        replace(turtle);

        System.out.println(turtle.name);
    }

    static void replace(Turtle target) {
        target = new Turtle();
        target.name = "새 거북이";
    }
}

실행 결과입니다.

마가렛트

replace(turtle)를 호출할 때 참조값이 target으로 복사됩니다. 하지만 target = new Turtle()을 실행하면 target이라는 지역 변수만 새 객체를 바라보게 됩니다.

호출한 쪽의 turtle 변수는 그대로 기존 객체를 바라봅니다.

호출 직후
turtle ─┐
target ─┘ -> 기존 Turtle 객체

target = new Turtle() 이후
turtle ----> 기존 Turtle 객체
target ----> 새 Turtle 객체

그래서 참조형을 메서드에 넘겼다고 해서, 변수 자체를 마음대로 교체할 수 있는 것은 아닙니다.


필드와 지역 변수의 초기화 차이

자바에서는 필드와 지역 변수의 초기화 규칙이 다릅니다.

필드는 객체가 만들어질 때 기본값으로 자동 초기화됩니다.

public class CareRecord {
    int feedingCount;
    boolean checked;
    Turtle turtle;
}

new CareRecord()를 만들면 각 필드는 기본값을 가집니다.

필드 타입기본값
숫자 기본형0
booleanfalse
참조형null

예를 들어 feedingCount0, checkedfalse, turtlenull로 시작합니다.

반면 지역 변수는 직접 초기화해야 사용할 수 있습니다.

public class LocalVariableExample {
    public static void main(String[] args) {
        int count;

        // System.out.println(count); // 컴파일 오류
    }
}

지역 변수는 메서드 안에서 잠깐 쓰는 변수입니다. 자바는 지역 변수를 사용할 때 개발자가 직접 값을 넣도록 요구합니다.


null은 아직 가리키는 객체가 없다는 뜻입니다

참조형 변수에는 null을 넣을 수 있습니다. null은 아직 어떤 객체도 가리키지 않는다는 뜻입니다.

Turtle turtle = null;

이 상태에서는 turtle을 통해 객체의 필드에 접근할 수 없습니다. 왜냐하면 찾아갈 객체가 없기 때문입니다.

public class NullExample {
    public static void main(String[] args) {
        Turtle turtle = null;

        System.out.println(turtle.name);
    }
}

이 코드는 실행 중에 NullPointerException이 발생합니다.

Exception in thread "main" java.lang.NullPointerException

null.name처럼 없는 객체에 점을 찍고 들어가려고 했기 때문입니다.


NullPointerException은 null에 점을 찍을 때 자주 발생합니다

NullPointerException은 자바를 배우면서 자주 만나는 오류입니다. 처음에는 무섭게 보이지만, 원리는 단순합니다.

참조형 변수가 null인데 그 변수로 필드나 메서드에 접근하면 발생합니다.

CareRecord record = new CareRecord();

System.out.println(record.turtle.name);

record 객체는 만들어졌습니다. 하지만 record.turtle 필드는 참조형이므로 기본값이 null입니다.

실제로는 아래 흐름과 비슷합니다.

record.turtle.name
null.name
NullPointerException

해결하려면 turtle 필드에도 객체를 넣어야 합니다.

CareRecord record = new CareRecord();
record.turtle = new Turtle();
record.turtle.name = "마가렛트";

System.out.println(record.turtle.name);

참조형 필드를 사용할 때는 그 안에 실제 객체가 연결되어 있는지 확인하는 습관이 중요합니다.


String은 참조형이지만 자주 쓰는 특별한 타입입니다

String은 기본형이 아닙니다. 문자열을 표현하는 클래스이므로 참조형입니다.

String name = "마가렛트";

다만 자바에서 너무 자주 쓰기 때문에 기본형처럼 편하게 사용할 수 있도록 여러 문법 지원이 들어가 있습니다. 그래서 처음에는 String을 문자열 타입이라고 편하게 이해해도 됩니다.

중요한 점은 String도 참조형이므로 null이 될 수 있다는 점입니다.

String name = null;

이 상태에서 name.length()처럼 메서드를 호출하면 NullPointerException이 발생할 수 있습니다.


실수하기 쉬운 부분

기본형과 참조형을 배울 때 자주 헷갈리는 부분은 아래입니다.

  • 참조형 대입은 객체 복사가 아니라 참조값 복사입니다.
  • 메서드에 참조형을 넘기면 객체 필드는 바뀔 수 있습니다.
  • 매개변수에 새 객체를 넣어도 호출한 쪽 변수 자체가 바뀌지는 않습니다.
  • 참조형 필드의 기본값은 null입니다.
  • null.을 찍으면 NullPointerException이 발생할 수 있습니다.

특히 “참조형은 객체가 복사된다”라고 외우면 이후 코드가 계속 헷갈립니다. “참조형은 객체를 찾아가는 값이 복사된다”라고 이해하는 편이 더 정확합니다.


정리

자바의 기본형과 참조형은 변수 안에 무엇이 들어 있는지를 기준으로 나눌 수 있습니다.

기본형 변수에는 실제 값이 들어 있습니다. 그래서 대입하거나 메서드에 넘기면 실제 값이 복사됩니다.

참조형 변수에는 객체를 찾아가기 위한 참조값이 들어 있습니다. 그래서 대입하거나 메서드에 넘기면 참조값이 복사되고, 여러 변수가 같은 객체를 바라볼 수 있습니다.

마지막으로 null은 아직 가리키는 객체가 없다는 뜻입니다. 참조형을 사용할 때 NullPointerException이 보이면, 대부분은 어딘가에서 null.을 찍고 있는지 확인하면 됩니다.

기본형과 참조형은 이후 객체지향, 배열, 메서드 호출, 컬렉션을 이해하는 바탕이 됩니다. 처음에는 어렵게 느껴져도 “값이 복사된다. 다만 기본형은 실제 값, 참조형은 참조값이다”라는 문장만 잡고 가면 훨씬 덜 헷갈립니다.