static 키워드

static 키워드는 클래스 차원의 변수와 메소드를 만들 때 사용한다. 정적(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; //인스턴스 메소드에서 static 변수를 쓸 수 있다.
    }
    //..중간 생략..
}    

다음은 total이란 정적 변수에 총 회원 수를 저장하는 예제다.

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)

객체가 단 하나만 만들어져야 할 때 싱글턴 패턴을 사용한다.

가정에 식탁이 하나만 있어야 한다. 각자 자기 식탁에서 식사한다면 이상적인 가정이 아니다. 식탁 테이블을 싱글톤 패턴으로 설계해 보자.

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 변수와 같은 레벨이다. 먼저 나오면 먼저 초기화된다.

인스턴스 블록의 구현 내용은 컴파일러가 모든 생성자에 복사한다. 따라서, 인스턴스 블록은 생성자에 앞서 실행되는 듯이 보이며 그렇게 생각해도 된다. 여러 생성자 간 공통 코드를 인스턴스 블록으로 관리할 수 있다.

A.java
package net.java_school.classvar;

public class A {

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

public class B {
    private A a = new A();//3,8,13
    
    {
        System.out.println("B 인스턴스 블록 실행");//5,10,15
    }
    
    static {
        System.out.println("B static 블록 실행");//1
    }
    
    private static B b = new B();//2

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

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