본문 바로가기
CS/java

java에서의 static의 의미와 사용법

by LDY3838 2024. 5. 18.
반응형

우선 static의 사전적 의미는 '정적', '고정된'이라는 것입니다. java에서 static은 정적이라는 키워드로 자주 사용되고 static method(정적 메서드), static field(정적 변수)로 주로 사용됩니다. 이 두 가지 사용법은 instance가 아니라 class에 고정된 함수와 변수로 사용됩니다. 지금부터 이 두 가지에 대해서 자세히 알아보도록 하겠습니다.

JVM Runtime Data Area

위의 그림은 JVM을 나타내는 그림입니다. 간단히 설명을 드리면 .java 확장자를 가진 java source 파일을 java 컴파일러가 컴파일하여. class 확장자를 가진 class 파일로 만든 다음 이 class 파일을 class loader가 필요할 때 loading 하여 runtime data area에 적재합니다. 이후 execution engine에서 이 runtime data area에 있는 데이터를 이용하여 프로그램을 실행시키고 실행하다가 필요가 없어진 heap area에 있는 객체들을 garbage collectior가 GC(Garbage Collection)을 진행합니다.

제가 이번 글에서 다룰 static field(정적 변수)는 위에서 설명드린 부분 중에서 class loader가 class 파일을 loading하여 runtime data area에 적재할 때 일반 변수들과 차이가 발생합니다. 이제 이에 대해서 설명드리도록 하겠습니다.


Runtime Data Area의 구조

위의 그림은 Runtime Data Area의 상세한 구조를 보여주고 있습니다. Runtime Data Area는 method area, heap area, PC register, JVM stack area, native method stack area 이렇게 5개의 공간으로 나뉘어 있습니다. 이 중에서 모든 스레드에서 공통으로 사용하는 영역은 method area, heap area이고 스레드별로 할당되는 공간은 PC register, JVM stack area, native method stack area입니다.

이제 각각의 영역에 대해서 간략히 설명드린 후 정적 변수와 일반 변수가 어떻게 다른지 추가로 설명드리도록 하겠습니다.


위의 표에 있는 내용들이 각각의 영역에 들어있는 정보들입니다.

method area는 JVM이 실행될 때 생성되고 method area에 있는 정보들은 class 파일들을 class loader가 loading해올 때 한번만 적재됩니다. 이 영역에는 클래스에 관련된 정보들이 존재합니다.

heap area에는 동적 메모리 할당을 통해서 만들어진 instance들이 저장되는 영역입니다. 이 instance들이 고아 객체가 되면 JVM 내의 Garbage Collector가 garbage collection을 진행하여 메모리에서 삭제를 해줍니다.

stack 영역부터 아래 설명드리는 영역들은 thread 별로 존재하는 영역으로 thread가 실행되면 메모리에 공간이 할당됩니다. stack 영역은 메서드에서 사용한 모든 지역 변수들이 존재하는 영역입니다. 리턴 값 등에 대한 정보도 들어가고 함수를 호출할 시 C언어에서 시스템 스택에 실행한 함수가 쌓이는 것과 유사하게 실행한 함수들이 stack 형식으로 쌓이고 함수가 return 되면 메모리에서 해제가 됩니다.

PC register는 현재 실행중인 명령어에 대한 정보를 가지고 있는 것으로 thread별로 존재합니다. 이 영역이 존재하는 이유는 core의 개수보다 thread의 개수가 많은 경우가 대부분인데 이러한 경우 core를 점유하고 있는 thread가 계속하여 바뀌게 됩니다. 이는 OS와 관련된 개념으로 I/O 작업을 해당 스레드가 한다거나 wait()이 호출되는 등의 경우 CPU의 점유를 포기하고 waiting 상태에 있다가 이후 해당 작업이 끝나고 다시 CPU에서의 연산이 필요하면 ready queue로 들어간 다음 CPU의 재할당을 기다리는데 이때 CPU를 다시 할당받는 경우 thread가 이전에 작업한 부분부터 작업을 다시 진행하기 위해서 이전에 실행 중이던 함수 정보를 PC register에 저장해 놓습니다.

마지막으로 native method stack area는 java 언어가 아니라 C, sql 등 다른 언어로 쓰여진 파일이나 코드를 사용하기 위한 공간입니다. JVM stack area와 비슷하게 작동하지만 내부에 쌓이는 함수들이 다른 언어라고 생각하시면 됩니다.


이제 JVM에 대한 간략한 설명이 끝났으니 static과 non-static에 대해서 설명하도록 하겠습니다.

