Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
JAVA/Java Intermediate(OOP)

6. static 정적

by Toughie 2024. 3. 7.

 


자바 메모리 구조

프로세스의 메모리 구조를 설명할 때 코드, 데이터, 힙, 스택 영역이 있다는 설명을 많이 들었다.

자바에서도 유사한데, 데이터 영역을 메서드 영역이라고 칭한다. (그냥 같다 생각하면 됨)

 

CODE AREA

프로그램의 코드가 저장되는 영역(read-only) 프로그램이 실행될 때 

메모리의 하단에서부터 적재된다.

 

METHOD (CLASS) AREA

static 이해를 위해 중요한 영역

프로그램의 실행에 필요한 공통 데이터를 관리하는 영역으로, 세부적으로는 아래와 같은 영역이 있다.

 

1. 클래스 정보(Class Information)

클래스의 실행을 위해 필요한 바이트코드(for jvm에 의한 클래스 로딩)가 저장되는 영역

클래스의 이름, 상위 클래스 및 인터페이스, 멤버 변수 및 메서드 정보

 

2.상수 풀(Constant Pool)

클래스 파일에 포함된 상수들이 저장됨.(클래스 파일에서 사용되는 문자열, 정수, 실수 등의 상수 값)

 

3. static

클래스에 선언된 static 변수들이 메서드 영역에 저장됨. 

JVM에 의해 로드될 때 생성됨. 

또한 static 메서드에 대한 정보도 저장되어 있음.

 

4. Method

클래스에 정의된 메서드들의 바이트 코드가 저장되어 있음.

JVM에 의해 실행될 때 사용됨

 

HEAP AREA

동적으로 런타임에 객체가 생성되는 영역 (ex.클래스 ,배열) 

C에서는 동적할당을 하는 경우 free를 통해 메모리를 해제해줘야 메모리 누수를 막을 수 있었지만,

자바는 개발자들의 메모리 직접 관리에 대한 노고를 줄이기 위해 크게 신경 쓴 언어로 

Garbage Collection을 통해 메모리의 해제가 이루어진다.

(Swift의 ARC와 유사하게 객체에 대한 참조값을 기반으로, 참조 카운트가 없는 녀석들을 메모리에서 해제시킨다.)

 

문자열 상수 풀(런타임 상수 풀)

new 키워드를 통해 새로운 객체를 생성하는 것이 아닌,

String 타입으로 선언하는 경우 Heap 내부에 문자열 상수 풀에 객체가 저장되고 관리됨. (Immutable)

public static void main(String args[]) {
  String str1 = "Hello";
  String str2 = new String("Hello");
  String str3 = new String("Hello");
}

출처: https://developer-talk.tistory.com/475 [DevStory:티스토리]

문자열 상수 풀은 일반적으로 gc의 대상이 아님. (계속 메모리에 유지되어 캐싱처럼 사용함)

 

 

STACK AREA

후입 선출로 동작 (LIFO)

지역변수, 중간 연산 결과, 호출 정보 등

스택 프레임 -> 메서드 호출 시마다 스택프레임이 쌓이고, 실행이 끝나며 사라짐

(스레드별로 실행 스택이 생김, 각 스레드는 코드,메서드,힙 영역 공유함)

물론 GC의 동작 알고리즘은 굉장히 복잡하고 최적화도 잘 되어 있겠지만, 

기본적으로는 자바는 개발하면서 메모리에 대한 신경을 크게 안 써도 된다라는 말을 해도 될 듯하다.

참조카운트 관리를 해주면 gc의 해제리스트에 빨리 올릴 수 있어서 최적화는 가능할 듯!


static?

 

static을 이해하기 위해 정적 vs 동적에 대해 간단하게 알아보자.

 

동적(Dynamic)

먼저 c의 동적할당의 개념을 생각해 보면, malloc을 통해 메모리 할당을 진행할 때마다 

heap 영역에 메모리가 할당되고, free를 통해 메모리가 해제된다. 즉 프로그램 이 실행되는 중(프로세스)에 

heap 영역에서 생겼다 사라졌다 dynamic 하게 동작한다는 것이다. 이걸 동적이라고 한다. (말 그대로 다이나믹임)

 

정적(static)

동적의 반대이다. 쉽게 말하면 동적은 생겼다 사라졌다 난리 부르스를 치지만, 정적은 프로그램이 실행될 때 생겨서

