Skip to main content

Command Palette

Search for a command to run...

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

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

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

More from this blog

클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? #42

이 아티클은 깃허브 nettee-space 조직의 디스커션 #42 항목을 옮겨 온 것입니다.관련 논의: nettee-space/backend-sample-hexagonal-simple-crud/discussions/42 Question: 클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? 여러 소스들을 접하면서 VO 객체 등에 Serializable를 구현받는 것을 많이 접했습니다. 저희 헥사고날(스터디 팀내 2단계 ...

Feb 7, 2025
클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? #42

개념 2. JWT 액세스 토큰의 생성과 전달, Stateful한 리프레시 토큰의 생성, 전달, 보존

이전 글에서 정리에 꽤 힘을 뺐기 때문에, 이번 글에서는 서두와 부연설명을 줄이고 필요한 정보를 담아 전달해 보겠습니다. JWT 액세스 토큰 JWT 액세스 토큰은 인가에 직접 사용되는 토큰이고, stateless 하다는 장점이 있었습니다. 액세스 토큰의 생성 JWT(JSON Web Token)로 생성합니다. 비밀번호 인증 등 자격 검토 후 JWT를 발급합니다. JWT는 헤더, 페이로드, 시그니처 세 영역을 점(.) 기호로 구분한다고 했습니다....

Oct 23, 2024
개념 2. JWT 액세스 토큰의 생성과 전달, Stateful한 리프레시 토큰의 생성, 전달, 보존

Merge Simpson의 매너 있고 다정한 한국어 개발자 블로그

37 posts

Java의 상수 필드는 왜 대부분 static final로 선언할까?라는 제목