We will find a way, we always have.

-interstellar

Programming Language/자바

[자바] final에 美친놈

Redddy 2024. 4. 12. 17:59

서론

우테코 레벨1 때 첫번째 미션이었던 자동차 경주 미션과 마지막 미션인 체스 미션의 main 메서드를 살펴보면 눈에 띄게 달라진 게 있다. 
 

자동차 경주 미션
체스 미션

 
바로 final
main의 파라미터인 args에도 final, 로컬 변수에도 final, try catch 문에서 잡은 예외에도 final이 붙어 있다.
 
제일 처음 꼬신건 :네오:였다. 문자열 덧셈 계산기 피드백 강의를 보는데 코드 여기저기서 final을 볼 수 있었다. 그때부터 이녀석에게 관심을 갖기 시작했다.
 
그럼 이제 final이 뭐길래 얘는 필사적으로 final을 붙이려고 하는지 알아보자.
 

본론

final 키워드를 추가할 수 있는 곳은 여럿 있는데 각각 의미가 다르다.
 

  • 클래스
    • 상속 제한

 

  • 메서드
    • 오버라이딩 제한

 

  • 변수
    • 재할당 제한

 
여기서 나는 변수에 초점을 두려한다.
 
재할당을 제한하기 위해 final을 붙일 수 있는 곳은 파라미터, 로컬변수, catch로 잡은 예외, for each 문의 변수, 인스턴스 필드 변수등이 있다.
 
여기서 퀴즈 아래 코드에서 final을 붙일 수 있는 곳은 총 몇군데일까? (재할당을 제한하는 용도에서만)
 

 
 
 
 
사실 원시타입에서 final은 뭐 어떻고, 참조타입에서 final은 뭐 어떻고는 다른 블로그에도 잔뜩 있으니 더이상의 자세한 설명은 생략한다. (하지만 catch한 예외에도 final을 붙일 수 있다는 걸 알고 있는 사람은 많지 않을 거 같다)
 
 

파이널 장점

높은 코드 안정성

final을 붙여 값의 재할당을 막으면 의도치 않은 값의 변경을 방지할 수 있다. 때문에 이 값이 재할당될 거라는 불안감에 떨지 않아도 되고, 값에 대한 검증을 하지 않아도 된다.
 
에이 설마 파라미터로 넘어온 값을 재할당하겠어~~ 라고 생각할 수도 있지만 언제 누가 내 코드를 볼지는 모르는 일이다.
 

Thread-safe

멀티스레딩 환경에서 thread-safe 하도록 만드는 것이 매우 중요하다. 만약 final을 붙여 값 변경을 막는다면 근본적으로 thread-safe하다.
 
명심해야할 점이 final을 붙이면 재할당을 제한하는 것이지 immutable하게 만들어주는 것이 아니다. Collection 같은 경우에도 final을 붙여주어도 원소를 추가하거나 삭제하는 것은 가능하다. 컬렉션을 불변으로 만들기 위해서는 List.of(), Map.of()를 사용하거나 unmodifiableXXX를 사용해주면 된다.
 

가비지 컬렉터 최적화

아래 사진과 같은 코드가 있다고 한다.
 

가비지 컬렉션 예시

 
user를 final로 설정해주지 않았기 때문에 "알유레디"로 생성한 이후에 "아임레디"로 재할당이 가능해졌다. 그렇다면 이제 "알유레디"의 객체는 참조를 잃어버리게 되고, 행복한 이든 영역에서 놀다가 GC에 의해 제거될 날만 기다릴 것이다. 
 
하지만 user에 final을 추가해준다면??
 

 
애초에 재할당을 막아 참조를 잃어버리는 객체를 막을 수 있다. 👍
 

또 다른 예시를 살펴보자.

 

 
 
Object 타입의 name 객체 생성하고, 불변 타입의 user 객체를 생성과 동시에 user가 name 객체를 참조하는 코드이다.
 
