서론
✅ 자바에서 상수 필드를 선언할 때 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) {
// 이곳에서 어느 파일의 값을 읽어 반환하고 있다고 가정합니다.
// 파일은 런타임에 읽습니다.
// (컴파일타임에는 외부 파일의 내용을 알 수 없고, 파일의 내용이 불변임을 보장할 수 없습니다.)
}
}