[Java] static 뜯어보기 !

박세진·2021년 1월 18일
4
post-thumbnail

자바를 공부하는 사람이라면 static에 대해 많이 들어보았고, 사용하고 있으리라 생각한다.
필자는 얼마 전 보았던 1차 면접에서 면접관님에게 "static에 대해 설명해주세요"라는 질문을 받았을 때, 머릿속이 하얘지는 경험을 했다. 많이 사용해왔지만, 막상 이것에 대해 설명해달라는 말을 들으니 어떻게 해야할지 생각이 잘 나지 않았다.
그래서 면접이 끝난 뒤 static에 대해 다시 한 번 공부해야 겠다는 생각이 들었고, 글을 쓰게 되었다.

static의 의미? 🤨

static은 고정된, 정적인 이라는 의미이다. static 이라는 키워드를 사용해서 static field (정적 필드)와 static method (정적 메소드) 를 만들 수 있다. 이 둘을 묶어서 정적 멤버(클래스 멤버)라고 한다. 즉 객체(인스턴스)에 소속된 멤버가 아닌, 클래스에 고정된 멤버 라는 뜻이다.
객체에 소속된 것이 아니므로, 객체를 생성하지 않고 사용할 수 있다.

참고 !

static 에 대해 공부하기 전 JVM에 대해 알아보자.
알고 계신 분들은 넘어가셔도 됩니다.🙃

자원들이 사용되려면 메모리를 할당받아야 하고, Java에서 메모리를 할당받는 방법은 2가지가 있다.
1. 객체 생성
2. static 사용

첫번째 방법 ( 객체 생성 )

package study.test

class BBB {
	int i = 1;
    }
    
public class AAA {
	public static void main(String[] args){
    		System.out.println(i); // i는 객체 생성을 하지 않아 메모리를 할당받지 못함 -> 사용할 수 없음.
            	System.out.println(BBB.i);
      	}
}
package study.test;

class BBB {
	int i = 1;
}

public class AAA {
	public static void main(String[] args){
    		BBB bbb = new BBB();
    		System.out.println(bbb.i); // 객체 생성 하고 나니까 (인스턴스화) int i 를 사용할 수 있다.
     }
}

두번째 방법 ( static 붙이기 )

package study.test

class BBB {
	static int i = 1; // static 붙임
    }
    
public class AAA {
	public static void main(String[] args){
    		System.out.println(BBB.i); // 객체 생성 없이 바로 사용할 수 있음.
      	}
}

static 에 대해 알아보기 전, JVM에 대해 알아보자.

JVM은 크게 Class Loader, Excution Engine, Garbage Collector, Runtime Data Area(Memory Area)로 나누어져 구성되어 있다.

JVM의 기본적인 수행과정

프로그램 실행시 개발자에 의해 작성된 소스코드(.java)는 자바 컴파일러(javac)를 통해 Byte Code(.class)파일로 변환된다.
why?🤔 JVM이 읽을 수 있도록!
이렇게 변환 된 Byte Code 파일을 JVM내부의 Class Loader가 읽어들여 Runtime Data Area에 저장하게 된다.
저장된 코드들을 Excution Engine이 하나의 명령어 단위로 읽어들여 프로그램을 실행하게 되고, 사용이 끝나 더 이상 사용하지 않는 코드들을 Garbage Collector 가 모아서 메모리에서 해제한다.

Class Loader : .class 파일을 가져와서(로드) 이를 Runtime Data Area에 올려주는 역할. 여기 올라간 파일을 Excution Engine에 의해 실행된다.

