자바에서 static은 한마디로 “객체(인스턴스)가 아니라 클래스에 소속된다” 는 뜻이다.
그래서 static이 붙은 멤버는 new로 만든 객체마다 따로 생기는 게 아니라, 클래스 전체가 공유하는 단 하나가 된다.
서블릿, 톰캣, 멀티스레드 환경에서 static이 자주 언급되는 이유도 바로 이 “공유” 특성 때문이다.
1) static이 붙으면 뭐가 달라지나?
인스턴스 멤버(기본)
class User {
int age;
}
- new User()를 할 때마다 age는 각 객체마다 따로 존재한다.
- 즉, 객체가 10개면 age도 10개다.
static 멤버
class User {
static int count;
}
- User 클래스에 count는 딱 1개만 존재한다.
- new User()를 100번 해도 count는 여전히 1개다.
- 모든 객체가 같은 User.count를 공유한다.
static 변수는 언제 만들어지나? (초기화 시점)
static은 클래스가 JVM에 로딩될 때 만들어진다.
즉,
- 프로그램 실행 중 User 클래스를 처음 사용하는 순간(정확히는 클래스 로딩/초기화 단계)
- static 필드가 메모리에 잡히고 초기화된다.
예:
class A {
static int x = 10;
}
A를 처음 참조하는 순간 x가 준비된다.
메모리 관점으로 보는 static (스택/힙과 비교)
자바 실행 시 메모리를 크게 나누면 이런 식으로 이해하면 된다.
- 스택(Stack): 메서드 호출 시 생기는 지역 변수(스택 프레임). 호출 끝나면 사라짐.
- 힙(Heap): new로 만든 객체가 저장됨. GC 대상.
- 클래스 영역(메서드 영역/Metaspace): 클래스 메타정보와 static 관련 데이터가 관리됨(개념적으로).
핵심은 이거다.
- static은 객체(new)와 무관하게 클래스 쪽에 붙는다.
- 그래서 “누가 new를 하든” 상관없이 항상 같은 하나를 바라본다.
static 접근 방식: 왜 클래스명.변수로 쓰는가?
static은 클래스 소속이므로 아래 방식이 정석이다.
User.count++;
System.out.println(User.count);
물론 객체로도 접근은 “가능”하지만(경고 느낌)
User u = new User();
u.count++; // 동작은 하지만 본질은 클래스 공유값이라 혼동 유발
실제로는 u.count도 내부적으로 User.count로 해석된다.
static 메서드란?
static 메서드도 동일하게 클래스 소속 메서드다.
class MathUtil {
static int add(int a, int b) { return a + b; }
}
호출:
MathUtil.add(1, 2);
static 메서드의 중요한 제약
static 메서드 안에서는 인스턴스 멤버에 바로 접근할 수 없다.
class A {
int n = 10;
static void f() {
// System.out.println(n); // 불가능
}
}
이유는 명확하다.
- static은 “특정 객체”가 없을 수도 있는 상태에서 호출된다.
- 그런데 n은 “특정 객체”에 속한다.
- 어떤 객체의 n인지 결정할 수 없으니 막는 것이다.
static의 대표적인 사용 패턴 4가지
(1) 상수(constants)
public class HttpStatus {
public static final int OK = 200;
}
- 공통으로 쓰는 값, 변경 불가, 어디서든 접근 가능
(2) 유틸리티 클래스(Utility)
public class StringUtils {
public static boolean isBlank(String s) { ... }
}
- 상태 없이 기능만 제공
(3) 공유 캐시 / 레지스트리
public class RequestMapping {
private static final Map<String, Controller> map = new HashMap<>();
}
- 서버 시작 시 한 번 세팅하고 계속 재사용하는 구조에서 자주 사용
(4) 싱글톤(singleton) 형태
public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
public static AppConfig getInstance() { return INSTANCE; }
}
- 애플리케이션 전역에서 1개만 쓰고 싶을 때
가장 중요한 주의점: static은 “전역 공유 상태”다
static이 강력한 이유는 “공유”지만, 위험한 이유도 “공유”다.
멀티스레드에서 문제 생기는 전형적인 상황
- 요청이 동시에 100개 들어온다.
- 모든 스레드가 같은 static 값을 동시에 읽고/쓴다.
- 동기화 없이 업데이트하면 경쟁 조건(race condition)이 발생한다.
예:
static int count = 0;
count++; // 이 한 줄도 스레드 안전하지 않을 수 있음
그래서 서블릿/톰캣 같은 환경에서는 특히 조심해야 한다.
- 서블릿 인스턴스도 보통 1개 공유
- static도 1개 공유
→ 둘 다 “공유 상태”가 되기 쉬움
“서블릿 맥락”에서 static을 어떻게 봐야 하나?
서블릿 컨테이너는 멀티스레드로 동작하고, 서블릿 인스턴스를 보통 하나만 만들어 공유한다는 특성이 있다.
여기서 static까지 쓰면 공유 범위가 더 커진다.
- 서블릿 인스턴스 필드: “그 서블릿 인스턴스 1개”에 공유됨
- static 필드: “클래스 자체”에 공유됨 (서블릿 인스턴스가 몇 개든 상관없이 공유)
결론:
요청마다 달라지는 값(사용자 정보, 요청 데이터, 임시 계산값)을 static에 저장하면 거의 무조건 사고 난다.
안전하게 쓰는 기준
static을 써도 되는 대표 케이스:
- public static final 상수
- 상태 없는 유틸 메서드
- 불변(immutable) 객체의 공유 참조
- 읽기 전용 캐시(초기화 후 변경 없음)
- 동시성 제어가 확실한 공유 구조(ConcurrentHashMap, Atomic*, synchronized 등)
조심해야 하는 케이스:
- 요청마다 변하는 값을 static에 저장
- 누적 카운터, 리스트에 add 같은 변경 작업을 동기화 없이 수행
- 테스트에서 전역 상태 때문에 케이스 간 간섭 발생
정리
static은 “클래스에 붙는 공유 멤버”라는 단순한 문법이지만, 실제로는 다음을 의미한다.
- 인스턴스와 무관하게 1개만 존재한다
- 어디서든 접근 가능한 전역 공유 상태가 된다
- 멀티스레드 환경에서는 동시성 이슈의 출발점이 된다
'Java' 카테고리의 다른 글
| 왜 new Integer()를 사용하면 안 될까? (0) | 2026.02.13 |
|---|---|
| enum: 사용이유와 예시 (0) | 2026.02.04 |
| 쓰레드란? (0) | 2026.01.29 |