자바 메모리 구조와 static

박병욱·2025년 5월 29일

Java

목록 보기
7/38
post-thumbnail

🗃️ 자바 메모리 구조

일단 비유를 통해 자바 메모리 구조를 알아보자.

자바 메모리 구조는 위와 같이 Method, Heap, Stack 영역으로 나눠져 있다. 먼저 메서드 영역은 클래스 정보를 보관한다. 힙 영역에는 클래스를 통해 생성된 실제 인스턴스나 배열이 보관된다. 스택 영역은 실제 프로그램이 실행되는 영역이라고 생각하면 된다. 메서드를 실행할 때마다 스택 영역에 하나씩 쌓이는 것이다.

 

좀 더 구체적으로 들어가면…

메서드 영역은 모든 영역에서 공유하는, 프로그램을 실행하는데 필요한 공통 데이터를 관리한다. 위에서 살펴봤듯이 클래스 정보, 즉 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재한다. 자바가 처음 실행되면 클래스 정보를 읽어서 메서드 영역에 올리는 것이다. 그 다음으로, static 영역이 있다. 여기에는 static 변수들을 보관한다. 이 부분은 나중에 자세히 살펴보자. 참고적으로 알아둘 런타임 상수 풀은 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관한다.

스택 영역 은 자바 실행 시, 하나의 실행 스택이 생성된다. 자바 프로그램을 생각해보면, 처음에 main() 메서드를 실행한다. 그 main() 메서드가 실행 스택에 처음으로 들어가고, 그 main() 메서드에서 추가적으로 메서드를 호출할 수도 있다. 그러면 호출된 메서드 스택 프레임이 실행 스택에 들어가는 것이다.

힙 영역은 아까 말했다시피, 객체 인스턴스나 배열이 생성되는 영역으로, 자바 가비지 컬렉션(Garbage Collection)이 이루어지는 주요 영역이다.

 

🤔 스택 프레임이란?

스택 영역에 쌓이는 네모 박스가 스택 프레임이다. 메서드가 호출될 때마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다. 위와 같이, 하나의 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.

 

💬 참고 사항

스택 영역은 각 스레드별로 하나의 실행 스택이 생성된다. 따라서 스레드 수 만큼 스택 영역이 생성되는 것이다. 위의 예시에서는 스레드를 1개만 사용하기 때문에 하나의 스택 영역만 있는 것이다.

 

👀 메서드 코드는 메서드 영역에…

자바에서 특정 클래스로 100개의 인스턴스를 생성했다고 해보자. 그럼 당연히 힙 영역에 100개의 인스턴스가 보관되고 있을 것이다. 각각의 인스턴스는 내부에 본인의 인스턴스 변수와 메서드를 가지고 있을 텐데, 변수 값은 각자 다를 수도 있지만, 메서드는 공통된 코드를 공유한다. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다. 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행하는 것이다.


🌿 스택과 큐 자료 구조

🧱 Stack

스택이라는 자료 구조에 대해 먼저 살펴보자. 여기서 자료 구조라는 것은, 데이터를 어떤 식으로 보관하고 관리하는지에 대한 구조를 말한다. 아래와 같은 1번, 2번, 3번 블록이 있다고 가정해보자.

이 블록들을 통에 넣을 건데, 위가 뚫려 있기 때문에 위쪽으로만 블록을 넣고 빼야 한다. 넣을 때는 아래처럼 순서대로 넣을 수 있을 것이다.

블록을 뺀다고 한다면…

이런 방식을 후입 선출(Last In First Out) 방식이라고 한다. 나중에 넣은 것이 가장 먼저 나오는 것이다. 다음은 큐 자료 구조에 대해 알아보자.

 

🥪 Queue

큐는 선입 선출(First In First Out) 방식이다. 가장 먼저 넣은 것이 가장 먼저 나오는 방식이다.

이런 자료 구조는 각자 필요한 영역이 있다. 예를 들어, 선착순 이벤트를 하는데 고객이 대기해야 한다면 어떤 자료 구조를 사용해야 할까? 만약 스택을 사용한다면 폭동이 일어날 것이다. 당연히 먼저 온 사람이 먼저 혜택을 받는 큐를 사용해야 할 것이다.

 