프로그램이 종료될 때까지 가만히 잘 있는다는 말이다. 여기서 프로그램이 실행될 때 '단 한 번만 초기화' 된다는 것이 중요하다.


그래서 static이 왜 필요한가?

어떻게 보면 프로그래밍 언어의 다양한 문법, 진화는 '개발자의 편의성 및 효율적인 메모리 관리'를 위해서라고 생각한다.

static의 존재 의의는 쉽게 말하면 '공유' 하기 위해서이다.

(프로세스의 메모리 영역에서 코드, 메서드, 힙은 하나씩 있고 스레드마다 스택이 생기는 것을 안다면 공유가 더 와닿을 것)

 

간단한 예시로, 클래스의 객체를 찍어낼 때마다 이 클래스의 객체가 몇 개 찍혔는지 알고 싶다고 가정하자.

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

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

 

이렇게 하면 인스턴스화 횟수를 알 수 있을까? 알 수 없다.

MyClass에는 멤버변수로 name과 count가 있다. 

멤버 변수는 '객체 생성'이 이루어져야 메모리 할당을 받는다. 즉 객체마다 다른 변수들이 생기는 것이다. 

MyClass 객체를 3개 만들면 힙에는 3개의 영역을 차지하고, 각 영역에 name과 count가 있다는 것이다.

 

아 그러면 객체가 생길 때마다 이 값을 저장할 수 있는 다른 클래스를 만들자. 저장용으로!

 

public class InstanceCounter {
    public int count;
}

 

음.. 이제 InstanceCounter 객체를 찍어내고 MyClass 객체를 찍어낼 때마다 count를 증가시켜 주면 되겠는데?

뭐 틀린 말은 아니다. 하지만 좀 번거롭고 too much라는 생각이 든다. 

겨우 숫자 하나 공유하겠다고 클래스를 만들고 객체까지 찍어내야 하나?

또 InstanceCounter는 오로지 MyClass의 객체 생성 횟수를 위해서 존재하는데,

이는 관련된 것들은 객체 안에 넣어 관리하는 객체지향적 프로그래밍과도 거리가 있어 보인다. 

여기서 static의 필요성이 느껴지는 것이다.


정적 변수(static, class variable)

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

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

 

자 멤버변수였던 count가 이번에는 정적 변수 (static, class 다 혼용해서 사용한다.)로 선언되었다.

이제 생성자를 통해서 객체가 생성될 때마다 count의 값이 증가할 것이다.

어 그러면 count는 객체마다 따로 생기는 것이 아닌가?? 아니다. 

정적 변수는 위에서 설명한 '메서드 영역'에 단 한 번만 초기화된다. 객체를 아무리 많이 찍어내도 

count라는 정적 변수는 메모리에 단 1개라는 것이다. 그리고 이 변수를 같이 공유하는 것이다. 

쉽게는 클래스가 붕어빵 틀이라면, 틀로 찍어내는 붕어빵이 아니라,

붕어빵 틀 자체에 붙어있는 녀석이라고 이해하면 좋다.

 

(cf. Swift에서는 타입 프로퍼티라고 불렀다.)


변수의 라이프사이클(생명주기)

이것을 알았다면, 이제 변수의 종류와 라이프사이클에 대해 이야기할 수 있다.

변수의 종류는 지역변수, 멤버변수, 정적변수가 있다.

 

지역변수

 '함수 내부에 선언되는 변수'로 함수가 호출되었을 때, 스택의 스택 프레임 내부에 할당되었다가

함수가 종료될 때 자동으로 소멸한다. 따라서 가장 생명주기가 짧다. 

 

멤버 변수(필드)

클래스 내부에 선언되는 변수로, 클래스의 객체가 생성되었을 때, 힙에 할당된다. 

객체의 참조값이 0이 되어 GC에 의해 객체가 해제될 때 멤버 변수가 소멸한다. 

 

클래스 변수(정적 변수)

클래스 내부에서 'static'이 붙은 변수이다.

프로그램 시작 시 메서드 영역에 할당되고, 프로그램 종료 시 소멸한다. 즉 제일 오래 살아있다.


정적 변수 접근

public class MyClassMain {
    public static void main(String[] args) {
        MyClass my1 = new MyClass("A");
        int count = MyClass.number;
        int count2 = my1.number; //비권장
    }
}

 

java에서 정적 변수의 접근법은 클래스명' + . 이다. 

다만 인스턴스의 참조 변수(위에서 my1)로도 접근이 '가능은 한데' 이는 

