Java final과 불변 객체

MinSeong Kang·2022년 7월 27일
0

java

목록 보기
1/5

스프링과 자바 공부를 하면서 final 키워드를 자주 쓰곤 했다. 정확한 용도를 알지 못하고 final과 Lombok의 @RequiredArgsConstructor과 습관적으로 사용하였다. 이번 포스팅을 통해 final의 정확한 용도에 대해서 공부하려 한다.

불변 객체

불변 객체란 객체의 내부 값을 수정할 수 없는 클래스를 말한다. 객체 생성 이후부터 객체가 없어질 때까지 절대 달라지지 않는다. 따라서 가변 객체보다 오류가 생길 여지가 적으며, 설계하고 구현하기 쉽다. Java의 대표적인 불변 객체로는 String, BigInteger, BigDecimal 등이 있다.

불변 객체의 장점

  • Thread-safe하여, 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다

멀티 스레드 환경에서 동기화 문제는 공유 자원에 여러 스레드가 동시에 쓰기 연산을 하기 때문에 필요로 하다. 하지만 공유 자원이 불변 객체라면, 항상 같은 값만 반환하고 쓰기 연산이 불가능하므로 동기화를 고려할 필요가 없다. 따라서 안정성 뿐만 아니라 동기화를 하지 않음으로써 성능 상의 이점도 준다.

  • 실패 원자적인 메서드를 만들 수 있다.

가변 객체의 경우 어떠한 작업 도중 예외가 발생하면 해당 객체가 불안정한 상태에 빠질 수 있다. 가변 객체의 변경된 상태로 인해 새로운 에러를 유발할 수도 있다. 하지만 불변 객체의 경우 어떠한 예외가 발생하더라도 예외 발생 전과 동일한 상태를 유지하기 때문에 추가적인 에러를 방지할 수 있다.

  • Cache, Map, Set 등의 요소로 활용하기에 적합하다.

Cache, Map, Set 등의 원소가 가변 객체라 변경되었다면 이를 갱신하는 등의 작업이 필요하다. 하지만 불변 객체라면 한 번 데이터가 저장된 이후에 다른 부가 작업을 고려하지 않아도 된다.

  • 부수 효과를 피해 오류 가능성을 최소화할 수 있다.

여기서 부수 효과는 변수의 값이나 상태 등의 변화로 인해 발생하는 효과를 말한다. 만약 객체의 수정자(Setter)를 통해 여러 객체들에서 값을 변경한다면 객체의 상태를 예측하기 어려울 것이다. 바뀐 상태를 파악하는 일도 복잡할 것이며, 이는 유지 보수성을 떨어뜨린다. 불변 객체의 경우 기본적으로 변경 가능성이 적고, 객체의 생성과 사용이 제한되어 있다. 따라서 다른 객체에서 호출하더라도 불변 객체의 상태가 유지되기 때문에 안전하게 사용가능하다. 이는 오류를 줄여주고 유지 보수성이 높은 코드를 작성하도록 도와줄 것이다.

- 다른 사람이 작성한 함수를 예측 가능하며 안전하게 사용할 수 있다.

대부분 개발은 혼자가 아닌 여러 사람들과 협업하며 이루어진다. 불변성이 보장된 객체라면, 다른 사람이 작성한 객체를 위험 부담없이 사용할 수 있다. 협업시 불안없이 다른 사람의 코드를 이용할 수 있는 것은 불필요한 시간을 절약할 수 있다.

- 가비지 컬렉션의 성능을 높일 수 있다.

Java에서는 final 키워드를 사용하여 불변의 객체를 생성할 수 있다. 이렇게 객체를 생성하기 위해서는 객체를 가지는 또 다른 컨테이너 객체(ImmutableHolder)도 존재한다는 것인데, 당연히 불변의 객체가 먼저 생성되어야 컨테이너 객체가 이를 참조할 수 있다. 즉 컨테이너는 컨테이너가 참조하는 가장 젊은 객체들보다 더 젊다는 것이다. 따라서 컨테이너 객체가 살아있다는 것은 하위의 불변 객체들이 처음에 할당된 상태로 참조되고 있음을 의미하기 때문에, GC는 컨테이너 객체 하위의 불변 객체들을 Skip할 수 있다.

Java에서 불변 객체를 생성하는 방법

final 키워드

Java에서 변수들은 기본적으로 가변적이며, final 키워드를 붙여 참조값을 변경 못하도록 하여 불변성을 확보할 수 있다. final이 붙은 변수의 값을 변경하려고 할 때 컴파일러 에러가 발생한다.

final long id = 0L;
id = 1L; // compile error

하지만 final 키워드가 내부의 객체 상태를 변경하지 못하도록 하는 것은 아니다. 아래와 같이 final로 선언된 List에는 새로운 객체가 더해져도 문제가 없다. 이를 방지하기 위해서는 불변 클래스로 만들어야한다.

final List<String> strList = new ArrayList<>();
strList.add("A");

불변 클래스 규칙

  • 클래스를 final로 선언하라
  • 모든 클래스 변수를 private과 final로 선언하라
  • 객체를 생성하기 위한 생성자 또는 정적 팩토리 메서드를 추가하라
  • 참조에 의해 변경가능성이 있는 경우 방어적 복사를 이용하여 전달하라

방어적 복사

생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화하거나, getter 메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것이다. 방어적 복사를 사용할 경우, 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.

*주의 : 방어적 복사는 깊은 복사가 아니다!! 컬렉션의 주소는 다르지만, 내부 요소들의 주소는 공유하기 때문에 원본의 내부 요소를 변경하면 복사본도 바뀐다.
→ 내부 요소도 불변으로 만들기 위해서는 Unmodifiable Collection 사용

참고문헌

이펙티브 자바 Effective Java 3/E
https://mangkyu.tistory.com/131
https://tecoble.techcourse.co.kr/post/2021-04-26-defensive-copy-vs-unmodifiable/

0개의 댓글