관리 메뉴

나구리의 개발공부기록

자바 메모리 구조와 static, 자바 메모리 구조, 스택과 큐 자료 구조, 스택 영역, 스택 영역과 힙 영역, static 변수, static 메서드 본문

인프런 - 실전 자바 로드맵/실전 자바 - 기본편

자바 메모리 구조와 static, 자바 메모리 구조, 스택과 큐 자료 구조, 스택 영역, 스택 영역과 힙 영역, static 변수, static 메서드

소소한나구리 2025. 1. 8. 18:55

출처 : 인프런 - 김영한의 실전 자바 - 기본편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 자바 메모리 구조

1) 자바 메모리 구조

(1) 비유

  • 자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있음
  • 메서드 영역: 클래스 정보를 보관, 이 클래스 정보가 붕어빵 틀이라고 볼 수 있음
  • 스택 영역: 실제 프로그램이 실행되는 영역, 메서드를 실행할 때마다 하나씩 쌓임
  • 힙 영역: 객체(인스턴스)가 생성되는 영역, new 명령어를 사용하면 이 영역을 사용하며 붕어빵 틀로부터 생성된 붕어빵이 존재하는 공간이라고 이해하면 됨, 참고로 배열도 이 영역에 생성됨

(2-1) 실제 구조

(2-2) 메서드 영역(Method Area)

  • 프로그램을 실행하는데 필요한 공통 데이터를 관리, 해당 영역은 프로그램의 모든 영역에서 공유함
  • 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드등 모든 실행 코드가 존재함
  • static 영역: static 변수들을 보관함(뒤에서 자세히 설명)
  • 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수를 보관함, 예를 들어 프로그램에 "hello"라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리함
    이 외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리함

(2-3) 스택 영역(Stack Area)

  • 자바 실행 시, 하나의 실행 스택이 생성되며 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함함
  • 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이며 메서드를 호출할 때마다 하나의 스택 프레임이 쌓이고 메서드가 종료되면 해당 스택 프레임이 제거됨

(2-4) 힙 영역(Heap Area)

  • 객체(인스턴스)와 배열이 생성되는 영역
  • 가비지 컬렉션(GC)이 이루어지는 주요 영역이며 더이상 참조되지 않는 객체는 GC에 의해 제거됨

** 참고

  • 문자열을 다루는 문자열 풀은 메서드 영역이였으나 자바 7부터 힙 영역으로 이동했음
  • 스택 영역은 더 정확하게는 각 쓰레드별로 하나의 실행 스택이 생성되어 쓰레드 수 만큼 스택 영역이 생성됨
  • 지금은 쓰레드를 1개만 사용하므로 스택 영역도 하나이며 쓰레드에 대한 부분은 멀티 쓰레드를 학습해야 이해할 수 있음

(3) 메서드 코드는 메서드 영역에

  • 자바에서 특정 클래스로 100개의 인스턴스를 생성하면 힙 메모리에 100개의 인스턴스가 생기며 각각의 인스턴스는 내부에 변수와 메서드를 가짐
  • 같은 클래스로부터 생성된 객체라도 인스턴스 내부의 변수 값은 서로 다를 수 있지만 메서드는 공통된 코드를 공유하기 때문에 객체가 생성될 때 인스턴스 변수에는 메모리가 할당되지만 메서드에 대한 새로운 메모리 할당은 없음
  • 즉, 메서드는 메서드 영역에서 공통으로 관리되고 실행되며 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행함

2. 스택과 큐 자료 구조

1) 스택과 큐 자료 구조

(1) 스택(Stack) 구조

스택 구조

  • 후입 선출(LIFO, Last In First Out)
  • 블럭을 네모난 통에 넣는다고 생각해보면 위쪽에만 열려있기 때문에 위쪽으로 블럭을 넣고 위쪽으로 블럭을 빼야함
  • 이렇게 입구가 하나로 가장 나중에 넣은 것이 가장 먼저 나오는 것을 후입 선출이라하는데 이런 자료 구조를 스택이라 함
  • 1(입력) -> 2(입력) -> 3(입력) -> 3(출력) -> 2(출력) -> 1(출력)

