final은 불변을 보장할까?

로마·2022년 2월 20일
5
post-thumbnail

우테코 자동차 미션을 하던 중 Cars 라는 일급컬렉션을 구현했다. 하지만 이 때 final로 지정한 List가 불변이 보장될 것이라는 안일한 생각을 하고 있었다. 하지만 크루들과 이야기를 나눠본 뒤 final만 붙여서는 불변을 보장할 수 없구나라는 것을 깨달았다. 그 동안 final 키워드에 대해 잘 알지 못했었던 것 같다.

오늘은 내가 알아본 final에 대해서 적어보려 한다. 무의식적으로 사용해왔던 final 이지만 깊게 공부해 본 기억은 없다. 이번 기회에 final이 과연 불변을 보장할지 제대로 파보자.

final 무엇인가?

In the Java Programming language, the final keyword is used in several contexts to define an entity that can only be assigned once. - wikipedia

  • final최종적이라는 의미를 가진다.

무엇이 최종적이라고 할 수 있을까? 자바에서 최종적이라는 말은 할당이 변경되지 않음을 의미한다. 즉 final 키워드는 초기에 한 번 할당을 하게 되면 최종적인 값이 되어 프로그램 실행 중에 할당을 바꿀 수 없게 제한하는 키워드다.

  • final 은 클래스, 메서드, 필드에 각각 붙을 수 있다.

  • final 클래스 는 상속할 수 없는 클래스를 만든다.

final 클래스는 최종적인 클래스이므로 상속할 수 없는 클래스가 된다. 즉 final 클래스는 부모 클래스가 될 수 없게 된다.

public final class Car { } // final 클래스는 상속할 수 없다!

public class Bus extends Car { }
  • final 메서드 는 오버라이딩 할 수 없다.

final 메서드 는 최종적인 메소드이므로 오버라이딩 할 수 없는 메소드가 된다. 부모 클래스에 final 메서드가 선언 되었다면 자식 클래스에서는 해당 클래스를 오버라이딩할 수 없게 된다.

public class Car {
    public final void go() {
        System.out.println("부릉부릉");
    }
}

public class Bus extends Car {
		// final 메서드를 오버라이딩 할 수 없다!
    @Override
    public void go(){
        System.out.println("부아앙"); // ERROR!
    }
}
  • final 필드 에서 초기값을 지정하는 방법에는 주로 두 가지 방법을 사용한다.
    1. 필드 선언 시 초기값을 지정하는 방식

      private final String name = "roma";
    2. 생성자에서 초기값을 지정하는 방식

      public class Car {
      	private final String name;
      	
      	public Car(String name) {
      			this.name = name;
      	}
      }
  • final 은 인자에도 붙을 수 있다.
private void check(final int num) {
	num = 10; // ERROR!
}

상수는 무엇인가?

  • 자바에서 상수는 불변의 값을 의미한다. 불변의 값이란 원주율이나 지구의 둘레, 파이 처럼 변하지 않는 값을 뜻한다.

우리는 자바에서 상수를 자주 사용하게 된다. 이번 자동차 미션에서 매직 넘버를 의미있는 단어로 치환할 때 특히 많이 사용했다.

  • 상수는 static final 을 이용해 지정할 수 있다.
    • 관례적으로 상수는 모두 대문자로 표현하고 서로 다른 단어가 혼합될 경우 언더바(_)를 이용한다.

      static final int INITIAL_ROUND_NUM = 0;
      static final int INITIAL_POSITION = 0;
      static final int CAN_GO_VALUE = 4;
      static final int MAX_NAME_LENGTH = 5;

그렇다면 앞서 말했듯 final 키워드 만으로 최종값을 지정하는 것이니 이후 변경되지 않으므로 상수가 아닌가? final 필드도 불변하지 않은가? 라는 생각이 든다.

이에 대해서 이것이 자바다에 따르면 다음과 같이 적혀있다.

final 필드는 상수라고 부르진 않는다. 왜냐하면 상수는 객체마다 저장할 필요가 없는 공용성을 띠고 있으며, 여러 가지 값으로 초기화 될 수 없다. - 이것이자바다 246p

즉 상수는 프로그램 어디서나 같은 값을 가져야 한다. 하지만 final 필드는 객체마다 저장되고 생성자의 매개값을 통해 여러 값을 가질 수 있기 때문에 상수가 아니다.

final 필드는 과연 불변일까?

fina 필드가 공용성을 가지지 않기 때문에 상수가 아니라는 것은 알았다. 그런데 상수가 아니더라도 final은 그 자체만으로 최종적인 값을 나타내기 때문에 불변을 나타내는 뜻이라 오해하기 쉽다.

