본문 바로가기

null과 참조 단절

필드 기본값, NullPointerException, 도달 가능성과 GC를 하나의 참조 경로로 설명합니다.

고정 크기 배열에 아직 회원을 채우지 않은 칸이 있거나, 객체의 참조형 필드를 초기화하지 않으면 null을 만나게 됩니다.

null은 빈 문자열이나 값이 0인 객체가 아닙니다.

“현재 어떤 인스턴스도 가리키지 않는다”는 특별한 참조값입니다.

문제는 null 자체보다 객체가 있다고 가정하고 점 연산을 적용할 때 생깁니다.

회원 저장소 CLI에서 members[i].email을 읽는다면 배열 참조, 배열 칸의 참조, 필드 접근이라는 경로가 모두 이어져야 합니다.

중간 참조가 null이면 JVM은 더 진행할 객체를 찾을 수 없습니다.


필드와 배열의 기본값

지역 변수는 읽기 전에 직접 초기화해야 하지만, 새 객체의 필드와 새 배열의 칸은 타입별 기본값으로 초기화됩니다.

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

        System.out.println("title=" + member.email);
        System.out.println("age=" + member.age);
        System.out.println("active=" + member.active);
        System.out.println("slot=" + members[0]);
    }
}
title=null
age=0
active=false
slot=null

String과 Member는 참조형이므로 기본값이 null입니다.

int는 0, boolean은 false입니다.

이 자동 초기화는 메모리에 예측할 수 없는 값이 남는 것을 막아 주지만, 객체가 유효하다는 보장은 아닙니다.

이메일이 반드시 있어야 하는 도메인이라면 null 이메일의 인스턴스가 생성된 순간부터 불완전한 상태입니다.

지금은 객체를 만든 뒤 필드를 직접 채우고 있습니다.

이후 생성자를 사용하면 필수 값을 생성 시점에 받을 수 있고, 접근 제어로 초기화되지 않은 객체를 만들기 어렵게 할 수 있습니다.


null 참조 예외

다음 예제는 의도적으로 NullPointerException을 만듭니다.

src/members/ch04/v8/NullDereference.java
public class NullDereference {
    public static void main(String[] args) {
        Member member = null;
        System.out.println(member.email);
    }
}
Exception in thread "main" java.lang.NullPointerException:
Cannot read field "email" because "member" is null
    at NullDereference.main(NullDereference.java:4)

이 출력은 상세 NPE 메시지의 한 예입니다. JVM 설정과 컴파일할 때 남긴 지역 변수 디버그 정보에 따라 member 대신 <local1> 같은 표기가 나올 수 있고, 줄 번호도 파일의 실제 배치에 따라 달라집니다. 문구를 그대로 외우기보다 실패한 필드 접근과 애플리케이션 소스 위치를 확인하세요.

메시지는 실패한 연산, null이었던 표현식, 소스 위치를 조사하는 단서입니다.

스택 트레이스를 위에서부터 읽고 애플리케이션 코드의 첫 줄로 이동합니다.

그 줄에서 점 왼쪽 값이 무엇인지 확인하면 조사 범위가 빠르게 줄어듭니다.

실패 줄에 members[index].email처럼 여러 접근이 있다면 한 번에 추측하지 말고 중간값을 나눠 봅니다.

디버깅을 위한 분해
Member[] members = new Member[3];
int index = 1;

Member selected = members[index];
System.out.println("selected=" + selected);
System.out.println(selected.email);

첫 출력이 null이면 배열과 인덱스 접근까지는 성공했고 해당 슬롯에 회원 참조가 없다는 뜻입니다.

members 자체가 null이면 첫 대입에서 이미 실패합니다.

인덱스가 잘못됐다면 NullPointerException이 아니라 ArrayIndexOutOfBoundsException이 발생합니다.

예외 종류는 경로의 어느 단계가 실패했는지를 알려 주는 단서입니다.


null 검사와 객체 규칙

NPE를 본 뒤 모든 점 연산 앞에 if (value != null)을 붙이면 당장의 예외는 사라질 수 있습니다.

하지만 이메일이 없는 회원을 조용히 건너뛰는 것이 올바른 동작인지는 별도 판단이 필요합니다.

침묵하는 회피
if (member != null) {
    System.out.println(member.email);
}

다음 세 상황은 처리 방식이 달라야 합니다.

  • 고정 배열의 사용하지 않은 슬롯: size 범위만 순회해 애초에 접근하지 않습니다.
  • 검색 결과가 없을 수 있음: 호출자에게 “없음”을 명시적으로 처리하게 합니다.
  • 반드시 있어야 하는 이메일이나 회원 정보: 생성 또는 입력 경계에서 거부하고 원인을 알립니다.