(2) 큐(Queue) 자료 구조

큐 자료 구조

  • 선입 선출(FIFO, First In First Out)
  • 후입 선출과 반대로 가장 먼저 넣은 것이 가장 먼저 나오는 것을 선입 선출이라하며 이런 자료 구조를 큐라고 함
  • 1(입력) -> 2(입력) -> 3(입력) -> 1(출력) -> 2(출력) -> 3(출력)

(3) 정리

  • 이런 자료 구조는 각자 필요한 영역이 있으며 선착순 이벤트를 진행시 고객이 대기해야한다면 스택구조를 사용하면 먼저 들어온 사용자가 가장 마지막에 꺼내지므로 큐 자료 구조를 사용해야함
  • 프로그램 실행과 메서드 호출에는 스택 구조가 적합함

3. 스택 영역

1) 스택 영역

(1) JavaMemoryMain1

  • main()메서드 내부에서 method1을 호출하고 method1에서 method2를 호출하는 구조
package memory;

public class JavaMemoryMain1 {
    public static void main(String[] args) {
        System.out.println("main start");
        method1(10);
        System.out.println("main end");
    }

    static void method1(int m1) {
        System.out.println("method1 start");
        int cal = m1 * 2;
        method2(cal);
        System.out.println("method1 end");
    }

    static void method2(int m2) {
        System.out.println("method2 start");
        System.out.println("method2 end");
    }
}
/* 실행결과
main start
method1 start
method2 start
method2 end
method1 end
main end
*/

 

(2) 호출 그림

  • 처음 자바 프로그램을 실행하면 main()을 실행하며 이때 main()을 위한 스택 프레임이 하나 생성됨
  • main() 스택 프레임은 내부에 args라는 매개변수를 가지는데 args는 뒤에서 다룸(외부에서 입력값을 주는 용도)
  • main()은 method1()을 호출하고 method1() 스택 프레임이 생성됨
  • method1()은 m1, cal 이라는 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수들이 스택 프레임에 포함됨
  • method1()은 method2()를 호출하고 method2() 프레임이 생성됨
  • method2()는 m2 지역 변수(매개변수 포함)를 가지므로 해당 지역 변수가 스택 프레임에 포함됨

(3) 종료 그림

  • method2()가 종료되면 method2() 스택 프레임이 제거되고 매개변수 m2도 제거되고 프로그램은 method1()로 돌아가는데 method1()을 처음부터 시작하는 것이 아니라 method1()에서 method2()를 호출한 지점으로 돌아감
  • method1()이 종료되면 method1() 스택 프레임이 제거되고 지역변수(매개변수 포함)도 제거되며 프로그램은 main()으로 돌아감
  • main()이 종료되면 더 이상 호출할 메서드가 없고 스택 프레임도 완전히 비워져 자바는 프로그램을 정리하고 종료함

(4) 정리

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

4. 스택 영역과 힙 영역

1) 스택 영역과 힙 영역이 함께 사용되는 경우

(1) Data

package memory;

public class Data {
    private int value;

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

    public int getValue() {
        return value;
    }
}

 

(2) JavaMemoryMain2

  • main() -> method1() -> method2() 순서로 호출하는 단순한 코드이며 method1()에서 Data 클래스의 인스턴스를 생성하고 method2()를 호출할 때 매개변수에 Data 인스턴스의 참조값을 전달
package memory;

public class JavaMemoryMain2 {
    public static void main(String[] args) {
        System.out.println("main start");
        method1();
        System.out.println("main end");
    }

    static void method1() {
        System.out.println("method1 start");
        Data data1 = new Data(10);
        method2(data1);
        System.out.println("method1 end");
    }

    static void method2(Data data2) {
        System.out.println("method2 start");
        System.out.println("data.value = " + data2.getValue());
        System.out.println("method2 end");
    }
}
/* 실행 결과
main start
method1 start
method2 start
data.value = 10
method2 end
method1 end
main end
*/

 

