본문 바로가기

Java

[Java] static 에 대해서 궁금하다면

지난 게시글에 자바의 자료 구조 스택(Stack)과 큐(Queue)에 대해서 알아보았습니다.

아직 자료 구조가 무엇인지 모르는 분들은 아래 링크를 참고해주시기 바랍니다.

 

Java 자료 구조 스택

자바의 메모리 구조 자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역 3개로 나눌 수 있습니다. 메서드 영역: 클래스 정보를 보관한다. 이 클래스 정보가 붕어빵 틀이다. 스택 영역: 실

madeprogame.tistory.com

 

static 변수

이번에는 새로운 키워드인 static 키워드에 대해서 학습해보겠습니다.

static 키워드는 주로 멤버 변수와 메서드에 사용됩니다.

먼저 멤버 변수에 static 키워드가 왜 필요한지 한번 알아봅시다.

 

특정 클래스를 통해서 생성된 객체의 수를 세는 단순한 프로그램입니다.

public class Data1 {

    public String name;
    public int count;

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

 

생성된 객체의 수를 세어야 합니다. 따라서 객체가 생성될 때 마다

생성자를 통해 인스턴스의 멤버 변수인 count 값을 증가시킵니다. 

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 변수도 새로 만들어지기 때문입니다.

 

 

처음 Data1("A") 인스턴스를 생성하면 count 값은 0 으로 초기화 됩니다.

생성자에서 count++을 호출했으므로 count 값은 1이 됩니다.

다음으로 Data1("B") 인스턴스를 생성하면 완전 새로운 인스턴스를 생성합니다.

이 새로운 인스턴스의 count 값은 0으로 초기화 됩니다. 생성자에서 count++을 호출했으므로 count의 값은 1이 됩니다.

Data1("C")도 마찬가지로 count의 값은 최종적으로 1이 됩니다.

 

인스턴스에 사용되는 멤버 변수 count 값은 인스턴스끼리 서로 공유되지 않습니다. 따라서 원하는 답을 구할 수 없습니다.

이 문제를 해결하려면 변수를 서로 공유해야 합니다.

 

외부 인스턴스에 카운트 저장

이번에는 카운트 값을 저장하는 별도의 객체를 만들어보겠습니다.

 

public class Counter {
    public int count;
}
public class Data2 {
    public String name;
    public Data2(String name, Counter count) {
        this.name = name;
        count.count++;
    }
}
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 인스턴스를 공용으로 사용한 덕분에 객체를 생성할 때 마다 값을 정확하게 증가시킬 수 있습니다.

 

Data2("A") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다. count의 값은 1.

 

 

Data2("B") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다. count의 값은 2.

.

 

Data2("C") 인스턴스를 생성하면 생성자를 통해 Counter 인스턴스에 있는 count 값을 하나 증가시킨다. count의 값은 3.

 

결과적으로 Data2 의 인스턴스가 3개 생성되고, count 값도 인스턴스 숫자와 같은 3으로 정확하게 측정됩니다.

 

그런데 여기에 약간 불편한 점이 있습니다.

  • Data2 클래스와 관련된 일인데, Counter 라는 별도의 클래스를 추가로 사용해야 한다.
  • 생성자의 매개변수도 추가되고, 생성자가 복잡해진다. 생성자를 호출하는 부분도 복잡해진다.

특정 클래스에서 공용으로 함께 사용할 수 있는 변수를 만들 수 있다면 편리할 것 입니다.

static 키워드를 사용하면 공용으로 함께 사용하는 변수를 만들 수 있습니다.

public class Data3 {

    public String name;
    public static int count;

    public Data3(String name) {
        this.name = name;
        count++;
    }
}
  • static int count 부분을 보자. 변수 타입(int) 앞에 static 키워드가 붙어있다.
  • 이렇게 멤버 변수에 static을 붙이게 되면 static 변수, 정적 변수 또는 클래스 변수라고 한다.
  • 객체가 생성되면 생성자에서 정적 변수 count의 값을 하나 증가시킨다.
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);
    }
}

 

