객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
한마디로, 위의 그림과 같이 객체의 내부를 변경 못하도록 막는 것이다.
public class RefMain2 {
public static void main(String[] args) {
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; //참조값 대입을 막고 싶은데 방법이 없다!
//b.setValue("부산"); //컴파일 오류 발생
//b에 a를 대입하는 것을 막을 수 있는 방법이 없으니,
//b에 a를 대입하되 값이 변경될 수 없도록 불변객체로 만드는 것이다.
}
이게 무슨말인지 왜 필요한지 차근차근 알아보자.
자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
- 기본형: 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
- 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
여기서 '공유'란 무엇인가?
a에서 변경을 하면 b도 함께 변경되고 b에서 변경하면 a도 변경되는 것을 말한다.
이때 a와 b는 서로 값을 '공유'하고 있다고 한다.
기본형 타입과 참조형 타입의 '공유'에 대해 알아보자.
기본형 타입 변수는 값을 복사할 뿐 공유하지 않는다.
public class PrimitiveMain {
public static void main(String[] args) {
int a = 10;
int b = a;
// a -> b, 값 복사 후 대입(기본형은 절대로 같은 값을 공유하지 않는다.)
System.out.println(a); //10
System.out.println(b); //10
b = 20;
System.out.println(a); //10
System.out.println(b); //20
}
}
b를 20으로 변경해도, a는 여전히 10이 출력되는 것을 볼 수 있다.
a와 b가 같은 값을 공유하는 것이 아닌, a의 값을 b에 복사하여 대입했기 때문이다.
참조형 변수는 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
public class RefMain1 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
Address a = new Address("서울");
Address b = a;
b.setValue("부산"); //b의 값을 부산으로 변경
System.out.println(a); //출력: 부산 (사이드 이펙트 발생)
System.out.println(b); //출력: 부산
}
}
1) 처음에는 a, b 둘다 서울이라는 주소를 가져야 한다고 가정하자.
2) 이후 b를 부산으로 변경한다.
3) 실행결과를 보면 b뿐만 아니라 a의 주소도 함께 부산으로 변경되어 버린다.
같은 주소값(인스턴스)을 참조하기 때문이다.
b=a라고 하면 a에 있는 참조값 x001을 복사해서 b에 전달한다.
참조값을 복사해서 전달하므로 결과적으로 a, b는 같은 x001 인스턴스를 참조한다.
참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.
여기서 b의 주소만 부산으로 변경했는데, a의 주소도 함께 변경되는 것을 사이드 이펙트라 한다.
개발자에게 내가 의도하지 않은 무언가가 나도 모르게 뒤에서 돌아가고 있는것만큼 무서운 것은 없다..
사이드 이펙트를 해결하려면 어떻게 해야할까?
위와 같은 경우에서는 a와 b가 서로 다른 인스턴스를 참조하도록 하면 된다.
Address a = new Address("서울");
Address b = new Address("서울")
위와 같이 여러 변수가 하나의 참조값을 공유하지 않으면 문제가 해결될 것 같다.
하지만, 개발자가 실수로 하나의 참조값을 공유하도록 코드를 작성한다면 문제가 발생할 것이다.
애초부터 b=a를 막는 방법은 없을까?
공유하면 안되는 인스턴스는 공유를 못하도록 막는 방법말이다.
슬프게도 그런 방법은 없다.
b=a로 하나의 인스턴스를 공유하게 코드를 작성해도 자바 문법상 아무런 오류가 나지 않는다. 막는 특별한 문법 또한 없다.
참조형 변수의 대입은 그 자체로 아무런 문제가 없기 때문이다.
실제로는 훨씬 더 복잡한 상황에서 이런 문제가 발생한다.
구조와 코드가 복잡할수록 사이드 이펙트는 원인을 찾기도 디버깅 하기도 어려워진다.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제이다.
하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지는 않는다.
문제는 바로 공유된 객체의 값을 변경한 것에 있다.
객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것이다.
이때 필요한 것이 바로 불변객체이다.
불변객체로 만드는 예시를 살펴보자.
▶ 기존 코드
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public void setValue(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
▶ 불변객체 코드
1. 내부 값이 변경되면 안되기 때문에 value의 필드를 final로 선언.
2. 값을 변경할 수 있는 setValue() 제거.
3. 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후 값을 변경하는 것이 불가능.
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}
코드의 목적
: ImmutableAddress를 사용하는 개발자가 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 것을 알고(b.setValue("부산")불가능) 이 객체가 불변객체인 것을 깨닫는 것.
따라서, 어쩔 수 없이 새로운 인스턴스를 생성하는 것!
객체의 공유 참조는 막을 수 없다. 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생한다. 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다. 불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단된다.
불변 객체는 값을 변경할 수 없다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 한다. 이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않는다.
정말 개발하면서 제일 무서운 것은.. 내가 수정한 코드에 사이드 이펙트가 발생해서 나도 모르게! 뒤에서 의도하지 않은 무언가가 변경되는 것이다..
그리고 개발하면서 제일 중요한 것은 인간을 믿지 말고 시스템을 믿자는 것이다^^! 실수하지 않기를 바라지말고 시스템에서 막아야 한다.
이는 개발자든 사용자든 모두에게 해당되는 말이다★
김영한의 java 중급1 - 불변객체