(3) 실행 순서

  • 1. 처음 main() 메서드를 실행하고 main() 스택 프레임이 생성됨
  • 2. main()에서 method1()을 실행하고 method1() 스택 프레임이 생성됨
    - method1()은 지역 변수로 Data data1을 가지고 있으므로 이 지역 변수도 스택 프레임에 포함됨며 new Data(10)메서드를 사용하여 힙 영역에 Data 인스턴스를 생성하고 참조값을 data1에 보관함
  • 3. method1()은 method2()를 호출하면서 Data data2 매개변수에 data1에 보관한 참조값을 넘김
    - method1()에 있는 data1과 method2()에 있는 data2는 둘다 같은 참조값을 가지고 있으므로 같은 인스턴스를 참조함
  • 4. method2()가 종료되면 method2()의 스택 프레임이 제거되면서 매개변수 data2도 함께 제거됨
  • 5. method1()이 종료되면 method1()의 스택 프레임이 제거되면서 지역변수 data1도 함께 제거됨
  • method1()이 종료된 직후의 상태
    - Data 인스턴스를 참조하는 곳이 없으므로 사용되는 곳도 없음
    - 결과적으로 프로그램에서 더는 사용하지 않는 메모리만 차지하는 객체가 되어버림
    - GC(가비지 컬렉션)이 이렇게 참조가 모두 사라진 인스턴스를 찾아서 메모리에서 제거함

** 참고

  • 힙 영역 외부가 아닌, 힙 영역 안에서만 인스턴스끼리 서로 참조하는 경우에도 GC의 대상이 됨

(4) 정리

  • 지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리됨

5. static 변수

1) 인스턴스 내부 변수에 카운트 저장

(1) Data1

  • 생성된 객체의 수를 세기 위한 목적으로 객체가 생성될 때마다 생성자를 통해 인스턴스의 멤버 변수인 count값을 증가시키도록 작성
  • 예제를 단순하게 만들기 위해 필드에 public을 사용하였으며 예제에서 필드에 public을 사용하면 예제를 간단히 하기 위해서 적용한 것이라고 보면됨
package static1;

public class Data1 {
    public String name;
    public int count;
    
    public Data1(String name) {
        this.name = name;
        count++;
    }
}

 

(2) DataCountMain1

  • 그러나 객체를 생성하고 카운트 값을 출력해보면 이 프로그램은 당연히 기대한대로 작동하지 않는데 객체를 생성할 때마다 Data1 인스턴스는 새로 만들어지므로 인스턴스에 포함된 count 변수도 새로 만들어지기 때문임
  • 처음 Data1("A") 인스턴스를 생성하면 count 값은 0으로 초기화되고 생성자에서 count++을 호출했으므로 count의 값은 1이됨
  • Data2("B") 인스턴스를 생성하면 완전히 새로운 인스턴스를 생성하며 마찬가지로 count의 값은 1이 되고 Data3("C") 인스턴스를 생성할 때도 마찬가지로 count의 값은 1이 됨
  • 즉, 인스턴스에 사용되는 멤버 변수 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
*/

2) 외부 인스턴스에 변수에 카운트 저장

(1) Counter

  • 카운트 값을 저장하는 별도의 객체
  • 해당 객체를 공유해서 필요할 때마다 카운트 값을 증가 시킴
package static1;

public class Counter {
    public int count;
}

 

(2) Data2

  • 여기에는 count 멤버 변수가 없는 대신 생성자에서 Counter인스턴스를 추가로 전달 받음
  • 생성자가 호출되면 counter인스턴스에 있는 count 변수의 값을 하나 증가시킴
package static1;

public class Data2 {
    public String name;

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

 

(3) DataCountMain2