null 검사는 값 없음이 허용되는 경계에서만 의미가 있습니다.

필수 데이터가 사라진 버그를 숨기는 용도로 사용하면 오류가 더 뒤에서 잘못된 결과로 나타납니다.


참조 소실과 객체 수명

참조 변수에 null을 대입하면 변수가 가리키던 객체를 즉시 지우는 것이 아닙니다.

src/members/ch04/v8/Reachability.java
public class Reachability {
    public static void main(String[] args) {
        Member member = new Member();
        member.email = "[email protected]";

        Member alias = member;
        member = null;

        System.out.println(alias.email); // 여전히 접근 가능

        alias = null;
        // 이제 애플리케이션 코드에서 이전 객체에 도달할 참조가 없다.
    }
}

첫 member = null 뒤에도 alias가 같은 객체를 가리키므로 객체는 계속 사용 가능합니다.

alias까지 null이 되면 코드에서 그 객체의 참조를 다시 구할 경로가 없습니다.

이런 도달 불가능 객체는 가비지 컬렉터가 회수할 후보가 됩니다.

Reachability의 두 참조를 차례로 끊은 상태

참조 복사 후 member=null이어도 alias가 도달 경로를 유지하고 alias=null 후에만 경로가 끊깁니다. 즉시 GC 회수를 추가하지 않고 참조 단절의 세 시점을 보여 줄 수 있습니다.

Reachability의 두 참조를 차례로 끊은 상태

member에 null을 대입해도 alias로 객체를 읽을 수 있고, alias까지 null이 되면 이 코드에서 보관하던 두 경로가 모두 사라집니다.

한 변수의 null 대입은 객체를 즉시 지우지 않습니다

member의 참조를 끊어도 alias가 같은 객체에 도달합니다. 이 코드의 마지막 참조까지 끊기면 GC 회수 후보가 됩니다.

Reachability · 각 대입 직후 같은 첫 객체를 추적
대입 직후memberalias첫 객체에 도달하는 경로
Member alias = member;첫 객체 참조첫 객체 참조두 변수 모두 같은 객체에 도달
member = null;null첫 객체 참조alias.email로 [email protected] 출력 가능
alias = null;nullnull이 코드가 보관하던 참조 경로 없음
한 변수의 null 대입은 객체를 즉시 지우지 않습니다 같은첫객체temp를세시점반복표시. 최초member/alias2참조→membernull때alias1참조→모두null0참조. 마지막도달불가는GC후보이며즉시회수·물리배치보장아님. 참조 복사 직후 member 첫 객체 참조 alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 member = null 뒤 member null alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 alias = null 뒤 member null alias null 첫 Member 객체 [email protected] 도달 경로 없음 같은 첫 객체 · 시점별 표시 참조 단절 ≠ 즉시 회수
한 변수의 null 대입은 객체를 즉시 지우지 않습니다 같은첫객체temp를세시점반복표시. 최초member/alias2참조→membernull때alias1참조→모두null0참조. 마지막도달불가는GC후보이며즉시회수·물리배치보장아님. 참조 복사 직후 member 첫 객체 참조 alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 member = null 뒤 member null alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 alias = null 뒤 member null alias null 첫 Member 객체 [email protected] 도달 경로 없음 같은 첫 객체 · 시점별 표시 참조 단절 ≠ 즉시 회수
Member alias = member;

member와 alias 모두 같은 첫 객체를 참조합니다.

member = null;

member는 null이지만 alias는 첫 객체 참조를 유지합니다. alias.email로 [email protected]을 출력할 수 있습니다.

alias = null;

두 변수 모두 null입니다. 이 코드가 보관하던 첫 객체로의 참조 경로가 없습니다.

참조 단절은 즉시 삭제가 아닙니다. 도달 불가능해진 객체는 GC 회수 후보이며, 이 표는 실제 메모리 회수 시점이나 System.gc()의 강제 실행을 보장하지 않습니다.

이 예의 조건

  • 참조 단절은 즉시 메모리 회수를 관측한 사실이 아니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2같은 첫 객체 temp를 세 시점에 반복 표시한다. 최초 member/alias의 2개 참조→member가 null일 때 alias의 1개 참조→모두 null인 0개 참조이다. 마지막 도달 불가 상태는 GC 후보이며 즉시 회수·물리 배치를 보장하지 않는다.