1. 인스턴스에 가본다. 2.어 이거 정적변수인데 3. 메서드 영역에 찾아간다.와 같이 돌아가기 때문에 비효율적이고,

개발자 입장에서도 이게 멤버변수야 정적변수야? 하고 헷갈리기 때문에 그냥 클래스명 + .으로 사용하도록 하자. 


자바의 클래스는 구조체다??

사실 클래스 변수는 c의 구조체 포인터와 동일하다고 볼 수 있는데,

my1 -> number를 자바에서는 .으로 더 쉽게 사용하도록 한다고 보면 되겠다.

즉 java에서 클래스 참조변수는 c에서 구조체포인터라고 볼 수 있겠다.

MyClass my1 = new MyClass(); 
// heap에 동적할당 된 MyClass 인스턴스를 가리키는 포인터,  참조변수 my1
//메모리 할당을 받아야만 멤버에 접근할 수 있다!
		
new의 비밀. 메모리 할당, 함수 포인터 매핑, 변수 초기화 까지 한 번에
MyClass * my1 = (MyClass*) malloc(sizeof(MyClass));

 

java에서 new라는 키워드도 동적할당을 통해 클래스 크기만큼의 메모리를 할당한 다음

해당 주소를 변수에 넘기는 것을 더 편리하게 만든 것이다. 

그래서 사실은 다 포인터인데, 객체 지향 언어에서는 '참조'라는 말을 쓴다. 결국 주소를 찾아가서 본다는 거라 동일한 개념.

정적변수의 경우도, 메서드 영역에 저장되어 있는 클래스 자체를 포인팅 해서 거기서 변수에 접근하는 것이다.

 

함수 포인터 

클래스에서 멤버 메서드가 있으면, 객체를 찍을 때마다 메서드가 객체마다 저장되는 것일까?? 아니다.

결국은 클래스라는 설계도에 있는 동일한 함수를 계속 실행할 텐데 이걸 객체마다 다 가지고 있으면 메모리 낭비다.

따라서 클래스에서 메서드는 '메서드 영역'에 저장되어 있고, 객체를 찍어내면 객체에는 메서드 영역에 있는 함수를 가리키는 함수 포인터만 가지고 있는 것이다. (포인터는 운영체제마다 다르지만 4byte, 8byte로 크기가 작아서 효율적이다.)

그렇다.. 객체지향 언어라고 메모리나 포인터의 개념이 없는 것이 아니라 그냥 다 숨겨져 있을 뿐이었다..ㅋㅋㅋㅋ

참고로 Swift에서 메서드 디스패치를 할 때 메서드 테이블을 통해 관리되는 것도 함수 포인터 개념으로 보면 되겠다.


정적 메서드(static method, class method)

정적 변수를 알아봤으니, 정적 메서드도 동일하게 이해할 수 있다.

똑같이 메서드 앞에 static을 붙이면 되고, 호출도 클래스명 + .으로 하면 된다.

아래 내용만 명심하자.

 

1. 멤버 변수, 멤버 메서드는 반드시 '객체 생성'이 되어야 접근 및 호출이 가능하다.

2. 정적 변수, 정적 메서드는 객체 생성을 하지 않아도 접근할 수 있다. 

 

이를 통해서 알 수 있는 것은

정적 메서드에서 멤버 변수나 멤버 변수를 사용할 수 없고, 정적 변수나 정적 메서드만 사용할 수 있다는 것이다. 

(당연하다. 없는데 어떻게 써요)

 

public class All {

    private int instanceValue;
    private static int staticValue;

    public static void staticCall() {
        staticValue++;
        staticMethod();
        //instanceMethod();//Non-static method instanceMethod() cannot be referenced from a static context
    }

    public void instanceCall() {
        instanceValue++;
        instanceMethod();

        staticValue++;
//        All.staticValue;
        staticMethod();
    }

    private void instanceMethod() {
        System.out.println("인스턴스메서드");
    }
    private static void staticMethod() {
        System.out.println("스태틱메서드");
    }
}

 

위 예시에서는 멤버 변수, static 변수, static  메서드, 인스턴스 메서드들이 선언되어 있다.

위에서 설명한 대로 static 메서드에서 static 변수나 static메서드 호출은 가능하지만, 인스턴스 메서드를 호출하려 하면 

'스태틱이 아닌 메서드는 스태틱에서 참조할 수 없습니다'라는 컴파일 에러가 발생한다.

 