  • Counter 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때마다 값을 정확하게 증가시킬 수 있음
  • Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값은 1이 됨
  • Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값이 2가 됨
  • Data2("C") 인스턴스를 생성해도 마찬가지로 Counter 인스턴스에 있는 count 값을 하나 증가시키고 count 값이 3이 됨
  • 결과적으로 Data2의 인스턴스가 3개 생성되고 count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정되었는데, 이렇게 하게 되면 아래와 같은 불편한 점들이 있음
    - Data2 클래스와 관련된 일인데 Counter라는 별도의 클래스를 추가로 사용해야함
    - 생성자의 매개변수도 추가되고, 생성자가 복잡해지며 생성자를 호출하는 부분도 복잡해짐
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
*/

3) Static 변수 사용

(1) Data3

  • 변수 타입 앞에 static 키워드를 붙이면 static 변수, 정적 변수, 클래스 변수라고 함
  • 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킴
package static1;

public class Data3 {
    public String name;
    public static int count;    // static 변수

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

 

(2) DataCountMain3

  • 코드를 보면 count 정적 변수에 접근하는 방법이 조금 특이한데 Data3.count와 같이 클래스명에 .(dot)을 사용하여 마치 클래스에 직접 접근하는 것처럼 사용함
  • 기대하는 바와 같이 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
*/

 

(3) 그림으로 이해

  • 1. Data3("A") 인스턴스를 생성하면 생성자가 호출되고 생성자에있는 count++코드가 호출되며 static 변수인 count의 값이 하나 증가함
    - static 이 붙은 멤버 변수 즉, 예제에서의 count 변수는 인스턴스 영역에 생성되지 않고 메서드 영역에서 이 변수를 관리함
    - 메서드 영역에 있는 count의 값이 하나 증가하게 됨
  • 2. Data3("B") 인스턴스를 생성하면 생성자가 호출되고 마찬가지로 count++코드가 있으므로 메서드 영역에 있는 count 변수의 값이 하나 증가함
  • 3. Data3("C") 도 동일하게 실행되며 메서드 영역에 있는 count 변수의 값이 하나 증가하게 됨
  • 4. 최종적으로 메서드 영역에 있는 count 변수의 값은 3이됨
  • static이 붙은 정적 변수에 접근하려면 Data3.count와 같이 클래스명 + .(dot) + 변수명으로 접근하면 되며 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있음
  • static 변수를 사용하면 공용으로 변수를 사용할 수 있어 편리하게 문제를 해결할 수 있게 됨

(4) 정리

  • static 변수는 클래스인 붕어빵 틀이 특별히 관리하는 변수라고 이해하면 됨
  • 붕어빵 틀은 1개이므로 클래스 변수도 1개만 존재하는 반면 인스턴스 변수는 붕어빵인 인스턴스의 수 만큼 존재할 수 있음

4) 용어 정리

public class Data3 {
    public String name;
    public static int count;    // static 변수
}

 

(1) 멤버 변수(필드)의 종류

  • 예제 코드에서 name, count는 둘다 멤버 변수(필드)이며 static이 붙은 것과 아닌 것에 따라 분류 할 수 있음
  • 인스턴스 변수: static이 붙지 않은 멤버 변수, ex) name
    - static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고 인스턴스에 소속되어서 인스턴스 변수라고 함
    - 인스턴스 변수는 인스턴스를 만들 때마다 새로 만들어짐
  • 클래스 변수: static이 붙은 멤버 변수, ex) count
    - 클래스 변수, 정적 변수, static 변수 등으로 부르며 모든 용어를 사용함
    - static이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고 클래스 자체에 소속되어 있어서 클래스 변수라고 함
    - 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어지며 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용됨

5) 변수와 생명주기

(1) 지역 변수(매개변수 포함)

  • 스택 영역에 있는 스택 프레임 안에 보관됨
  • 메서드가 종료되면 스택 프레임도 제거되고 이때 해당 스택 프레임에 포함된 지역 변수도 함께 제거됨
  • 지역 변수는 생존 주기가 짧음

(2) 인스턴스 변수

  • 인스턴스에 있는 멤버 변수를 인스턴스 변수라 하며 힙 영역을 사용함
  • 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지는 생존하기 때문에 일반적으로는 지역 변수보다 생존 주기가 긺