코드를 보면 count 정적 변수에 접근하는 방법이 조금 특이한데 Data3.count 와 같이 클래스명에 .(점) 을 사용합니다.

마치 클래스에 직접 접근하는 것 처럼 느껴집니다.

 

실행 결과

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

 

  • static 이 붙은 멤버 변수는 메서드 영역에서 관리한다.
  • static이 붙은 멤버 변수 count는 인스턴스 영역에 생성되지 않고, 메서드 영역에서 이 변수를 고나리한다.
  • Data3("A") 인스턴스를 생성하면 생성자가 호출된다.
  • 생성자에는 count++ 코드가 있다. count는 static이 붙은 정적 변수다. 정적 변수는 인스턴스 영역이 아니라 메서드 영역에서 관리한다. 따라서 이 경우 메서드 영역에 있는 count 값이 하나 증가된다.

  • Data3("B") 인스턴스를 생성하면 생성자가 호출된다.
  • count++ 코드가 있다. count는 static이 붙은 정적 변수다. 메서드 영역에 있는 count 변수의 값이 하나 증가된다.

  • Data3("C") 인스턴스를 생성하면 생성자가 호출된다.
  • count++ 코드가 있다. count는 static이 붙은 정적 변수다. 메서드 영역에 있는 count 변수의 값이 하나 증가된다.

최종적으로 메서드 영역에 있는 count 변수의 값은 3이 됩니다.

static이 붙은 정적 변수에 접근하려면 Data3.count 와 같이 클래스명 + .(점) + 변수명으로 접근하면 됩니다.

참고로 Data3의 생성자와 같이 자신의 클래스에 있는 정적 변수라면 클래스명을 생략할 수 있습니다.

 

static 변수를 사용한 덕분에 공용 변수를 사용해서 편리하게 문제를 해결할 수 있었습니다.

 

정리

static 변수는 쉽게 이야기해서 붕어빵 틀이 특별히 관리하는 변수입니다.

붕어빵 틀은 1개 이므로 클래스 변수도 하나만 존재합니다.

반면에 인스턴스인 붕어빵은 인스턴스의 수 만큼 변수가 존재한다.

 

public class Data3 {

    public String name;
    public static int count;
}

 

예제 코드에서 name, count 둘다 멤버 변수입니다.

멤버 변수(필드)는 static이 붙은 것과 아닌 것에 따라 다음과 같이 분류할 수 있습니다.

 

멤버 변수(필드)의 종류

인스턴스 변수: static이 붙지 않은 멤버 변수

  • static이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라 한다.
  • 인스턴스 변수는 인스턴스를 만들 때 마다 새로 만들어진다.

클래스 변수: static이 붙은 멤버 변수

  • 클래스 변수, 정적 변수, static 변수 등으로 부른다.
  • static이 븥은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있다. 따라서 클래스 변수라 한다.
  • 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러곳에서 공유하는 목적으로 사용된다.

 

변수와 생명 주기

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

static이 정적이라는 이유는 바로 여기에 있습니다. 힙 영역에 생성되는 인스턴스 변수를 동적으로 생성되고, 제거됩니다.

반면에 static인 정적 변수는 거의 프로그램 실행 시점에 딱 만들어지고, 프로그램 종료 시점에 제거됩니다.

정적 변수는 이름 그대로 정적입니다.

 

 

정적 변수 접근법

static 변수는 클래스를 통해 바로 접근할 수도 있고, 인스턴스를 통해서도 접근할 수 있습니다.

// 추가
// 인스턴스를 통한 접근
Data3 data4 = new Data3("D");
System.out.println(data4.count);
// 클래스를 통한 접근
System.out.println(Data3.count);

 

둘다 차이는 없습니다. 둘다 결과적으로 정적 변수에 접근합니다.

 

인스턴스를 통한 접근 data4.count

정적 변수의 경우 인스턴스를 통한 접근은 추천하지 않는다.

왜냐하면 코드를 읽을 때 마치 인스턴스 변수에 접근하는 것처럼 오해할 수 있기 때문이다.

 

클래스를 통한 접근 Data3.count

