본문으로 건너뛰기

안동민 개발노트

본문 시작

클래스와 인스턴스

new가 Member 객체를 만들고 참조값을 반환한 뒤 필드에 접근하는 순서를 메모리 흐름과 함께 추적합니다.

앞 절에서 Member라는 타입을 선언했지만 아직 회원 정보는 하나도 존재하지 않습니다.

클래스는 필드의 구성만 설명합니다.

실제 이메일과 나이가 들어갈 저장 공간은 new Member()를 실행해야 생깁니다.

이 차이를 놓치면 “클래스를 만들었는데 왜 값이 저장되지 않지?” 또는 “변수에 객체 전체가 들어 있나?” 같은 혼란이 이어집니다.

이번 절의 핵심은 문법 한 줄을 네 단계로 풀어 읽는 것입니다.

Member member = new Member();

오른쪽의 new가 인스턴스를 만들고, JVM이 그 인스턴스에 도달할 수 있는 참조값을 돌려주며, 왼쪽 변수는 그 참조값을 보관합니다.

HTML 다이어그램: /docs/java/ch4/ch4-2/1.html

클래스와 인스턴스

먼저 클래스 파일과 실행 파일을 분리합니다.

src/members/ch04/v4/Member.java
public class Member {
    String email;
    int age;
    boolean active;
}
src/members/ch04/v4/MemberMain.java
public class MemberMain {
    public static void main(String[] args) {
        Member member = new Member();

        member.email = "member@example.com";
        member.age = 45;
        member.active = false;

        System.out.printf(
            "%s / %d세 / %s%n",
            member.email, member.age, member.active
        );
    }
}

두 파일을 함께 컴파일하고 main이 있는 클래스를 실행합니다.

실행
javac -d out src/members/ch04/v4/Member.java src/members/ch04/v4/MemberMain.java
java -cp out MemberMain

member@example.com / 45세 / false

Member.java만 컴파일해도 클래스 파일은 만들 수 있지만 실행 시작점인 main이 없습니다.

반대로 MemberMainMember 타입을 사용하므로 두 소스가 모두 컴파일 대상에 있어야 합니다.

클래스 선언과 프로그램 실행 역할을 분리해 두면 이후 여러 프로그램이 같은 타입을 재사용할 수 있습니다.


new의 실행 순서

Member member = new Member();를 한 덩어리로 외우지 말고 다음 순서로 읽어 봅시다.

  1. JVM이 Member 인스턴스를 저장할 공간을 힙에 확보합니다.
  2. 각 필드를 타입의 기본값으로 초기화합니다. emailnull, age0, activefalse입니다.
  3. 새 인스턴스에 도달할 수 있는 참조값을 결과로 돌려줍니다.
  4. 지역 변수 member에 그 참조값을 복사합니다.

실제 참조값의 숫자나 주소 표현은 JVM 구현에 맡겨집니다.

교재에서는 관계를 설명하려고 x101 같은 기호를 쓰지만 애플리케이션이 그 값을 계산하거나 바꾸지는 않습니다.

HTML 다이어그램: /docs/java/ch4/ch4-2/2.html

다음 프로그램으로 필드를 대입하기 전의 기본값을 확인할 수 있습니다.

src/members/ch04/v4/DefaultFieldValues.java
public class DefaultFieldValues {
    public static void main(String[] args) {
        Member member = new Member();

        System.out.println("email=" + member.email);
        System.out.println("age=" + member.age);
        System.out.println("active=" + member.active);
    }
}
email=null
age=0
active=false

기본값이 있다는 사실과 그 값이 업무적으로 올바르다는 사실은 다릅니다.

이메일 없는 회원 정보를 허용하지 않을 예정이라면 객체를 만든 직후 유효한 값을 넣어야 합니다.

생성 시점에 유효성을 강제하는 방법은 생성자를 다루는 장에서 개선합니다.


점 연산자와 참조

member.email을 “변수 안에 있는 email”이라고만 이해하면 참조형이 어려워집니다.

실제로는 다음 두 동작이 결합됩니다.

  1. member 변수에서 참조값을 읽어 인스턴스에 도달합니다.
  2. 그 인스턴스의 email 필드를 선택합니다.
member.email = "member@example.com";
int copiedAge = member.age;

첫 줄은 참조를 따라가 email 필드에 문자열 참조를 저장합니다.

둘째 줄은 같은 방식으로 age 필드에서 숫자 45를 읽어 copiedAge에 복사합니다.

점 왼쪽에 객체가 아니라 null이 있으면 첫 단계에서 경로가 끊깁니다.

그 실패는 뒤의 null 절에서 자세히 다룹니다.