(3) 클래스 변수

  • 클래스 변수는 메서드 영역의 static 영역에 보관되는 변수이며 메서드 영역은 프로그램 전체에서 사용하는 공용공간임
  • 클래스 변수는 해당 클래스가 JVM에 로딩 되는 순간 생성이되고 JVM이 종료될때 까지 생명주기가 이어져 가장 긴 생명주기를 가짐
  • static이 정적이라는 이유는 바로 여기에 있는데 힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고 제거되는 반면, static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고 프로그램 종료 시점에 제거되므로 정적 변수는 이름그대로 정적임

6) 정적 변수 접근법

(1) DataCountMain3 - 추가

  • static 변수는 클래스를 통해 바로 접근할 수도 있고 인스턴스를 통해 접근할 수도 있음
  • 실행해보면 둘의 차이는 없고 둘다 결과적으로 정적 변수에 접근함
package static1;

public class DataCountMain3 {
    public static void main(String[] args) {
        // ... 기존 코드 동일
        
        Data3 data4 = new Data3("D");
        System.out.println(data4.count);    // 인스턴스를 통한 접근
        System.out.println(Data3.count);    // 클래스를 통한 접근
    }
}
/* 실행결과(생략된 코드의 실행결과 제외)
4
4
*/

 

(2) 올바른 정적 변수 접근 방법

  • 그러나 정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는데 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것 처럼 오해할 수 있기 때문임
  • 정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하므로 정적 변수에 접근 할 때는 클래스를 통해서 접근해야함

6. static 메서드

1) 인스턴스 메서드

(1) DecoUtil1

  • 특정 문자열의 앞뒤에 *을 붙여서 꾸며주는 deco()메서드를 가지고 있음
package static2;

public class DecoUtil1 {
    public String deco(String str) {
        return "*" + str + "*";
    }
}

 

(2) DecoMain1

  • 앞서 개발한 deco() 메서드를 호출하기 위해 DecoUtil1의 인스턴스를 먼저 생성해야함
  • 그러나 deco()라는 기능은 멤버 변수도 없고 단순히 기능만 제공할 뿐임
  • 인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데 이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공함
  • 즉, 인스턴스를 계속 생성하는 의미가 없음
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*
*/

2) static 메서드

(1) DecoUtil2

  • 메서드 앞에 static을 붙여서 정적 메서드를 만들 수 있음
  • 이렇게 static이 붙은 정적 메서드는 정적 변수처럼 인스턴스 생성 없이 클래스 명을 통해서 바로 호출할 수 있음
package static2;

public class DecoUtil2 {
    static public String deco(String str) {
        return "*" + str + "*";
    }
}

 

(2) DecoMain2

  • static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + .(dot) + 메서드 명으로 바로 호출할 수 있으며 정적 메서드 덕분에 불필요한 객체 생성 없이 편리하게 메서드를 사용할 수 있게 됨
  • 클래스 메서드: 메서드 앞에도 static을 붙일 수 있는데 이것을 정적 메서드 또는 클래스 메서드라고 함
    - 정적 메서드라는 용어는 static이 정적이라는 뜻이기 때문이고 클래스 메서드라는 용어는 인스턴스 생성 없이 마치 크래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문임
  • 인스턴스 메서드: static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있어서 인스턴스 메서드라고 부름
package static2;

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

        System.out.println("before: " + s);
        System.out.println("after: " + deco);
    }
}
/* 실행결과는 동일 */

3) 정적 메서드 사용법

(1) static 메서드는 static만 사용할 수 있음

  • 클래스 내부의 기능을 사용할 때 정적 메서드는 static이 붙은 정적 메서드나 정적 변수만 사용할 수 있음
  • 클래스 내부의 기능을 사용할 때 정적 메서드는 인스턴스 변수나 인스턴스 메서드를 사용할 수 없음

(2) 반대로 모든 곳에서 static을 호출할 수 있음

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