하지만, 지금과 같은 프로그램을 실행하고 메서드를 호출하는 작업에서는 스택 구조가 적합하다. 자바에서 스택 영역이 어떤 방식으로 작동하는지 알아보자.

package memory;

public class JavaMemoryMain1 {

    public static void main(String[] args) {
        System.out.println("main 메서드 시작");
        method1(10);
        System.out.println("main 메서드 종료");
    }

    static void method1(int m1) {
        System.out.println("method1 메서드 시작");
        int cal = m1 * 2;
        method2(cal);
        System.out.println("method1 메서드 종료");
    }

    static void method2(int m2) {
        System.out.println("method2 메서드 시작");
        System.out.println("method2 메서드 종료");
    }
}

/*
main 메서드 시작
method1 메서드 시작
method2 메서드 시작
method2 메서드 종료
method1 메서드 종료
main 메서드 종료
*/

프로그램을 실행하면, main() 메서드를 실행한다. 이때 main()을 위한 스택 프레임이 하나 생성되고, main() 스택 프레임은 내부 args라는 매개 변수를 가진다. 그리고 main() 메서드는 method1()을 호출하고, method1() 스택 프레임이 생성된다. 코드를 보면, method1() 메서드는 매개 변수 m1, 지역 변수 cal를 가지고 있으므로 해당 변수들이 스택 프레임에 포함된다. 그리고 나서 method1() 메서드가 method2() 메서드를 호출하고, method2() 스택 프레임을 생성한다. method2() 메서드도 마찬가지로, 매개 변수 m2를 가지고 있으므로, 스택 프레임에 포함된다. 이제 종료될 때 어떻게 되는지 살펴보자.

method2() 메서드가 종료되면, 일단 스택 프레임에서 매개 변수 m2와 함께 제거된다. method2() 메서드가 종료되었기 때문에 프로그램은 method1() 메서드로 돌아간다. 이때 method1() 메서드를 처음부터 시작하는게 아니라 method1() 메서드에서 method2()를 호출한 지점으로 돌아가는 것이다. method1() 메서드가 종료될 때도 지역 변수와 매개 변수들과 함께 스택 프레임에서 제거된다. 프로그램은 main() 메서드로 돌아가고, main() 메서드가 종료되면, 더 이상 호출할 메서드가 없기 때문에 스택 프레임이 완전히 비워진다. 자바는 프로그램을 정리하고 종료된다.

 

정리하자면,

  • 자바는 스택 영역을 사용해서 메서드 호출과 매개 변수를 포함한 지역 변수를 관리한다.
  • 메서드를 계속 호출하면 스택 프레임이 계속 쌓인다.
  • 스택 프레임이 종료되면 지역 변수도 함께 제거된다.
  • 스택 프레임이 모두 제거되면 프로그램도 종료된다.

⛲️ 스택 영역과 힙 영역

이번에는 스택 영역과 힙 영역이 함께 사용되는 경우를 살펴보자.

package memory;

public class Data {
    private int value;

    public Data(int value) {
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}
package memory;

public class JavaMemoryMain2 {

    public static void main(String[] args) {
        System.out.println("main 메서드 시작");
        method1();
        System.out.println("main 메서드 종료");
    }

    static void method1() {
        System.out.println("method1 메서드 시작");
        Data data1 = new Data(10);
        method2(data1); // 참조값 복사해서 전달
        System.out.println("method1 메서드 종료");
    }

