불변 객체
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
기본형과 참조형의 공유
- 기본형: 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
- 참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
공유 참조와 사이드 이펙트
사이드 이펙트(Side Effect)는 프로그래밍이 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말함
예제
public class RefMain1_1 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산"); // b의 값을 부산으로 변경해야 함
System.out.println("부산 -> b");
System.out.println("a = " + a); // 사이드 이펙트 발생
System.out.println("b = " + b);
}
}
- b의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했다.
- a, b는 같은 인스턴스를 참조하기 때문에 a의 값도 함께 부산으로 변경된다.
프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생하고, 이로인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.
사이드 이펙트 해결 방안
a와 b가 처음부터 서로 다른 인스턴스를 참조하도록 하면 된다.
public class RefMain1_2 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
Address a = new Address("서울"); //x001
Address b = new Address("서울"); //x002
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산"); // b의 값을 부산으로 변경해야 함
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
- a와 b가 서로 다른 인스턴스를 참조하기 때문에 b의 값을 변경해도 a의 값은 변하지 않는다.
여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다
사이드 이펙트는 같은 객체(인스턴스)를 변수 a, b가 함께 공유하기 때문에 발생하는데, 객체를 공유하지 않는 것으로 문제를 해결할 수 있다.
하지만 자바 문법상 참조값의 공유를 막을 수 있는 방법이 없다.
Address a = new Address("서울");
Address b = a;
- 위 코드는 자바 문법 상 아무런 문제가 없기 때문에 개발 도중 개발자가 실수할 수 있다.
예제
public class RefMain1_3 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
Address a = new Address("서울");
Address b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
change(b, "부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
private static void change(Address address, String changeAddress) {
System.out.println("주소 값을 변경합니다 -> " + changeAddress);
address.setValue(changeAddress);
}
}
- change() 메서드만 하나 추가 된 코드이며, change() 메서드에서 Address 인스턴스에 있는 value 값을 변경한다.
- main() 메서드만 보면 a의 값이 함께 부산으로 변경된 이유를 더 찾기 어려움
불변 객체
사이드 이펙트 등의 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.
- 참조형 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었다.
불변 객체 도입
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
- 내부 값이 변경되면 안되므로 value의 필드를 final로 선언
- 값을 변경할 수 있는 setValue() 제거
- 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.
불변 객체는 필드 값을 변경할 수 없도록 클래스를 설계하면 된다.
예제 - 불변 객체 값 변경
public class RefMain2 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
//b.setValue("부산"); // b의 값을 부산으로 변경해야 함
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
- ImmutableAddress는 불변 객체이므로 값을 변경할 수 잇는 setValue() 메서드가 제거 됨
- 생성된 불변 객체의 값을 변경할 수 있는 방법은 없다
- b의 값을 변경하기 위해서는 새로운 ImmutableAddress 객체를 생성해서 b에 대입해야 된다
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 해결할 수 있다.
- 객체의 공유 참조는 막을 수 없고, 따라서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생한다.
- 사이드 이펙트가 발생하면 안 되는 상황에는 불변 객체를 만들어 사용하면 된다.
- 불변 객체는 값을 변경할 수 없으므로 값을 변경하고 싶으면 변경하려는 값으로 새로운 불변 객체를 생성해야 한다.
- 이 경우 기존 변수들이 참조하는 값에는 영향이 없다.
가변(Mutable) 객체 vs 불변(Immutable) 객체
- 가변 : 처음 만든 이후 상태가 변할 수 있다.
- 불변 : 처음 만든 이후 상태가 변하지 않는다.
심화 예제 - 불변 객체 사용
public class MemberV2 {
private String name;
private ImmutableAddress address;
public MemberV2(String name, ImmutableAddress address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("회원A", address);
MemberV2 memberB = new MemberV2("회원B", address);
//회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
//회원B의 주소를 부산으로 변경해야 함
//memberB.getAddress().setValue("부산");
memberB.setAddress(new ImmutableAddress("부산"));
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
- 회원의 주소를 중간에 변경해야 할 경우 ImmutableAddress를 사용해 새로운 객체를 만들어서 전달하는 방식으로 주소를 바꿀 수 있다.
- ImmutableAddress는 불변 객체이므로 사이드 이펙트 문제가 발생하지 않는다.
불변 객체 - 값 변경
package lang.immutable.change;
public class ImmutableObj {
private final int value;
public ImmutableObj(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public ImmutableObj add(int addValue) {
int result = value + addValue;
return new ImmutableObj(result);
}
}
package lang.immutable.change;
public class ImmutableMain {
public static void main(String[] args) {
ImmutableObj obj1 = new ImmutableObj(10);
ImmutableObj obj2 = obj1.add(20);
//계산 이후에도 기존값과 신규값 모두 확인 가능
System.out.println("obj1 = " + obj1.getValue());
System.out.println("obj2 = " + obj2.getValue());
}
}
- 불변 객체는 값이 변경되면 안되므로, 기존 값을 변경하지 않고 계산 결과를 바탕으로 새로운 객체를 만들어서 반환하는 것으로 불변 객체의 값을 변경한다.
- 위 코드에서 새로 생성된 반환 값을 사용하지 않으면 아무것도 처리되지 않은 것처럼 보인다.
- 불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 반환 값을 받아야 한다.
정리
모든 클래스를 불변으로 만드는 것은 아니다.
우리가 만드는 대부분의 클래스는 값을 변경할 수 있게 만들어진다. 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에만 만들어서 사용한다.
때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있다.
클래스를 불변으로 설계하는 이유
- 캐시 안정성
- 멀티 쓰레드 안정성
- Entity의 값 타입
'항해 99 > Java' 카테고리의 다른 글
Java - Wrapper, Class, System, Math, Random (0) | 2024.06.13 |
---|---|
String 클래스 (0) | 2024.06.05 |
Java - Object (0) | 2024.05.29 |
정렬 알고리즘 (0) | 2024.05.09 |
Array & LinkedList (0) | 2024.05.03 |