(3-1) DecoData

  • 접근 제어자를 활용하여 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었음
  • instanceValue: 인스턴스 변수
  • staticValue: 정적 변수(클래스 변수)
  • instanceMethod(): 인스턴스 메서드
  • staticMethod(): 정적 메서드(클래스 메서드)
package static2;

public class DecoData {

    private int instanceValue;
    private static int staticValue;

    public static void staticCall() {
//        instanceValue++;    // 인스턴스 변수 접근 불가, compile error
//        instanceMethod();   // 인스턴스 메서드 접근 불가, compile error

        staticValue++;      // 정적 변수 접근 OK
        staticMethod();     // 정적 메서드 접근 OK
    }
    
    public void instanceCall() {
        instanceValue++;    // 인스턴스 변수 접근 OK
        instanceMethod();   // 인스턴스 메서드 접근 OK

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

    private void instanceMethod() {
        System.out.println("instanceValue=" + instanceValue);    }

    private static void staticMethod() {
        System.out.println("staticValue=" + staticValue);    
    }
}

 

(3-2) DecoData - staticCall() 메서드

  • staticCall()메서드는 정적메서드이므로 static만 사용할 수 있음
  • 정적 변수, 정적 메서드에는 접근할 수 있지만, static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생함

(3-3) DecoData - instanceCall() 메서드

  • instanceCall()메서드는 인스턴스 메서드이므로 인스턴스 변수, 인스턴스 메서드를 사용할 수 있음
  • 또한 모든 곳에서 공용인 static을 호출할 수 있으므로 정적 변수, 정적 메서드에도 접근할 수 있음

(4) DecoDataMain

  • 정적 메서드와 인스턴스 메서드를 각각 호출
  • staticValue는 스태틱 변수이므로 각 메서드에서 공용으로 사용하여 값이 누적이 되고 instanceValue는 인스턴스 변수이므로 해당 인스턴스에서만 사용되는 변수이므로 값이 인스턴스마다 다르게 사용됨
package static2;

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

        System.out.println("2. 인스턴스 호출1");
        DecoData data1 = new DecoData();
        data1.instanceCall();

        System.out.println("3. 인스턴스 호출3");
        DecoData data2 = new DecoData();
        data2.instanceCall();
    }
}
/* 실행 결과
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출3
instanceValue=1
staticValue=3
*/

 

(5) 정적 메서드가 인스턴스의 기능을 사용할 수 없는 이유

  • 정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있어서 인스턴스처럼 참조값의 개념이 없음
  • 특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데 정적 메서드는 참조값 없이 호출하므로 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없음
  • 당연히 아래처럼 객체의 참조값을 직접 매개변수로 전달하면 정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있음
public static void staticCall(DecoData data) {
    data.instanceValue++;
    data.instanceMethod();
}

4) 용어 정리

(1) 멤버 메서드의 종류

  • 인스턴스 메서드: static이 붙지 않은 멤버 메서드
  • 클래스 메서드: static이 붙은 메서드
    - 클래스 메서드, 정적 메서드, static 메서드등으로 부름
  • static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고 인스턴스에 소속되어 있으므로 인스턴스 메서드라함
  • static이 붙은 멤버 메서드는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고 클래스 자체에 소속되어 있어 클래스 메서드라 함
  • 해당 설명은 멤버 변수에도 똑같이 적용됨

(2) 정적 메서드 활용

  • 정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용함
  • 예를 들어 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용함
  • 수학의 여러가지 기능을 담은 클래스를 만들 수 있는데 이 경우 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분인데 이럴 때 정적 메서드를 사용해서 유틸리티성 메서드를 만들면 좋음

5) 정적 메서드 접근법

(1) 클래스를 통해서 접근 해야 함

  • static 변수에서 다뤘던 것처럼 static 메서드 메서드도 인스턴스를 통해서도 접근할 수 있는데 권장하지 않으며 이유도 동일함
  • static 변수, static 메서드는 인스턴스에 접근하는 것처럼 오해하지 않도록 클래스를 통해서 접근해야 함

