Technology/Java

Java final 키워드의 여러가지 쓰임새

ikjo 2022. 8. 26. 19:08

final 키워드란?

final이란 '마지막'이라는 뜻을 가지고 있는데, 실제 자바에서 쓰임새도 이 의미와 유사하게 사용된다. 이때 final 키워드가 사용될 수 있는 대상은 '클래스', '메서드', '변수' 세가지 뿐으로 각각의 경우에 대해 알아보자.

 

클래스에 사용되는 final

클래스에 final 키워드가 사용되면 해당 클래스는 상속받을 수 없게 된다. 대표적인 final class에는 java.lang 패키지에서 제공하는 String 클래스와 Math 클래스가 있다.

 

public final class Math {

    // ...
    
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

    // ...

 

이러한 final 클래스를 만든데에는 사용자(개발자)가 특정 클래스를 상속함으로써 확장하여 사용하지 않도록 하기 위함이라고 볼 수 있다. 클래스 상속이 안되니 당연히 메서드를 오버라이딩하는 것도 불가능해진다. 만일 이러한 final 클래스를 상속하도록 코드를 작성하면 컴파일 에러가 발생한다.

 

메서드에 사용되는 final

메서드에 final 키워드가 사용되면 해당 메서드는 하위 클래스에서 오버라이딩 하는 것이 불가능해진다.

 

class Figure {

    final void print() {
        System.out.println("Figure");
    }
}

class Triangle extends Figure {

    @Override
    void print() { // 컴파일 에러 : 'print()' cannot override 'print()' in 'Figure'; overridden method is final
        System.out.println("Triangle");
    }

}

 

마찬가지로 final 메서드를 하위 클래스에서 오버라이딩할 경우 컴파일 에러가 발생한다. final 메서드는 final 클래스 보다는 좀 더 규제를 완화한 것이라고 볼 수 있다. 상속은 허용하되 특정 메서드에 대해서는 재정의하지 않고 상위 클래스에 있는 것을 그대로 사용하도록 하기 위함이라고 볼 수 있다.

 

변수에 사용되는 final

사실 개발할 때는 클래스나 메서드 보다는 변수에 final을 자주 사용하는 편이다. 이때 final이 변수에 사용되면 값을 변경할 수 없는 상수가 된다.

 

우선 지역변수에 사용될 때부터 알아보자. 다음과 같이 구매한 금액을 입력했을 때 살 수 있는 로또의 개수를 반환해주는 메서드가 있다고 해보자. 이때 로또를 구매할 수 있는 단위는 1000원이라고 가정했는데, 이 값은 애플리케이션의 정책이 바뀌지 않는 이상 변경될 일이 없다. 따라서 이를 final 키워드로 변경이 불가능하도록 해주었다.

 

    public int countOfLotto(int buyMoney) {
        final int buyMoneyUnit = 1000;
        
        /* 다음과 같이 선언과 초기화를 분리할 수도 있다. (단, 이후에 재할당은 불가능)
        final int buyMoneyUnit;
        buyMoneyUnit = 1000;
        */
        
        return buyMoney / buyMoneyUnit;
    }

 

하지만 매번 메서드를 호출할 때마다 똑같은 buyMoneyUnit이라는 변수를 생성해주는 것은 비효율적이다. 이러한 상수는 변하지 않으므로 보통 해당 클래스의 멤버(private statci final)로 선언하여 사용한다. 해당 값은 변하지 않는다는 가정하에 final 키워드를 사용했고, 모든 인스턴스들이 동일한 값을 가져야한다고 가정했기 때문에 static 키워드를 붙여주었다.

 

public class Seller {

    private static final int BUY_MONEY_UNIT = 1000;

    public int countOfLotto(int buyMoney) {
        return buyMoney / BUY_MONEY_UNIT;
    }
    
    // ...

 

이때 각각의 객체별로 상태값은 달리하고 싶은데 초기화 이후로 값이 수정되지 않도록 불변 객체를 만들고 싶을 때도 final 키워드를 사용할 수 있다.

 

public class Coffee {
    
    private final String name;
    private final String barista;

    public Coffee(String name, String barista) {
        this.name = name;
        this.barista = barista;
    }
    
    // ...
}

 

위 코드를 보면 Coffee 라는 클래스의 멤버 변수로 커피의 이름(name)과 바리스타(barista)가 정의되어 있다. 이때 커피 객체의 이름과 바리스타라는 상태 값은 객체가 생성되는 시점에 정할 수 있어야 하고 이후에 변경되지 않도록(불변 객체) 하기 위해 생성자를 이용하여 final 멤버 변수를 초기화 시켜주었다.

 

추가적으로 이러한 final 키워드는 메서드의 매개변수에도 선언해줄 수 있다. 어떤 복잡한 로직을 처리함에 있어 중간에 매개변수가 바뀔 우려가 있는 경우 이를 사전에 방지하고자 매개변수 앞에 final 키워드를 붙여줄 수 있다.

 

    public int sum(final int a, final int b) {
        int newA = 0, newB = 0;
        // newA = a + ...
        // newB = b + ... 
        return newA + newB;
    }

 

final 키워드가 주는 진정한 의미

지금까지 final 키워드가 클래스, 메서드, 변수 각각에 사용되었을 때 어떤 효과를 주는지 알아보았다. 사실 final 키워드가 없어도 개발 자체를 못하는 것은 아니다. 앞서 클래스의 경우 '개발자가 알아서' 해당 클래스를 상속받지 않으면 될 일이고, 메서드의 경우 '개발자가 알아서' 해당 메서드를 오버라딩하지 않으면 될 일이고, 변수의 경우 '개발자가 알아서' 해당 변수를 변경하지 않으면 될 일이긴 하다.

 

하지만, 애플리케이션이 확장됨에 따라 기능들이 많아지고, 각각의 객체를 이루는 상태와 기능뿐만 아니라 객체들 간의 협력관계가 복잡해짐에 따라 개발자는 의도치 않은 실수를 하게 될 확률이 존재한다. 이때 final 키워드가 주는 진정한 효과는 '개발자가 알아서' 해야할 일을 '컴파일러'의 도움을 받아 실행 중에 발생할 수 있는 여러 오류들을 미연에 방지할 수 있는 효과를 주는 것이다.

 

 

참고자료

  • 도우출판 "자바의 정석"
  • 위키북스 "스프링 입문을 위한 자바 객체 지향의 원리와 이해"