java-school logo

static

static 키워드는 클래스 차원의 변수와 메서드를 만들 때 사용한다. 정적(static)이란 클래스가 로딩될 때 결정된 메모리 공간이 변하지 않음을 의미한다. static 변수와 메서드는 객체를 생성하지 않고도 아래와 같이 사용할 수 있다.

메인 메서드가 static 메서드이다. 프로그램을 실행하면 클래스 로더가 관련 클래스를 클래스 패스에서 찾아서 메모리에 로딩한다. 그다음 JVM이 java 다음에 나오는 시작 클래스에서 메인 메서드를 실행한다.

자바 프로그램이 실행되기 위해서는 클래스가 우선 메모리에 로딩되어야 한다. 클래스 로더(Class Loader)가 클래스를 찾아서 메모리에 로딩한다. 클래스가 로딩되는 메모리 공간과 객체가 활동하는 메모리 공간은 다르다.

객체가 할당되는 공간은 JVM의 힙heap 메모리 영역이다. new가 실행될 때마다 힙 메모리에 객체의 인스턴스 변수를 위한 공간이 할당된다. 클래스가 메모리에 로딩될 때 static 변수와 static 메서드를 위한 공간이 할당된다. 한번 메모리에 공간이 할당되면 인스턴스 변수와 달리 객체가 생성될 때마다 메모리 공간이 할당되지 않는다. static 메서드 안에서는 인스턴스 변수를 쓸 수 없다. 논리적으로 만들지도 않는 객체의 속성을 참조할 수 없기 때문이다.

학생 클래스에 메인 메서드를 추가하고 다음과 같이 구현하면 컴파일 에러를 만나게 된다.

public static void main(String[] args) {
    absentNum++;
}

그 반대의 경우인 인스턴스 메서드 내에서 static 변수에 접근하거나 static 메서드를 호출할 수 있다.

결석, 지각, 조퇴하면 벌금을 내야 한다. 학생은 결석 3천원, 지각이나 조퇴는 천원의 벌금을 벌금통에 넣어야 한다. 여기서 벌금통을 코드로 어떻게 구현하면 되겠는가? 벌금통은 학생마다 하나씩 가져야 하는 건 아니다. 단 하나이면서 모든 학생이 공유해야 한다. 그러기 위해선, 아래처럼 모든 객체가 공유하는 변수인 static 변수로 벌금통 변수를 생성한다.

public class Student {
    static int penaltyBin; //벌금통

    public void absent() {
        this.absent++;
        Student.penaltyBin += 3000;
    }
    //..중간 생략..
}    

정적 변수 예제

package net.java_school.user;

public class User {

    public static int total; //접속된 회원 수
    private String id;
    
    public User(String id) {
        this.id = id;
        total++;
    }

    public static void main(String[] args) {
        User user1 = new User("hong1440");
        User user2 = new User("im1562");
        User user3 = new User("jang1692");
        
        System.out.println("접속된 회원수 : " + User.total);
    }

}
C:\ Command Prompt
C:\..>java net.java_school.user.User
접속된 회원수 : 3

싱글톤 패턴(Singleton pattern)

객체가 단 하나만 만들어져야 할 때 싱글턴 패턴을 사용한다. 클래스가 로딩될 때 static 변수는 초기화된다.

싱글톤 패턴을 적용한 예제를 만들어 보겠다. 가정집에는 식탁이 하나만 있어야 한다. 식탁이 여러개 있어서 각자 식사한다면 이상적인 가정이 아니다. 식탁 테이블을 싱글톤 패턴으로 설계해 보자.

package net.java_school.house;

public class CookTable {

    private static CookTable instance = new CookTable();
    
    public static CookTable getInstance() {
        return instance;
    }
    
    private CookTable() {}
    
    //..중간 생략..
  
}

클래스 정보가 클래스 로더에 의해 로딩될 때 static 변수는 초기화된다. CookTable의 static 변수 instance가 초기화 되기 위해 CookTable 객체가 힙에 생성되고 참조값은 instance에 할당된다. 이 값은 공개된 getInstance() 메서드를 통해 얻을 수 있다. 하나뿐인 생성자의 접근자를 private로 지정하여 외부에서 생성자를 호출 못하게 한다. 이렇게 구현하면 프로그램 종료시까지 CookTable 인스턴스는 하나로 유지된다.

초기화 순서

변수는 메모리 공간이 할당될 때 초기화된다. 이때 초기값이 없다면 불린형은 false, 숫자형은 0에 준하는 값으로, 레퍼런스 형은 null로 초기화된다. static이 아닌 멤버 변수가 초기화되는 시점은 객체가 생성될 때이다.

초기화 순서는 static 변수 -> 인스턴스 변수 -> 생성자다. static 변수와 static 블록은 같은 레벨이다. 먼저 나오면 먼저 초기화된다. 인스턴스 블록의 경우 컴파일러가 인스턴스 블록의 구현부를 모든 생성자의 마지막 라인에 추가한다. 다음 문제의 결과를 예측해본다.

A.java
package net.java_school.classvar;

public class A {

    public A() {
        System.out.println("A() 생성자 실행");
    }
        
}
B.java
package net.java_school.classvar;

public class B {
    private A a = new A();
    
    {
        System.out.println("B 인스턴스 블록 실행");
    }
    
    static {
        System.out.println("B static 블록 실행");
    }
    
    private static B b = new B();

    private B() {
        System.out.println("B() 생성자 실행");
    }
    
    public B(int a) {
        System.out.println("B(int) 생성자 실행");
    }

    public static void main(String[] args) {
        new B();
        new B(1);
    }
        
}
C:\ Command Prompt
C:\..>java net.java_school.classvar.B
B static 블록 실행
A() 생성자 실행
B 인스턴스 블록 실행
B() 생성자 실행
A() 생성자 실행
B 인스턴스 블록 실행
B() 생성자 실행
A() 생성자 실행
B 인스턴스 블록 실행
B(int) 생성자 실행
참고