6) static import

(1) DecoDataMain - static import 적용

  • 정적 메서드를 사용할 때 스태틱 메서드를 자주 호출해야한다면 static import 기능을 사용할 수 있음
  • 특정 클래스의 정적 메서드 하나만 적용하려면 import static static2.DecoData.staticCall; 처럼 생략할 메서드 명을 적어주면 되며 특정 클래스의 모든 메서드에 적용하려면 *을 사용하면 됨
  • static import는 정적 변수에도 사용할 수 있음
package static2;

import static static2.DecoData.staticCall;  // 특정 메서드를 지정
import static static2.DecoData.*;           // 해당 클래스의 모든 메서드를 static import

public class DecoDataMain {
    public static void main(String[] args) {
        System.out.println("1. 정적 호출");
        staticCall();   // 클래스 명이 생략 됨
        staticCall();
        staticCall();
        staticCall();
    }
}

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

(1) main() 메서드

  • 인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main() 메서드임
  • main()메서드는 프로그램을 시작하는 시작점이 되는데, 객체를 생성하지 않아도 main() 메서드가 작동했던 이유가 main()메서드가 static이기 때문임
  • 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있으므로 정적 메서드인 main() 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했음

7. 문제와 풀이

1) 구매한 자동차 수

(1) 요구사항 및 문제

  • 다음 코드를 참고해서 생성한 차량 수를 출력하는 프로그램을 작성
  • Car 클래스를 작성
더보기
package static2.ex;
public class CarMain {
    public static void main(String[] args) {
        Car car1 = new Car("K3");
        Car car2 = new Car("G80");
        Car car3 = new Car("Model Y");
        Car.showTotalCars(); //구매한 차량 수를 출력하는 static 메서드
    }
}

 

실행 결과

차량 구입, 이름: K3

차량 구입, 이름: G80

차량 구입, 이름: Model Y

구매한 차량 : 3

 

(2) 정답

더보기
package static2.ex;

public class Car {

    private static int totalCars;
    private String name;

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

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

 

실행 결과

차량 구입, 이름: K3
차량 구입, 이름: G80
차량 구입, 이름: Model Y
구매한 차량 수: 3

2) 수학 유틸리티 클래스

(1) 문제

  • 다음 기능을 제공하는 배열용 수학 유틸리티 클래스(MathArrayUtils)를 생성
  • sum(int[] array): 배열의 모든 요소를 더하여 합계를 반환
  • average(int[] array): 배열의 모든 요소의 평균값을 계산
  • min(int[] array): 배열에서 최소값을 찾음
  • max(int[] array): 배열에서 최대값을 찾음

(2) 요구사항

  • MathArrayUtils은 객체를 생성하지 않고 사용해야하며, 누군가 실수로 MathArrayUtils의 인스턴스를 생성하지 못하도록 막아야함
  • 실행 코드에 static import를 사용해도 됨
더보기
package static2.ex;
public class MathArrayUtilsMain {
    public static void main(String[] args) {
        int[] values = {1, 2, 3, 4, 5};
        System.out.println("sum=" + MathArrayUtils.sum(values));
        System.out.println("average=" + MathArrayUtils.average(values));
        System.out.println("min=" + MathArrayUtils.min(values));
        System.out.println("max=" + MathArrayUtils.max(values));
    }
}

 

실행 결과

sum=15

average=3.0

min=1

max=5

 

(3) 정답

더보기
package static2.ex;

public class MathArrayUtils {
    
    // 인스턴스 생성 막기
    private MathArrayUtils() {
    }

    public static int sum(int[] array) {
        int sum = 0;
        for (int value : array) {
            sum += value;
        }
        return sum;
    }

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

    public static int max(int[] array) {
        int max = array[0];
        for (int value : array) {
            if (value > max) {
                max = value;
            }
        }
        return max;
    }

    public static int min(int[] array) {
        int min = array[0];
        for (int value : array) {
            if (value < min) {
                min = value;
            }
        }
        return min;
    }
}