본문 바로가기

static 변수: 모든 객체가 함께 쓰는 값

객체마다 따로 생기는 인스턴스 변수와 클래스에 하나만 있는 static 변수의 차이를 이해하고, 어떤 값을 static으로 두어야 하는지 판단합니다.

회원 객체가 지금까지 모두 몇 개 만들어졌는지 세고 싶다고 해 봅시다. 생성자가 실행될 때마다 숫자를 1씩 늘리면 될 것 같은데, 그 숫자를 어디에 두어야 할까요? 회원의 이메일이나 나이는 회원마다 다르니 각 객체에 두는 것이 맞습니다. 하지만 '지금까지 만든 회원 수'는 특정 회원 한 명의 정보가 아닙니다. 이 글에서는 이렇게 객체 하나가 아니라 클래스 전체에 속하는 값을 담는 static 변수를 다룹니다.

객체마다 세면 생기는 문제

먼저 생성 횟수를 일반 필드에 세어 보겠습니다. 다음 프로그램은 Member 객체를 세 개 만든 뒤, 각 객체의 count 필드를 출력합니다.

lab/InstanceCountBug.java
public final class InstanceCountBug {
    public static void main(String[] args) {
        Member first = new Member("[email protected]");
        Member second = new Member("[email protected]");
        Member third = new Member("[email protected]");

        System.out.println(first.count);
        System.out.println(second.count);
        System.out.println(third.count);
    }

    private static final class Member {
        String email;
        int count;

        Member(String email) {
            this.email = email;
            count++;
        }
    }
}
실행 결과(의도와 다름)
1
1
1

객체를 세 개 만들었으니 어딘가에서 3이 나오기를 기대했다면 결과가 이상해 보일 것입니다. 원인은 앞 글의 메모리 모델로 설명됩니다. count는 static이 붙지 않은 인스턴스 변수(instance variable)라서, new Member(...)를 실행할 때마다 힙에 생기는 새 객체 안에 count도 새로 생기고 기본값 0으로 시작합니다. 생성자의 count++는 방금 만든 그 객체의 count를 0에서 1로 올릴 뿐입니다. 결국 객체 세 개가 각자 자기 count를 한 번씩 올렸으니 셋 다 1이 됩니다.

문제는 값과 그 값을 담는 자리가 맞지 않는다는 데 있습니다. 회원의 나이는 회원마다 달라야 하므로 객체마다 하나씩 있는 것이 맞지만, 생성 횟수는 클래스 전체에서 단 하나여야 합니다. 객체 안에 둔 카운터로는 '모든 객체가 함께 보는 하나의 값'을 만들 수 없습니다.

static 변수는 클래스에 하나만 있다

필드 선언에 static을 붙이면 그 필드는 객체가 아니라 클래스에 속합니다. 이런 필드를 static 변수 또는 클래스 변수(class variable)라고 부릅니다. 객체를 몇 개 만들든 클래스 변수는 하나뿐이고, 모든 객체가 그 하나를 함께 씁니다.

src/StaticMemberCount.java
public final class StaticMemberCount {
    public static void main(String[] args) {
        new Member("[email protected]");
        new Member("[email protected]");
        new Member("[email protected]");

        System.out.println("created=" + Member.createdCount);
    }

    private static final class Member {
        private static int createdCount;
        private final String email;

        Member(String email) {
            this.email = email;
            createdCount++;
        }
    }
}
created=3

이번에는 생성자가 세 번 실행되는 동안 모두 같은 createdCount를 올렸으므로 값이 0, 1, 2, 3으로 이어집니다. 반면 email은 static이 없는 인스턴스 변수이므로 세 객체에 각각 따로 있습니다. main에서 만든 객체를 변수에 담지 않았는데도 개수를 셀 수 있었다는 점도 눈여겨보세요. createdCount는 어느 객체에도 들어 있지 않으므로, 객체 참조 없이 클래스 이름으로 Member.createdCount처럼 읽습니다.

static 변수도 인스턴스 변수처럼 따로 초기화하지 않으면 타입의 기본값으로 시작합니다. int라면 0입니다. 그래서 객체를 하나도 만들기 전에 Member.createdCount를 읽으면 0이 나옵니다. 객체가 없어도 읽을 수 있다는 것 자체가 이 값이 객체가 아니라 클래스에 속한다는 뜻입니다.

자바 문법은 first.createdCount처럼 객체 참조를 통해 static 변수를 읽는 것도 허용합니다. 하지만 이렇게 쓰면 마치 first 객체만의 값처럼 보여서 읽는 사람이 오해하기 쉽습니다. 심지어 참조가 null인 변수를 통해 읽어도 NullPointerException 없이 값이 나오는데, 컴파일러가 이 접근을 변수의 타입, 즉 클래스를 기준으로 처리하기 때문입니다. 그러니 static 변수는 항상 Member.createdCount처럼 클래스 이름으로 접근하는 것이 좋습니다. 컴파일할 때 -Xlint:static 옵션을 주면 javac가 객체를 통한 접근에 경고를 띄워 줍니다.

이 글의 예제는 모두 java 명령으로 한 번 실행되는 동안의 이야기입니다. 프로그램이 끝나면 static 변수도 함께 사라지므로, StaticMemberCount를 다시 실행하면 createdCount는 이전 실행의 3에서 이어지지 않고 0부터 다시 셉니다. 실행이 끝나도 남아야 하는 값은 파일 같은 곳에 따로 저장해야 하며, 그 방법은 24장에서 다룹니다.

인스턴스 변수와 static 변수를 함께 두기

실제 클래스에는 두 종류의 변수가 함께 있는 경우가 많습니다. 다음 프로그램의 Member는 회원마다 다른 email과 age를 인스턴스 변수로, 생성 횟수를 static 변수로 가집니다. 첫 번째 회원의 생일만 반영했을 때 무엇이 바뀌는지 보겠습니다.

src/InstanceAndStaticState.java
public final class InstanceAndStaticState {
    public static void main(String[] args) {
        Member first = new Member("[email protected]", 40);
        Member second = new Member("[email protected]", 50);

        first.celebrateBirthday();

        System.out.println(first.describe());
        System.out.println(second.describe());
        System.out.println("created=" + Member.createdCount());
    }

    private static final class Member {
        private static int createdCount;
        private final String email;
        private int age;

        Member(String email, int age) {
            this.email = email;
            this.age = age;
            createdCount++;
        }

        void celebrateBirthday() {
            if (age >= 120) throw new IllegalStateException("maximum age");
            age++;
        }

        String describe() {
            return email + "=" + age;
        }

        static int createdCount() {
            return createdCount;
        }
    }
}

first.celebrateBirthday()는 first가 가리키는 객체의 age만 40에서 41로 바꿉니다. second가 가리키는 객체는 힙에 따로 있는 다른 객체이므로 그 age는 50 그대로입니다. 생성 횟수는 생성자가 두 번 실행되면서 2가 되었고, 생일은 생성자를 다시 부르지 않으므로 2에서 변하지 않습니다.

한 회원의 age 변화와 클래스의 생성 누계

