
Java를 배우다 보면 가장 먼저 접하지만, 가장 오래 헷갈리는 키워드 중 하나가 바로 final이다. 단순히 “변경 불가”라고만 이해하고 넘어가기 쉽지만, final 키워드는 위치에 따라 전혀 다른 의미와 의도를 가진다.
이 글에서는 final 키워드가 클래스, 메서드, 변수에서 각각 어떤 역할을 하는지 살펴보고, 특히 실무에서 자주 혼동되는 재할당 금지와 불변(Immutable)의 차이를 중심으로 정리해본다.
final 키워드 클래스와 메서드 의미
final 키워드를 클래스나 메서드에 사용하면 가장 먼저 떠올려야 할 개념은 상속 제한이다.
final 클래스는 더 이상 상속될 수 없다. 즉, 해당 클래스는 확장 지점이 없다는 것을 명확히 선언하는 의미를 가진다. final 메서드는 오버라이딩이 불가능하며, 하위 클래스에서 동작을 변경할 수 없다.
이 제한은 단순한 문법 제약을 넘어서 JVM과 JIT 컴파일러에게 중요한 힌트를 제공한다. 이 메서드는 더 이상 확장되지 않으니 가상 메서드 탐색을 하지 않아도 된다는 의미가 된다.
그 결과 JVM은 메서드 인라이닝과 같은 공격적인 최적화를 수행할 수 있고, 클래스 계층 구조 분석 비용도 줄어든다. 즉 final은 “의도 표현”이면서 동시에 “성능 최적화 힌트”다.
실무에서 final 클래스나 메서드를 사용하는 이유는 무조건 상속을 막기 위함이 아니라 “이 지점은 변경되지 않는 계약이다”라는 신호를 주기 위함인 경우가 많다.
final 변수 재할당 금지 개념
final 키워드를 변수에 사용할 때의 핵심 의미는 재할당 금지다. 이 개념은 매우 중요하지만, 동시에 가장 많이 오해된다.
final 변수는 한 번 값이 할당되면 그 이후 다른 값으로 다시 할당할 수 없다. 원시 타입이든 참조 타입이든 동일하다.
하지만 이것이 곧 불변을 의미하지는 않는다. final은 “참조를 바꾸지 못한다”는 뜻이지 “객체의 상태가 바뀌지 않는다”는 뜻은 아니다.
예를 들어 final로 선언된 List는 다른 리스트를 가리키도록 재할당할 수는 없지만, 그 내부 요소를 추가하거나 수정하는 것은 가능하다.
또 하나 중요한 포인트는 메서드 파라미터다. 메서드에 전달되는 값은 복사본이기 때문에 final 여부는 해당 메서드 스코프 내에서만 유효하다. 호출한 쪽의 값에는 아무 영향도 주지 않는다.
즉 final 변수는 값의 안정성을 높여주지만, 불변 객체와는 전혀 다른 개념이라는 점을 반드시 구분해야 한다.
불변 객체와 final 차이 정리
불변(Immutable)은 “객체의 상태가 생성 이후 절대로 변경되지 않는다”는 성질을 의미한다. 이는 final보다 훨씬 강한 개념이다.
Java의 List.of()로 생성한 리스트는 내부적으로 상태 변경 메서드를 모두 차단한다. add, remove 같은 메서드를 호출하면 즉시 예외가 발생한다.
반면 Arrays.asList()는 크기 변경은 불가능하지만 기존 요소의 값은 set()을 통해 변경할 수 있다. 즉 완전한 불변은 아니다.
이 차이는 final과 불변을 구분하는 데 매우 좋은 예시다. final은 “이 변수는 다시 가리키지 않는다”는 선언이고, 불변은 “이 객체는 절대 변하지 않는다”는 계약이다.
또한 람다 표현식과 Stream API에서도 외부 변수는 final 또는 effectively final이어야 한다. 이는 클로저 특성과 멀티 스레드 환경에서 값의 일관성을 보장하기 위한 언어 차원의 제약이다.
실무에서는 final 키워드를 단순한 문법 요소가 아니라 “이 값은 다시 생각하지 않아도 된다”라는 마커처럼 사용하는 경우가 많다.
마무리 정리
final 키워드는 단순히 변경을 막는 키워드가 아니다. 의도를 드러내고, 코드의 예측 가능성을 높이며, JVM 최적화까지 돕는 중요한 신호다.
재할당 금지와 불변을 명확히 구분하고, 필요한 지점에 final을 사용한다면 코드는 훨씬 읽기 쉬워지고 유지보수 비용도 줄어든다.
기본 문법을 넘어서 final이 주는 부수적인 효과까지 이해한다면 Java 코드를 바라보는 시선이 분명 달라질 것이다.