static 변수란 위에서 설명드린 JVM Runtime Data Area 중에서 method 영역에 존재합니다. 따라서 class loader에 해당 class가 loading 될 때부터 사용할 수 있습니다.

하지만 non-static 변수는 지역 변수로 보통 사용을 하고 위에서 설명드린 영역 중에서 JVM Stack Area에 존재합니다. 이는 해당 thread에서만 사용이 가능합니다.

static 메서드도 위에서 말씀드린 method 영역에 존재하기 때문에 class가 loading될 때부터 사용이 가능합니다. 하지만 non-static 메서드도 method 영역에 존재하는데 왜 static과 별개로 다루어지는가 의문이 드실 수 있습니다. 이는 위의 표를 보시면 non-static 메서드에서의 return 값들과 지역 변수들을 stack 영역에서 다루어지기 때문입니다. 이러한 값들이 stack 영역에서 다루어지기 때문에 static 메서드 안에서 non-static 메서드나 non-static 변수를 사용하고자 하면 JVM stack area에 관련된 변수들이 존재하지 않을 수 있는데 이를 사용하고자 하는 것이기 때문에 불가하여 compile error가 발생합니다. 하지만 반대로 non-static 메서드 안에서 static 메서드나 static 변수를 사용하는 것은 non-static에서 필요한 스레드와 변수가 이미 존재하고 static은 JVM에 해당 class가 loading 될 때부터 존재하기 때문에 사용이 가능합니다.


이제부터 static과 non-static을 사용하는 예시를 들며 더 자세히 알아보도록 하겠습니다.

import java.io.*;

public class Main {

    public static void main(String[] args) throws IOException {
        printA();
        printB();
    }

    static void printA() {
        System.out.println("A");
    }
    
    void printB() {
        System.out.println("B");
    }
}

위와 같은 코드가 있습니다. 이때 해당 코드는 실행이 불가합니다. 이유는 아래와 같습니다.

main 함수는 static 이기 때문에 class loader가 class를 loading할 때부터 존재하지만 printB 함수는 non-static으로 스레드가 필요합니다. 따라서 non-static에 필요한 스레드가 존재하지 않을 수 있기 때문에 호출이 불가합니다.

    class A {

        int a = 2;
        static int b = 3;

        static void printA() {
            System.out.println(a);
            System.out.println(b);
        }

        void printB() {
            System.out.println(a);
            System.out.println(b);
        }
    }

이제 위와 같은 코드를 만들어 보았습니다. 이는 static method와 non-static method에서 static, non-static 변수를 사용하는 모습을 보여줍니다.

이때 아래와 같은 문제가 발생합니다.

위와 같은 컴파일 에러가 발생하는 이유는 non-static이 static 안에서 호출되기 때문입니다. 따라서 위와 같은 코드는 불가합니다.


이제 static method, static field에 대해서 알아보았으니 static class에 대해서 알아보도록 하겠습니다.

static 이 키워드로 class에 붙을 수 있는 경우는 nested class 즉 다른 class에 내재된 class의 경우에만 가능합니다. class 자체가 독립적으로 사용이 되면 해당 class는 static이 이미 붙어 있다고 생각하시면 이해가 편하실 겁니다.

nested class에는 static nested class, inner class로 나뉘고 inner class는 또다시 member inner class, local inner class, anonymous inner class로 나뉘는데 이에 대해서는 따로 공부하시면 좋을 거 같습니다.

이번 게시글에서는 static nested class와 member inner class의 차이에 대해서 다루도록 하겠습니다.


class Test {

    //member inner class
    class A {

        static int a = 1;
        int b = 2;

        static void printA() {
            System.out.println(a);
        }
        void printB() {
            System.out.println(b);
        }
    }

    //static nested class
    static class B {

        static int a = 1;
        int b = 2;


        static void printA() {
            System.out.println(a);
        }
        void printB() {
            System.out.println(b);
        }
    }
}

코드를 테스트 하기 위해서 위와 같은 테스트 코드를 만들었습니다. 이때 아래와 같이 main 함수에서 실행을 하면 printB()는 실행이 안됩니다.

public class Main {

    public static void main(String[] args) throws IOException {
        Test.A.printA();
        Test.A.printB();

        Test.B.printA();
        Test.B.printB();
    }
}

 

이전에 설명드린 것과 같이 printB는 non-static인데 static처럼 사용되려 하기 때문입니다.

그럼 이제 이 non-static 메서드와 필드를 어떻게 써야 하는지 확인해보겠습니다. non-static을 사용하기 위해서는 class에서 instance를 만들어서 해당 instance에 해당하는 정보들이 stack에 존재하게 만들어 주어야 합니다.

public class Main {