정적 변수는 클래스에서 공용으로 관리하기 때문에 클래스를 통해서 접근하는 것이 더 명확하다.

따라서 정적 변수에 접근할 때는 클래스를 통해서 접근하자.

 

 

static 메서드

이번에는 static이 붙은 메서드에 대해서 알아보겠습니다.

이해를 돕기위해 특정 문자열을 꾸며주는 간단한 기능을 만들겠습니다.

 

인스턴스 메서드

public class DecoUtil1 {

    public String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}
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);
    }
}

 

앞서 개발한 deco( ) 메서드를 호출하기 위해서는 DecoUtil1 의 인스턴스를 먼저 생성해야 합니다.

그런데 deco( ) 라는 기능은 멤버 변수도 없고, 단순히 기능만 제공할 뿐입니다.

인스턴스가 필요한 이유는 멤버 변수(인스턴스 변수)등을 사용하는 목적이 큰데,

이 메서드는 사용하는 인스턴스 변수도 없고 단순히 기능만 제공하고 있습니다.

 

public class DecoUtil2 {

    public static String deco(String str) {
        String result = "*" + str + "*";
        return result;
    }
}
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);
    }
}

 

DecoUtil2.deco(s) 코드를 봅시다.

static이 붙은 정적 메서드는 객체 생성 없이 클래스명 + .(점) + 메서드 명으로 바로 호출할 수 있습니다.

정적 메서드 덕분에 불필요한 객체 없이 편리하게 메서드를 사용했습니다.

 

클래스 메서드

메서드 앞에도 static을 붙일 수 있습니다. 이것을 정적 메서드 또는 클래스 메서드라고 합니다.

정적 메서드라는 용어는 static이 정적이라는 뜻 때문이고, 클래스 메서드라는 용어는 인스턴스 생성 없이 마치

클래스에 있는 메서드를 바로 호출하는 것 처럼 느껴지기 때문입니다.

 

인스턴스 메서드

static이 붙지 않은 메서드는 인스턴스를 생성해야 호출할 수 있습니다. 이것을 인스턴스 메서드라 합니다.

 

정적 메서드는 객체 생성없이 클래스에 있는 메서드를 바로 호출할 수 있다는 장점이 있다.

하지만 정적 메서드는 언제나 사용할 수 있는 것이 아니다.

 

정적 메서드 사용법

 

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

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

반대로 모든 곳에서 static을 호출할 수 있다.

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

    private int instanceValue;
    private static int staticValue;

    public static void staticCall() {
        //instanceValue++; <= 인스턴스 변수 접근, 컴파일 에러
        //instanceMethod(); <= 인스턴스 메서드 접근, 컴파일 에러

        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);
    }
}

 

이번 예제에서는 접근 제어자를 활용하여 필드를 포함한 외부에서 직접 필요하지 않은 기능은 모두 막아두었습니다.

  • instanceValue는 인스턴스 변수이다.
  • staticValue는 정적 변수(클래스 변수)이다.
  • instanceMethod( )는 인스턴스 메서드다.
  • staticMethod( )는 정적 메서드(클래스 메서드)이다.

staticCall( )

이 메서드는 정적 메서드 입니다. 따라서 static만 사용할 수 있습니다. 정적 변수, 정적 메서드에는 접근할 수 있지만,

static이 없는 인스턴스 변수나 인스턴스 메서드에 접근하면 컴파일 오류가 발생합니다.

코드를 보면 staticCall( ) => staticMethod( )로 static에서 static을 호출하는 것을 확인할 수 있습니다.

 

instanceCall( )

이 메서드는 인스턴스 메서드 입니다. 모든 곳에서 공용인 static을 호출할 수 있습니다.

따라서, 정적 변수, 정적메서드에 접근할 수 있습니다. 물론 인스턴스 변수, 인스턴스 메서드에도 접근할 수 있습니다.

 

실행 결과

1. 정적 호출
staticValue = 1
2. 인스턴스 호출1
instanceValue = 1
staticValue = 2
3. 인스턴스 호출2
instanceValue = 1
staticValue = 3

 

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

 