두 new로 createdCount=2가 된 후 first의 birthday만 age 40→41로 바뀌고 second 50/누계 2는 유지됩니다. 객체별 필드와 공유 누계의 실제 변화 대상을 비교할 수 있습니다.

한 회원의 age 변화와 클래스의 생성 누계

InstanceAndStaticState에서 두 객체 생성 후 첫 회원의 생일을 반영해도 둘째 age 50과 클래스 생성 누계 2는 그대로인 상태 비교다.

age는 객체별 값이고 createdCount는 공유 누계다

첫 회원의 생일은 첫 age만 바꾸며 두 번째 회원과 생성 누계는 그대로입니다.

InstanceAndStaticState · 동일 실행의 생일 호출 전후
시점first의 agesecond의 ageMember.createdCount
두 Member 생성 뒤40502
first.celebrateBirthday() 뒤4150 · 그대로2 · 그대로
age는 객체별 값이고 createdCount는 공유 누계다 Member클래스의createdCount2하나와서로다른Kim/Lee인스턴스;Kim생일40→41,Lee50유지/생성누계2유지.점선동일클래스소속,호출이나객체참조아님. 클래스 · 공유 상태 Member createdCount = 2 힙 · 다른 객체 두 개 first · [email protected] age: 40 → 41 생일은 자기 필드만 변경 second · [email protected] age: 50 유지 다른 객체의 필드 점선: 같은 클래스의 인스턴스 · 누계 2는 생일 뒤에도 유지
age는 객체별 값이고 createdCount는 공유 누계다 Member클래스의createdCount2하나와서로다른Kim/Lee인스턴스;Kim생일40→41,Lee50유지/생성누계2유지.점선동일클래스소속,호출이나객체참조아님. 클래스 · 공유 상태 Member createdCount = 2 힙 · 다른 객체 두 개 first · Kim age: 40 → 41 생일은 자기 age 변경 [email protected] second · Lee age: 50 유지 다른 객체의 필드 [email protected] 점선: 같은 클래스의 인스턴스 생일은 생성 누계 변경 안 함

InstanceAndStaticState · 같은 실행의 두 시점

두 Member 생성 뒤
first.age=40, second.age=50, Member.createdCount=2입니다.
first의 생일 호출 뒤
first.age=41만 바뀝니다. second.age=50과 Member.createdCount=2는 그대로입니다.

first는 [email protected], second는 [email protected]인 별개 객체입니다. createdCount는 생성자에서 증가한 누계이지 현재 살아 있는 객체 수가 아닙니다. 지역 참조가 사라지거나 객체가 회수 대상이 되어도 이 코드에는 누계를 자동 감소시키는 연산이 없습니다.

이 예의 조건

  • createdCount는 공유 생성 누계이며 생일 횟수가 아닙니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2Member 클래스의 createdCount 2 하나와 서로 다른 Kim/Lee 인스턴스를 구분한다. Kim의 생일은 40→41, Lee는 50 유지/생성 누계 2 유지이다. 점선은 동일 클래스 소속이며 호출이나 객체 참조가 아니다.
원 도표의 설명과 주석
구분내용
설명Member 클래스의 createdCount 2 하나와 서로 다른 Kim/Lee 인스턴스를 구분한다. Kim의 생일은 40→41, Lee는 50 유지/생성 누계 2 유지이다. 점선은 동일 클래스 소속이며 호출이나 객체 참조가 아니다.
전체 읽기 정보한 회원의 age 변화와 클래스의 생성 누계 한 회원의 age 변화와 클래스의 생성 누계 InstanceAndStaticState에서 두 객체 생성 후 첫 회원의 생일을 반영해도 둘째 age 50과 클래스 생성 누계 2는 그대로인 상태 비교다. age는 객체별 값이고 createdCount는 공유 누계다 첫 회원의 생일은 첫 age만 바꾸며 두 번째 회원과 생성 누계는 그대로입니다. InstanceAndStaticState · 동일 실행의 생일 호출 전후 시점 first의 age second의 age Member.createdCount 두 Member 생성 뒤 40 50 2 first.celebrateBirthday() 뒤 41 50 · 그대로 2 · 그대로 InstanceAndStaticState · 같은 실행의 두 시점 두 Member 생성 뒤 first.age=40 , second.age=50 , Member.createdCount=2 입니다. first의 생일 호출 뒤 first.age=41 만 바뀝니다. second.age=50 과 Member.createdCount=2 는 그대로입니다. first 는 [email protected] , second 는 [email protected] 인 별개 객체입니다. createdCount 는 생성자에서 증가한 누계이지 현재 살아 있는 객체 수가 아닙니다. 지역 참조가 사라지거나 객체가 회수 대상이 되어도 이 코드에는 누계를 자동 감소시키는 연산이 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 생성

Kim 40과 Lee 50의 객체를 만듭니다.

  • createdCount: 2
  • 각 age: 40,50
  • 공유 누계와 각 객체 필드는 다른 저장 위치입니다.
첫 생일

Kim의 생일이 자기 age를 증가시킵니다.

  • Kim: 40→41
  • Lee: 50 유지
  • 인스턴스 행동이 다른 객체 필드를 바꾸지 않습니다.
누계 확인

생일 뒤에도 클래스 누계는 2입니다.

  • createdCount: 2 유지
  • 개별 상태: Kim 41,Lee 50
  • 같은 클래스 소속 표시는 객체 간 공유 age 참조가 아닙니다.

한 회원의 age 변화와 클래스의 생성 누계

두 new로 createdCount=2가 된 후 first의 birthday만 age 40→41로 바뀌고 second 50/누계 2는 유지됩니다. 객체별 필드와 공유 누계의 실제 변화 대상을 비교할 수 있습니다.

한 회원의 age 변화와 클래스의 생성 누계

InstanceAndStaticState에서 두 객체 생성 후 첫 회원의 생일을 반영해도 둘째 age 50과 클래스 생성 누계 2는 그대로인 상태 비교다.

age는 객체별 값이고 createdCount는 공유 누계다

첫 회원의 생일은 첫 age만 바꾸며 두 번째 회원과 생성 누계는 그대로입니다.

InstanceAndStaticState · 동일 실행의 생일 호출 전후
시점first의 agesecond의 ageMember.createdCount
두 Member 생성 뒤40502
first.celebrateBirthday() 뒤4150 · 그대로2 · 그대로
age는 객체별 값이고 createdCount는 공유 누계다 Member클래스의createdCount2하나와서로다른Kim/Lee인스턴스;Kim생일40→41,Lee50유지/생성누계2유지.점선동일클래스소속,호출이나객체참조아님. 클래스 · 공유 상태 Member createdCount = 2 힙 · 다른 객체 두 개 first · [email protected] age: 40 → 41 생일은 자기 필드만 변경 second · [email protected] age: 50 유지 다른 객체의 필드 점선: 같은 클래스의 인스턴스 · 누계 2는 생일 뒤에도 유지
age는 객체별 값이고 createdCount는 공유 누계다 Member클래스의createdCount2하나와서로다른Kim/Lee인스턴스;Kim생일40→41,Lee50유지/생성누계2유지.점선동일클래스소속,호출이나객체참조아님. 클래스 · 공유 상태 Member createdCount = 2 힙 · 다른 객체 두 개 first · Kim age: 40 → 41 생일은 자기 age 변경 [email protected] second · Lee age: 50 유지 다른 객체의 필드 [email protected] 점선: 같은 클래스의 인스턴스 생일은 생성 누계 변경 안 함