원 도표의 설명과 주석
구분내용
설명같은 첫 객체 temp를 세 시점에 반복 표시한다. 최초 member/alias의 2개 참조→member가 null일 때 alias의 1개 참조→모두 null인 0개 참조이다. 마지막 도달 불가 상태는 GC 후보이며 즉시 회수·물리 배치를 보장하지 않는다.
전체 읽기 정보Reachability의 두 참조를 차례로 끊은 상태 Reachability의 두 참조를 차례로 끊은 상태 member에 null을 대입해도 alias로 객체를 읽을 수 있고, alias까지 null이 되면 이 코드에서 보관하던 두 경로가 모두 사라집니다. 한 변수의 null 대입은 객체를 즉시 지우지 않습니다 member의 참조를 끊어도 alias가 같은 객체에 도달합니다. 이 코드의 마지막 참조까지 끊기면 GC 회수 후보가 됩니다. Reachability · 각 대입 직후 같은 첫 객체를 추적 대입 직후 member alias 첫 객체에 도달하는 경로 Member alias = member; 첫 객체 참조 첫 객체 참조 두 변수 모두 같은 객체에 도달 member = null; null 첫 객체 참조 alias.email 로 [email protected] 출력 가능 alias = null; null null 이 코드가 보관하던 참조 경로 없음 Member alias = member; member 와 alias 모두 같은 첫 객체를 참조합니다. member = null; member 는 null 이지만 alias 는 첫 객체 참조를 유지합니다. alias.email 로 [email protected] 을 출력할 수 있습니다. alias = null; 두 변수 모두 null 입니다. 이 코드가 보관하던 첫 객체로의 참조 경로가 없습니다. 참조 단절은 즉시 삭제가 아닙니다. 도달 불가능해진 객체는 GC 회수 후보이며, 이 표는 실제 메모리 회수 시점이나 System.gc() 의 강제 실행을 보장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
별칭 생성

member와 alias가 같은 객체를 참조합니다.

  • member: 첫 객체
  • alias: 첫 객체
  • 도달 가능: 예
  • 참조값 복사가 새 객체를 만들지 않는다.
첫 참조 제거

member를 null로 바꿉니다.

  • member: null
  • alias: 첫 객체
  • 도달 가능: 예
  • 다른 경로가 남아 객체를 계속 사용할 수 있다.
마지막 참조 제거

alias도 null로 바꿉니다.

  • member: null
  • alias: null
  • 도달 가능: 아니오
  • 애플리케이션 참조 경로가 없어 GC 회수 후보가 된다.

Reachability의 두 참조를 차례로 끊은 상태

참조 복사 후 member=null이어도 alias가 도달 경로를 유지하고 alias=null 후에만 경로가 끊깁니다. 즉시 GC 회수를 추가하지 않고 참조 단절의 세 시점을 보여 줄 수 있습니다.

Reachability의 두 참조를 차례로 끊은 상태

member에 null을 대입해도 alias로 객체를 읽을 수 있고, alias까지 null이 되면 이 코드에서 보관하던 두 경로가 모두 사라집니다.

한 변수의 null 대입은 객체를 즉시 지우지 않습니다

member의 참조를 끊어도 alias가 같은 객체에 도달합니다. 이 코드의 마지막 참조까지 끊기면 GC 회수 후보가 됩니다.

Reachability · 각 대입 직후 같은 첫 객체를 추적
대입 직후memberalias첫 객체에 도달하는 경로
Member alias = member;첫 객체 참조첫 객체 참조두 변수 모두 같은 객체에 도달
member = null;null첫 객체 참조alias.email로 [email protected] 출력 가능
alias = null;nullnull이 코드가 보관하던 참조 경로 없음
한 변수의 null 대입은 객체를 즉시 지우지 않습니다 같은첫객체temp를세시점반복표시. 최초member/alias2참조→membernull때alias1참조→모두null0참조. 마지막도달불가는GC후보이며즉시회수·물리배치보장아님. 참조 복사 직후 member 첫 객체 참조 alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 member = null 뒤 member null alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 alias = null 뒤 member null alias null 첫 Member 객체 [email protected] 도달 경로 없음 같은 첫 객체 · 시점별 표시 참조 단절 ≠ 즉시 회수
한 변수의 null 대입은 객체를 즉시 지우지 않습니다 같은첫객체temp를세시점반복표시. 최초member/alias2참조→membernull때alias1참조→모두null0참조. 마지막도달불가는GC후보이며즉시회수·물리배치보장아님. 참조 복사 직후 member 첫 객체 참조 alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 member = null 뒤 member null alias 첫 객체 참조 첫 Member 객체 [email protected] 도달 가능 alias = null 뒤 member null alias null 첫 Member 객체 [email protected] 도달 경로 없음 같은 첫 객체 · 시점별 표시 참조 단절 ≠ 즉시 회수
Member alias = member;