    static void method2(Data data2) {
        System.out.println("method2 메서드 시작");
        System.out.println("data.value=" + data2.getValue());
        System.out.println("method2 메서드 종료");
    }
}

/*
main 메서드 시작
method1 메서드 시작
method2 메서드 시작
data.value=10
method2 메서드 종료
method1 메서드 종료
main 메서드 종료
*/

먼저 main() 메서드가 실행되면서 main() 스택 프레임이 생성된다. 그런 다음, 아래와 같이 메서드 내부에서 method1() 메서드가 실행되고, method1() 스택 프레임이 생성되면서 data1 매개 변수도 포함시킨다. 이때 method1()은 힙 영역에 Data 인스턴스를 생성하고, 참조값을 data1에 보관한다.

그리고 나서 method1() 메서드는 method2() 메서드를 호출하면서 data2 매개 변수에 참조값을 넘긴다. 이제 method1()에 있는 data1method2()에 있는 data2는 같은 인스턴스를 바라보는 것이다.

이제 method2() 메서드가 종료됨에 따라, method2() 스택 프레임과 매개 변수 data2가 제거되고, method1()이 종료되면서, 해당 스택 프레임과 매개 변수 data1가 제거된다. method1() 메서드가 종료된 직후의 상태를 살펴보면,

매개 변수 data1이 제거됨에 따라 이제 x001 이라는 참조값을 가진 인스턴스는 아무도 찾아주지 않는다. 사용되는 곳도 없고, 힙 메모리 영역에 표류 중인 것이다. 이때 가비지 컬렉션은 이러한 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거한다. 참고로, 만약 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 가비지 컬렉션의 사냥감이 된다.


🧑🏻‍🏫 static 변수

static 키워드는 주로 멤버 변수와 메서드에 사용된다. 일단 멤버 변수에 static 키워드가 왜 필요한지에 대해 생성된 객체의 수를 카운트하는 간단한 예제를 통해 알아보자.

package static1;

public class Data1 {
    public String name;
    public int count;

    public Data1(String name) {
        this.name = name;
        count++;
    }
}
package static1;

public class DataCountMain1 {

    public static void main(String[] args) {
        Data1 data1 = new Data1("A");
        System.out.println("A count=" + data1.count);

        Data1 data2 = new Data1("B");
        System.out.println("B count=" + data2.count);

        Data1 data3 = new Data1("C");
        System.out.println("C count=" + data3.count);
    }
}

/*
A count=1
B count=1
C count=1
*/

위의 코드에서는 객체를 생성할 때마다 Data1 인스턴스는 계속 새로 만들어지기 때문에 안에 있는 count 변수도 새롭게 만들어지고 있다.

보다시피, 객체를 생성해서 아무리 count 값을 증가시켜 봐도 각각 모두 다른 count이기 때문에 서로의 count에 전혀 영향을 주지 않는다. 의도했던 결과를 얻기 위해서는 변수를 서로 공유해야 할 필요성이 있는 것이다. 아래 예제를 보자.

package static1;

public class Counter {
    public int count;
}
package static1;

public class Data2 {
    public String name;

    public Data2(String name, Counter counter) {
        this.name = name;
        counter.count++;
    }
}
package static1;

public class DataCountMain2 {

    public static void main(String[] args) {
        Counter counter = new Counter();
        
        Data2 data1 = new Data2("A", counter);
        System.out.println("A count=" + counter.count);

        Data2 data2 = new Data2("B", counter);
        System.out.println("B count=" + counter.count);

        Data2 data3 = new Data2("C", counter);
        System.out.println("C count=" + counter.count);
    }
}

/*
A count=1
B count=2
C count=3
*/

위와 같이, Counter 인스턴스를 공유했기 때문에 객체를 생성할 때마다 의도대로 값을 증가시키는 것을 볼 수 있다.

 

먼저 첫 번째 Data 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 증가시킨다. 마찬가지로, 두 번째와 세 번째 인스턴스를 생성할 때도 생성자를 통해 공유되고 있는 Counter 인스턴스의 count 값을 증가시키는 것이다. 아래 그림을 살펴보자.

보다시피, Data2의 인스턴스가 3개 생성되고, count 값도 인스턴스 숫자와 같은 3으로 설정되는 것이다. 하지만, 지금 고작 객체의 개수를 세는 프로그램인데, Counter라는 별도의 클래스를 외부에서 만들어서 끌고 오고 있다. 더군다나 Data2 클래스의 생성자에서 매개 변수를 추가해야 한다. 그냥 간편하게 Data2 클래스 내부에서 공용 변수를 만들어 해결할 수 있는 방법이 없을까?

 

✍🏻 static 변수 사용

이때 바로 static 키워드를 사용하면 공용으로 함께 사용할 수 있는 변수를 만들 수 있다.

package static1;

public class Data3 {
    public String name;
    public static int count; 