InstanceAndStaticState · 같은 실행의 두 시점

두 Member 생성 뒤
first.age=40, second.age=50, Member.createdCount=2입니다.
first의 생일 호출 뒤
first.age=41만 바뀝니다. second.age=50과 Member.createdCount=2는 그대로입니다.

first는 [email protected], second는 [email protected]인 별개 객체입니다. createdCount는 생성자에서 증가한 누계이지 현재 살아 있는 객체 수가 아닙니다. 지역 참조가 사라지거나 객체가 회수 대상이 되어도 이 코드에는 누계를 자동 감소시키는 연산이 없습니다.

이 예의 조건

  • createdCount는 공유 생성 누계이며 생일 횟수가 아닙니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2Member 클래스의 createdCount 2 하나와 서로 다른 Kim/Lee 인스턴스를 구분한다. Kim의 생일은 40→41, Lee는 50 유지/생성 누계 2 유지이다. 점선은 동일 클래스 소속이며 호출이나 객체 참조가 아니다.
원 도표의 설명과 주석
구분내용
설명Member 클래스의 createdCount 2 하나와 서로 다른 Kim/Lee 인스턴스를 구분한다. Kim의 생일은 40→41, Lee는 50 유지/생성 누계 2 유지이다. 점선은 동일 클래스 소속이며 호출이나 객체 참조가 아니다.
전체 읽기 정보한 회원의 age 변화와 클래스의 생성 누계 한 회원의 age 변화와 클래스의 생성 누계 InstanceAndStaticState에서 두 객체 생성 후 첫 회원의 생일을 반영해도 둘째 age 50과 클래스 생성 누계 2는 그대로인 상태 비교다. age는 객체별 값이고 createdCount는 공유 누계다 첫 회원의 생일은 첫 age만 바꾸며 두 번째 회원과 생성 누계는 그대로입니다. InstanceAndStaticState · 동일 실행의 생일 호출 전후 시점 first의 age second의 age Member.createdCount 두 Member 생성 뒤 40 50 2 first.celebrateBirthday() 뒤 41 50 · 그대로 2 · 그대로 InstanceAndStaticState · 같은 실행의 두 시점 두 Member 생성 뒤 first.age=40 , second.age=50 , Member.createdCount=2 입니다. first의 생일 호출 뒤 first.age=41 만 바뀝니다. second.age=50 과 Member.createdCount=2 는 그대로입니다. first 는 [email protected] , second 는 [email protected] 인 별개 객체입니다. createdCount 는 생성자에서 증가한 누계이지 현재 살아 있는 객체 수가 아닙니다. 지역 참조가 사라지거나 객체가 회수 대상이 되어도 이 코드에는 누계를 자동 감소시키는 연산이 없습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
두 생성

Kim 40과 Lee 50의 객체를 만듭니다.

  • createdCount: 2
  • 각 age: 40,50
  • 공유 누계와 각 객체 필드는 다른 저장 위치입니다.
첫 생일

Kim의 생일이 자기 age를 증가시킵니다.

  • Kim: 40→41
  • Lee: 50 유지
  • 인스턴스 행동이 다른 객체 필드를 바꾸지 않습니다.
누계 확인

생일 뒤에도 클래스 누계는 2입니다.

  • createdCount: 2 유지
  • 개별 상태: Kim 41,Lee 50
  • 같은 클래스 소속 표시는 객체 간 공유 age 참조가 아닙니다.

이번에는 createdCount 필드를 직접 읽지 않고 Member.createdCount() 메서드로 읽었습니다. 이 메서드에도 static이 붙어 있어서 객체 없이 클래스 이름으로 호출할 수 있습니다. static 메서드가 무엇을 할 수 있고 무엇을 할 수 없는지는 다음 글에서 자세히 다룹니다.

static 변수는 언제 생기고 언제까지 남을까

두 종류의 변수는 수명도 다릅니다. 인스턴스 변수는 객체와 함께 태어나고, 객체가 어떤 참조로도 닿지 않게 되면 객체와 함께 가비지 컬렉션 대상이 됩니다. 반면 static 변수는 클래스가 처음 쓰일 준비를 할 때 만들어집니다. JVM이 클래스를 불러오고 초기화하는 이 과정은 객체를 하나라도 만들기 전에 이루어지며, 그 뒤로는 일반적인 프로그램에서 실행이 끝날 때까지 남아 있다고 보면 됩니다. 클래스를 불러오고 초기화하는 과정은 클래스 로딩과 초기화를 다루는 심화 글에서 자세히 설명합니다.

구분인스턴스 변수static 변수
몇 개 있나객체마다 하나클래스에 하나
언제 생기나new로 객체를 만들 때클래스가 처음 쓰일 준비를 할 때
언제까지 남나객체가 회수될 때까지일반적으로 프로그램이 끝날 때까지
어떻게 접근하나member.age처럼 객체 참조로Member.createdCount처럼 클래스 이름으로

이 차이 때문에 생기는 오해가 하나 있습니다. 위의 createdCount는 지금까지 생성자가 실행된 누적 횟수이지, 지금 살아 있는 회원 수가 아닙니다. 회원 객체가 더 이상 쓰이지 않아 가비지 컬렉션 대상이 되어도 createdCount를 줄이는 코드는 어디에도 없습니다. 그렇다고 객체가 회수될 때 카운터를 줄이게 만들어도 정확한 값은 얻을 수 없습니다. 앞 글에서 본 대로 객체가 언제 회수될지는 JVM이 정하므로, 이미 쓰지 않는 객체도 회수되기 전까지는 계속 세어지기 때문입니다. 정말로 '지금 등록된 회원 수'가 필요하다면, 저장소가 회원을 추가하고 삭제할 때 직접 세는 것이 정확합니다.

더 깊이: 클래스 로더마다 따로 생기는 static 변수

'클래스에 하나'라는 말은 정확히 하면 'JVM이 구분하는 클래스 하나에 하나'입니다. JVM은 클래스를 이름만으로 구분하지 않고, 그 클래스를 정의한 클래스 로더(class loader)와 이름을 함께 묶어 구분합니다(JVMS 5.3). 그래서 같은 .class 파일이라도 서로 다른 클래스 로더가 각각 불러오면 JVM 안에서는 두 개의 다른 클래스가 되고, static 변수도 두 벌 생깁니다. 웹 애플리케이션 서버나 플러그인 시스템처럼 클래스 로더를 여러 개 쓰는 환경에서 'static인데 값이 공유되지 않는' 현상이 이 때문에 일어납니다. 이 장의 예제처럼 클래스 로더를 따로 다루지 않는 프로그램에서는 클래스 하나에 static 변수가 하나라고 생각해도 됩니다. 수명에 '일반적으로'라는 말을 붙인 것도 클래스 로더 때문입니다. 클래스는 그것을 정의한 클래스 로더가 가비지 컬렉션될 수 있을 때에만 메모리에서 내려갈(unload) 수 있습니다. 평범한 애플리케이션의 클래스를 불러오는 기본 클래스 로더는 프로그램이 끝날 때까지 쓰이므로, 그 클래스의 static 변수도 끝까지 남습니다.