member와 alias 모두 같은 첫 객체를 참조합니다.

member = null;

member는 null이지만 alias는 첫 객체 참조를 유지합니다. alias.email로 [email protected]을 출력할 수 있습니다.

alias = null;

두 변수 모두 null입니다. 이 코드가 보관하던 첫 객체로의 참조 경로가 없습니다.

참조 단절은 즉시 삭제가 아닙니다. 도달 불가능해진 객체는 GC 회수 후보이며, 이 표는 실제 메모리 회수 시점이나 System.gc()의 강제 실행을 보장하지 않습니다.

이 예의 조건

  • 참조 단절은 즉시 메모리 회수를 관측한 사실이 아니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2같은 첫 객체 temp를 세 시점에 반복 표시한다. 최초 member/alias의 2개 참조→member가 null일 때 alias의 1개 참조→모두 null인 0개 참조이다. 마지막 도달 불가 상태는 GC 후보이며 즉시 회수·물리 배치를 보장하지 않는다.
원 도표의 설명과 주석
구분내용
설명같은 첫 객체 temp를 세 시점에 반복 표시한다. 최초 member/alias의 2개 참조→member가 null일 때 alias의 1개 참조→모두 null인 0개 참조이다. 마지막 도달 불가 상태는 GC 후보이며 즉시 회수·물리 배치를 보장하지 않는다.
전체 읽기 정보Reachability의 두 참조를 차례로 끊은 상태 Reachability의 두 참조를 차례로 끊은 상태 member에 null을 대입해도 alias로 객체를 읽을 수 있고, alias까지 null이 되면 이 코드에서 보관하던 두 경로가 모두 사라집니다. 한 변수의 null 대입은 객체를 즉시 지우지 않습니다 member의 참조를 끊어도 alias가 같은 객체에 도달합니다. 이 코드의 마지막 참조까지 끊기면 GC 회수 후보가 됩니다. Reachability · 각 대입 직후 같은 첫 객체를 추적 대입 직후 member alias 첫 객체에 도달하는 경로 Member alias = member; 첫 객체 참조 첫 객체 참조 두 변수 모두 같은 객체에 도달 member = null; null 첫 객체 참조 alias.email 로 [email protected] 출력 가능 alias = null; null null 이 코드가 보관하던 참조 경로 없음 Member alias = member; member 와 alias 모두 같은 첫 객체를 참조합니다. member = null; member 는 null 이지만 alias 는 첫 객체 참조를 유지합니다. alias.email 로 [email protected] 을 출력할 수 있습니다. alias = null; 두 변수 모두 null 입니다. 이 코드가 보관하던 첫 객체로의 참조 경로가 없습니다. 참조 단절은 즉시 삭제가 아닙니다. 도달 불가능해진 객체는 GC 회수 후보이며, 이 표는 실제 메모리 회수 시점이나 System.gc() 의 강제 실행을 보장하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
별칭 생성

member와 alias가 같은 객체를 참조합니다.

  • member: 첫 객체
  • alias: 첫 객체
  • 도달 가능: 예
  • 참조값 복사가 새 객체를 만들지 않는다.
첫 참조 제거

member를 null로 바꿉니다.

  • member: null
  • alias: 첫 객체
  • 도달 가능: 예
  • 다른 경로가 남아 객체를 계속 사용할 수 있다.
마지막 참조 제거

alias도 null로 바꿉니다.

  • member: null
  • alias: null
  • 도달 가능: 아니오
  • 애플리케이션 참조 경로가 없어 GC 회수 후보가 된다.

“후보”라는 표현이 중요합니다.

alias = null을 실행한 직후 메모리가 반드시 회수되는 것은 아닙니다.

GC 실행 시점은 JVM이 결정합니다.

System.gc()를 호출해도 강제 실행 계약이 아니므로 프로그램의 정확성을 즉시 회수에 의존해서는 안 됩니다.


중첩 참조

회원 프로필에 운영 메모 객체가 있다고 가정해 봅시다.

src/members/ch04/v8/NestedNull.java
class ProfileNote {
    String text;
}

class MemberProfile {
    String email;
    ProfileNote note;
}

public class NestedNull {
    public static void main(String[] args) {
        MemberProfile member = new MemberProfile();
        member.email = "[email protected]";

        System.out.println(member.note.text);
    }
}

member는 실제 객체를 가리키므로 첫 점은 성공합니다.

그러나 note 필드의 기본값이 null이므로 두 번째 점에서 실패합니다.

해결은 member만 검사하는 것이 아니라 ProfileNote를 언제 만들어야 하는지 정하는 것입니다.

member.note = new ProfileNote();
member.note.text = "본인 확인 완료";

