Java 상수는 왜 static final 일까?

Kim Dong Kyun·2024년 10월 25일
6

우테코 7기 프리코스 1주차 영상 피드백에, "상수를 왜 private static final 로 해야하나요?" 이 제기되었다. 그래서 좀 궁금했다. 왜일까?

알아보니, 상수 선언 시 private static final 키워드를 사용하는 것은 단순한 관례가 아닌 명확한 이유가 있다. 각각의 키워드가 가진 특징과 장점을 예제와 함께 살펴보자.


1. 매직 넘버/스트링에 의미 있는 이름 부여

매직 넘버나 매직 스트링은 코드에 직접 작성된 의미를 알 수 없는 값들이다. 이러한 값들에 이름을 부여하면 코드의 의도를 명확히 전달할 수 있다.

public class PaymentCalculator {
    public int calculateFee(int price) {
        return price + (price * 10 / 100);  // 10이 무슨 의미인지 모호함
    }
}

// 좋은 예
public class PaymentCalculator {
    private static final int VAT_RATE = 10;  // 부가가치세율이라는 의미가 명확함
    
    public int calculateFee(int price) {
        return price + (price * VAT_RATE / 100);
    }
}

2. static을 통한 메모리 최적화

static 키워드를 사용한 변수는 JVM Runntime Data area 의 Method Area 에 저장된다. 그리고 모든 인스턴스가 하나의 메모리를 공유한다. 간단하게 그림으로 확인하자

간단한 예제 코드로 저장 위치를 확인하면

class Student {
    static String schoolName = "Seoul High";  // Method Area에 저장
    String name;                             // Heap Area에 저장
    
    void study() {
        int hour = 2;                        // Stack Area에 저장
    }
}

위와 같다.

오케이. Method Area 가 모든 인스턴스가 공유하는 영역이므로, static 으로 생성한 변수는 인스턴스가 생성 될 때마다 heap 에 새로운 공간을 차지하는 게 아니라, Method area 공간을 참조하는 형식으로 동작한다는 것을 알았다.

2 - 1 진짜로?

정말 그런 지 한 번 학습 테스트를 작성해보자.

public class StaticClass {
    private static Integer MAX_BALUE = new Integer(Integer.MAX_VALUE);
}


public class NonStaticClass {
     private Integer maxValue = new Integer(Integer.MAX_VALUE);
}

JVM 내부에서 리터럴이나 값이 여러 번 사용될 경우 메모리를 절약하기 위해 이들을 재사용할 수 있다,
따라서, 9버전부터 deprecated 된 new Integer(); 생성자 호출을 통해서 인스턴스를 생성한다.
이를 통해 heap 메모리에 NonStaticClass 의 maxValue 변수가 시행 횟수만큼 저장됨을 보장한다.

class StaticMemoryTest {

    private static final int TRIAL_COUNT = 1_000_000;
    
    @Test
    void static_키워드_사용() {
        long memoryUsage = getMemoryUsage(StaticClass.class);
        System.out.println("static 사용: " + memoryUsage / 1024 + "KB");
    }

    @Test
    void static_키워드_미사용() {
        long memoryUsage = getMemoryUsage(NonStaticClass.class);
        System.out.println("static 미사용: " + memoryUsage / 1024 + "KB");
    }

    // 메모리 사용량을 측정한다.
    private <T> long getMemoryUsage(Class<T> clazz) {
        Runtime runtime = Runtime.getRuntime();
        List<T> instances = new ArrayList<>();

        // 이전 메모리
        long beforeMemory = runtime.totalMemory() - runtime.freeMemory();

        try {
            for (int i = 0; i < TRIAL_COUNT; i++) {
                instances.add(clazz.getDeclaredConstructor().newInstance());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 인스턴스 생성 후 메모리
        long afterMemory = runtime.totalMemory() - runtime.freeMemory();

        return afterMemory - beforeMemory;
    }
}

Integer 는 일반적으로 16바이트 내외의 크기를 가진다. 따라서 static 변수와 non-static 변수는

16 * 1,000,000byte = 16000 KB 정도의 차이가 날 것이다.

결과는 16893KB. 시행 횟수마다 수치가 조금씩 틀려서 정확하진 않지만, 근사치라고 보아도 될 것같다.

3. final 사용 이유

final 키워드가 붙은 변수는 재할당 이 불가능하다.

따라서, 해당 키워드를 사용함으로써

  1. 의도 전달 - "이건 바뀌지 말아야 하는 변수야"
  2. 재할당이 불가능함을 보장 - 컴파일이 불가능하므로 캐치가 빠르다

효과를 얻을 수 있다.


정리: static final을 사용해야 하는 이유

  1. 명확성 📖
  • 코드를 읽는 순간 의도 파악 가능
  1. 효율성 🚀
    메모리 사용량 절감 (인스턴스 수가 많을수록, 변수가 클수록 효과 극대화)
  1. 안전성 🔒
  • 값 변경이 불가능하므로

위와 같은 이유로 사용한다. 땅땅.

2개의 댓글

comment-user-thumbnail
2024년 10월 25일

많은 도움이 되었습니다

답글 달기
comment-user-thumbnail
2024년 10월 27일

저는 static을 사용하는 이유에 매직 넘버를 대체하기 위한 이유도 있다고 생각해요.
매직넘버는 어디서는 숫자로 사용될 수 있지만 static이 없는 상수는 인스턴스에만 존재하기 때문에 어디서든 사용할 수 없죠.

답글 달기