static에 대한 고찰

Seo-Faper·2023년 2월 21일
0

스태틱의 단검

static 키워드의 특징

static 키워드의 특징은 다음과 같다. static은 변수 앞에 쓸 수도, 메소드 앞에 쓸 수도 있다.
이렇게 생성된 정적멤버들은 다음과 같은 특징을 지닌다.

  1. 메모리에 고정적으로 할당된다.

  2. 객체 생성 없이 사용할 수 있다.

  3. 프로그램이 시작되면 메모리의 static 영역에 적재되고, 프로그램이 종료될 때 해제된다.

  4. static 메서드 내에서는 인스턴스 변수를 사용할 수 없다.

이러한 특성들을 자세히 알아보자.

1. 메모리에 고정적으로 할당된다.

static으로 선언된 멤버들은 선언 시 heap 영역에 저장되지 않는다.
static이 붙지 않는 메소드나 변수의 경우 해당 메소드나 변수가 객체에 의해 생성될 때 마다 호출되어 서로 다른 메모리 주소를 가질 수 있다. 그래서 각 객체들이 공통적으로 하나의 값이 유지되어야 할 경우 static으로 선언해 주는 것이 좋다.

2. 객체 생성 없이 사용할 수 있다.

일반적인 인스턴스 변수나 메소드라면 자신이 소속된 객체를 new 키워드를 통해 인스턴스를 생성할 후 접근한 수 있다.
그러나 static은 별도의 객체 생성 없이 즉시 접근해 사용할 수 있다.

public class Main {
 
    public void print(){
        System.out.println("Hello!");
    }
 
    public static void main(String[] args){
        print();
    }
}

이런 메소드가 있다면 print() 함수는 실행되지 않는다.
대신 new를 통해 Main 클래스의 객체를 생성한 후 사용할 수 있다.

public class Main {
 
    public void print(){
        System.out.print("Hello!");
    }
 
    public static void main(String[] args){
        Main main = new Main();
        main.print();
    }
}

이렇게 해야 하는 이유는 바로 print()가 아직 메모리상에 올라가 있지 않은 메소드이기 때문이다.
명시적으로 *.class 파일에 올라가는 있지만 사용하려면 new를 통해 Heap 영역에 객체를 만들어 줘야한다. 그리고 Heap역역에 존재하는 객체의 주소값을 변수에 담아 Stack에서 관리하게 된다.

public class Main {
 
    public static void print(){
        System.out.println("Hello!");
    }
 
    public static void main(String[] args){
        print();
    }
}

하지만 이렇게 static으로 선언한 메소드라면 Main 클래스가 로드될 때 main함수와 같이 메모리에 적제된다. 그래서 이미 메모리상 존재하는 메소드 이므로 별도의 객체 생성 없이 사용할 수 있다. 이 부분에 대해 더 자세히 알아보자.

3.프로그램이 시작되면 메모리의 static 영역에 적재되고, 프로그램이 종료될 때 해제된다.

프로그램이 시작되고 JVM이 구동되면 static이 붙은 멤버들은 자동으로 메모리의 static 영역에 생성된다. 자동으로 static 영역에 적재되므로 별도의 선언 없이 사용할 수 있는 것이다.

일반적인 메서드는 객체를 생성할 때 Heap에 올라가는데, 이는 GC(가비지 컬렉터)의 탐색 범위이므로 자동으로 관리되는 반면 static으로 선언된 정적 멤버들은 GC(가비지 컬렉터)의 관리 대상에서 제외되는 영역에 존재하기 때문에 자동으로 관리되지 않는다. 그래서 프로그램이 종료될 때 까지 사용하지 않더라도 일단 선언되어 있으면 해제되지 않는다.

그래서 과도한 static 사용은 메모리 낭비를 초례한다.

즉 정리하자면 정적 필드와 정적 메서드는 객체(인스턴스)에 소속된 멤버가 아니라 클래스에 고정된 멤버이다.

그렇기에 클래스 로더가 클래스를 로딩해서 메서드 메모리 영역(이른바 static 영역) 에 적재할 때 클래스별로 관리된다.

따라서 클래스의 로딩이 끝나는 즉시 바로 사용할 수 있다.

4. static 메서드 내에서는 인스턴스 변수를 사용할 수 없다.

static 메서드는 프로그램이 실행됨과 동시에 메모리에 올라가기 때문에 인스턴스 변수를 안에 쓸 수 없다. 인스턴스 변수는 생성과 호출이 이루어져야 하는데, static 메서드는 객체를 생성하기 전에 먼저 메모리에 올라가기 때문에 사용할 수 없다.

static 퀴즈

...
public static void main(String[] args){

	int ans = 0;
	ArrayList<Integer> list = new ArrayList<>();
	for(int i = 1; i<=100; i++){
      list.add(i);
	}
	//1~100 까지 중 짝수인 수의 합을 ans에 구하고 싶어!

	list.stream().filter(e->e % 2 == 0).forEach(e->{
		ans += e;
	});

	System.out.println(ans);

}

실행 결과는??
ans가 static으로 선언되어 있지 않아 컴파일 에러가 난다.
어떻게 이런 일이 일어나는 것 일까?