한편 앞 글의 메모리 모델에서는 static 변수를 클래스 정보와 함께 메서드 영역 쪽에 그렸습니다. 이 그림은 '클래스에 하나'라는 성질을 나타낸 것이고, 실제 위치는 JVM 구현이 정합니다. 현재의 HotSpot JVM은 static 변수의 값을 일반 Java 힙에 둡니다.

static 변수를 아무 데서나 바꿀 수 있으면

static 변수는 클래스 이름만 알면 접근할 수 있으므로 편리합니다. 그런데 바로 그 점이 위험이 되기도 합니다. createdCount가 public이라면 프로그램의 어느 코드든 다음 한 줄로 카운터를 바꿀 수 있습니다.

Member.createdCount = 0;

회원 가입 기능이 회원 수를 세는 동안 통계 기능이 이 줄을 실행하면, 가입 기능이 보는 값도 함께 0이 됩니다. 값을 바꾼 코드와 영향을 받는 코드가 서로 멀리 떨어져 있어서, 숫자가 틀렸을 때 어디서 바뀌었는지 찾기도 어렵습니다. 그래서 변경할 수 있는 static 변수는 private으로 숨기고, 밖에서는 읽기 메서드처럼 허용할 동작만 메서드로 열어 두는 것이 좋습니다. 앞의 InstanceAndStaticState가 createdCount 필드는 private으로 두고 createdCount() 메서드로만 읽게 한 것도 이 때문입니다.

private으로 숨겨도 남는 문제가 있습니다. 공유가 필요 없는 값을 static으로 만들면, 서로 독립적이어야 할 객체들이 원하지 않는 값을 함께 쓰게 됩니다. 예를 들어 회원 저장소마다 회원 번호를 1번부터 따로 발급해야 한다고 해 봅시다. 이때 번호 카운터는 저장소마다 있어야 하므로 인스턴스 변수여야 합니다.

src/PerRegistrySequence.java
public final class PerRegistrySequence {
    public static void main(String[] args) {
        MemberRegistry personal = new MemberRegistry();
        MemberRegistry work = new MemberRegistry();

        System.out.println(personal.nextId());
        System.out.println(personal.nextId());
        System.out.println(work.nextId());
    }

    private static final class MemberRegistry {
        private int sequence;

        int nextId() {
            sequence++;
            return sequence;
        }
    }
}
1
2
1

personal과 work는 각자 자기 sequence를 가지므로, personal에서 두 번 발급한 뒤에도 work의 첫 번호는 1입니다. 그런데 선언에 static 한 단어만 더해 private static int sequence;로 바꾼 프로그램을 따로 실행하면 출력은 1, 2, 3이 됩니다. 두 저장소가 하나의 sequence를 함께 쓰므로 work의 첫 호출이 personal이 남긴 2에서 이어지기 때문입니다.

레지스트리별 sequence와 static 파생 비교

같은 세 호출에서 인스턴스 sequence는 1·2·1, 별도 static 버전은 1·2·3을 반환합니다. 두 독립 프로그램의 지역/공유 상태를 대응 단계로 비교할 수 있습니다.

레지스트리별 sequence와 static 파생 비교

PerRegistrySequence의 실제 1,2,1 결과와 sequence 선언만 static으로 바꾼 독립 파생 프로그램의 1,2,3 결과를 같은 세 호출로 비교한다.

인스턴스 필드는 저장소마다 독립 번호를 만든다

같은 세 호출을 비교하면 원문은 1·2·1, static 파생 프로그램은 1·2·3입니다.

PerRegistrySequence · 같은 호출 순서, 서로 다른 두 프로그램
호출원문: private int sequence파생 비교: private static int sequence
첫 personal.nextId()1 · personal의 필드1 · 클래스 공유 필드
둘째 personal.nextId()2 · personal의 필드2 · 같은 공유 필드
첫 work.nextId()1 · work의 독립 필드3 · 앞 호출의 누계가 이어짐
인스턴스 필드는 저장소마다 독립 번호를 만든다 원인스턴스필드와static파생형각초기0독립실행;동일personal두번/work한번의반환1,2,1vs1,2,3;다른실행의상태이어붙이지않음. 인스턴스 필드 원문 personal.nextId() 반환값 1 personal=1, work=0 각 객체의 필드 personal.nextId() 반환값 2 personal=2, work=0 각 객체의 필드 work.nextId() 반환값 1 personal=2, work=1 각 객체의 필드 static 파생 프로그램 personal.nextId() 반환값 1 클래스 sequence=1 한 공유 값 personal.nextId() 반환값 2 클래스 sequence=2 한 공유 값 work.nextId() 반환값 3 클래스 sequence=3 한 공유 값 같은 호출 순서 · 독립 실행, 각 초기값은 0
인스턴스 필드는 저장소마다 독립 번호를 만든다 원인스턴스필드와static파생형각초기0독립실행;동일personal두번/work한번의반환1,2,1vs1,2,3;다른실행의상태이어붙이지않음. 인스턴스 필드 원문 personal.nextId() 반환값 1 personal=1 work=0 personal.nextId() 반환값 2 personal=2 work=0 work.nextId() 반환값 1 personal=2 work=1 static 파생 프로그램 personal.nextId() 반환값 1 클래스 sequence=1 한 공유 값 personal.nextId() 반환값 2 클래스 sequence=2 한 공유 값 work.nextId() 반환값 3 클래스 sequence=3 한 공유 값 두 프로그램은 독립 실행 각 실행의 초기 sequence=0

PerRegistrySequence · 서로 다른 두 프로그램 비교

  1. 첫 personal.nextId(): 원문은 personal의 필드가 1, static 파생형은 클래스 공유 필드가 1입니다.
  2. 둘째 personal.nextId(): 원문은 personal의 필드가 2, static 파생형은 같은 공유 필드가 2입니다.
  3. 첫 work.nextId(): 원문은 work의 독립 필드에서 1, static 파생형은 공유 누계가 이어져 3입니다.

static 파생 비교는 원문 실행 결과가 아닙니다. sequence 선언에 static만 추가하고 나머지 세 호출은 유지한 별도 파생 비교입니다. 두 실행의 상태를 서로 이어 붙이지 않습니다.

이 예의 조건

  • 인스턴스 원문과 static 파생형은 각 초기 0의 독립 실행입니다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2원 인스턴스 필드와 static 파생형은 각각 초기 0의 독립 실행이다. 동일한 personal 두 번/work 한 번의 반환은 1,2,1과 1,2,3이며 다른 실행의 상태를 이어 붙이지 않는다.