    public Data3(String name) {
        this.name = name;
        count++;
    }
}

위와 같이, count 변수 앞에 static 키워드가 붙었다. 이렇게 멤버 번수에 static을 붙인 것을 정적 변수, 혹은 클래스 변수라고 한다. 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킨다.

 

package static1;

public class DataCountMain3 {

    public static void main(String[] args) {
        Data3 data1 = new Data3("A");
        System.out.println("A count=" + Data3.count);

        Data3 data2 = new Data3("B");
        System.out.println("B count=" + Data3.count);

        Data3 data3 = new Data3("C");
        System.out.println("C count=" + Data3.count);
    }
}

/*
A count=1
B count=2
C count=3
*/

근데 왜 각각의 참조값을 사용하지 않고, 왜 Data3.count와 같이 클래스를 통해 호출하지? 약간 느낌이 어색하다. 아래 그림을 보자.

보다시피 static 키워드가 붙은 멤버 변수는 힙 영역이 아닌, 메서드 영역에서 관리한다. 힙 영역을 보면 name만 있고, count는 없다. 하여간, 인스턴스를 생성하면 생성자를 호출할 것이다. 근데 지금 생성자에 count++라는 코드가 있다. count 변수는 앞에 static 키워드가 붙은 정적 변수다. 그렇기 때문에 메서드 영역에 있는 count값이 증가하는 것이다.

위와 같이, 최종적으로 메서드 영역에 있는 count 변수의 값은 3이 된다. 기억하자. static이 붙은 정적 변수에 접근하려면 Data3.count와 같이 “클래스명 + dot(.) + 변수명” 으로 접근하면 된다. 여기서 참고로 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있다.

 

static 변수는 클래스인 붕어빵 틀이 특별히 관리하는 변수다. 근데 붕어빵 틀은 1개니까 클래스 변수도 하나만 존재하는 것이다. 반면, 인스턴스인 붕어빵은 인스턴스의 개수만큼 변수가 존재한다. 이제 static 변수를 정리해보자.

 

먼저 아래 간단한 코드를 보고 용어를 확실히 정립해보자.

public class Data3 {
	public String name;
	public static int count;
}

분명히 namecountData3 클래스의 멤버 변수이다. 근데 멤버 변수는 static 키워드가 붙은 것과 아닌 것에 따라 아래와 같이 분류할 수 있다.

 

  • 인스턴스 변수: name과 같이static이 붙지 않은 멤버 변수

    • static 키워드가 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용 가능하다. 그리고 인스턴스에 소속되어 있기 때문에 인스턴스 변수라고 부르는 것이다.
    • 인스턴스 변수는 인스턴스가 생성될 때마다 새로 만들어진다.
  • 클래스 변수: count와 같이 static 키워드가 붙은 멤버 변수

    • 클래스 변수, 정적 변수, static 변수 모두 같은 말이다.

    • static이 붙은 클래스 변수는 클래스 자체에 소속되어 있고, 인스턴스와 무관하게 클래스에 바로 접근해서 사용 가능하다.

    • 클래스 변수는 자바 프로그램이 시작될 때 딱 1개만 만들어진다. 인스턴스와 다르게 여러 곳에서 공유하는 목적으로 사용된다.

       

💖 변수와 생명주기

지금까지 살펴본 결과, 변수는 지역 변수, 인스턴스 변수, 클래스 변수로 나눌 수 있다.

매개 변수를 포함한 지역 변수는 스택 영역에 있는 스택 프레임 안에 보관된다. 메서드가 종료되면 스택 프레임과 함께 제거되기 때문에 생존 주기가 짧다. 인스턴스 변수는 인스턴스에 있는 멤버 변수를 말한다. 힙 영역에 보관되며, GC가 발생하기 전까지는 생존하기 때문에 지역 변수보다는 대체로 생존 주기가 길다. 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수다. 메서드 영역은 프로그램 전체에서 사용하는 공용 공간이다. 클래스 변수는 해당 클래스가 JVM에 로딩되는 순간 생성되고, JVM이 종료될 때까지 생존하기 때문에 가장 긴 생명주기를 가진다.

static이 정적이라는 이유가 여기에 있는 것이다. 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성, 제거된다. 반면, 정적 변수는 프로그램 실행 시점에 만들어지고, 종료 시점에 제거되기 때문에 정적이라는 이름이 붙은 것이다.

 

🎯 정적 변수 접근법

클래스 변수는 클래스를 통해 직접 접근할 수도 있고, 인스턴스를 통해 접근할 수도 있다. 아래 코드를 보자.

package static1;

public class DataCountMain3 {

