Java

static 키워드

Recfli 2024. 1. 17. 20:50

[ 작성 이유 ]

  이전 포스팅에서 Java의 메모리와 관련돼 글을 작성했었다. 당시에 명확하게 각각의 메모리가 올라가는 영역과 시점에 관해서 공부를 했었고 내가 알기로는 static 메서드는 해당 클래스의 첫 객체가 생성되는 시점 혹은 내부 static메서드가 호출되는 시점에 static 영역에 올라가는 것으로 알고 있었다. 모던자바인액션을 공부하던 중 static이 마구 써있는 상황에서 이게 어떻게 쓰이는 지 이해가 안됐다. 메모리를 공부할 때에 정확하게 모르고 넘겼던 것 같다. 이 부분을 한 번 명확하게 다시 구분해보자.

[ static 키워드의 사용 ]

 우선, static을 쓰면 좋은 상황을 이해하기 위해서는 static영역과 heap영역에 관한 이해가 필요하다. static영역의 코드는 static영역 상의 메모리에서 해당 메서드 혹은 변수가 담긴 클래스의 객체를 생성하거나 혹은 static 메서드 혹은 변수에 접근을 하는 시점부터 프로그램이 종료될 때까지 남는다. 이와 다르게 heap영역에 있는 것들은 new와 함께 새로운 객체를 생성할 때에 heap영역 상의 메모리에 올라가고 해당 객체가 더 이상 참조가 없어진 경우 GC의 규칙에 따라서 일정시간이 지나면 사라지게 된다.

 

 그러면 이제 static 키워드를 쓸까 말까를 고민하는 프로그래머는 해당 변수, 메서드, 클래스의 특징을 잘 생각해보아야 한다고 생각한다. static 키워드를 쓴다는 것은 메모리 관점으로만 보면 유연한 메모리 사용을 버리고 지속적인 상황에서 과도한 메모리 생성을 하지 않겠다는 것을 의미하고 static 키워드를 사용하지 않는다는 것은 메모리 용량을 포기하고 유연한 메모리를 선택하겠다는 것을 의미하기 때문이다. 이러한 관점에서 둘 사이의 tradeoff를 따져본다면 static 키워드를 사용하는 것은 cache처럼 호출시점부터 마지막까지 계속 호출될 가능성이 높은 것들을 선택해야 하는 것이 아닐까 생각이 된다. 

 

 또한 static 키워드를 사용할만한 장소도 생각을 해봤는데 4가지 정도가 있는 것 같다. static 변수, static 메서드, static 클래스, static 초기화 블록 이렇게 4가지가 있는 것 같다.

 

static 초기화 블록과 관련해서는 초기화 블록과 함께 정리해놓은 곳이 있으니 여기를 참고 바란다.

[ static 변수 ]

 위의 관점에서 static 변수로 설정하면 좋은 게 무엇이 있을지 한번 고민을 해보자. 첫 번째로 생각해볼 건  static 변수는 static 영역에 올라갈 내용이기 때문에 Multi thread 환경에서도 공유되는 값이다. 물론 아닌 환경에서 사용할거면 상관없겠다만 되도록 변수 중에서 더이상 변경될 필요가 없는 걸 사용하는게 좋다. 두 번째로 생각해볼 건 메모리의 관점에서 더이상 반복될 필요가 없는 걸 설정하는게 좋다. 만약에 static 변수가 아닌 일반 변수로 설정을 해두면 인스턴스가 생성될 때마다 똑같은 값을 불필요하게 계속 반복해서 생성하는 것이다. 그러면 불필요한 메모리의 낭비를 일으키게 된다.

 

 이런 걸 모두 만족하는 예시는 아마도 뭔가 규격이 있는 경우가 좋을 것 같다. 만약에 아이폰11이라는 클래스를 만든다고 하자. 모든 아이폰11은 제조사에서 규정한 크기로 만들어야 한다. 아이폰의 크기는 세로 150.9mm, 가로 75.7mm, 높이 9.3mm이다.

public class Iphone11 {

    public static final float width = 75.7f;
    public static final float length = 150.9f;
    public static final float height = 9.3f;
}

 

 이렇게 생성을 하면 float 3개만큼의 메모리를 인스턴스 생성마다 아낄 수 있고 접근도 직접 static영역에서 하면 돼서 더 빠르다.

[ static 메서드 ]

 static 메서드는 해당 클래스와 관련된 무언가 호출될 일이 있을 때 메모리상에 자동적으로 올라가는 메서드이다. static 메서드의 이러한 초기화 시점 때문에 static 메서드는 해당 객체 내의 인스턴스 변수를 사용할 수 없다. 그렇기 때문에 생성되는 객체 내의 인스턴스 변수를 사용하지 않고, 모든 인스턴스에 공통적으로 사용해야 하는 메서드에 사용하면 좋다. 대표적인 예시는 Math에서 max와 같은 게 있다. 한번 그냥 가짜 Math로 List가 들어왔을 때 최댓값을 찾는 메서드를 만들어보자.

public class staticMath {
    