Runtime Data Area(=Memory Area) : JVM이 프로그램을 수행하기 위해 OS로부터 할당받은 메모리 공간 !
또한 이 메모리 영역은 크게 Method Area,Stack, Heap, PC Register, Native Method Area 로 구분 된다.

  • Method Area
    - 자바 프로그램에서 사용되는 클래스에 대한 정보와 클래스 변수 저장
    (필드, 메소드, 생성자, ... 등)

  • Heap
    - new 연산자를 이용해 동적으로 생성된 객체, 배열 등을 저장하는 영역.
    Stack영역에는 주소가, Heap영역에는 주소에 해당하는 실제 값이 저장된다.
    JVM이 중단되거나 GC가 실행되기 전까지 영구적으로 저장된다.
    여기서 사용이 끝난 객체들을 Garbage Collector가 모아서 처리하게 된다.

  • Stack
    - 프로그램 실행 중 메서드가 호출되면, Stack 영역에 각각의 메서드를 위한 메모리가 할당된다 (즉 각 메서드는 하나씩의 Stack을 가지게 된다)
    Stack 영역은 이 메서드들 안에서 사용되어지는 값을 저장하며, 호출된 메서드의 지역변수, 매개변수, 리턴 값 및 연산값들을 임시로 저장하는 공간이다. 임시로 저장하기 때문에 사용이 끝나면 Stack 영역에서 해제된다.

본문으로 돌아와서

왜 static에 대해 학습하기 전 JVM에 대한 얘기를 써놓았을까?
필자가 static이라는 키워드에 대해 공부하려고 구글링을 하고, 다른 블로그를 열심히 찾아봤을 때 Method Area, Class Loader등의 얘기를 보았는데, JVM에 대해서 잘 모르고 있어서 잘 이해가 가지 않았다. 그래서 static이라는 녀석을 더 잘 이해하기 위해서 JVM에 대해서도 정리하였다.

정리해서 다시 설명하자면,

Class Loader가 .class 파일을 탐색하던 중 static키워드를 보는 순간
아 얘는 객체가 생성되지 않아도 항상 메모리를 할당해야 하는 멤버야!
라고 보고, Method Area(=Static Area)에 메모리를 할당한다.
Static영역에 할당된 메모리는 모든 객체가 공유하므로, 하나의 멤버를 어디서든지 참조할 수 있다. 하지만 Garbage Collector의 관리 영역 밖에 존재하기 때문에 Static Area에 있는 멤버들은 프로그램의 종료시까지 메모리가 할당된 채로 존재하게 된다.(너무 남발하면 시스템 성능에 안좋은 영향을 줄 수 있으니 주의하자.)

static은 언제 사용할까?

  • 클래스를 설계할 때, 멤버변수 중에서 모든 인스턴스에 공통적으로 사용해야 하는 것이 있다면 static을 붙인다.
인스턴스를 생성하면, 각 인스턴스는 서로 독립적이므로 서로 다른 값을 유지한다.  
하지만 경우에 따라서 각 인스턴스들이 공용으로 사용해야 하는 것이 있다면 그때 static 키워드를 붙여주는 것이다.
  • 작성한 메서드 중에서 인스턴스 변수를 사용하지 않는 메서드가 있다면 static을 붙여준다
static이 붙은 멤버변수(필드/메서드)는 인스턴스 생성을 하지 않아도 호출이 가능하다.  
하지만 인스턴스 변수는 인스턴스를 생성해야만 존재한다. static이 붙은 메서드(클래스 메서드)는 인스턴스 생성 없이 호출이 가능하므로,  
클래스 메서드를 호출할 때 인스턴스가 생성이 되어있을 수도 있고, 아닐 수도 있다.
따라서 static 메서드 (정적 메서드)에서 인스턴스 변수의 사용을 금지하다.
반대로 인스턴스 멤버(변수/메서드)에서는 클래스 멤버들을 언제나 사용할 수 있다.  
(이미 메모리에 존재하기 때문에 !)

그러므로 메서드의 작업내용 중 인스턴스 변수를 필요로 한다면, static을 붙일 수 없다.  
반대로 인스턴스 변수를 필요로 하지 않는다면, 가능하면 static을 붙이는 것이 좋다.  
(메서드 호출시간이 짧아지므로 효율이 높아짐.
static 안붙인 메서드는 실행 시 호출되어야 할 메서드를 찾는 과정이 추가적으로 필요하므로, 시간이 더 걸린다)