복잡한 표현에서 NPE가 나면 왼쪽부터 참조 경로를 그려 보세요.

a.b.c에서는 a와 a.b가 각각 null일 수 있습니다.

null 접근에서 성공한 읽기와 실패한 역참조

null 값 읽기·유효 배열 칸 읽기·member.note 읽기는 성공하고 이후 null 대상 필드 접근에서 각각 NPE가 납니다. 세 독립 예제의 첫 실패 위치를 접근 단계로 구분할 수 있습니다.

null 접근에서 성공한 읽기와 실패한 역참조

세 원문 예제에서 null 참조값 읽기까지는 가능하지만 그 참조로 필드를 읽는 지점에서 NPE가 발생합니다.

null 값 읽기와 객체 필드 접근은 다릅니다

세 독립 예제에서 마지막으로 성공한 읽기와 처음 실패하는 역참조를 비교합니다. 참조 경로를 왼쪽부터 확인합니다.

독립된 세 예제 · 왼쪽 참조 경로부터 확인
예제실패 전까지 읽은 값실패하는 필드 접근
NullDereferencemember의 값은 nullmember.email: 따라갈 Member가 없어 NPE
디버깅을 위한 분해길이 3인 배열의 1번 칸을 읽어 selected = null. selected=null 출력까지 성공selected.email: 그다음 필드 읽기에서 NPE
NestedNullmember는 MemberProfile 객체. member.note 읽기는 성공하고 결과는 nullmember.note.text: null인 note에서 text를 읽다 NPE
null 값 읽기와 객체 필드 접근은 다릅니다 A membernull읽기성공뒤.email NPE; B존재길이3배열1칸유효/selectednull출력성공뒤.email NPE;C member→MemberProfile/노트null읽기성공뒤.text NPE. 각예제독립. A · NullDereference B · 배열 읽기 분해 C · NestedNull member = null 값 읽기는 가능 member.email 접근 NPE 따라갈 객체 없음 배열은 있음 · 길이3 1번 칸 읽기는 유효 selected = null null 출력까지 성공 selected.email 접근 NPE 필드 읽기에서 실패 member MemberProfile 참조 MemberProfile note = null member.note 읽기는 성공 note 값: null 메모 객체 참조 없음 note.text 접근 NPE null에서 text 읽기 실패 독립 세 예제 · NPE는 NullPointerException · 화살표는 접근/평가 경로
null 값 읽기와 객체 필드 접근은 다릅니다 A membernull읽기성공뒤.email NPE; B존재길이3배열1칸유효/selectednull출력성공뒤.email NPE;C member→MemberProfile/노트null읽기성공뒤.text NPE. 각예제독립. A · NullDereference member = null 참조값 읽기 가능 member.email 필드 접근 NPE 발생 따라갈 Member 없음 B · 배열 읽기 분해 배열은 있음 · 길이 3 1번 칸 인덱스는 유효 selected = null selected=null 출력 성공 selected.email 접근 NPE 발생 필드 읽기에서 실패 C · NestedNull member 객체 참조값 MemberProfile note = null member.note 읽기는 성공 note 값: null 메모 객체 참조 없음 note.text 필드 접근 NPE 발생 null에서 text 읽기 실패 각 예제는 독립 실행 참조 읽기와 필드 역참조 구별
NullDereference

member의 값은 null입니다. member.email은 따라갈 Member가 없어 NPE가 납니다.

디버깅을 위한 분해

길이 3인 배열의 1번 칸을 읽어 selected = null을 얻습니다. selected=null 출력은 성공하지만 다음 selected.email에서 NPE가 납니다.

NestedNull

member는 MemberProfile 객체입니다. member.note 읽기는 성공하고 결과는 null입니다. member.note.text는 그 null 참조로 text를 읽는 지점에서 NPE가 납니다.

null을 읽거나 출력하는 것과, 그 값으로 객체의 필드에 접근하는 것은 다릅니다. 위 예제는 서로 독립된 코드이며 하나의 실행 순서가 아닙니다.

이 예의 조건

  • A,B,C는 독립 예제이며 NPE는 NullPointerException입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2A는 member의 null 읽기 성공 뒤 .email에서 NPE가 발생한다. B는 길이 3인 배열이 있고 1번 칸이 유효하여 selected의 null 출력 성공 뒤 .email에서 NPE가 발생한다. C는 member→MemberProfile/노트의 null 읽기 성공 뒤 .text에서 NPE가 발생한다. 각 예제는 독립적이다.