    public static void main(String[] args) {
        Data3 data1 = new Data3("A");
        System.out.println("A count=" + Data3.count);

        Data3 data2 = new Data3("B");
        System.out.println("B count=" + Data3.count);

        Data3 data3 = new Data3("C");
        System.out.println("C count=" + Data3.count);

        Data3 data4 = new Data3("D");
        System.out.println("인스턴스를 통해 접근한 D의 count는 " + data4.count);
        System.out.println("클래스를 통한 직접 접근한 D의 count는 " + Data3.count);
    }
}

/*
A count=1
B count=2
C count=3
인스턴스를 통해 접근한 D의 count는 4
클래스를 통한 직접 접근한 D의 count는 4
*/

하지만 정적 변수의 경우, 인스턴스를 통한 접근은 권장되지 않는다. 왜냐하면 위의 코드를 보다시피 count를 딱 봤을 때, 인스턴스 변수에 접근하는 것처럼 보일 수 있다. 반면, Data3.count처럼 클래스를 통해 직접 접근한 코드는 100% 클래스 변수라는 확신이 든다.


🥣 static 메서드

이번엔 static이 붙은 메서드에 대해 알아보기 위해 특정 문자열을 꾸며주는 간단한 기능을 만들어보자.

package static2;

public class DecoUtil1 {

    public String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}
package static2;

public class DecoMain1 {
    public static void main(String[] args) {
        String s = "Hello Java!";
        DecoUtil1 utils = new DecoUtil1();
        String deco = utils.deco(s);

        System.out.println("Before: " + s);
        System.out.println("After: " + deco);
    }
}

/*
Before: Hello Java!
After: *Hello Java!*
*/

보다시피 문자열이 잘 꾸며져 나오는 것을 확인할 수 있다. 근데 DeocoUtil1이라는 클래스는 멤버 변수도 없고, 달랑 매개 변수 넘어온 걸로 작업하고 뱉어주는 메서드밖에 없다. 여기서 인스턴스를 사용하는 이유에 대해 다시 한번 짚고 넘어갈 필요가 있다. 인스턴스는 인스턴스 변수를 사용하기 위해 쓴다. 그런 의미에서 위의 예제에서는 객체를 생성하는 것이 크게 의미가 없어 보인다. 예제를 살짝 바꿔보자.

 

package static2;

public class DecoUtil2 {

    public static String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}

이번엔 메서드 앞에 static이 붙어 있다. 그럼 해당 메서드는 “정적 메서드” 가 된다. 정적 메서드는 정적 변수처럼 클래스 소속이 되어 버려서, 인스턴스 생성을 하지 않아도 클래스명을 통해 바로 호출할 수 있다.

 

package static2;

public class DecoMain2 {
    public static void main(String[] args) {
        String s = "Welcome Java~";
        String deco = DecoUtil2.deco(s);

        System.out.println(s);
        System.out.println(deco);
    }
}

/*
Welcome Java~
*Welcome Java~*
*/

보다시피, 정적 메서드 덕분에 불필요하게 객체를 생성하지 않고 편리하게 메서드를 호출할 수 있다. 하지만 정적 메서드는 아무때나 사용할 수 있는 것은 아니다.

  • static 메서드는 static만 사용할 수 있다.

    • “클래스 내부의 기능을 사용할 때, 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있다.”
    • 클래스 내부의 기능을 사용할 때, 정적 메서드는 인스턴스 변수나, 인스턴스 메서드를 사용할 수 없다.
  • 반대로, 모든 곳에서 static을 호출할 수 있다.

    • 정적 메서드는 공용 기능이다. 따라서 접근 제어자만 허락한다면 클래스를 통해 모든 곳에서 static을 호출할 수 있다.

       

이해를 위해 아래 코드를 보자.

package static2;

public class DecoData {

    private int instanceValue;
    private static int staticValue;

    public static void staticCall() {
        staticValue++;  // 정적 변수에 접근 가능
        staticMethod();  // 정적 메서드에 접근 가능

        // Non-static field 'instanceValue' cannot be referenced from a static context
        // instanceValue++;
        // instanceMethod();
    }