왼쪽은 기존 for문, 오른쪽은 스트림의 forEach이다.
왼쪽은 흔히들 말하는 for-loop 방식, 오른쪽은 sequential stream 방식이다.
스트림은 내부 반복자(iterator)를 이용해 개발자 코드가 스트림에게 처리 방식만을 제공하고 나머지 연산은 스트림이 처리하는 원리이다.

내부 반복자의 경우, 기본적으로 원시 타입(primitive type)이 아닌 ArrayList, 또는 String 같이 참조 타입(wrapped type) 이기 때문에 메모리에 저장되는 영역은 Heap이다. for-loop의 경우 main함수 안에 선언된 모든 원시 타입 변수들이 Stack에서 관리되기 때문에 즉시 사용할 수 있는 것이고 스트림의 내부 반복자의 경우 Heap 영역에서 전달받은 처리 코드에 기반해 반복이 진행되고 결과값만이 Stack영역에 변수로서 호출될 수 있다. 그렇기 때문에 미리 사전에 접근할 수 있는 static으로 선언된 멤버만이 내부 연산자에서 사용될 수 있는 것이다.

...
public static void main(String[] args){

	int ans = 0;
    
	ArrayList<Integer> list = new ArrayList<>();
	for(int i = 1; i<=100; i++){
      list.add(i);
	}
	//1~100 까지 중 짝수인 수의 합을 ans에 구하고 싶어!

	list.stream().filter(e->e % 2 == 0).forEach(e->{
		ans += e;
	});
    
	Main m = new Main();
    m.printNumber();
    
	System.out.println(ans);

}
public void printNumber(){
	System.out.println(ans);
}

내부 클래스와 static

static은 클래스로더가 클래스를 읽을 때 클래스와 함께 올라간다고 했다.
그렇다면 클래스 안에 내부 클래스를 만들었을 때 static으로 선언하게 되면 어떻게 될까?

class MyClass {
    class A{}
	static class B{} //내부 클래스에 static이 붙는다면?
}

클래스 B는 정적 멤버 클래스가 된다.

MyClass.B test1 = new MyClass.B();
MyClass.B test2 = new MyClass.B();

if(test1==test2) System.out.println("static으로 선언한 내부 클래스는 같은 참조.");
else System.out.println("아무리 그래도 인스턴스를 만들었는데 어떻게 같은 참조냐.");

정답은 바로 false이다.
클래스는 인스턴스를 만들어 주는 설계도와 같기 때문에 그 자체가 인스턴스로 존재할 수는 없기 때문이다. 그럼 그냥 내부 클래스와 static으로 만든 내부 클래스가 같느냐? 그건 또 아니라는 것이다.

사실 우리가 만드는 모든 클래스들은 원래 static 영역에 올라가는 'static'이다. 내부 클래스에 static 키워드를 붙이면, 외부 인스턴스 없이 내부 클래스의 인스턴스를 바로 생성할 수 있다는 차이점이 존재 할 뿐 기능적 차이는 없다.

내부 클래스 A는 MyClass의 인스턴스가 존재해야지 만들어 질 수 있다. 그렇다면 내부 클래스 A는 자신을 만들어준 인스턴스에 대한 '외부 참조'를 갖게 된다. 그리고 이 참조는 숨겨져 있어서 '숨은 외부 참조' 라고 불린다.

그러나 B 처럼 내부 클래스에 static을 붙이게 되면 숨은 외부 참조를 차단할 수 있다.

MyClass mc = new MyClass();
MyClass.A test1 = mc.new A(); //test1은 "mc에 대한 숨은 외부 참조"를 갖는다.

MyClass.B test2 = new MyClass.B(); //test2는 그딴 거 없다.

내부 클래스에서 이런 '외부 참조'가 존재할 시, 내부 클래스에서 외부 클래스의 메소드에 접근 할 수 있게 되는데 이는 다음과 같은 단점을 가진다.

class MyClass {
    void myMethod() {
        ...
    }

    class A{
        void Method_A() {
            MyClass.this.myMethod(); //숨은 외부 참조가 있기 때문에 가능
        }
    }

    static class B{
        void Method_B() {
            MyClass.this.myMethod(); //접근 불가, 컴파일 에러
        }
    }
}
  1. 외부 인스턴스에 대한 참조값을 담아야 하기 때문에 인스턴스 생성시 시간적, 공간적으로 성능이 낮아진다.
  2. 외부 인스턴스에 대한 참조가 존재하기 때문에, 외부 클래스와 내부 클래스가 연결된 관계에서 외부 클래스를 사용하지 않더라도 계속 남아있어 가비지 컬렉션이 인스턴스 수거를 하지 못하여 메모리 누수가 생길 수 있다.

그러므로 내부 클래스를 만들때에는 static을 쓰는 것이 좋다.

요약

  1. 필드나 메소드를 static으로 선언할 때는 공통된 하나의 참조가 필요할 때 쓰자.
  2. 내부 클래스를 만들 때는 꼭 static으로 만들어 주자.

참고자료

https://siyoon210.tistory.com/141
https://sigridjin.medium.com/java-stream-api%EB%8A%94-%EC%99%9C-for-loop%EB%B3%B4%EB%8B%A4-%EB%8A%90%EB%A6%B4%EA%B9%8C-50dec4b9974b
https://jooona.tistory.com/m/164
https://minhamina.tistory.com/211
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dungga77&logNo=80015030666
https://sorjfkrh5078.tistory.com/108

profile
gotta go fast

0개의 댓글