원 도표의 설명과 주석
구분내용
설명A는 member의 null 읽기 성공 뒤 .email에서 NPE가 발생한다. B는 길이 3인 배열이 있고 1번 칸이 유효하여 selected의 null 출력 성공 뒤 .email에서 NPE가 발생한다. C는 member→MemberProfile/노트의 null 읽기 성공 뒤 .text에서 NPE가 발생한다. 각 예제는 독립적이다.
전체 읽기 정보null 접근에서 성공한 읽기와 실패한 역참조 null 접근에서 성공한 읽기와 실패한 역참조 세 원문 예제에서 null 참조값 읽기까지는 가능하지만 그 참조로 필드를 읽는 지점에서 NPE가 발생합니다. null 값 읽기와 객체 필드 접근은 다릅니다 세 독립 예제에서 마지막으로 성공한 읽기와 처음 실패하는 역참조를 비교합니다. 참조 경로를 왼쪽부터 확인합니다. 독립된 세 예제 · 왼쪽 참조 경로부터 확인 예제 실패 전까지 읽은 값 실패하는 필드 접근 NullDereference member 의 값은 null member.email : 따라갈 Member가 없어 NPE 디버깅을 위한 분해 길이 3인 배열의 1번 칸을 읽어 selected = null . selected=null 출력까지 성공 selected.email : 그다음 필드 읽기에서 NPE NestedNull member 는 MemberProfile 객체. member.note 읽기는 성공하고 결과는 null member.note.text : null인 note에서 text를 읽다 NPE NullDereference member 의 값은 null 입니다. member.email 은 따라갈 Member가 없어 NPE가 납니다. 디버깅을 위한 분해 길이 3인 배열의 1번 칸을 읽어 selected = null 을 얻습니다. selected=null 출력은 성공하지만 다음 selected.email 에서 NPE가 납니다. NestedNull member 는 MemberProfile 객체입니다. member.note 읽기는 성공하고 결과는 null 입니다. member.note.text 는 그 null 참조로 text를 읽는 지점에서 NPE가 납니다. null 을 읽거나 출력하는 것과, 그 값으로 객체의 필드에 접근하는 것은 다릅니다. 위 예제는 서로 독립된 코드이며 하나의 실행 순서가 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
null 값 읽기

A의 member=null을 읽거나 B의 유효 배열 1번 칸을 읽습니다.

  • A: null 값 읽기 가능
  • B: length 3, selected null 출력 성공
  • null 값을 보관·출력하는 것 자체는 객체 필드 접근이 아닙니다.
필드 접근 실패

A와 B가 null.email을 읽으려 합니다.

  • 대상 객체: 없음
  • 결과: NPE
  • 따라갈 객체가 없을 때 필드 접근이 실패합니다.
중첩 접근 경계

C는 member.note 읽기 성공 뒤 null.text에서 실패합니다.

  • member: MemberProfile 있음
  • note: null, .text에서 NPE
  • 긴 접근식 중 어느 참조가 null인지 분해해야 합니다.

null 접근에서 성공한 읽기와 실패한 역참조

null 값 읽기·유효 배열 칸 읽기·member.note 읽기는 성공하고 이후 null 대상 필드 접근에서 각각 NPE가 납니다. 세 독립 예제의 첫 실패 위치를 접근 단계로 구분할 수 있습니다.

null 접근에서 성공한 읽기와 실패한 역참조

세 원문 예제에서 null 참조값 읽기까지는 가능하지만 그 참조로 필드를 읽는 지점에서 NPE가 발생합니다.

null 값 읽기와 객체 필드 접근은 다릅니다

세 독립 예제에서 마지막으로 성공한 읽기와 처음 실패하는 역참조를 비교합니다. 참조 경로를 왼쪽부터 확인합니다.