ex) Math 클래스의 모든 메서드 : 클래스 메서드!  
(인스턴스 변수 하나도 없고, Math 클래스의 함수들은 작업 수행시 필요한 값들을 모두 매개변수로 받아서 처리함.)

예시

package study.test;

public class StaticEx {
	public static int cnt = 0; // static 붙어있음 !
	
	public StaticEx() {
		this.cnt++;
		System.out.println(this.cnt);
	}
	public static void main(String[] args) {
		StaticEx ex1 = new StaticEx();
		StaticEx ex2 = new StaticEx();
	}
}

출력결과 : 1, 2

why? 🤔

static 키워드가 붙은 멤버들은 모든 객체가 메모리 영역을 공유하므로.

package study.test;

public class StaticEx {
	public int cnt = 0; // static이 안 붙었으니까 인스턴스 변수임.
	public StaticEx() {
		this.cnt++;
	}
	public static int getCnt() {
		return cnt; // 오류 발생
	}
	public static void main(String[] args) {
		StaticEx ex1 = new StaticEx();
		StaticEx ex2 = new StaticEx();
		System.out.println(StaticEx.getCnt());
	}
}

클래스 멤버(여기서는 static 메서드를 말함)에서는 인스턴스 변수에 접근이 불가능 하므로 위의 코드는 에러가 발생한다.
static 키워드를 만난 순간 메모리에 적재시켜야 하는데, cnt 변수에 대해 선언 및 메모리가 할당되어 있지 않아서 에러가 발생하는 것!
해결하기 위해서는 cnt 변수를 static 으로 만들면 된다. 그러면 메모리 로드 시점에 cnt 변수에 대한 선언이 존재하므로, 에러가 발생하지 않는다.

static의 사용을 지양해야 한다?

static에 대해 공부를 하면서,
객체를 생성하지 않고 바로 사용할 수 있으니 메모리 낭비도 적고 괜찮지 않을까?
하는 생각을 은연중에 했다. 그런데 어느 블로그 글에서 static의 사용을 지양해야 한다 는 얘기들을 보고, 이유가 궁금해져서 찾아보게 되었다.

필자가 정리해본 이유는 다음과 같다.

1. 전역변수의 경우, 프로그램 시작시 메모리가 할당된다. 이때 a라는 전역변수를 한번도 사용하지 않는다면, a는 사용도 하지 않는데 쓸데없이 메모리를 차지하게 된다.
   그렇기 때문에 굳이 이렇게 안 쓸수도 있는 변수는 전역변수 보다는 싱글톤 클래스의 멤버변수로 만드는 방법이 있다.  
   a라는 변수를 전역으로 선언하지 않고, 싱글톤 클래스의 멤버변수로 만들게 된다면   
   싱글톤 클래스는 instance를 호출하기 전까지는 인스턴스 자체가 생성이 되지 않도록 구현이 되기 때문에  
   한번도 호출하지 않을 경우에 대해 불필요한 메모리 낭비를 막을 수 있다.
   즉, 꼭 필요한 시점에서만 메모리가 소비된다.
   
2. 전역 이라는 것은 곳 어디서든 접근이 가능하다는 것이므로, 내가 의도하지 않았는데 값이 들어갔을 경우 추적이 힘들다.  

즉, static을 용도에 맞게 잘 사용하라는 것이지, 반드시 static의 사용을 지양해라!라는 뜻은 아니었다.

글을 마치면서

static에 대해 제대로 알지 못하고 사용했던 나 자신에 대해 반성하는 계기가 되었다. 공부하면서 JVM에 대해서도 찾아보고, 여러 블로그를 찾아가면서 내용을 최대한 정리해보았다. 이걸 바탕으로 static을 사용할 때 용도를 잘 생각하면서 써야겠다는 다짐을 했다.

profile
계속해서 기록하는 개발자. 조금씩 성장하기!

2개의 댓글

comment-user-thumbnail
2021년 9월 27일

스태틱 배우려고 들어왔다가 JVM 동작 방식을 알게 되네요

좋은 내용 감사합니다!

답글 달기
comment-user-thumbnail
2023년 3월 29일

많이 배우고 갑니다.

답글 달기