    public void instanceCall() {
        instanceValue++; // 인스턴스 변수 접근
        instanceMethod(); // 인스턴스 메서드 접근

        staticValue++; // 정적 변수 접근
        staticMethod(); // 정적 메서드 접근
    }

    private static void staticMethod() {
        System.out.println("정적 변수 값은: " + staticValue);
    }

    private void instanceMethod() {
        System.out.println("인스턴스 변수 값은: " + instanceValue);
    }
}

staticCall() 이라는 정적 메서드는 현재 DecoData 클래스 소속이다. 하지만, 인스턴스 변수나 인스턴스 메서드는 인스턴스를 생성해야 뭘 할 수가 있는 것이다. 힙 영역에 인스턴스가 생성이 됐는지 안 됐는지 클래스는 알 수가 없다.

instanceCall() 인스턴스 메서드는 인스턴스 변수와 인스턴스 메서드는 물론이고, 정적 변수와 정적 메서드에도 접근할 수 있다.

 

public class DecoDataMain {

    public static void main(String[] args) {
        System.out.println("정적 호출...");
        DecoData.staticCall();

        System.out.println("첫 번째 인스턴스 호출...");
        DecoData data1 = new DecoData();
        data1.instanceCall();

        System.out.println("두 번째 인스턴스 호출...");
        DecoData data2 = new DecoData();
        data2.instanceCall();
    
    }
}

/*
정적 호출...
정적 변수 값은: 1
첫 번째 인스턴스 호출...
인스턴스 변수 값은: 1
정적 변수 값은: 2
두 번째 인스턴스 호출...
인스턴스 변수 값은: 1
정적 변수 값은: 3
*/

보다시피 staticCall()을 호출하면 객체 없이 클래스 이름으로 호출이 가능(DecoData.staticCall())하다. staticCall() 메서드는 정적 변수와 정적 메서드를 호출하지만, 인스턴스에 대해서는 참조값을 모르기 때문에 그 인스턴스의 존재 자체를 알 방법이 없다. 따라서 인스턴스 변수 및 인스턴스 메서드에는 접근이 불가능하다.

반면, DecoData의 인스턴스에서는 각각의 인스턴스 변수와 인스턴스 메서드에 접근할 수 있고, static 영역에 공유되고 있는 정적 변수와 정적 메서드에도 접근이 가능하다. 따라서 각각의 인스턴스가 instanceCall() 메서드를 호출할 때마다 정적 변수의 값이 증가하는 것을 볼 수 있다.

 

여기서 굳이 정적 메서드로 인스턴스 변수나 인스턴스 메서드를 호출하고 싶다면, 객체의 참조값을 아래와 같이 직접 매개 변수로 전달하면 된다.

public static void staticCall(DecoData data) {
	data.instanceValue++;
	data.instanceMethod();
}

하지만, 다시 한번 말하지만 자칫 잘못 보면 인스턴스 메서드에 접근하는 것처럼 보일 수도 있기 때문에 정적 메서드의 경우에 인스턴스를 통한 접근은 권장되지 않는다.

 

추가로, 정적 메서드를 자주 호출해야 하는 상황이라면 static import를 통해 클래스명을 생략함으로써 코드를 더 간결하게 할 수 있다.

// 이런 식으로 정적 메서드에 접근할 때마다 클래스명을 붙이기 번거롭다면...
DecoData.staticCall();
DecoData.staticCall();
DecoData.staticCall();
// 이런 식으로 메서드를 import해서 코드를 간결화
package static2;

import static static2.DecoData.staticCall;

public class DecoDataMain {
	public static void main(String[] args) {
		System.out.println("정적 호출...");
		staticCall();
		staticCall();
		staticCall();
		...
	}
}

위와 같이, 특정 정적 메서드를 끌어 올 수도 있고, 모든 정적 메서드를 import 하고 싶다면, *를 붙여주면 된다. 참고로 정적 메서드 뿐만 아니라 정적 변수를 끌어오는 것도 당연히 가능하다.

 

🚪 main() 메서드는 정적 메서드

생각해보면, main() 메서드도 정적 메서드다. public static void main(String[] args) … 진짜 그렇다. 인스턴스 생성 없이 실행할 수 있는 가장 대표적인 메서드가 바로 이 main() 메서드다. 이 메서드는 프로그램을 시작하는 시작점이 된다.

정적 메서드는 정적 메서드만 호출할 수 있다. 더 정확하게 말하자면, 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있는 것이다.

그래서 main() 메서드에서 어떤 메서드를 호출할 때, 항상 앞에 static을 붙여준 것이다.

// main이라는 정적 메서드에서는...
public class ValueDataMain {
    public static void main(String[] args) {
        ValueData valueData = new ValueData();
        add(valueData);
    }
    
