Java의 상수 필드는 꼭 static final로 선언할까?

Hello, I am Korean. Welcome, visitor. You are very cool. 안녕하세요, 저는 한국어입니다. 방문자여 환영한다. 당신은 매우 시원해.
서론
✅ 자바에서 상수 필드를 선언할 때 static final을 세트처럼 자주 사용합니다.
이 조합은 워낙 빈번하게 등장해서 마치 관례로 보일 정도입니다.
// 어느 클래스의 static 멤버
public static final String EXAMPLE =
SomeClass.getPropertyValue("example");
✅ 자바 인류는 어쩌다 이 표기를 습관적으로 쓰게 되었을까요?
자바 인류가 어쩌다 이 표기를 습관적으로 쓰게 되었는지, 이 선언 방식이 가져다 주는 이점을 알아 보고, 다른 상수 관리 방식도 가볍게 살펴 보겠습니다.
키워드의 역할
Static
static으로 선언하면, 인스턴스(≈ 객체)를 만들지 않아도 클래스를 통해 바로 접근할 수 있죠.
// 인스턴스를 만들지 않고 클래스를 통해 접근
Example.MAX_VALUE
// 인스턴스를 만들지 않고 클래스를 통해 접근
Example.staticMethod();
자바에서
static을 사용하면 필드의 소유권을 인스턴스 대신 클래스로 옮길 수 있습니다.그래서
static변수는 새 객체를 만들 때마다 새로 생기지 않고, 프로그램 생명주기 동안 하나의 정체성으로 존재하게 됩니다.static은 (처음 생성된 이후) 프로그램의 생명 주기 동안 살아 있습니다.static은 클래스에 종속되어 있기는 하지만, 어느 정도 독립적인 핸들링이 가능합니다.
Final
자바에서 final은 해당 요소가 한 번만 할당될 수 있도록 합니다.
// 선언 (주소 할당)
final int num;
// 대입 (값 할당)
num = 10;
final+ 변수인 기호 상수는 값을 한 번만 대입할 수 있습니다. 이 변수는 자신이 갖고 있는 값이 최종 값임을 보장해요.final+ 메서드는 가상 함수가 아니게 되어서 오버라이딩을 예약할 수 없게 됩니다. 오버라이딩을 할 수 없게 된 이 메서드는 자신이 담고 있는 내용이 최종 동작임을 보장해요.final+ 클래스는 상속을 할 수 없습니다. 이 클래스는 상속되기 위한 목적이 아니라고 선언하여 자신을 구성하는 설계가 더 이상 하위로 상속되어 변경되지 않고 최종 설계임을 보장해요. 메서드도 모두 final 메서드와 같은 효과를 얻겠죠.
지역변수가 아닌 것들에 final을 붙이면 컴파일러는 안정성도 보장하지만, 종종 최적화에서도 도움이 될 때가 있습니다. (컴파일러는 예측하기 쉬운 것일수록 잘 최적화하는 편이거든요.)
덧붙이는 정보
💡 자바는 기본적으로 final이 아닌 메서드는 모두 가상 메서드입니다.
가상 메서드는 추상 메서드와 다릅니다.
추상 메서드(abstract method)는 선언만 있고 내용이 아직 없는 메서드를 말합니다.
가상 메서드(virtual method)는 오버라이딩이 가능한 메서드를 말합니다.C++에서는
virtual키워드를 붙여야 가상 함수가 되지만, 자바에서는 따로 키워드가 없다면 모두 가상 함수입니다.
💡 자바에서는 상속에 열린 클래스를 기본으로 합니다.
따로 키워드를 붙이지 않은 자바 클래스는 언제든 상속이 가능한 상태에 있습니다.
자바에서는
final,sealed등 키워드를 붙여야 상속에 (조금 또는 완전히) 닫힌 클래스가 되지만, 코틀린에서는 따로open키워드를 붙이지 않으면 상속에 완전히 닫힌 클래스가 됩니다.
💡 상수를 사용하면 일부가 컴파일타임에 해석되어 최적화됩니다.
컴파일러는 자신이 예측하기 쉬운 것들은 쉽게 최적화할 수 있습니다. 즉, 우리 코드를 컴파일러가 더 빠른 코드로 살려 줍니다. 이런 최적화 덕분에 우리가 코드 수준에서는 가독성을 우선시할 수 있죠.
상수 전파, 상수 폴딩
상수 폴딩은 값을 미리 계산할 수 있는 것들은 컴파일타임에 미리 계산해 준다는 최적화 개념입니다.
기호 상수를 사용할 때는 상수 전파도 적용될 수 있습니다. 그 기호 상수가 컴파일타임에도 값을 알 수 있는 것일 때 기호 상수를 리터럴 상수로 대치하기도 합니다. 상수 전파를 한 후 상수 폴딩으로 간단한 수식 정도는 컴파일타임에 계산해서 결과를 넣습니다.
함수 인라이닝
함수를 호출하는 비용도 아낄 수 있도록 함수의 동작을 그대로 함수 호출부로 넣는 작업입니다.
메서드가 결국 상수를 반환할 때는 메서드 호출 대신 그 상수를 넣을 수 있습니다.
Static과 Final을 함께 쓰는 이유
static으로 선언하지 않은 상수는 그 클래스의 모든 인스턴스에 해당 값에 대한 메모리를 새로 할당해 메모리 낭비가 발생합니다. 또 인스턴스 생성·정리 비용도 조금은 증가하겠죠.
여러 이유로 상수 사용은 권장되는 방식입니다. (안정성, 최적화, 일부 로직에서 쉬운 핸들링 등)
static사용으로 메모리를 아낄 수 있습니다. (인스턴스마다 중복 생성을 하지 않으니까요.)static사용으로 인스턴스에 종속되지 않은 상수를 사용할 수 있습니다.static이지만final이기 때문에 최적화에 용이합니다.이렇게 작성한 일부 상수는 컴파일타임에 값을 보장하기도 합니다.
💻 자기야, 오해야.
"상수 필드는 꼭 static final로 선언해야 할까요?"라는 오해 풀기
✅ 자바에서 상수 필드를 선언할 때 static final을 세트처럼 자주 사용합니다.
그러다 보니 몇몇 분들이 오해하기를, 자바에서 상수 필드를 선언할 때는 반드시 static final로 해야 바람직하다고 생각하는 것 같습니다.
public class Example {
public static final int MAX_VALUE = 100;
public static final int MIN_VALUE = 0;
}
✅ 사실 처음 가르칠 때는 그렇게 설명하는 게 편하고 좋습니다.
값을 바로 기호 상수에 담는 거라면 그게 좋거든요. (위 예시처럼요.)
✅ 객체마다 다른 값을 갖는 상수는 static이 아닙니다.
이러면 보통 생성자에서 초기화합니다. (공통되는 작업은 인스턴스 초기화 블록을 쓸 수도 있습니다만, 유연한 관리를 위해 상수는 반드시 생성자에서 초기화하는 것을 권합니다.)
public class ExampleClass {
private final String message;
public ExampleClass(String message) {
this.message = message;
}
}
✅ 객체에 종속되지 않고 항상 일정한 값을 static final로 선언하는 겁니다.
어차피 객체마다 만들어도 같은 값을 사용할 거라면, 재생성을 할 필요 없이 static으로 취급합니다.
💡 "static을 붙이면 컴파일타임에 값을 할당한다."라는 오해 풀기
static은 정적으로 관리되는 것은 맞지만, 컴파일타임에 값을 항상 보장하는 것은 아니에요.
컴파일러가 똑똑해서 일부는 컴파일타임에 해석해서 보장해 주는 거예요.
✅ 리터럴 상수를 바로 대입하고 있을 때 → 컴파일타임에 해석이 가능합니다.
✅ static이더라도 값의 결정이 런타임에 될 때 → 컴파일타임에는 값을 정할 수 없습니다.
예를 들어, 런타임에 결과가 정해지는 함수를 사용할 때 그렇습니다.
✅ 이건 static final이더라도 마찬가지예요.
자바의 final은 어떤 식으로든 한 번만 대입이 된다는 게 보장되면 되고, 이건 다음 예시처럼 런타임에 값을 결정해서 대입하는 것을 방지하진 않거든요.
// 어느 클래스의 static 멤버
static final String EXAMPLE =
SomeClass.getPropertyValue("example");
// 사용된 클래스와 함수
public class SomeClass {
public static String getPropertyValue(String propertyName) {
// 이곳에서 어느 파일의 값을 읽어 반환하고 있다고 가정합니다.
// 파일은 런타임에 읽습니다.
// (컴파일타임에는 외부 파일의 내용을 알 수 없고, 파일의 내용이 불변임을 보장할 수 없습니다.)
}
}