    public static int max(List<Integer> integers){
        int maxValue = Integer.MIN_VALUE;
        for(Integer integer: integers){
            if(maxValue < integer)
                maxValue = integer;
        }
        return maxValue;
    }
}

 

 아직 람다와 스트림이 익숙하지 않아서 다음처럼 코드를 작성하긴 했는데, 다음과 같은 함수는 인스턴스의 생성없이 static 영역에 올라간 채로 계속 해서 재활용이 가능하다. 이건 딱히 인스턴스가 다르다고 바뀌어야할 것도 아닌 함수이기 때문에 static영역에 올리면 좋다. 많이 생성되고 자주 쓰일 것 같은 경우에는 좋다.

 

 static 메서드와 관련해서 내가 오해가 있는 것 같아서 추가적으로 작성을 하는데, static 메서드라고 해서 반드시 인스턴스 객체를 사용하지 못하는 건 아니다. 두 가지 경우에 있어서는 사용이 가능하다. 첫 번째는 static 메서드 내부에서 새로운 객체를 생성하는 경우, 두 번째는 메서드에 인자로 들어오는 경우로 이 두 가지는 인스턴스 객체를 static 메서드 내부에서 사용이 가능하다. 아래는 좀 억지스러운 예시지만 한번 가능함을 이야기하기 위해 만들어보았다. 

public class Iphone11 {

    public static final float width = 75.7f;
    public static final float length = 150.9f;
    public static final float height = 9.3f;

    public LocalDateTime manufacturingDate;
    
    Iphone11(){
        manufacturingDate = LocalDateTime.now();
    }

    public static void whatTimeIsIt(){
        Iphone11 iphone11 = new Iphone11();
        System.out.println(iphone11.manufacturingDate);
    }
    
    public static void whichOneIsMadeEarly(Iphone11 obj1, Iphone11 obj2){
        if(obj1.manufacturingDate.isBefore(obj2.manufacturingDate))
            System.out.println("left one made early.");
        else
            System.out.println("right one made early.");
    }
}

[ static 클래스 ]

 static 클래스가 사실 처음 이 글의 작성이유였는데 모던자바인액션에서 처음 2장부분을 보면 predicate 메서드를 함수화하는 과정에서 static class를 사용한다. 처음에 이걸 왜 쓴 건지 잘 몰라서 찾아보니 내부 클래스에 대해서도 알아야하고 내부 클래스에서 왜 반드시 static 키워드를 사용해야하는지도 알아야 돼서 매우 복잡했다.

 

 일단 내부 클래스를 사용하는 이유는 서로 연관이 있는 클래스를 구분지어서 사용하기 보다는 해당 클래스에서만 쓴다는게 확실할 때 사용된다. 이렇게 사용하면 외부에서 확인할 수 없게 캡슐화되는 효과를 가질 수 있고 또한 가독성이 상승하게 된다. 아무튼 이런 이유 등으로 내부 클래스가 사용된다. 그런데 이런 내부 클래스를 사용할 때에 조심해야 할 점이 있다. 바로 순환참조의 가능성이 때문인데, JVM에서 GC는 더이상 참조가 없는 객체를 특정 시간마다 수거해서 메모리를 비워준다. 그런데 내부 클래스에서 static 키워드가 없이 정의를 해버리면 외부 클래스에 대한 참조를 생성자에서 내가 만들어놓지 않아도 자동으로 java가 만들어준다. 그렇기 때문에 순환참조가 발생하고 결과적으로 이런 과정이 반복되게 되면 메모리 고갈로 인해서 프로그램이 죽을 수 있다. 이런 경우에 static 클래스를 쓰는 것 같다. 이것 외에는 딱히 잘 쓰지 않는 것 같다.

 

 이 부분은 JVM과 Memory Leak에 예제 코드와 함께 자세하게 적어두도록 하겠다. ( 해당 글은 아직 작성이 안됐다.)

[ 정리 ]

 글이 난잡하게 작성이 되었지만 static 키워드가 사용되는 곳엔 총 4가지 경우의 수가 있고 그 경우마다 사용을 고려할 상황에 대해서 예시를 들었다. 해당 고려가 이루어지는 이유는 static 키워드가 들어가는 것들의 취급에 있어 메모리와 관련된 이유, 생성시점과 관련된 이유를 예시로 들었다. 이 부분을 고려해서 어떤 것들끼리의 tradeoff가 일어나고 어떤 이점을 얻는지를 이해할 수 있는 글이었으면 좋겠다.

[ 참고 자료 ]

https://mangkyu.tistory.com/47

https://velog.io/@mooh2jj/Java-static-%EB%B3%80%EC%88%98-static-%EB%A9%94%EC%84%9C%EB%93%9C-%EA%B7%B8%EB%A6%AC%EA%B3%A0-static-%ED%81%B4%EB%9E%98%EC%8A%A4

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EB%82%B4%EB%B6%80-%ED%81%B4%EB%9E%98%EC%8A%A4Inner-Class-%EC%9E%A5%EC%A0%90-%EC%A2%85%EB%A5%98 

https://velog.io/@agugu95/%EC%99%9C-Inner-class%EC%97%90-Static%EC%9D%84-%EB%B6%99%EC%9D%B4%EB%8A%94%EA%B1%B0%EC%A7%80

https://www.delftstack.com/ko/howto/java/static-class-in-java/