    // 이처럼 static이 붙은 정적 메서드만 호출할 수 있다.
    static void add(ValueData valueData) {
        valueData.value++;
        System.out.println("숫자 증가 value=" + valueData.value);
    }
}

만약 add() 메서드가 static이 붙지 않은 인스턴스 메서드라면 ValueDataMain 클래스로 인스턴스를 생성해야만 호출할 수 있을 것이다.


🚘 문제 - 구매한 자동차 수

아래 코드를 참고해서 생성한 차량 수를 출력하는 프로그램을 만들어보자.

package static2.ex;

public class CarMain {
	public static void main(String[] args) {
		Car car1 = new Car("Porsche");
		Car car2 = new Car("Cyber Truck");
		Car car3 = new Car("Bentley");
				
		Car.showTotalCars();  // 구매한 차량 수를 출력하는 정적 메서드
	}
}
// 내가 푼 풀이
package static2.ex;

public class Car {

    private String name;
    private static int count;

    public Car(String name) {
        System.out.println("차량 구입, 이름: " + name);
        this.name = name;
        count++;
    }

    public static void showTotalCars() {
        System.out.println("구매한 차량 수: " + count);
    }
}

/*
차량 구입, 이름: Porsche
차량 구입, 이름: Cyber Truck
차량 구입, 이름: Bentley
구매한 차량 수: 3
*/

Car 클래스는 별도의 인스턴스들을 만드니까 name이라는 인스턴스 변수, Car() 라는 인스턴스 메서드를 선언하고, 안에 생성된 만큼 차량 수를 누적해야 하므로 공용으로 쓰일 정적 변수 count를 선언하고, 증가시키도록 했다.


🧮 문제 - 수학 유틸리티 클래스

다음 기능을 제공하는 배열용 수학 유틸리티 클래스(MathArrayUtils)를 만들어보자.

  • sum(int[] array) : 배열의 모든 요소를 더하여 합계를 반환
  • average(int[] array) : 배열의 모든 요소의 평균값을 계산
  • min(int[] array) : 배열에서 최솟값을 찾는다.
  • max(int[] array) : 배열에서 최댓값을 찾는다.

MathArrayUtils는 객체를 생성하지 않고 사용해야 한다. 누군가 실수로 MathArrayUtils의 인스턴스를 생성하지 못하게 막아야 하는 것이다.

package static2.ex;

import static static2.ex.MathArrayUtils.*;

public class MathArrayUtilsMain {

    public static void main(String[] args) {
        int[] values = {1, 2, 3, 4, 5};
        System.out.println("sum=" + sum(values));
        System.out.println("average=" + average(values));
        System.out.println("min=" + min(values));
        System.out.println("max=" + max(values));
    }
}
// 내가 푼 풀이
package static2.ex;

public class MathArrayUtils {

    private MathArrayUtils() {}

    public static int sum(int[] arr) {
        int sum = 0;
        for (int i : arr) {
            sum += i;
        }

        return sum;
    }

    public static float average(int[] arr) {
        return (float)sum(arr) / arr.length;
    }

    public static int min(int[] arr) {
        int min = arr[0];
        for (int i : arr) {
            if (i < min) {
                min = i;
            }
        }

        return min;
    }

    public static int max(int[] arr) {
        int max = arr[0];
        for (int i : arr) {
            if (i > max) {
                max = i;
            }
        }

        return max;
    }
}

/*
sum=15
average=3.0
min=1
max=5
*/
profile
도메인을 이해하는 백엔드 개발자(feat. OOP)

0개의 댓글