이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.
기초개념 › Java
static
static은 무엇이고, 붙이면 무엇이 달라지는가
static은 필드(클래스 안에 선언한 변수)나 메서드 앞에 붙이는 키워드입니다(이 실습은 필드만 봅니다). static이 없는 필드는 객체를 만들 때마다 그 객체 안에 따로 하나씩 생깁니다. static을 붙이면 객체마다가 아니라 클래스에 하나만 있고, 그 클래스의 모든 객체가 그 하나를 같이 씁니다. 그래서 static인 필드는 객체 없이 Counter.count처럼 클래스 이름으로 쓸 수 있고, static이 아닌 필드는 "어느 객체의 것인지"가 정해져 있어야 해서 클래스 이름만으로는 쓸 수 없습니다. 이 실습은 count 앞에 static을 붙였다 떼고 읽는 방법도 바꿔 가며 같은 코드를 컴파일하고 실행해, 컴파일러가 무엇을 알려 주고 무엇이 출력되는지, count가 어디에 있는지를 봅니다.
실행으로 대조 · JDK 21에서 네 가지 조합을 javax.tools로 컴파일하고 실행해 대조, count가 있는 곳은 읽는 줄만 a.count와 b.count로 바꾼 소스를 실행해 실측
이 실습에 나오는 개념
- static
- 인스턴스가 아니라 클래스에 속하는 멤버임을 나타내는 키워드다. static 필드는 클래스당 하나만 존재해 모든 인스턴스가 공유하고, static 메서드는 객체를 만들지 않고 호출한다. 값이 바뀌는 static 필드는 사실상 전역 변수이므로 여러 스레드가 동시에 접근할 때 문제가 되기 쉽다.
- 필드
- 필드는 클래스 안에 선언해 두는 변수이다. class Card { String owner; }에서 owner가 String 타입의 필드이다. static을 붙이지 않은 필드는 인스턴스 변수라고도 부르며, 그 클래스의 객체가 새로 만들어질 때마다 객체마다 따로 하나씩 생긴다. 그래서 같은 클래스에서 만든 객체라도 필드의 값은 서로 다를 수 있다. 값을 따로 주지 않으면 필드에는 기본값이 들어가고, 참조 타입의 필드는 null이다.
- 인스턴스 필드와 static 필드
- static을 붙이지 않은 필드는 인스턴스 필드이고, 그 클래스의 객체를 새로 만들 때마다 객체 안에 그 필드의 변수가 하나씩 따로 생긴다. static을 붙인 필드는 객체를 몇 개 만들든(하나도 만들지 않아도) 변수가 정확히 하나만 있고, 그 클래스의 모든 객체가 그 하나를 같이 쓴다. static 필드는 Counter.count처럼 클래스 이름으로 쓸 수 있고, 어느 객체의 것인지 정해야 하는 인스턴스 필드를 클래스 이름으로 쓰면 컴파일 오류이다. static 필드를 a.count처럼 객체로 읽어도 컴파일되며 읽는 것은 객체 안의 칸이 아니라 클래스에 하나뿐인 변수이다.
- 클래스와 객체
- 클래스는 어떤 필드(데이터)와 메서드(동작)를 가질지 적어 둔 설계도이고, 객체는 그 설계도로 new를 해서 메모리(힙)에 실제로 만든 것이다. 객체를 인스턴스라고도 부른다. 클래스 하나로 객체를 여러 개 만들 수 있고, 객체마다 필드 값을 따로 가진다. 변수에는 객체 자체가 아니라 객체가 있는 곳을 가리키는 참조가 담긴다.
이렇게 바꿔 보세요
- 처음 설정으로 — count 앞에 static이 있고 클래스 이름으로 읽습니다. 컴파일에 성공하고 2가 출력됩니다.
- static을 지우고 a.count로 읽으면 — count가 객체마다 따로 있어서 a의 count만 1입니다. 컴파일에 성공하고 1이 출력됩니다.
- static을 지우고 Counter.count로 읽으면 — 클래스 이름만으로는 어느 객체의 count인지 정할 수 없어 컴파일 오류가 납니다.
- static을 둔 채 a.count로 읽으면 — 객체 a로 읽어도 읽는 것은 클래스에 하나뿐인 count입니다. 컴파일에 성공하고 2가 출력됩니다.
처음 설정으로 실행하면
- 컴파일러가 소스를 위에서부터 읽습니다. 먼저 Counter 클래스의 count 선언을 읽습니다. count 앞에 static이 있으므로, count는 객체마다 생기는 것이 아니라 Counter 클래스에 하나만 있는 변수로 정해집니다.
- 컴파일러가 main 안의 System.out.println(Counter.count); 줄을 확인합니다. Counter.count는 클래스 이름으로 count를 가리킵니다. count가 static이어서 Counter 클래스에 하나 있으므로, 객체 없이도 가리킬 수 있습니다. 오류가 아닙니다.
- 컴파일이 성공해 실행할 Main 클래스 파일이 만들어집니다.
- java Main으로 실행합니다. main이 시작되고, 첫 new Counter()가 Counter 객체를 만들어 변수 a에 담습니다. count는 static이어서 이 객체 안에는 count 칸이 생기지 않습니다. Counter 클래스에 count 칸이 하나 있고 값은 0입니다.
- 두 번째 new Counter()가 Counter 객체를 하나 더 만들어 변수 b에 담습니다. 객체는 둘이 되었지만 count 칸은 새로 생기지 않았습니다. 여전히 Counter 클래스에 하나뿐입니다.
- a.up() 호출이 실행됩니다. up 메서드의 count = count + 1; 은 count를 읽어 1을 더해 다시 담습니다. 이 count는 Counter 클래스에 하나뿐인 칸이어서, 그 칸의 값이 0에서 1이 됩니다. 객체 b를 거쳐 읽어도 지금 이 칸의 값은 1입니다.
- b.up() 호출이 실행됩니다. 같은 count = count + 1; 입니다. b의 up도 같은 하나뿐인 칸을 올리므로, 값이 1에서 2가 됩니다. a가 올린 것 위에 b가 더한 것입니다.
- System.out.println(Counter.count); 줄이 실행됩니다. Counter.count는 Counter 클래스에 하나 있는 count이고, 그 값 2가 출력됩니다.
실행 결과
- static 필드는 필드 앞에 static을 붙인 것입니다. 일반 필드와 다른 곳은 객체마다가 아니라 클래스에 하나만 있고, 그 클래스의 모든 객체가 그 하나를 같이 쓴다는 것입니다. a와 b가 up()을 한 번씩 불러 count가 2가 된 것은, 둘이 같은 count를 올렸기 때문입니다.
- static이어서 Counter.count처럼 객체 없이 클래스 이름으로 읽을 수 있었습니다. static이 없는 필드는 객체마다 따로 있어서, 클래스 이름만으로는 어느 객체의 것인지 정할 수 없습니다.
코드
class Counter {
// static: 객체마다가 아니라 Counter 클래스에 하나만 있다
static int count = 0;
void up() {
count = count + 1;
}
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
a.up();
b.up();
System.out.println(Counter.count);
}
}
이 모형의 전제
컴파일러와 실행 환경은 JDK 21.0.12.1(javac 21)입니다. 네 가지 조합을 모두, 화면의 코드와 같은 소스(줄 단위로 같은 글)를 javax.tools로 컴파일하고, 컴파일에 성공한 것은 클래스 파일을 새 클래스 로더로 읽어 Main.main을 실행해 표준 출력을 잡은 결과와 대조했습니다. 대조한 것은 컴파일 성공 여부, 첫 번째 오류의 진단 코드(compiler.err.non-static.cant.be.ref를 화면의 오류 설명으로 옮겼습니다), 오류의 개수와 오류가 난 줄, Main 클래스 파일이 만들어졌는지, 실행했을 때의 출력입니다. "count가 있는 곳" 열은 같은 선언에서 읽는 줄만 a.count와 b.count를 각각 출력하도록 바꾼 소스를 따로 컴파일해 실행한 값으로 확인했습니다(static이 있으면 2와 2, 없으면 1과 1). 컴파일 오류가 나는 조합에서도 읽는 줄만 바꾼 소스는 컴파일되어 실행됩니다. 단계 문장이 기대는 사실인 a.up() 직후의 값(static이면 a.count와 b.count가 1과 1, 아니면 1과 0)도 따로 실행해 확인했습니다. 문서는 Java Language Specification(Java SE 21)을 직접 열어 확인했습니다. static 필드는 객체를 몇 개 만들든 변수가 정확히 하나라는 것과 static이 아닌 필드는 객체를 만들 때마다 그 객체의 변수가 생긴다는 것은 8.3.1.1절을, 객체로 static 필드를 읽으면 그 식은 계산하고 결과는 버리며 필드의 값이 쓰인다는 것은 15.11.1절을, 클래스 이름으로 static이 아닌 필드를 쓰면 컴파일 오류라는 것은 6.5.6.2절을 따랐습니다. 오류의 종류, 개수, 어느 줄에서 나는지는 명세가 정한 것이 아니라 이 javac에서 관찰한 것입니다. a.count처럼 객체로 static 필드를 읽을 때 이 javac는 기본 설정에서 경고를 내지 않았고, -Xlint:all 옵션을 주면 그 줄에 "static variable should be qualified by type name, Counter, instead of by an expression"라는 경고 하나를 냈습니다. 경고는 명세가 정한 것이 아니라 이 javac의 동작입니다. static은 이 밖에도 메서드와 초기화 블록에 붙일 수 있고, static 메서드 안에서는 대상 객체가 없어 static이 아닌 필드를 이름만으로 쓸 수 없는데, 이 실습은 static 필드만 다뤘습니다. static 필드의 값이 정해지는 시점이나 여러 스레드가 함께 고칠 때의 문제도 다루지 않았습니다. static 메서드와 static이 아닌 멤버의 관계는 "세부 동작 실습: static과 인스턴스"에서 다룹니다.
앞 실습: 생성자 · 다음 실습: 상속