|

자바 static과 final 차이: 함께 자주 나오지만 역할이 다른 문법 정리

자바 static과 final 차이를 설명하는 대표 이미지
공유 범위를 뜻하는 static과 변경 제한을 뜻하는 final을 같은 축으로 헷갈리지 않도록 정리한다

자바 static과 final 차이는 입문자가 정말 자주 헷갈리는 주제입니다. 둘 다 변수 선언 앞에 자주 붙고, static final처럼 함께 나오기도 해서 더 섞이기 쉽습니다. 하지만 static은 어디에 속하고 누구와 공유하는지를, final은 무엇을 다시 바꿀 수 없는지를 다룹니다. 이 기준만 잡히면 훨씬 덜 헷갈립니다.

이번 글에서는 static 필드와 static 메서드, final 변수, 참조형 final의 오해, static final 상수 패턴, final 클래스와 final 메서드까지 실제 코드 판단 기준으로 짧고 분명하게 정리하겠습니다. Oracle의 class members 설명final classes and methods 문서를 기준으로, 정의 암기보다 실전 감각에 집중하겠습니다.


자바 static과 final 차이

  • static은 인스턴스가 아니라 클래스에 속한다
  • final은 그 대상에 대해 재할당 또는 재정의를 막는다
  • static final은 공유되는 값을 바꾸지 못하게 하는 상수 패턴이다

한 줄로 줄이면 static은 소속과 공유 범위, final은 변경 제한입니다. 둘은 같은 뜻이 아니라 서로 다른 질문에 답하는 키워드입니다.

자바 static과 final의 역할 차이를 카드형으로 비교한 이미지
먼저 이 한 장으로 역할 차이를 잡으면 뒤 예시들이 훨씬 쉬워진다

왜 헷갈릴까

헷갈리는 이유는 단순합니다. 둘 다 선언부에 붙고, private static final int MAX_RETRY = 3; 같은 코드에서 한 줄에 같이 보이기 때문입니다. 그래서 많은 입문자가 둘을 통째로 “상수 만드는 키워드”처럼 외워 버립니다.

하지만 실제로는 역할이 완전히 다릅니다. static이 없으면 final이어도 객체마다 따로 가진 값일 수 있고, final이 없으면 static이어도 모두가 공유하지만 바뀔 수 있는 값일 수 있습니다. 즉, 공유와 불변은 같은 개념이 아닙니다.


static

static은 이 멤버가 특정 객체가 아니라 클래스 자체에 속한다는 뜻입니다. Oracle 튜토리얼도 static field를 class variable로 설명합니다. 즉, 객체를 여러 개 만들어도 static 값은 하나를 공유합니다.

class VisitorCounter {
    static int totalVisitors = 0;

    VisitorCounter() {
        totalVisitors++;
    }
}

VisitorCounter a = new VisitorCounter();
VisitorCounter b = new VisitorCounter();

System.out.println(VisitorCounter.totalVisitors); // 2

여기서 totalVisitors는 객체 a의 값도 아니고 b의 값도 아닙니다. VisitorCounter 클래스 전체가 함께 보는 값입니다. 이런 성질이 필요할 때 static이 자연스럽습니다.

  • 전체 생성 개수처럼 객체들이 함께 공유해야 하는 값
  • 유틸리티 함수처럼 객체 상태 없이 동작하는 기능
  • 설정값이나 상수처럼 클래스 이름으로 읽는 편이 자연스러운 값

static 메서드

static은 메서드에도 붙을 수 있습니다. static 메서드는 객체를 만들지 않고 ClassName.method() 형태로 호출합니다. 대신 클래스 메서드라서 인스턴스 변수와 인스턴스 메서드에 직접 접근할 수 없고, this도 쓸 수 없습니다.

class MathHelper {
    static int add(int a, int b) {
        return a + b;
    }
}

int result = MathHelper.add(3, 5);
System.out.println(result); // 8

이런 메서드는 계산기, 파서, 포맷터처럼 객체 상태를 읽지 않아도 되는 일에 잘 맞습니다. 반대로 객체마다 다른 상태를 써야 하는 메서드라면 static보다 인스턴스 메서드가 자연스러운 경우가 많습니다.

class User {
    String name;

    static void printName() {
        // System.out.println(name);
        // 컴파일 오류: static 메서드에서 인스턴스 필드에 직접 접근할 수 없다
    }
}

이 제약이 바로 static의 핵심입니다. static은 편한 키워드가 아니라, 인스턴스 맥락 없이 동작해야 한다는 선언에 가깝습니다.


final

final은 대상이 무엇이냐에 따라 의미가 조금씩 달라지지만, 공통 핵심은 하나입니다. 다시 바꾸는 일을 막는다는 점입니다. 필드에 붙으면 재할당을 막고, 메서드에 붙으면 override를 막고, 클래스에 붙으면 상속을 막습니다.

final int maxRetry = 3;
// maxRetry = 5;
// 컴파일 오류: final 변수는 다시 대입할 수 없다

이때 많이 하는 오해가 있습니다. final은 항상 “영원히 안 바뀌는 값”이라고만 이해하면 위험합니다. 숫자 같은 기본형에서는 거의 그렇게 느껴지지만, 참조형에서는 이야기가 조금 다릅니다.


참조형 final

참조형 변수에 final을 붙였다고 해서 객체 내부 상태까지 모두 얼어붙는 것은 아닙니다. 그 변수가 다른 객체를 다시 가리키지 못하게 하는 것이지, 그 객체가 mutable이면 내부 값은 여전히 바뀔 수 있습니다.

import java.util.ArrayList;
import java.util.List;

final List<String> names = new ArrayList<>();
names.add("java");
names.add("spring");