물론 이 말은 primitive 타입의 필드의 경우에는 적용될 수 있다.

primitive 타입은 리터럴을 참조한다. 즉 이미 데이터 영역에 저장된 리터럴이라는 값을 참조하는 형태이기 때문에 다른 값으로 변경하려고 한다면 다른 값을 참조한다는 의미와 마찬가지기 때문에 final을 붙인 primitive 타입의 필드는 불변하다고 볼 수 있다.

  • primitive 타입에서는 참조값이 존재하지 않기 때문에 외부에서도 그대로 불변으로 존재하게 된다.
public class FinalTest{

	private final int num = 3;

	private void changeNum(int newNum) {
		// error: cannot assign a value to final variable num!
		num = newNum; // ERROR!
	}
}

하지만 만약 참조타입인 객체나 List와 같은 컬렉션이 들어간다해도 불변이 될까? 한번 테스트해보자.

  • Car 객체
public class Car {
    private int curPosition = 2;

    public void setPosition(int position) {
				curPosition = position;
    }

}
  • FinalTest 객체
public class FinalTest {
    public final Car car;

    @Test
    public void objectTest() throws Exception {
        car.setPosition(3);
    }
}

위에서 보면 Car 객체를 정의하고 FinalTest 내부에서 Car 객체를 final로 선언했다. 이 때 car에 setPosition() 이라는 메세지를 보냄으로써 Car의 내부 상태가 변경 될 수 있다.

이렇게 하게 되면 car 객체에 final 처리를 해줬음에도 외부에서 내부 상태를 변경시킬 수 있게 된다. 과연 이게 불변을 보장하는 것이라 할 수 있을까? 필자는 불변을 보장하지 못한다고 생각한다.

또 다음 예제를 살펴보자. 이번엔 컬렉션을 사용한다.

  • Game 객체
public class Game {
    private final List<String> playerNames = new ArrayList<>();

    public List<String> getPlayerNames() {
        return playerNames;
    }
}
  • FinalTest 객체
public class FinalTest {
    @Test
    public void objectTest() throws Exception {
        Game game = new Game();
        List<String> playerNames = game.getPlayerNames();

        playerNames.add("roma");
        playerNames.add("king");

        assertThat(playerNames).containsExactly("roma", "king");
        assertThat(game.getPlayerNames()).containsExactly("roma", "king");
    }
}

Game 객체 내부에서 final 로 지정했던 List 컬렉션도 외부에서 그 상태를 변경가능한 것을 볼 수 있다. 그러므로 컬렉션 또한 final이 불변을 보장해주지는 않는다.

불변을 보장하는 방법?

그럼 어떻게 객체와 컬렉션에서는 외부로부터 내부 상태의 불변을 보장할 수 있을까?

다음과 같은 방법을 생각해볼 수 있다.

  • 객체의 경우 생성자를 통해서만 값을 주입받는다.
    public class Car {
        private final int curPosition;
    
        public Car(int curPosition) {
    			this.curPosition = curPosition
    		}
    
    }
    이와 같이 변경하게 되면 Car에 curPosition을 바꿀 수 있는 메서드가 존재하지 않게 되고 외부로부터 변경을 방지할 수 있게 된다. 즉 상태의 불변을 보장할 수 있게 된다.
  • 컬렉션의 경우 Unmodifiable Collection을 사용한다.
    public class Game {
        private final List<String> playerNames = new ArrayList<>();
    
        public List<String> getPlayerNames() {
            return Collctions.unmodifiableList(playerNames);
        }
    }
    unmodifiable 컬렉션은 해당 컬렉션을 read-only로 사용하도록 강제하는 컬렉션으로 add() 와 같은 수정을 시도할 시에는 UnsupportedOperationException 이 발생하게 된다. 이로써 컬렉션의 불변을 어느정도 보장할 수 있다고 볼 수 있다.
    • 하지만 unmodifiable 컬렉션을 이용해도 해당 구현체로 넘겨주어야만 수정이 불가능해지지 playerNames 레퍼런스 자체를 가지고 있는 다른 곳에서는 수정이 가능하므로 완전한 불변이라고는 볼 수 없다. (https://soft.plusblog.co.kr/71)

결론

final 키워드는 재할당만 제한할 뿐 무조건 불변을 보장하는 것이 아니다.

profile
우공이산

1개의 댓글

comment-user-thumbnail
2023년 3월 15일

감사합니다! 궁금했던 부분인데 덕분에 해결되었습니다 :D

답글 달기