독립된 세 예제 · 왼쪽 참조 경로부터 확인
예제실패 전까지 읽은 값실패하는 필드 접근
NullDereferencemember의 값은 nullmember.email: 따라갈 Member가 없어 NPE
디버깅을 위한 분해길이 3인 배열의 1번 칸을 읽어 selected = null. selected=null 출력까지 성공selected.email: 그다음 필드 읽기에서 NPE
NestedNullmember는 MemberProfile 객체. member.note 읽기는 성공하고 결과는 nullmember.note.text: null인 note에서 text를 읽다 NPE
null 값 읽기와 객체 필드 접근은 다릅니다 A membernull읽기성공뒤.email NPE; B존재길이3배열1칸유효/selectednull출력성공뒤.email NPE;C member→MemberProfile/노트null읽기성공뒤.text NPE. 각예제독립. A · NullDereference B · 배열 읽기 분해 C · NestedNull member = null 값 읽기는 가능 member.email 접근 NPE 따라갈 객체 없음 배열은 있음 · 길이3 1번 칸 읽기는 유효 selected = null null 출력까지 성공 selected.email 접근 NPE 필드 읽기에서 실패 member MemberProfile 참조 MemberProfile note = null member.note 읽기는 성공 note 값: null 메모 객체 참조 없음 note.text 접근 NPE null에서 text 읽기 실패 독립 세 예제 · NPE는 NullPointerException · 화살표는 접근/평가 경로
null 값 읽기와 객체 필드 접근은 다릅니다 A membernull읽기성공뒤.email NPE; B존재길이3배열1칸유효/selectednull출력성공뒤.email NPE;C member→MemberProfile/노트null읽기성공뒤.text NPE. 각예제독립. A · NullDereference member = null 참조값 읽기 가능 member.email 필드 접근 NPE 발생 따라갈 Member 없음 B · 배열 읽기 분해 배열은 있음 · 길이 3 1번 칸 인덱스는 유효 selected = null selected=null 출력 성공 selected.email 접근 NPE 발생 필드 읽기에서 실패 C · NestedNull member 객체 참조값 MemberProfile note = null member.note 읽기는 성공 note 값: null 메모 객체 참조 없음 note.text 필드 접근 NPE 발생 null에서 text 읽기 실패 각 예제는 독립 실행 참조 읽기와 필드 역참조 구별
NullDereference

member의 값은 null입니다. member.email은 따라갈 Member가 없어 NPE가 납니다.

디버깅을 위한 분해

길이 3인 배열의 1번 칸을 읽어 selected = null을 얻습니다. selected=null 출력은 성공하지만 다음 selected.email에서 NPE가 납니다.

NestedNull

member는 MemberProfile 객체입니다. member.note 읽기는 성공하고 결과는 null입니다. member.note.text는 그 null 참조로 text를 읽는 지점에서 NPE가 납니다.

null을 읽거나 출력하는 것과, 그 값으로 객체의 필드에 접근하는 것은 다릅니다. 위 예제는 서로 독립된 코드이며 하나의 실행 순서가 아닙니다.

이 예의 조건

  • A,B,C는 독립 예제이며 NPE는 NullPointerException입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2A는 member의 null 읽기 성공 뒤 .email에서 NPE가 발생한다. B는 길이 3인 배열이 있고 1번 칸이 유효하여 selected의 null 출력 성공 뒤 .email에서 NPE가 발생한다. C는 member→MemberProfile/노트의 null 읽기 성공 뒤 .text에서 NPE가 발생한다. 각 예제는 독립적이다.
원 도표의 설명과 주석
구분내용
설명A는 member의 null 읽기 성공 뒤 .email에서 NPE가 발생한다. B는 길이 3인 배열이 있고 1번 칸이 유효하여 selected의 null 출력 성공 뒤 .email에서 NPE가 발생한다. C는 member→MemberProfile/노트의 null 읽기 성공 뒤 .text에서 NPE가 발생한다. 각 예제는 독립적이다.
전체 읽기 정보null 접근에서 성공한 읽기와 실패한 역참조 null 접근에서 성공한 읽기와 실패한 역참조 세 원문 예제에서 null 참조값 읽기까지는 가능하지만 그 참조로 필드를 읽는 지점에서 NPE가 발생합니다. null 값 읽기와 객체 필드 접근은 다릅니다 세 독립 예제에서 마지막으로 성공한 읽기와 처음 실패하는 역참조를 비교합니다. 참조 경로를 왼쪽부터 확인합니다. 독립된 세 예제 · 왼쪽 참조 경로부터 확인 예제 실패 전까지 읽은 값 실패하는 필드 접근 NullDereference member 의 값은 null member.email : 따라갈 Member가 없어 NPE 디버깅을 위한 분해 길이 3인 배열의 1번 칸을 읽어 selected = null . selected=null 출력까지 성공 selected.email : 그다음 필드 읽기에서 NPE NestedNull member 는 MemberProfile 객체. member.note 읽기는 성공하고 결과는 null member.note.text : null인 note에서 text를 읽다 NPE NullDereference member 의 값은 null 입니다. member.email 은 따라갈 Member가 없어 NPE가 납니다. 디버깅을 위한 분해 길이 3인 배열의 1번 칸을 읽어 selected = null 을 얻습니다. selected=null 출력은 성공하지만 다음 selected.email 에서 NPE가 납니다. NestedNull member 는 MemberProfile 객체입니다. member.note 읽기는 성공하고 결과는 null 입니다. member.note.text 는 그 null 참조로 text를 읽는 지점에서 NPE가 납니다. null 을 읽거나 출력하는 것과, 그 값으로 객체의 필드에 접근하는 것은 다릅니다. 위 예제는 서로 독립된 코드이며 하나의 실행 순서가 아닙니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
null 값 읽기

