객체가 아니라 클래스에 속하기
지금까지 작성해 온 대부분의 필드와 메서드는 객체에 속합니다. new를 호출할 때마다 각 인스턴스는 필드의 자기만의 복사본을 갖고, 메서드는 그 특정 객체의 상태에 대해 동작합니다. static 키워드는 이를 뒤집습니다. static 멤버는 클래스 자체에 속합니다 — 복사본은 하나뿐이고 모든 인스턴스가 공유하며, 객체를 한 번이라도 만들든 말든 존재합니다.
이 단 하나의 차이가 static 필드, static 메서드, 상수, 그리고 심지어 main이 왜 항상 static인지까지 설명해 줍니다.
공유되는 static 필드
비정적 필드는 각 객체에 자기만의 공간을 줍니다. static 필드는 클래스에 모두가 공유하는 하나의 공간을 줍니다. 고전적인 예는 객체가 몇 개나 생성되었는지 추적하는 카운터입니다.
카운터를 User.count로, 즉 클래스 이름을 통해 읽는다는 점에 주목하세요. 그 값은 특정 User에 속하지 않기 때문입니다. 반면 각 name은 자신의 객체 안에 존재합니다. 어떤 인스턴스를 통해서든 count를 바꾸면 모든 인스턴스가 새 값을 봅니다. 그것은 하나뿐이기 때문입니다.
static 메서드
static 메서드도 클래스에 속하므로, 객체 없이 클래스에서 호출합니다.
이것이 바로 표준 라이브러리 뒤에 있는 패턴입니다. Math.max, Integer.parseInt, Arrays.sort, List.of는 모두 static이며 — 동작하는 데 객체가 필요 없는 유틸리티 동작입니다. 작업이 객체별 상태가 아니라 인수에만 의존할 때 static 메서드를 선택하세요.
큰 함정: static은 instance를 볼 수 없다
static 메서드는 객체 없이 실행되므로 this가 없습니다. 즉, 인스턴스 필드에 접근하거나 인스턴스 메서드를 직접 호출할 수 없습니다 — 그것들을 읽어 올 특정 객체가 없기 때문입니다. 이것이 static에서 가장 흔한 초보자 실수입니다.
class Account {
int balance = 100; // 인스턴스 필드
static int show() {
return balance; // 컴파일 에러: 비정적 필드 'balance'는
} // 정적 컨텍스트에서 참조할 수 없습니다
}
해결책은 메서드를 인스턴스 메서드로 만들거나(static을 제거해 this를 갖게 함), 객체를 명시적으로 전달하는 것입니다.
반대 방향은 괜찮습니다. 인스턴스 메서드는 static 필드를 자유롭게 읽고 static 메서드를 호출할 수 있습니다. 클래스 수준의 공유 데이터는 항상 존재하기 때문입니다.
static final로 만드는 상수
static과 final을 결합하면 상수가 됩니다. 절대 변할 수 없는, 공유되는 단 하나의 값입니다. 관례적으로 UPPER_SNAKE_CASE로 이름을 짓습니다.
static final은 "타입에 속하는 고정된 값"을 표현하는 관용적인 방법입니다 — 표준 라이브러리의 Integer.MAX_VALUE나 Math.PI 같은 것들이죠. static으로 만들면 객체마다 복사본을 낭비하지 않고, final로 만들면 누구도 재할당할 수 없습니다.
static 초기화 블록
단순한 static 필드는 인라인으로 초기화할 수 있습니다. 설정에 실제 로직이 필요할 때 — 조회 테이블 구성, 설정 읽기 등 — 에는 static 블록을 사용하세요. 이 블록은 클래스가 처음 로드될 때, 어떤 객체도 존재하기 전에 한 번 실행됩니다.
이 블록은 클래스를 몇 번 사용하든 단 한 번만 실행되므로, 일회성으로 클래스 전체에 걸친 설정을 두기에 적절한 자리입니다.
static을 언제 사용할까
빠른 판단 기준입니다.
- static 필드는 모든 인스턴스가 진정으로 공유하는 데이터 — 카운터, 캐시, 상수 — 에만 사용하세요. 두 객체가 합리적으로 서로 다른 값을 가질 수 있다면 인스턴스 필드여야 합니다.
- static 메서드는 결과가 어떤 객체의 상태가 아니라 인수에만 의존할 때(순수 유틸리티 함수) 사용하세요.
- 기본적으로 인스턴스 멤버를 선택하세요.
static을 남용하면 프로그램이 슬그머니 테스트하기 어렵고 이해하기 어려운 전역 상태 더미로 변합니다.static은 예외이지 출발점이 아닙니다.
다음: 열거형(Enum)
static final 상수는 하나의 고정된 값에는 적합하지만, 관련된 값들의 작고 고정된 집합 — 방향, 요일, 주문 상태 등 — 을 다룰 때 Java에는 흩어진 상수보다 더 안전하고 표현력 있는 전용 타입이 있습니다. 그것이 enum이며, 다음 페이지의 주제입니다.
자주 묻는 질문
Java에서 static은 무슨 의미인가요?
static은 필드나 메서드가 개별 객체가 아니라 클래스 자체에 속한다는 뜻입니다. static 필드는 모든 인스턴스가 공유하는 복사본이 정확히 하나만 존재하며, static 메서드는 객체 없이 클래스에서 호출합니다(Math.max(...)). 반면 비정적(인스턴스) 멤버는 객체마다 새로운 복사본을 갖습니다.
Java에서 static 변수와 인스턴스 변수의 차이는 무엇인가요?
인스턴스 변수는 객체마다 하나의 값을 가집니다 — 두 객체가 서로 다른 값을 보유할 수 있습니다. static 변수는 클래스의 모든 객체가 공유하는 단일 값을 가지므로, 한 객체를 통해(또는 클래스 이름을 통해) 변경하면 다른 모든 객체에서도 보입니다. 객체별 상태에는 인스턴스 필드를, 카운터나 상수처럼 진정으로 클래스 전체에 공통인 데이터에는 static 필드를 사용하세요.
Java에서 main 메서드는 왜 static인가요?
JVM은 여러분 클래스의 객체가 하나라도 존재하기 전에 main을 호출해야 합니다. static 메서드는 인스턴스가 아니라 클래스에 속하므로, 런타임은 먼저 Main을 생성하지 않고도 Main.main(args)를 직접 호출할 수 있습니다. 시그니처가 항상 public static void main(String[] args)인 이유가 바로 이것입니다.