29같은 호출 순서 · 독립 실행, 각 초깃값은 0
원 도표의 설명과 주석
구분내용
설명원 인스턴스 필드와 static 파생형은 각각 초기 0의 독립 실행이다. 동일한 personal 두 번/work 한 번의 반환은 1,2,1과 1,2,3이며 다른 실행의 상태를 이어 붙이지 않는다.
전체 읽기 정보레지스트리별 sequence와 static 파생 비교 레지스트리별 sequence와 static 파생 비교 PerRegistrySequence의 실제 1,2,1 결과와 sequence 선언만 static으로 바꾼 독립 파생 프로그램의 1,2,3 결과를 같은 세 호출로 비교한다. 인스턴스 필드는 저장소마다 독립 번호를 만든다 같은 세 호출을 비교하면 원문은 1·2·1, static 파생 프로그램은 1·2·3입니다. PerRegistrySequence · 같은 호출 순서, 서로 다른 두 프로그램 호출 원문: private int sequence 파생 비교: private static int sequence 첫 personal.nextId() 1 · personal의 필드 1 · 클래스 공유 필드 둘째 personal.nextId() 2 · personal의 필드 2 · 같은 공유 필드 첫 work.nextId() 1 · work의 독립 필드 3 · 앞 호출의 누계가 이어짐 PerRegistrySequence · 서로 다른 두 프로그램 비교 첫 personal.nextId(): 원문은 personal의 필드가 1, static 파생형은 클래스 공유 필드가 1입니다. 둘째 personal.nextId(): 원문은 personal의 필드가 2, static 파생형은 같은 공유 필드가 2입니다. 첫 work.nextId(): 원문은 work의 독립 필드에서 1 , static 파생형은 공유 누계가 이어져 3 입니다. static 파생 비교는 원문 실행 결과가 아닙니다. sequence 선언에 static 만 추가하고 나머지 세 호출은 유지한 별도 파생 비교입니다. 두 실행의 상태를 서로 이어 붙이지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 personal

personal.nextId를 처음 호출합니다.

  • instance: 1,personal1/work0
  • static: 1, 클래스 1
  • 첫 호출만으로 공유 범위를 구별할 수 없습니다.
둘째 personal

personal을 다시 호출합니다.

  • instance: 2,personal2/work0
  • static: 2, 클래스 2
  • 동일 객체 호출은 각 경로에서 해당 번호를 늘립니다.
work 호출

다른 work 객체를 처음 호출합니다.

  • instance: 1,personal2/work1
  • static: 3, 클래스 3
  • 공유 필드이면 다른 객체도 같은 누계를 이어갑니다.

레지스트리별 sequence와 static 파생 비교

같은 세 호출에서 인스턴스 sequence는 1·2·1, 별도 static 버전은 1·2·3을 반환합니다. 두 독립 프로그램의 지역/공유 상태를 대응 단계로 비교할 수 있습니다.

레지스트리별 sequence와 static 파생 비교

PerRegistrySequence의 실제 1,2,1 결과와 sequence 선언만 static으로 바꾼 독립 파생 프로그램의 1,2,3 결과를 같은 세 호출로 비교한다.

인스턴스 필드는 저장소마다 독립 번호를 만든다

같은 세 호출을 비교하면 원문은 1·2·1, static 파생 프로그램은 1·2·3입니다.

PerRegistrySequence · 같은 호출 순서, 서로 다른 두 프로그램
호출원문: private int sequence파생 비교: private static int sequence
첫 personal.nextId()1 · personal의 필드1 · 클래스 공유 필드
둘째 personal.nextId()2 · personal의 필드2 · 같은 공유 필드
첫 work.nextId()1 · work의 독립 필드3 · 앞 호출의 누계가 이어짐
인스턴스 필드는 저장소마다 독립 번호를 만든다 원인스턴스필드와static파생형각초기0독립실행;동일personal두번/work한번의반환1,2,1vs1,2,3;다른실행의상태이어붙이지않음. 인스턴스 필드 원문 personal.nextId() 반환값 1 personal=1, work=0 각 객체의 필드 personal.nextId() 반환값 2 personal=2, work=0 각 객체의 필드 work.nextId() 반환값 1 personal=2, work=1 각 객체의 필드 static 파생 프로그램 personal.nextId() 반환값 1 클래스 sequence=1 한 공유 값 personal.nextId() 반환값 2 클래스 sequence=2 한 공유 값 work.nextId() 반환값 3 클래스 sequence=3 한 공유 값 같은 호출 순서 · 독립 실행, 각 초기값은 0
인스턴스 필드는 저장소마다 독립 번호를 만든다 원인스턴스필드와static파생형각초기0독립실행;동일personal두번/work한번의반환1,2,1vs1,2,3;다른실행의상태이어붙이지않음. 인스턴스 필드 원문 personal.nextId() 반환값 1 personal=1 work=0 personal.nextId() 반환값 2 personal=2 work=0 work.nextId() 반환값 1 personal=2 work=1 static 파생 프로그램 personal.nextId() 반환값 1 클래스 sequence=1 한 공유 값 personal.nextId() 반환값 2 클래스 sequence=2 한 공유 값 work.nextId() 반환값 3 클래스 sequence=3 한 공유 값 두 프로그램은 독립 실행 각 실행의 초기 sequence=0

PerRegistrySequence · 서로 다른 두 프로그램 비교

  1. 첫 personal.nextId(): 원문은 personal의 필드가 1, static 파생형은 클래스 공유 필드가 1입니다.
  2. 둘째 personal.nextId(): 원문은 personal의 필드가 2, static 파생형은 같은 공유 필드가 2입니다.
  3. 첫 work.nextId(): 원문은 work의 독립 필드에서 1, static 파생형은 공유 누계가 이어져 3입니다.

static 파생 비교는 원문 실행 결과가 아닙니다. sequence 선언에 static만 추가하고 나머지 세 호출은 유지한 별도 파생 비교입니다. 두 실행의 상태를 서로 이어 붙이지 않습니다.

이 예의 조건

  • 인스턴스 원문과 static 파생형은 각 초기 0의 독립 실행입니다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2원 인스턴스 필드와 static 파생형은 각각 초기 0의 독립 실행이다. 동일한 personal 두 번/work 한 번의 반환은 1,2,1과 1,2,3이며 다른 실행의 상태를 이어 붙이지 않는다.
29같은 호출 순서 · 독립 실행, 각 초깃값은 0
원 도표의 설명과 주석
구분내용
설명원 인스턴스 필드와 static 파생형은 각각 초기 0의 독립 실행이다. 동일한 personal 두 번/work 한 번의 반환은 1,2,1과 1,2,3이며 다른 실행의 상태를 이어 붙이지 않는다.
전체 읽기 정보레지스트리별 sequence와 static 파생 비교 레지스트리별 sequence와 static 파생 비교 PerRegistrySequence의 실제 1,2,1 결과와 sequence 선언만 static으로 바꾼 독립 파생 프로그램의 1,2,3 결과를 같은 세 호출로 비교한다. 인스턴스 필드는 저장소마다 독립 번호를 만든다 같은 세 호출을 비교하면 원문은 1·2·1, static 파생 프로그램은 1·2·3입니다. PerRegistrySequence · 같은 호출 순서, 서로 다른 두 프로그램 호출 원문: private int sequence 파생 비교: private static int sequence 첫 personal.nextId() 1 · personal의 필드 1 · 클래스 공유 필드 둘째 personal.nextId() 2 · personal의 필드 2 · 같은 공유 필드 첫 work.nextId() 1 · work의 독립 필드 3 · 앞 호출의 누계가 이어짐 PerRegistrySequence · 서로 다른 두 프로그램 비교 첫 personal.nextId(): 원문은 personal의 필드가 1, static 파생형은 클래스 공유 필드가 1입니다. 둘째 personal.nextId(): 원문은 personal의 필드가 2, static 파생형은 같은 공유 필드가 2입니다. 첫 work.nextId(): 원문은 work의 독립 필드에서 1 , static 파생형은 공유 누계가 이어져 3 입니다. static 파생 비교는 원문 실행 결과가 아닙니다. sequence 선언에 static 만 추가하고 나머지 세 호출은 유지한 별도 파생 비교입니다. 두 실행의 상태를 서로 이어 붙이지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
첫 personal