System.out.println(names); // [java, spring]

// names = new ArrayList<>();
// 컴파일 오류: final 참조는 다른 객체를 다시 가리킬 수 없다

이 예시는 실무에서 아주 중요합니다. final 참조와 immutable 객체는 같은 말이 아닙니다. 객체 자체가 안 바뀌게 만들려면 클래스 설계나 불변 컬렉션 같은 다른 도구도 함께 봐야 합니다.

문자열 비교를 다룬 자바 ==와 equals 차이 글에서도 보듯, 자바에서는 변수 이름만 보고 값의 성질을 단정하면 자주 틀립니다. 참조형 final도 같은 함정이 있습니다.


static final

static final은 static과 final을 함께 쓴 대표 패턴입니다. 자주 상수를 만들 때 등장합니다. 여기서 static은 모든 객체가 하나를 함께 본다는 뜻이고, final은 그 값을 다시 바꿀 수 없다는 뜻입니다.

class RetryPolicy {
    static final int MAX_RETRY = 3;
    static final double PI = 3.141592653589793;
}

즉, static final은 두 키워드가 합쳐진 결과입니다. 상수처럼 읽히는 이유는 static과 final이 각각 다른 역할을 맡고 있기 때문입니다. 둘이 같은 뜻이어서가 아닙니다.

  • 모든 객체가 같은 값을 봐야 한다면 static
  • 그 값을 코드 중간에 다시 바꾸면 안 된다면 final
  • 둘 다 필요하면 static final

final 메서드

메서드에 final을 붙이면 하위 클래스가 그 메서드를 override할 수 없습니다. Oracle 문서도 객체의 일관된 상태를 지키기 위해 final 메서드를 쓸 수 있다고 설명합니다. 즉, 설계자가 “이 동작은 바꾸지 말라”고 선을 그은 것입니다.

class PaymentService {
    final void validate() {
        System.out.println("validate payment");
    }
}

class CardPaymentService extends PaymentService {
    // void validate() { }
    // 컴파일 오류: final 메서드는 override할 수 없다
}

이 지점은 자바 오버로딩과 오버라이딩 차이 글과도 연결됩니다. 오버라이딩이 가능한 메서드인지 아닌지부터 구분해야 상속 설계가 깔끔해집니다.


final 클래스

클래스에 final을 붙이면 그 클래스를 상속할 수 없습니다. Oracle 튜토리얼은 immutable class 예시로 String을 듭니다. 물론 final class라고 해서 자동으로 모든 내부 상태가 불변이 되는 것은 아니지만, 확장을 막는다는 설계 의도는 분명하게 전달됩니다.

final class ApiKey {
    private final String value;

    ApiKey(String value) {
        this.value = value;
    }

    public String value() {
        return value;
    }
}

// class SpecialApiKey extends ApiKey { }
// 컴파일 오류: final 클래스는 상속할 수 없다

상속을 열어 둘지, 막을지 고민된다면 추상 클래스와 인터페이스 차이 글도 함께 보면 좋습니다. 확장 지점을 열어 두는 설계와 닫는 설계는 이유가 분명해야 합니다.


실전 판단 기준

  1. 여러 객체가 같은 값을 함께 봐야 하면 static을 먼저 본다
  2. 한 번 정한 값이나 참조를 다시 바꾸면 안 되면 final을 붙일지 본다
  3. 모든 곳에서 같은 상수로 읽히고 바뀌면 안 되면 static final이 자연스럽다
  4. 객체 상태를 읽는 동작이라면 static 메서드보다 인스턴스 메서드가 더 맞는지 먼저 점검한다
  5. 상속 확장을 막고 싶은 명확한 이유가 있을 때만 final 메서드나 final 클래스를 쓴다

여기서 중요한 건 키워드 자체를 외우는 것이 아니라, 내가 지금 해결하려는 문제가 공유 범위 문제인지, 변경 제한 문제인지 먼저 묻는 습관입니다. 이 질문만으로도 절반 이상은 정리됩니다.


자주 하는 오해

  • static이면 자동으로 안 바뀐다고 생각하는 오해
  • final이면 객체 내부 상태까지 모두 못 바꾼다고 생각하는 오해
  • 상수는 무조건 final만 붙이면 된다고 생각하는 오해
  • 객체 상태를 다루는 메서드도 편하다는 이유로 static으로 빼는 오해

특히 shared scope와 immutability는 다른 축이라는 점을 꼭 기억해두면 좋습니다. static은 공유를 말하고, final은 제한을 말합니다. 둘이 만나면 공유되는 상수가 되지만, 각각 따로 보면 전혀 다른 이야기입니다.


정리

자바에서 static은 클래스 소속과 공유 범위를, final은 재할당과 재정의 제한을 뜻합니다. 그래서 static 필드는 모두가 함께 보는 값이고, final 변수는 다시 대입할 수 없는 값입니다. static final은 그 둘이 합쳐진 상수 패턴일 뿐, 두 키워드가 같은 뜻이라는 의미는 아닙니다.

실전에서는 “이 값이 모든 객체에 공통인가?”를 먼저 물어 static을 판단하고, “이 값이나 동작을 다시 바꾸지 못하게 해야 하나?”를 물어 final을 판단하면 됩니다. 이렇게 두 질문으로 나누면 static과 final은 훨씬 덜 헷갈립니다.


같이 보면 좋은 글

자바 ==와 equals 차이: 문자열 비교에서 가장 많이 틀리는 문법 정리
자바 오버로딩과 오버라이딩 차이: 면접용 암기보다 중요한 실제 코드 판단 기준
추상 클래스와 인터페이스 차이: 자바와 객체지향 설계 관점에서 언제 무엇을 써야 할까

함께보면 좋은 글