    public static void main(String[] args) throws IOException {
        Test.A a = new Test().new A();
        a.printA();
        a.printB();

        Test.B b = new Test.B();
        b.printA();
        b.printB();
    }
}

위의 코드를 보시면 이제 new 키워드를 사용하여 객체를 만들어주었기 때문에 non-static의 사용이 가능합니다. 아래의 사진을 보시면 static 함수를 non-static처럼 사용했기 때문에 waning이 발생하지만 잘 작동하는 코드입니다.

이때 non-static을 사용할 수 있게 된 이유는 변수 a, b 때문입니다. 해당 변수들이 stack 영역에 생성되어 non-static 함수와 변수들이 해당 쓰레드의 해당 객체에서 사용될 수 있습니다.

이때 조심하셔야 하는 것은 아래와 같은 코드가 있을 때 Test.B 부분은 method area, b는 JVM stack area, new Test.B() 부분은 Heap에 적재가 된다는 것입니다. 클래스 자체는 method 영역에, 변수 b는 함수 내부에서 만든 변수이기 때문에 JVM stack area에, new를 해서 만든 객체는 동적 메모리 할당에 의한 객체이므로 heap 영역에 할당이 됩니다.

Test.B b = new Test.B();

 

그런데 자세히 보시면 Test.A 객체와 Test.B 객체를 만드는 방식이 아래와 같이 다릅니다.

Test.A a = new Test().new A();
Test.B b = new Test.B();

위와 같이 객체를 생성하는 방식이 다른 이유는 A는 member inner class이고 B는 nested static class이기 때문입니다. A는 존재하기 위해서 Test 객체가 필요하지만 B는 Test 객체가 필요 없이 단독으로 instance를 만들고 존재할 수 있습니다. 이러면 무조건 static nested class가 좋아 보이지만 용도가 다릅니다. 아래의 코드를 이용해서 이에 대해서 설명드리겠습니다.

class Test {
    
    int classField = 2;

    //member inner class
    class A {

        static int a = 1;
        int b = 2;

        static void printA() {
            System.out.println(a);
        }
        void printB() {
            System.out.println(classField);
        }
    }

    //static nested class
    static class B {

        static int a = 1;
        int b = 2;


        static void printA() {
            System.out.println(a);
        }
        void printB() {
            System.out.println(classField);
        }
    }
}

Test 함수에는 classField라는 변수가 존재하고 이는 non-static입니다. 따라서 static 안에서는 non-static을 아래와 같이 사용할 수 없습니다. 따라서 외부 클래스의 변수를 사용하고 싶을 때는 inner class로, 아닌 경우는 nested static class를 사용하시면 되겠습니다.


이제 static 변수와 non-static 변수를 사용하는 간단한 예시를 보여드리고 글을 마치도록 하겠습니다.

public class Main {

    public static void main(String[] args) throws IOException {
        A a1 = new A();
        A a2 = new A();
        A a3 = new A();
        A a4 = new A();
        A a5 = new A();
        
        a1.print();
        a2.print();
        a3.print();
        a4.print();
        a5.print();
    }
}

class A {
    static int a = 0;
    int b = 0;

    public A() {
        a++;
        b++;
    }

    public void print() {
        System.out.println("a = " + a + ", b = " + b);
    }
}

위와 같은 코드의 출력은 아래와 같습니다.

a = 5, b = 1
a = 5, b = 1
a = 5, b = 1
a = 5, b = 1
a = 5, b = 1

위와 같은 출력이 나오는 이유는 static 변수는 method 영역에 존재하기 때문에 모든 class가 공유하고 non-static 변수는 stack 영역에서 객체별 따로 관리가 되기 때문입니다.

결과를 좀 더 잘 확인해보기 위해서 아래와 같이 코드를 변경하겠습니다.

public class Main {

    public static void main(String[] args) throws IOException {
        A a1 = new A();
        a1.print();
        A a2 = new A();
        a2.print();
        A a3 = new A();
        a3.print();
        A a4 = new A();
        a4.print();
        A a5 = new A();
        a5.print();
    }
}

 

a = 1, b = 1
a = 2, b = 1
a = 3, b = 1
a = 4, b = 1
a = 5, b = 1

위와 같이 생성자가 호출될 때마다 a의 값은 1씩 늘고, b는 1로 동일합니다. 위와 같은 static, non-static의 특성을 이용하시면 좋은 코드를 만드실 수 있을 겁니다. 또한 static은 stateful 하기 때문에 이전의 상태를 유지하면 안 되는 경우 위험할 수 있으니 조심하여 사용하셔야 합니다.

반응형

'CS > java' 카테고리의 다른 글

자바에서 PECS란?  (0) 2024.03.20

댓글