Java 프로그래밍 06 : static
static이라는 키워드는 유명하다. 나는 이 키워드를 주로 알고리즘을 풀 때 사용했는데, 해당 프로그램에서 전역 변수로 선언하여, 재귀 함수를 비롯한 main을 제외한 함수에서 사용하기 위해 static 변수를 썼었다.
사실 나에게 있어 static 변수는 '메모리에 적재되어 프로그램 종료까지 사라지지 않는 변수' 정도로만 인식되었다.
그런데, static 챕터 강의를 듣다보니 클래스 내에서 static으로 선언된 변수는 서로 다른 객체에서도 공유한다는 것을 알게 되었다.
클래스로 만들어진 인스턴스, 그러니까 객체라는 건 메모리 상에 다른 주소에 저장이 될 텐데 어떻게 static 변수가 공유가 된다는 걸까? 이번 포스트에서는 그걸 알아보려고 한다.
그 전에, 우선 static 키워드에 대해 다시 한 번 짚고 가자.
Static
키워드 혹은 예약어(reserved word)라고 불린다. 클래스 단위로 존재하는 변수나 메서드를 만들 때 사용한다. 즉, 객체마다 따로 존재하는 게 아니라 클래스 하나가 공유하는 하나의 공간에 존재하는 변수나 메서드가 된다.
아래 예시를 보자.
public class Counter {
static int count = 0;
public Counter() {
count++; // 생성자 호출될 때마다 count 증가
}
public void printCount() {
System.out.println("count: " + count);
}
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
a.printCount(); // 출력: count: 2
b.printCount(); // 출력: count: 2
}
}
Counter 클래스의 count는 static 변수다. 따라서 객체 a와 객체 b가 공유하는 변수다. a와 b는 서로 다른 객체지만 count는 하나만 존재하기 때문에 count는 객체가 생성될 때마다 생성자로 인해 1이 증가한다. 그러므로 a와 b가 생성된 후의 count는 2가 된다.
반대로 static이 없으면 아래와 같다.
public class Counter {
int count = 0;
public Counter() {
count++;
}
public void printCount() {
System.out.println("count: " + count);
}
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
a.printCount(); // 출력: count: 1
b.printCount(); // 출력: 1
}
}
객체 a와 객체 b는 count 변수를 공유하지 않는다. 따라서 count는 1이 된다.
즉, 정리하면 다음과 같다.
키워드 | 작동 범위 | 공유 여부 |
일반 변수 | 객체마다 존재 | X |
static 변수 | 클래스 단위로 존재 | O |
Static은 어떻게 객체들이 공유할까?
static이 클래스 단위로 존재해서 서로 다른 객체라도 공유한다는 것을 알게 되었다. 그렇다면 어떻게 이를 공유하고 관리하는 것일까?
우선, 다음과 같은 전제를 알아야한다.
기본 전제
Java는 프로그램이 실행되면 JVM이 메모리를 3개로 나누어 관리한다.
- Heap: 객체들이 생성되는 공간 (new 키워드로 생성한 인스턴스)
- Stack: 메서드 호출 시 사용하는 임시 저장 공간
- Method Area (또는 Metaspace): 클래스 정보, static 변수, static 메서드, 상수 풀 같은 클래스 단위의 정보가 저장됨
코드와 함께 살펴보자.
public class Sample {
static int sharedValue = 0; // 클래스 변수 (static)
int instanceValue = 0; // 인스턴스 변수
}
이 코드를 실행하면,
1. Sample 클래스가 JVM에 로딩될 때,
→ sharedValue는 Method Area에 저장된다.
→ 이건 Sample 클래스 전체가 공유하는 메모리 공간이다.
2. 객체를 다음과 같이 2개 만든다 해도
Sample a = new Sample(); // Heap에 객체 a 생성
Sample b = new Sample(); // Heap에 객체 b 생성
→ 이때 a.instanceValue와 b.instanceValue는 Heap에 따로따로 존재하지만,
→ a.sharedValue와 b.sharedValue는 사실상 같은 주소(같은 static 공간)를 참조한다.
즉, a.sharedValue++ 하면 b.sharedValue도 같이 바뀐 값을 보게 되는 것이다.
그러고보니 저번 포스트에서 Garbage Collection(GC)에 대해 다루었다. 자동으로 쓰지 않는 객체를 회수해서 메모리 관리를 해주는 GC는 "Heap" 영역의 메모리만 관리한다. 즉, static 변수가 있는 Method Area는 회수 대상이 아니다.
따라서, 명시적으로 바꿔주지 않는 이상 프로그램 종료까지 계속 살아있다.
여기에서 '명시적으로 바꾼다'는 것은 코드로 직접 static 변수의 값을 바꾸거나 참조를 끊는 행위를 의미한다.
예를 들어, 아래 코드에서처럼 static 변수인 message 값을 명시적으로 바꿔준다거나,
public class Example {
static String message = "Hello";
public static void main(String[] args) {
System.out.println(message); // "Hello"
message = "Goodbye"; // 🔸 값을 명시적으로 변경
System.out.println(message); // "Goodbye"
}
}
객체를 참조하는 경우, 해당 객체의 참조를 끊는 것을 의미한다.
public class Holder {
static SomeClass obj = new SomeClass();
public static void main(String[] args) {
Holder.obj = null; // 🔸 참조를 명시적으로 끊음
}
}
특히, 위의 코드처럼 static 변수라도 참조하는 객체가 Heap에 있으면, 그 객체는 GC 대상이 될 수 있다.
여기서 obj는 static이지만, new SomeClass()는 Heap에 생성된 객체다.
→ 즉, obj가 계속 참조하고 있으니 GC는 이 객체를 수거하지 못한다.
→ 그러나 Holder.obj = null; 해버리면
→ 참조가 끊기니까 SomeClass 객체는 GC의 수거 대상이 된다.
요소 | 위치 | GC 대상 여부 |
static 변수 자체 | Method Area | ❌ (GC 대상 아님) |
static이 참조하는 객체 | Heap | ⭕ (참조 없으면 GC 대상) |
일반 인스턴스 변수 | Heap | ⭕ |
Static은 왜 사용할까?
이제 우리는 static이 무엇인지, 그리고 static으로 선언된 변수가 어떻게 서로 다른 객체에서 공유되는지 알게 되었다. 그러나 한 가지 중요한 것을 여전히 모른다.
그동안 했던 프로그래밍을 떠올려보면, 딱히 객체끼리 변수를 공유해야할만한 상황이 있지 않았다. (물론 내가 실무를 해보지 않았기 때문에 그럴 수도 있다.)
그렇다면 왜 static을 사용할까?
Static의 주요 목적
static을 사용하는 이유는 다음과 같이 세 가지로 나눌 수 있다.
1. 공통 데이터 저장 (전역 변수처럼 사용)
- 모든 인스턴스가 동일한 값을 써야 하는 변수
- 예: 사용자 수, 전체 시스템 상태 등
class User {
static int userCount = 0;
public User() {
userCount++;
}
}
2. 유틸리티 메서드
- 특정 객체의 상태와 무관하게 작동하는 함수
- 그래서 객체를 만들지 않고도 사용 가능해야 함.
Math.random(); // 대표적인 static 메서드!
Integer.parseInt("123"); // 이것도!
알고리즘을 풀 때 자주 사용하던 메서드인데, 이것들 역시 static으로 선언된 메서드였다. 이 메서드를 사용하기 위해 굳이 매번 객체를 생성할 필요가 없었다.
여기에서 알 수 있는 한 가지.
static 변수나 메서드는 객체를 만들지 않고, 클래스 이름으로 바로 접근할 수 있다.
3. 싱글톤 패턴에서의 활용
- static 변수를 통해 프로그램 전체에서 단 하나의 인스턴스를 유지할 수 있다.
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
싱글톤 패턴은 Spring 웹 개발에서 정말 많이 쓰는 패턴이다. 하나의 인스턴스를 재사용함으로써 메모리 낭비를 줄일 수 있다.
정리하자면, 다음과 같다.
상황 | static 필요 여부 | 이유 |
객체마다 값이 달라야 할 때 | ❌ | 인스턴스 변수 사용 |
모든 객체가 같은 값을 써야 할 때 | ✅ | static 변수 사용 |
특정 기능이 객체와 무관할 때 | ✅ | static 메서드 사용 |
유틸 함수 모음 클래스 만들 때 | ✅ | 객체 생성 불필요 |
static은 공유라는 목적도 있지만, 그보다 더 중요한 건 "객체와 무관하게 존재해야 할 정보나 기능"을 위해서 쓰는 것!