personal.nextId를 처음 호출합니다.

  • instance: 1,personal1/work0
  • static: 1, 클래스 1
  • 첫 호출만으로 공유 범위를 구별할 수 없습니다.
둘째 personal

personal을 다시 호출합니다.

  • instance: 2,personal2/work0
  • static: 2, 클래스 2
  • 동일 객체 호출은 각 경로에서 해당 번호를 늘립니다.
work 호출

다른 work 객체를 처음 호출합니다.

  • instance: 1,personal2/work1
  • static: 3, 클래스 3
  • 공유 필드이면 다른 객체도 같은 누계를 이어갑니다.

두 프로그램에서 personal.nextId()를 한 번 더 호출하면 차이가 더 분명해집니다. 인스턴스 변수 버전은 personal의 sequence만 2에서 3으로 올라 3을 출력하고, static 버전은 공유 값이 3에서 4로 올라 4를 출력합니다. 두 결과는 같은 프로그램을 이어서 실행한 것이 아니라, 필드 선언만 다른 두 프로그램을 각각 처음부터 실행해 비교한 것입니다. 첫 호출에서는 둘 다 1이 나와 구분할 수 없고, 두 번째 저장소를 쓰기 시작해야 차이가 드러난다는 점도 기억해 두세요. 테스트가 저장소 하나만으로 끝난다면 static을 잘못 붙인 실수를 놓치기 쉽습니다.

무엇을 static으로 둘까

정리하면, static으로 둘지는 '어디서나 쓰기 편한가'가 아니라 '이 값이 객체 하나의 상태인가, 클래스 전체의 상태인가'로 정합니다. 회원 저장소 예제에서 클래스 전체에 하나만 있어도 의미가 흔들리지 않는 것은 다음과 같습니다.

  • 모든 회원에게 똑같이 적용되는 최대 허용 나이 같은 상수
  • 객체의 상태를 쓰지 않고 입력만으로 결과를 내는 함수, 예를 들어 이메일 문자열 정리
  • 진단용으로 세는 전체 생성 횟수
  • 프로그램 전체에서 하나로 정한 버전 문자열

반대로 다음은 객체마다 달라야 하므로 인스턴스 변수가 자연스럽습니다.

  • 저장소마다 가진 members 배열과 size
  • 회원마다 다른 email과 age
  • 저장소마다 따로 발급하는 회원 번호
  • 사용자마다 다른 설정과 집계

이 가운데 상수는 final과 상수를 다루는 글에서, 입력만으로 결과를 내는 함수는 다음 글의 static 메서드에서 다룹니다.

여러 객체가 값을 공유해야 한다고 해서 꼭 static을 써야 하는 것도 아닙니다. 공유할 값을 담은 객체를 하나 만들어 필요한 객체들의 생성자에 넘겨 주는 방법도 있습니다. 예를 들어 번호 발급기 객체를 하나 만들어 두 저장소에 넘기면, 두 저장소가 번호를 함께 쓴다는 사실이 생성자 호출에 드러납니다. 나중에 저장소마다 발급기를 따로 주도록 바꾸기도 쉽습니다. 단순한 전체 생성 횟수처럼 정말로 클래스 전체에 하나인 값에는 static이 가장 간단한 선택이지만, 공유 범위가 바뀔 수 있는 값이라면 이런 방법이 더 유연합니다.

정리

  • 인스턴스 변수는 객체마다 하나씩 생기고, static 변수는 클래스에 하나만 있어 모든 객체가 함께 씁니다.
  • static 변수는 객체 없이 Member.createdCount처럼 클래스 이름으로 접근합니다. 객체 참조를 통해서도 접근할 수 있지만 읽는 사람을 헷갈리게 하므로 피합니다.
  • static 변수는 클래스가 처음 쓰일 준비를 할 때 생겨 일반적으로 프로그램이 끝날 때까지 남습니다. 객체가 회수되어도 줄지 않으므로, 생성 누계를 '살아 있는 객체 수'로 읽으면 안 됩니다.
  • 변경할 수 있는 static 변수는 private으로 숨기고, 공유가 정말 필요한 값에만 씁니다. 객체마다 독립적이어야 할 값에 static을 붙이면 서로 다른 객체가 값을 섞어 쓰게 됩니다.

연습 문제

  1. 회원 저장소 MemberRegistry마다 자기에게 추가된 횟수 addedCount를 세고, 모든 저장소에 추가된 전체 횟수는 static 변수 globalAddedCount로 세는 프로그램을 작성하세요. personal 저장소에 두 번, work 저장소에 한 번 추가한 뒤 세 값을 출력합니다. 해설을 보기 전에 add()를 호출할 때마다 (personal, work, global) 세 값이 어떻게 변하는지 (0, 0, 0)부터 적어 보세요. 여기서 add()는 실제로 회원을 저장하지 않고 횟수만 세는 단순한 모델입니다.
  2. 1번 프로그램의 마지막에 work.add()를 한 번 더 호출하면 세 값은 각각 얼마가 될까요?
  3. 학생 객체마다 다른 이름과 점수, 그리고 모든 학생이 같이 쓰는 '합격 기준 점수'가 있습니다. 각각 인스턴스 변수와 static 변수 중 무엇으로 두어야 할지 이유와 함께 답하세요.
해설 보기

1번은 다음과 같이 작성할 수 있습니다.

src/LocalAndGlobalCounts.java
public final class LocalAndGlobalCounts {
    public static void main(String[] args) {
        MemberRegistry personal = new MemberRegistry();
        MemberRegistry work = new MemberRegistry();

        personal.add();
        personal.add();
        work.add();

        System.out.println("personal=" + personal.addedCount());
        System.out.println("work=" + work.addedCount());
        System.out.println("global=" + MemberRegistry.globalAddedCount());
    }

    private static final class MemberRegistry {
        private static int globalAddedCount;
        private int addedCount;

        void add() {
            addedCount++;
            globalAddedCount++;
        }

        int addedCount() {
            return addedCount;
        }

        static int globalAddedCount() {
            return globalAddedCount;
        }
    }
}
personal=2
work=1
global=3

add()는 호출을 받은 저장소의 addedCount 하나와 클래스의 globalAddedCount 하나를 함께 올립니다. 그래서 세 값은 (0, 0, 0)에서 시작해 personal.add() 뒤에 (1, 0, 1), 두 번째 personal.add() 뒤에 (2, 0, 2), work.add() 뒤에 (2, 1, 3)이 됩니다. 저장소별 값은 각 객체의 몫이고, 전체 값은 어느 저장소에서 호출하든 같은 하나의 변수에 이어서 쌓입니다.

