static 변수와 수명
인스턴스 변수와 static 변수의 소유·수명을 비교하고 Member 생성 수처럼 정말 공유할 상태만 클래스에 둡니다.
인스턴스 필드는 객체마다 하나씩 존재합니다.
static 필드는 클래스에 하나만 존재해 모든 인스턴스가 공유합니다.
어느 쪽이 더 편한지가 아니라 값이 “개별 객체의 상태”인지 “클래스 전체의 상태”인지에 따라 선택해야 합니다.
인스턴스 카운터의 한계
public final class InstanceCountBug {
public static void main(String[] args) {
Member first = new Member("kim@example.com");
Member second = new Member("lee@example.com");
Member third = new Member("park@example.com");
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세 객체는 각각 자기 count 필드를 0에서 1로 증가시킵니다.
전체 생성 수라는 요구와 저장 위치가 맞지 않습니다.
한 객체의 회원 나이는 인스턴스마다 달라야 하지만, 해당 클래스가 지금까지 몇 개 생성됐는지는 하나의 공유 값이어야 합니다.
별도 Counter 객체를 모든 생성자에 전달해 공유할 수도 있습니다.
의존성이 명시되는 장점이 있지만 단순한 클래스 전체 카운터에는 static 필드가 직접적인 선택입니다.
static 필드 접근
public final class StaticMemberCount {
public static void main(String[] args) {
new Member("kim@example.com");
new Member("lee@example.com");
new Member("park@example.com");
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=3createdCount는 Member 인스턴스마다 만들어지지 않고 클래스에 하나만 존재합니다.
어느 생성자 호출이 증가시켜도 같은 값을 바꿉니다.
접근할 때 first.createdCount처럼 인스턴스를 통해 쓸 수도 있지만, 클래스 소유라는 사실을 가리므로 Member.createdCount가 명확합니다.
인스턴스 상태와 공유 상태
public final class InstanceAndStaticState {
public static void main(String[] args) {
Member first = new Member("kim@example.com", 40);
Member second = new Member("lee@example.com", 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;
}
}
}kim@example.com=41
lee@example.com=50
created=2first의 생일을 반영해도 second 객체의 age는 변하지 않습니다.
두 값은 서로 다른 힙 객체의 인스턴스 필드입니다.
createdCount는 어느 객체를 통해 묻지 않고 클래스 메서드로 읽습니다.
static 필드의 수명은 특정 인스턴스 참조가 사라지는 것과 연결되지 않습니다.
클래스가 로드돼 사용하는 동안 공유됩니다.
객체가 가비지 컬렉션 대상이 되어도 단순 생성 누계는 자동 감소하지 않습니다.
“현재 살아 있는 객체 수”를 static 카운터만으로 정확히 재려는 시도는 GC 시점과 수명 추적 문제 때문에 훨씬 어렵습니다.
공유 상태의 결합
static 변경 가능 상태는 어디서든 클래스 이름으로 바꿀 수 있어 호출 순서에 따라 결과가 달라질 수 있습니다.
Member.createdCount = 0;필드를 public으로 열면 한 기능이 다른 기능의 카운터를 초기화할 수 있습니다.
private으로 숨기고 필요한 읽기·초기화 정책만 메서드로 제한합니다.
테스트나 여러 회원 저장소가 서로 독립적이어야 한다면 static 공유가 오히려 요구에 맞지 않습니다.
예를 들어 MemberRegistry마다 발급 번호가 1부터 시작해야 한다면 카운터를 MemberRegistry 인스턴스 필드로 둡니다.
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
1sequence를 static으로 만들었다면 work의 첫 번호가 3이 됩니다.
공유 범위가 요구와 같은지 확인해야 합니다.
회원 저장소의 static 후보
다음은 클래스 전체에 하나여도 의미가 안정적입니다.
- 모든 Member가 공유하는 최대 허용 나이 상수
- 상태를 사용하지 않는 입력 정규화 함수
- 진단 목적의 전체 생성 누계
- 프로그램 수준에서 하나로 합의한 버전 문자열
반대로 다음은 인스턴스에 두는 편이 자연스럽습니다.
- 각 레지스트리의
members와size - 각 회원의
email과age - 레지스트리별 목표와 발급 순서
- 사용자별 설정과 집계
공유가 필요하다는 이유만으로 static부터 선택하지 않습니다.
공유 객체를 생성자에 전달하면 소유 관계와 교체 가능성이 더 명시적인 경우도 있습니다.
연습 문제
MemberRegistry 인스턴스마다 addedCount를 갖고, 모든 레지스트리가 성공적으로 추가한 전체 회원 수는 static globalAddedCount로 세세요.
두 레지스트리에 각각 2개와 1개를 추가해 값을 출력합니다.
해설 보기
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개별 필드는 수신자마다 다르고 static 필드는 두 수신자의 성공 횟수를 합칩니다.
두 숫자의 의미와 수명이 다릅니다.