이렇게 코드를 작성한다면 GC가 수행될 때, 가비지 컬렉터가 객체 하위의 불변 객체들은 skip할 수 있도록 도와준다. 왜냐 user가 살아있다는 것은 하위의 불변 객체들(name) 역시 처음에 할당된 상태로 참조되고 있음을 의미하기 때문이다. 
 
불변 객체를 사용하면 가비지 컬렉터가 스캔해야 하는 객체 수가 감소하고, 또한 스캔해야 하는 메모리 영역과 빈도수도 감소할 것이다. 그러면 최종적으로 GC의 stop the wolrd 시간도 감소할 것이다.
 
 

컴파일러 최적화

String에 final을 붙였을 때와 붙이지 않았을 때의 차이점을 살펴봐보자!
 

 
 
위 두 코드의 실행 결과는 동일하지만 바이트 코드는 다르다!
 

final을 붙이지 않는 코드의 바이트코드
final을 붙인 코드의 바이트코드

 
 
final을 붙이지 않았을 때엔 makeConcatWithConstants 어쩌구 연산을 하여 ab를 만들어주는데, final을 붙였을 때에는 저런 연산 없이 바로 뙇 ab를 만들어준다. 
final로 인해 컴파일러가 문자열 연결 결과가 절대 변하지 않을 것을 알게 되기에 바로 연결하는 것이다. 
아예 연산을 하지 않기 때문에 속도측에서도 final을 사용한 것이 2.5배나 더 빨랐다. [참고자료]
 
 

파이널 단점

 
사실 나 역시 final을 이렇게 까지 덕지덕지 붙이는게 맞나 의구심을 품었던 적이 있다.
 

자동차 미션 pr 중 리뷰어에게 남긴 질문

 
그리고 체스 미션 리뷰중 final이 너무 많은데, 보일러 플레이트 코드인가? 라고 질문을 남겨주기도 하였다.
 

체스 미션 리뷰

 
가독성을 떨어뜨린다는게 final의 단점이 될 수 있다. 나 역시도 그렇게 생각했었는데 이제는 오히려 final이 없으면 허전하다. 스프링 들어가게 되면 파라미터에 어노테이션 잔뜩 붙게 될텐데 그 어노테이션들이 가독성을 떨어뜨린다고 사용하지 않는건 아니잔슴?
 
또 보일러 플레이트 코드로 볼 수 있지 않냐는 의견을 주었는데, 이제 이건 자바가 잘못했다고 본다.
스칼라를 보아라. final 붙이지 않아도 default가 final이다. [참고영상]
 
getter, toString, hashCode, equals와 같은 보일러 플레이트 코드를 없애기 위해 record가 등장했던 것처럼 모든 변수의 default가 final인 클래스가 등장하길 기대해봐야겠다. 
 
아니지 절이 싫으면 중이 떠나라했던가...
 
 

결론

단점보다 장점이 더 크다고 생각하여 final을 사용하기로 했다.
만약 다 붙어있는데 하나 실수로 붙이지 않는다면 어? 이 값은 재할당 해도 괜찮은가보네? 하고 큰일이 일어날 수 있기 때문에 한번 붙이기로 했으면 필사적으로 붙이는게 맞다고 본다. (글쓰면서 체스 코드 다시 살펴보는데 final 놓친 부분이 보인다... 흑흑 레벨 2에선 더 신경써야지)
 
정리해보자면 final을 붙임으로써 이후에 내 코드를 볼 사람에게 이 값을 재할당하지 말라는 의미를 전달할 수 있고 또 컴파일러에게도 이 값은 재할당 되지 않을거야라고 전해줄 수 있다. 
 
 
변수에 final을 사용하지 않았던 분들이 이 글을 보고 사용해보았으면 하는 바람이다.
 

 
 


참고 자료

Optimize Final Field Loads In Generated Code
final vs immutability java
java-final-performance
final 키워드의 장단점
불변 객체 및 final을 사용해야 하는 이유
Java: 그가 final로 도배 하는 이유 / 컴파일러 너 내 String 어떻게 했어?!