각 add가 바꾸는 로컬 카운터와 공유 카운터

personal의 두 add와 work의 한 add에서 수신자의 addedCount와 공유 globalAddedCount가 각각 증가합니다. 공유 누계는 0→1→2→3으로 이어집니다. 단일 스레드의 실제 호출 순서에 따라 수신자와 공유 값의 변화를 비교합니다.

각 add가 바꾸는 로컬 카운터와 공유 카운터

LocalAndGlobalCounts의 personal.add 두 번과 work.add 한 번이 끝난 뒤 각 인스턴스 addedCount와 클래스 globalAddedCount에 남는 값을 비교한다. 원문에는 실패 분기가 없다.

각 호출은 자기 개수와 클래스의 공유 누계를 늘린다

personal에 두 번, work에 한 번 추가하면 개별 값은 2·1이고 공유 값은 3입니다.

LocalAndGlobalCounts · 각 add() 호출이 끝난 뒤
완료한 호출personal.addedCountwork.addedCountMemberRegistry.globalAddedCount
첫 personal.add()101
둘째 personal.add()202
첫 work.add()213
각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 초기개별0/0공유0→personal첫add1/0/1→둘째2/0/2→work첫2/1/3. 개인addedCount와클래스globalAddedCount구별;원add실패분기없음/원자성주장없음. 초기 상태 personal=0 / work=0 globalAddedCount=0 첫 personal 뒤 personal=1 / work=0 globalAddedCount=1 둘째 personal 뒤 personal=2 / work=0 globalAddedCount=2 첫 work 뒤 personal=2 / work=1 globalAddedCount=3 personal.add() work.add() 각 호출: 자기 addedCount++ 뒤 공유 globalAddedCount++ 순차 단일스레드 모델 · 원 add에는 실패 분기 없음
각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 초기개별0/0공유0→personal첫add1/0/1→둘째2/0/2→work첫2/1/3. 개인addedCount와클래스globalAddedCount구별;원add실패분기없음/원자성주장없음. 초기 상태 personal = 0 work = 0 globalAddedCount=0 personal.add() 첫 personal 뒤 personal = 1 work = 0 globalAddedCount=1 personal.add() 둘째 personal 뒤 personal = 2 work = 0 globalAddedCount=2 work.add() 첫 work 뒤 personal = 2 work = 1 globalAddedCount=3 personal/work: 각 객체 필드 globalAddedCount: 클래스 공유 필드

LocalAndGlobalCounts · 각 호출 완료 뒤

  1. 첫 personal.add(): personal의 addedCount=1, work의 addedCount=0, 클래스의 globalAddedCount=1입니다.
  2. 둘째 personal.add(): personal은 2, work는 0, 공유 누계는 2입니다.
  3. 첫 work.add(): personal은 2, work는 1, 공유 누계는 3입니다.

원문 add()는 수신자의 addedCount++ 다음에 globalAddedCount++를 실행합니다. 실패·취소 분기는 없으며 두 증가가 동시적이거나 스레드 안전하다는 뜻도 아닙니다. 최종 출력은 personal=2, work=1, global=3입니다.

이 예의 조건

  • 원문은 순차 단일 스레드 실행이며 ++가 자동으로 스레드 안전하지 않다.
관계도의 보충 설명
원 도표의 전체 관계·라벨·조건
구분내용
2초기 개별 0/0, 공유 0→personal 첫 add 1/0/1→둘째 2/0/2→work 첫 2/1/3이다. 개별 addedCount와 클래스 globalAddedCount를 구별하며 원 add에는 실패 분기가 없고 원자성을 주장하지 않는다.
4personal=0
5work=0
8personal=1
9work=0
13work=0
22순차 단일 스레드 모델 · 원 add에는 실패 분기 없음
원 도표의 설명과 주석
구분내용
설명초기 개별 0/0, 공유 0→personal 첫 add 1/0/1→둘째 2/0/2→work 첫 2/1/3이다. 개별 addedCount와 클래스 globalAddedCount를 구별하며 원 add에는 실패 분기가 없고 원자성을 주장하지 않는다.
전체 읽기 정보각 add가 바꾸는 로컬 카운터와 공유 카운터 각 add가 바꾸는 로컬 카운터와 공유 카운터 LocalAndGlobalCounts의 personal.add 두 번과 work.add 한 번이 끝난 뒤 각 인스턴스 addedCount와 클래스 globalAddedCount에 남는 값을 비교한다. 원문에는 실패 분기가 없다. 각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 personal에 두 번, work에 한 번 추가하면 개별 값은 2·1이고 공유 값은 3입니다. LocalAndGlobalCounts · 각 add() 호출이 끝난 뒤 완료한 호출 personal.addedCount work.addedCount MemberRegistry.globalAddedCount 첫 personal.add() 1 0 1 둘째 personal.add() 2 0 2 첫 work.add() 2 1 3 LocalAndGlobalCounts · 각 호출 완료 뒤 첫 personal.add(): personal의 addedCount=1 , work의 addedCount=0 , 클래스의 globalAddedCount=1 입니다. 둘째 personal.add(): personal은 2 , work는 0 , 공유 누계는 2 입니다. 첫 work.add(): personal은 2 , work는 1 , 공유 누계는 3 입니다. 원문 add() 는 수신자의 addedCount++ 다음에 globalAddedCount++ 를 실행합니다. 실패·취소 분기는 없으며 두 증가가 동시적이거나 스레드 안전하다는 뜻도 아닙니다. 최종 출력은 personal=2 , work=1 , global=3 입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초깃값

두 Registry와 공유 누계를 0으로 둡니다.

  • personal: 0
  • work: 0
  • global: 0
  • 객체별 필드와 클래스 필드는 다른 저장 위치다.
첫 personal

personal.add를 호출합니다.

  • personal: 1
  • work: 0
  • global: 1
  • 수신자 개별 값과 공유 값을 각각 증가시킨다.
둘째 personal

같은 객체를 다시 호출합니다.

  • personal: 2
  • work: 0
  • global: 2
  • work 개별 값은 이 호출 대상이 아니다.
첫 work

work.add를 호출합니다.

  • personal: 2
  • work: 1
  • global: 3
  • 수신자가 바뀌어도 공유 누계는 이어진다.

각 add가 바꾸는 로컬 카운터와 공유 카운터

personal의 두 add와 work의 한 add에서 수신자의 addedCount와 공유 globalAddedCount가 각각 증가합니다. 공유 누계는 0→1→2→3으로 이어집니다. 단일 스레드의 실제 호출 순서에 따라 수신자와 공유 값의 변화를 비교합니다.

각 add가 바꾸는 로컬 카운터와 공유 카운터

LocalAndGlobalCounts의 personal.add 두 번과 work.add 한 번이 끝난 뒤 각 인스턴스 addedCount와 클래스 globalAddedCount에 남는 값을 비교한다. 원문에는 실패 분기가 없다.