아 물론, static 메서드의 파라미터로 인스턴스를 전달하면 당연히 멤버 변수나, 멤버 메서드 호출이 가능하다.

인스턴스를 전달한다는 거 자체가 static 메서드 호출 전 인스턴스가 생겼다는 말이니까. 

 

물론 인스턴스 메서드에서 static 변수에 접근하거나 static메서드 호출은 가능하다.

static은 객체 생성과 상관없이 메서드 영역에 살아있기 때문에 참조할 수 있기 때문이다. 


import static

static메서드는 공용으로 많이 쓰는 함수들을 만들 때 유용하다.

예시로 배열의 요소의 합계, 평균을 구할 수 있는 공용 클래스를 만든다고 치자.

 

public class ArrayUtils {
    private ArrayUtils() { }
    
    public static int sum(int[] array) {
        int result = 0;
        for (int i = 0; i < array.length; i++) {
            result += array[i];
        }
        return result;
    }

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

 

위 클래스에는 static 메서드만 넣을 예정이다.(다른 연산을 수행할 수 있는 메서드를 더 만들거라 가정)

멤버 변수나 멤버 메서드도 없기 때문에 다른 데서 객체를 만들지 않았으면 하기에, private 생성자를 사용해 줬다.

이제 다른 클래스에서 배열에 대한 연산이 필요할 때 

 

ArrayUtils.sum(a);    ArrayUtils.average(b); 이런 식으로 사용할 수 있다.

근데 앞에 계속 클래스명을 적는 것이 귀찮을 수 있는데, 이렇게 호출이 잦은 경우 static import를 할 수 있다.

import static myPackage.ArrayUtils.sum; //특정 static 임포트

import static myPackage.ArrayUtils.*;// 클래스 내부 모든 static  임포트

 

import static을 하면 클래스 내부의 static 요소에 접근 가능하고, 클래스명을 생략하고 바로 사용할 수 있게 된다.


public static main(String[] args) { }

인텔리제이에서 무심하게 psvm만 딸깍 했었다면, static을 배웠으니 조금 알아보자.

메인 함수는 '프로그램의 시작점'으로 객체 생성 없이 실행되는 가장 대표적인 메서드이다.

JVM이 클래스를 로드하고, 클래스에서 main 메서드를 찾아서 실행하는 것이다. 

 

main 메서드가 static 메서드이기 때문에, 클래스 하나에서 다른 메서드 예시를 구현할 때 

앞에 다 static을 붙였던 것이다. (그래야 main 안에서 실행할 수 있으니까)

 

main 메서드가 있는 클래스는 따로 힙에 할당되는 것이 아니라, 정말 프로그램의 엔트리 포인트로 사용된다고 이해하자.

public class Entry {
    public static void main(String[] args) {
        staticMethod();
//        instanceMethod();
        //Non-static method 'instanceMethod()' cannot be referenced from a static context
    }

    public void instanceMethod() {
        System.out.println("메인 메서드가 있는 클래스는 인스턴스화 되지 않아요 ㅜㅜ");
    }

    public static void staticMethod() {
        System.out.println("static만 되지롱");
    }
}

 

 

이렇게 static에 대해 알아봤는데, 

메서드 영역에 존재해서 여러 곳에서 공유하기 편하다는 장점도 있지만 무분별하게 사용하면 안 된다고 생각한다.

 

static 변수는 생명주기상 프로그램 종료 시까지 메모리에 살아있어 메모리 낭비가 일어날 수 있다.

또한 static은 class 자체에 완전 붙어있기 때문에 상속을 통한 유연한 변화도 불가능하고,

여러 곳에서 접근 가능하다는 점에서 변경에 대한 사이드이펙트가 큰 것도 단점이 될 수 있다.

 

따라서 적절하게 공용 함수 클래스나, 디자인시스템 등을 만들 때 잘 생각하고 사용하는 것이 좋겠다고 느껴졌다.

 


학습 출처

김영한의 실전 자바 - 기본편

'JAVA > Java Intermediate(OOP)' 카테고리의 다른 글

8. 상속, 오버라이딩, super  (0) 2024.03.11
7. final  (0) 2024.03.07
5. 접근 제어자와 캡슐화  (0) 2024.03.03
4. 생성자 Construct  (0) 2024.03.02
3. 절차 지향 vs 객체 지향(OOP)  (0) 2024.02.23