2. 불변 객체

기본형과 참조형의 공유

자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type) 과 참조형(Reference Type) 으로 나눌 수 있다.

  • 기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.

    • 예를 들어, b = a 라는 기본형 타입 연산이 있다고 하면, 자바는 항상 a 의 값을 복사해서 대입한다.

    • 결과적으로 a,b 모두 같은 숫자값을 가지지만, a 가 가진 숫자값과 b가 가진 숫자값은 완전히 다른 값이다.

      • 여기서 다르다는 의미는 a, b 는 아예 다른 메모리 공간에 저장되는 공간이 다르다는 의미로 해석한다.

int a = 10;
int b = a;

b = 20;

System.out.println(a); // 👉 10
System.out.println(b); // 👉 20
  • 참조형 : 하나의 객체의 참조값을 통해 여러 변수에서 공유할 수 있다.

공유 참조와 사이드 이펙트

사이드 이펙트(Side Effect) 는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.

다음의 코드는 사이드 이펙트가 발생할 수 있다.

  • 여러 변수에서 같은 레퍼런스 값을 공유하는 경우 사이드이펙트가 발생할 수 있다.

사이드 이펙트는 본래 부정적인 의미로 사용되는데, 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.

사이드 이팩트 해결 방안

해결방안은 생각보다 간단하다 a, b 가 각각 인스턴스를 생성하면 된다.

그러나 여기서 문제가 발생하는데, 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것이다. (문법적으로 틀린 것이 없다! -> 컴파일 에러 발생 X)

불변 객체 - 도입

지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제이다. 하지만 앞서 살펴보았듯이 레퍼런스의 공유를 막을 수 있는 방법은 없다.

그런데 사이드 이펙트의 더 근본적인 원인은 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지는 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.

진짜 문제는 이후에 b 가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생한다.

불변 객체 도입

객체의 상태(객체 내부의 값, 필드, 멤버 변수) 가 변하지 않는 객체를 불변 객체(Immutable Object) 라고 한다. 앞서 사용했던 Address 클래스를 불변 상태로 다시 만들어보자.

  • 내부 값이 변경되면 안된다. 따라서 value 의 필드를 final 로 선언했다.

  • 값을 변경할 수 있는 setValue() 를 제거했다.

  • 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.

불변 클래스를 만드는 방법은 아주 단순하다. 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.

정리

불변이라는 단순한 제약을 사용해서 사이드 이팩트라는 큰 문제를 막을 수 있다.

  • 객체의 공유 참조를 막을 수 없다. 그래서 객체의 값을 변경하는 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이팩트가 발생한다. 사이드 이팩트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다.

  • 불변 객체는 값을 변경할수 없다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 한다. 이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않는다.

불변 객체 - 값 변경

불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요할 때가 있다.

예를 들어서 기존 값에 새로운 값을 더하는 add() 와 같은 메서드가 있다.

이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있다.

정리

지금까지 왜 이렇게 불변 객체 이야기를 많이 했을까?

자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer, LocalDate 등 수 많은 클래스가 불변으로 설계되어 있다. 따라서 불변 객체가필요한 이유와 원리를 제대로 이해해야, 이런 기본 클래스들도 제대로 이해할 수 있다.

모든 클래스를 불변으로 만드는 것은 아니다.

우리가 만드는 대부분의 클래스는 값을 변경할 수 있게 만들어진다. 예를 들어서 회원 클래스의 경우 회원의 여러 속성 을 변경할 수 있어야 한다. 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있다.

✅ 불변 객체(Immutable Object)를 사용해야 하는 이유

1. 🔒 스레드 안전(Thread-Safe)하게 만들 수 있다.

  • 불변 객체는 상태가 바뀌지 않기 때문에 락 없이도 여러 스레드에서 안전하게 공유 가능하다.

  • 멀티스레드 환경에서도 값이 변하지 않으므로 경합 조건(race condition) 이 발생하지 않는다 .

2. 🔍 상태 추적과 디버깅이 쉬워진다.

  • 객체의 상태가 한 번 정해지면 바뀌지 않으므로, "언제, 어디서 바뀌었는지" 고민할 필요가 없다.

  • 이는 디버깅, 테스트, 로직 추적을 단순하게 만들며 유지 보수성을 높인다.

  • 특히 멀티스레딩 같이 디버깅 하기 힘든 환경에서 디버깅이 쉬워진다는 의미는 크다!

3. 🚫 사이드 이펙트(side effect) 없는 메서드 구성이 가능하다.

  • 불변 객체는 상태 변경이 없으므로, 모든 메서드가 순수 함수처럼 동작한다.

순수 함수란?

  • 동일한 입력이 들어오면 항상 동일한 출력을 반환하고,

  • 사이드 이펙트가 없는 함수를 말한다.

  • 불변 객체는순서 함수를 가능하게 만들고,

  • 순수 함수는 자연스레 불변 객체를 반환한다.

  • 사이드 이펙트가 없기 때문에, 테스트가 쉽고 예측이 가능하다.

4. 🧩 컬렉션(HashMap, HashSet 등)에서 안전하게 사용할 수 있다.

  • 이건 실무에서 자주 발생할 수 있는 문제이다..

  • 해시 기반 컬렉션(HashMap, HashSert) 은 내부적으로 hashCode(), equals() 값을 사용하여 데이터를 저장한다.

  • 그런데 key 로 들어간 객체가 나중에 내부 상태를 바꾼다면?

    • 컬렉션 내부의 해시값은 갱신되지 않기 때문에, contains(), remove() 같은 메서드가 동작하지 않게 된다..

  • 🔒 불변 객체를 사용하면 이런 문제가 발생하지 않습니다.

    • 컬렉션에서 키로 사용되는 객체는 반드시 불변이거나 hashCode()와 관련 없는 필드만 수정해야 합니다.

Last updated