A의 member=null을 읽거나 B의 유효 배열 1번 칸을 읽습니다.

  • A: null 값 읽기 가능
  • B: length 3, selected null 출력 성공
  • null 값을 보관·출력하는 것 자체는 객체 필드 접근이 아닙니다.
필드 접근 실패

A와 B가 null.email을 읽으려 합니다.

  • 대상 객체: 없음
  • 결과: NPE
  • 따라갈 객체가 없을 때 필드 접근이 실패합니다.
중첩 접근 경계

C는 member.note 읽기 성공 뒤 null.text에서 실패합니다.

  • member: MemberProfile 있음
  • note: null, .text에서 NPE
  • 긴 접근식 중 어느 참조가 null인지 분해해야 합니다.

NPE 조사 순서

재현할 수 있는 NPE라면 다음 순서로 조사합니다.

  1. 스택 트레이스에서 애플리케이션의 첫 실패 줄을 찾습니다.
  2. 그 줄의 점·대괄호 접근을 중간 지역 변수로 분리합니다.
  3. null이 처음 생긴 대입, 반환, 배열 초기화 위치를 거슬러 올라갑니다.
  4. 값 없음이 정상 상태인지 버그인지 계약을 판단합니다.
  5. 정상이라면 분기나 반환 타입으로 드러내고, 버그라면 더 이른 생성·입력 경계에서 막습니다.

단순히 실패 줄에 null 검사를 넣는 것은 3번과 4번을 생략한 처방일 수 있습니다.

원인이 배열의 size 관리 오류라면 순회 경계를 고쳐야 하고, 필수 필드 초기화 누락이라면 생성 과정을 고쳐야 합니다.


연습 문제

길이 5인 배열에 실제 회원은 앞의 세 칸만 들어 있습니다.

이메일로 회원을 찾는 findByEmail을 작성하세요. 배열 자체와 앞의 size개 회원 참조, 각 회원의 이메일은 모두 null이 아니라고 가정합니다. 사용 구간이 잘 채워졌는지와 size 숫자가 범위 안인지가 서로 다른 전제임을 구분하세요.

  1. size를 매개변수로 받아 채운 범위만 순회합니다.
  2. 이메일이 같으면 해당 Member 참조를 반환합니다.
  3. 찾지 못하면 이번 절에서는 null을 반환합니다.
  4. 호출자는 반환값을 검사한 뒤에만 필드를 읽습니다.
  5. size가 배열 범위를 벗어나는 호출은 IllegalArgumentException으로 거부합니다.
해설 보기
src/members/ch04/exercise/FindMember.java
public class FindMember {
    public static void main(String[] args) {
        Member[] members = new Member[5];
        members[0] = create("[email protected]", 30);
        members[1] = create("[email protected]", 40);
        members[2] = create("[email protected]", 20);
        int size = 3;

        Member found = findByEmail(members, size, "[email protected]");
        if (found == null) {
            System.out.println("찾지 못했습니다.");
        } else {
            System.out.println(found.email + " / " + found.age + "세");
        }
    }

    static Member findByEmail(
        Member[] members,
        int size,
        String email
    ) {
        if (size < 0 || size > members.length) {
            throw new IllegalArgumentException("잘못된 회원 수: " + size);
        }
        for (int i = 0; i < size; i++) {
            Member member = members[i];
            if (member.email.equals(email)) {
                return member;
            }
        }
        return null;
    }

    static Member create(String email, int age) {
        Member member = new Member();
        member.email = email;
        member.age = age;
        return member;
    }
}

size 검사는 음수나 용량 초과를 막습니다. 여기에 “앞의 size개 칸에는 회원이 있다”는 전제가 함께 있어야 미사용 null 슬롯을 데이터처럼 읽지 않습니다. 범위 안인 size=3이어도 1번 칸이 null이면 이 메서드는 필드 접근에서 실패합니다. 잘못 채운 저장 상태를 숫자 범위 검사만으로 복구할 수는 없습니다.

findByEmail이 null을 반환할 수 있다는 사실을 호출자가 반드시 기억해야 한다는 단점도 보입니다.

이후 Optional을 배울 때 “없음”을 반환 타입에 드러내는 방법과 비교할 수 있습니다.


null은 객체가 없다는 참조값이고 NPE는 객체로 이어져야 할 경로가 중간에서 끊겼다는 신호입니다.

다음 절에서는 지금까지 배운 클래스, 객체 배열, 값 복사, 메서드 반환, null 경계를 하나의 실행 가능한 회원 저장소 CLI로 조립합니다.