정적 메서드는 클래스의 이름을 통해 바로 호출할 수 있다. 그래서 인스턴스 처럼 참조값의 개념이 없다.

특정 인스턴스의 기능을 사용하려면 참조값을 알아야 하는데, 정적 메서드는 참조값 없이 호출한다. 

따라서 정적 메서드 내부에서 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.

 

물론 당연한 이야기지만 다음과 같이 객체의 참조값을 직접 매개변수로 전달하면

정적 메서드도 인스턴스의 변수나 메서드를 호출할 수 있다.

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

 

 

멤버 메서드의 종류

  • 인스턴스 메서드: static이 붙지 않은 멤버 메서드
  • 클래스 메서드: static이 붙은 멤버 메서드

static이 붙지 않은 멤버 메서드는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있습니다.

따라서 인스턴스 메서드라 합니다. static이 붙은 멤버 메서드는 인스턴스와 무관하게 클래스에 바로 접근해서

사용할 수 있고, 클래스 자체에 소속되어 있습니다. 따라서 클래스 메서드라고 합니다.

 

정적 메서드 활용

정적 메서드는 객체 생성이 필요 없이 메서드의 호출만으로 필요한 기능을 수행할 때 주로 사용합니다.

예를 들어 간단한 메서드 하나로 끝나는 유틸리티성 메서드에 자주 사용합니다. 수학의 여러가지 기능을

담은 클래스를 만들 수 있는데, 이 경우 인스턴스 변수 없이 입력한 값을 계산하고 반환하는 것이 대부분 입니다.

이럴 때 정적 메서드를 사용해서 유틸리티성 메서드를 만들면 좋습니다.

 

정적 메서드 접근법

static 메서드는 static 변수와 마찬가지로 클래스를 통해 바로 접근할 수 있고, 인스턴스를 통해서도 접근할 수 있습니다.

DecoData data3 = new DecoData();
data3.staticCall();
DecoData.staticCall();

 

static import

DecoData.staticCall();
DecoData.staticCall();
DecoData.staticCall();

 

정적 메서드를 사용할 때 해당 메서드를 다음과 같이 자주 호출해야 할 경우에는 static import를 고려할 수 있습니다.

 

이 기능을 사용하면 다음과 같이 클래스 명을 생략하고 메서드를 호출할 수 있습니다.

staticCall();
staticCall();
staticCall();
// import static static2.DecoData.staticCall;
import static static2.DecoData.*;

public class DecoDataMain {

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

 

참고로 import static은 정적 메서드 뿐만 아니라 정적 변수에도 사용할 수 있습니다.

 

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

인스턴스 생성 없이 실행하는 가장 대표적인 메서드가 바로 main( ) 메서드 입니다.

main( ) 메서드는 프로그램을 시작하는 시작점이 되는데, 생각해보면 객체를 생성하지 않아도

main( ) 메서드가 작동했습니다. 이것은 main( ) 메서드가 static이기 때문입니다.

 

정적 메서드는 정적 메서드만 호출할 수 있습니다. 따라서 정적 메서드인 main( )이 호출하는 메서드에는 정적 메서드를

사용했습니다. 물론 더 정확히 말하면 정적 메서드는 같은 클래스 내부에서 정적 메서드만 호출할 수 있습니다.

따라서 정적 메서드인 main( ) 메서드가 같은 클래스에서 호출하는 메서드도 정적 메서드로 선언해서 사용했었습니다.

public class ValueDataMain {

    public static void main(String[] args) {

        ValueData valueData = new ValueData();
        add(valueData);
        add(valueData);
        add(valueData);
        System.out.println("최종 숫자 = " + valueData.value);
    }

    static void add(ValueData valueData) {
        valueData.value++;
        System.out.println("숫자 증가 value = " + valueData.value);
    }
}

 

 

'Java' 카테고리의 다른 글

[Java] 상속이 왜 필요할까? (feat.오버라이딩)  (0) 2024.03.14
[Java] static final  (0) 2024.03.13
[Java] 자료 구조 스택  (0) 2024.03.11
[Java] 접근 제어자  (0) 2024.03.07
[Java] 패키지  (1) 2024.03.07