각 호출은 자기 개수와 클래스의 공유 누계를 늘린다

personal에 두 번, work에 한 번 추가하면 개별 값은 2·1이고 공유 값은 3입니다.

LocalAndGlobalCounts · 각 add() 호출이 끝난 뒤
완료한 호출personal.addedCountwork.addedCountMemberRegistry.globalAddedCount
첫 personal.add()101
둘째 personal.add()202
첫 work.add()213
각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 초기개별0/0공유0→personal첫add1/0/1→둘째2/0/2→work첫2/1/3. 개인addedCount와클래스globalAddedCount구별;원add실패분기없음/원자성주장없음. 초기 상태 personal=0 / work=0 globalAddedCount=0 첫 personal 뒤 personal=1 / work=0 globalAddedCount=1 둘째 personal 뒤 personal=2 / work=0 globalAddedCount=2 첫 work 뒤 personal=2 / work=1 globalAddedCount=3 personal.add() work.add() 각 호출: 자기 addedCount++ 뒤 공유 globalAddedCount++ 순차 단일스레드 모델 · 원 add에는 실패 분기 없음
각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 초기개별0/0공유0→personal첫add1/0/1→둘째2/0/2→work첫2/1/3. 개인addedCount와클래스globalAddedCount구별;원add실패분기없음/원자성주장없음. 초기 상태 personal = 0 work = 0 globalAddedCount=0 personal.add() 첫 personal 뒤 personal = 1 work = 0 globalAddedCount=1 personal.add() 둘째 personal 뒤 personal = 2 work = 0 globalAddedCount=2 work.add() 첫 work 뒤 personal = 2 work = 1 globalAddedCount=3 personal/work: 각 객체 필드 globalAddedCount: 클래스 공유 필드

LocalAndGlobalCounts · 각 호출 완료 뒤

  1. 첫 personal.add(): personal의 addedCount=1, work의 addedCount=0, 클래스의 globalAddedCount=1입니다.
  2. 둘째 personal.add(): personal은 2, work는 0, 공유 누계는 2입니다.
  3. 첫 work.add(): personal은 2, work는 1, 공유 누계는 3입니다.

원문 add()는 수신자의 addedCount++ 다음에 globalAddedCount++를 실행합니다. 실패·취소 분기는 없으며 두 증가가 동시적이거나 스레드 안전하다는 뜻도 아닙니다. 최종 출력은 personal=2, work=1, global=3입니다.

이 예의 조건

  • 원문은 순차 단일 스레드 실행이며 ++가 자동으로 스레드 안전하지 않다.

관계도의 보충 설명

원 도표의 전체 관계·라벨·조건
구분내용
2초기 개별 0/0, 공유 0→personal 첫 add 1/0/1→둘째 2/0/2→work 첫 2/1/3이다. 개별 addedCount와 클래스 globalAddedCount를 구별하며 원 add에는 실패 분기가 없고 원자성을 주장하지 않는다.
4personal=0
5work=0
8personal=1
9work=0
13work=0
22순차 단일 스레드 모델 · 원 add에는 실패 분기 없음
원 도표의 설명과 주석
구분내용
설명초기 개별 0/0, 공유 0→personal 첫 add 1/0/1→둘째 2/0/2→work 첫 2/1/3이다. 개별 addedCount와 클래스 globalAddedCount를 구별하며 원 add에는 실패 분기가 없고 원자성을 주장하지 않는다.
전체 읽기 정보각 add가 바꾸는 로컬 카운터와 공유 카운터 각 add가 바꾸는 로컬 카운터와 공유 카운터 LocalAndGlobalCounts의 personal.add 두 번과 work.add 한 번이 끝난 뒤 각 인스턴스 addedCount와 클래스 globalAddedCount에 남는 값을 비교한다. 원문에는 실패 분기가 없다. 각 호출은 자기 개수와 클래스의 공유 누계를 늘린다 personal에 두 번, work에 한 번 추가하면 개별 값은 2·1이고 공유 값은 3입니다. LocalAndGlobalCounts · 각 add() 호출이 끝난 뒤 완료한 호출 personal.addedCount work.addedCount MemberRegistry.globalAddedCount 첫 personal.add() 1 0 1 둘째 personal.add() 2 0 2 첫 work.add() 2 1 3 LocalAndGlobalCounts · 각 호출 완료 뒤 첫 personal.add(): personal의 addedCount=1 , work의 addedCount=0 , 클래스의 globalAddedCount=1 입니다. 둘째 personal.add(): personal은 2 , work는 0 , 공유 누계는 2 입니다. 첫 work.add(): personal은 2 , work는 1 , 공유 누계는 3 입니다. 원문 add() 는 수신자의 addedCount++ 다음에 globalAddedCount++ 를 실행합니다. 실패·취소 분기는 없으며 두 증가가 동시적이거나 스레드 안전하다는 뜻도 아닙니다. 최종 출력은 personal=2 , work=1 , global=3 입니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
초깃값

두 Registry와 공유 누계를 0으로 둡니다.

  • personal: 0
  • work: 0
  • global: 0
  • 객체별 필드와 클래스 필드는 다른 저장 위치다.
첫 personal

personal.add를 호출합니다.

  • personal: 1
  • work: 0
  • global: 1
  • 수신자 개별 값과 공유 값을 각각 증가시킨다.
둘째 personal

같은 객체를 다시 호출합니다.

  • personal: 2
  • work: 0
  • global: 2
  • work 개별 값은 이 호출 대상이 아니다.
첫 work

work.add를 호출합니다.

  • personal: 2
  • work: 1
  • global: 3
  • 수신자가 바뀌어도 공유 누계는 이어진다.

2번의 답은 personal=2, work=2, global=4입니다. 이번 호출을 받은 객체는 work이므로 work의 addedCount만 1에서 2로 오르고, personal의 값은 그대로 2입니다. globalAddedCount는 호출을 받은 객체와 상관없이 3에서 4로 오릅니다.

static으로 공유한다고 해서 여러 스레드가 동시에 써도 안전해지는 것은 아닙니다. 이 예제는 main 스레드 하나에서 ++가 차례로 실행되므로 값이 정확하지만, 여러 스레드가 동시에 같은 static 변수를 올리면 일부 증가가 사라질 수 있습니다. 이 문제는 21장의 AtomicInteger 같은 도구로 해결합니다.

3번에서 이름과 점수는 학생마다 다르므로 인스턴스 변수여야 합니다. 합격 기준 점수는 모든 학생에게 똑같이 적용되는 하나의 값이므로 static으로 두는 것이 자연스럽습니다. 학생마다 기준을 복사해 두면 기준이 바뀔 때 모든 객체를 고쳐야 하고, 일부만 고치면 학생마다 기준이 달라지는 문제가 생깁니다. 기준이 실행 중에 바뀌지 않는다면 final과 상수를 다루는 글에서 배울 static final 상수로 만드는 것이 더 좋습니다.

다음 글

다음 글에서는 createdCount()처럼 객체 없이 호출하는 static 메서드를 다룹니다. static 메서드가 왜 인스턴스 변수에 바로 접근할 수 없는지, 어떤 기능을 static 메서드로 만들면 좋은지 살펴봅니다.