HTML 다이어그램: /docs/java/ch4/ch4-2/3.html

참조 변수와 객체 생성

다음 코드는 컴파일되지 않습니다.

실패 사례: 초기화하지 않은 지역 변수
public class UninitializedReference {
    public static void main(String[] args) {
        Member member;
        System.out.println(member.email);
    }
}
error: variable member might not have been initialized

Member member;는 참조값을 담을 지역 변수만 선언합니다.

객체도 만들지 않았고 변수에 null조차 대입하지 않았습니다.

자바는 초기화되지 않은 지역 변수를 읽는 것을 컴파일 단계에서 막습니다.

아래 코드는 컴파일되지만 실행 중 실패합니다.

실패 사례: null을 저장한 참조 변수
Member member = null;
System.out.println(member.email);

이번에는 변수에 null이라는 값이 분명히 들어 있으므로 컴파일러 규칙은 통과합니다.

그러나 실행할 때 따라갈 인스턴스가 없어 NullPointerException이 발생합니다.

“변수 선언”, “참조값 대입”, “객체 생성”을 같은 동작으로 보지 않아야 두 오류의 시점을 구분할 수 있습니다.


다중 인스턴스

클래스는 한 번 선언하고 new는 필요한 만큼 호출할 수 있습니다.

src/members/ch04/v4/TwoMembers.java
public class TwoMembers {
    public static void main(String[] args) {
        Member firstMember = new Member();
        firstMember.email = "kim@example.com";
        firstMember.age = 25;

        Member secondMember = new Member();
        secondMember.email = "lee@example.com";
        secondMember.age = 40;

        System.out.println(firstMember.email + " / " + firstMember.age);
        System.out.println(secondMember.email + " / " + secondMember.age);
    }
}

두 변수의 타입과 두 인스턴스의 필드 구조는 같지만 저장된 상태는 독립적입니다.

firstMember.age를 30으로 바꿔도 secondMember.age는 40으로 남습니다.

각각의 new가 다른 인스턴스를 만들고 다른 참조값을 반환했기 때문입니다.

HTML 다이어그램: /docs/java/ch4/ch4-2/4.html

디버깅할 때는 클래스 이름보다 참조 관계를 먼저 봅니다.

같은 Member 타입이라고 같은 객체는 아닙니다.

반대로 변수 이름이 다르더라도 같은 참조값이 복사되면 같은 객체를 바라볼 수 있습니다.

그 공유 문제는 기본형과 참조형을 비교할 때 직접 재현합니다.


연습 문제

다음 조건을 만족하는 프로그램을 작성하세요.

  1. Member 인스턴스를 두 개 만듭니다.
  2. 첫 회원은 이메일 "kim@example.com", 나이 25세, 활성 상태 true로 설정합니다.
  3. 둘째 회원은 이메일 "lee@example.com", 나이 40세, 활성 상태 false로 설정합니다.
  4. 두 인스턴스의 필드가 서로 영향을 주지 않는지 확인하기 위해 첫 회원의 나이를 30세로 바꾸고 둘 다 다시 출력합니다.
  5. Member shared = first;를 추가했을 때 shared.age = 99가 어느 인스턴스를 바꾸는지 실행 전에 예측합니다.
해설 보기
src/members/ch04/exercise/TwoMemberExercise.java
public class TwoMemberExercise {
    public static void main(String[] args) {
        Member first = new Member();
        first.email = "kim@example.com";
        first.age = 25;
        first.active = true;

        Member second = new Member();
        second.email = "lee@example.com";
        second.age = 40;
        second.active = false;

        first.age = 30;
        System.out.println(first.email + "=" + first.age);
        System.out.println(second.email + "=" + second.age);

        Member shared = first;
        shared.age = 99;
        System.out.println("first=" + first.age);
        System.out.println("second=" + second.age);
    }
}
kim@example.com=30
lee@example.com=40
first=99
second=40

첫 번째와 두 번째 new는 서로 다른 객체를 만들었습니다.

shared = first에서는 새 객체를 만들지 않고 첫 객체의 참조값을 복사했으므로 sharedfirst가 같은 필드를 읽고 씁니다.

이 결과를 아직 완전히 설명하기 어렵다면 정상입니다.

다음 두 절에서 객체 배열과 참조값 복사를 차례로 분해합니다.


클래스는 필드 구조이고 인스턴스는 new가 만든 실제 상태입니다.

변수에는 인스턴스 전체가 아니라 그 상태에 도달하는 참조값이 들어갑니다.

다음 절에서는 여러 참조를 배열에 넣을 때 메모리 구조가 어떻게 한 층 더 생기는지 살펴봅니다.