불변객체
기본형과 참조형의 공유
자바의 데이터 타입을 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
참조형 : 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
public class PrimitiveMain {
public static void main(String[] args) {
int a = 10;
int b = a;
System.out.println("a = " + a);
System.out.println("b = " + b);
b = 20;
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
a와 b는 절대로 하나의 값을 공유하지 않는다.
b = a라고 해도 자바는 항상 값을 복사해서 대입한다. 이 경우 a 에 있는 값 10을 복사해서 b에 전달한다.
결과적으로 a와 b는 둘 다 10 이라는 똑같은 숫자의 값을 가진다. 하지만 a와 b가 가지는 10은 완전히 다른 10이다.
public class Address {
private String value;
public Address(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return "Address{" +
"value='" + value + '\'' +
'}';
}
}
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("부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
위의 코드를 보면 a,b둘 다 서울이라는 주소를 가져야한다고 가정하자.
Address b = a코드를 작성했고 b만 부산으로 바꿨다고 생각했지만 a의 값도 변경이 되었다.
왜 그럴까?
a와 b는 같은 참조값을 가지기 때문에 a도 부산으로 변경 된 것이다.
참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있다.
b = a 라고 하면 a에 있는 참조값 x001을 b에 전달한다.
참조값을 복사해서 전달하므로 결과정으로 a, b는 같은 x001인스턴스를 참조하게 된다.
*참조형 변수는 참조값을 통해 같은 객체(인스턴스)를 공유할 수 있다.
**사이드 이펙트(Side Effect) : 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.
위의 코드처럼 개발자는 b의 값만 부산으로 변경하려고 했으나 a의 값도 함께 부산으로 변경되어버린 것처럼
주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라 한다. 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생한다. 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.
위의 문제를 어떻게 해결해야할까?
그냥 객체 하나를 더 만들어 버리면 그만이다. a와 b가 처음부터 서로 다른 인스턴스를 참조하면 된다.
public class RefMain1_2 {
public static void main(String[] args) {
//참조형 변수는 하나의 인스턴스를 공유할 수 있다.
Address a = new Address("서울");
Address b = new Address("서울");
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("부산");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
하지만 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 따로 없다.
Address a = new Address("서울");
Address b = a;
이런 경우 자바의 문법적으로는 틀린 부분이 없기 때문에 에러가 아니다.
객체의 공유가 꼭 필요할 때도 있지만, 때로는 공유하는 것이 지금과 같은 사이드 이펙트를 만드는 경우도 있다.
이런 문제를 어떻게 해결해야할까??
단순히 개발자가 조심하며 개발해야할까??
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제이다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 사이드 이펙트가 발생하지 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.
Address a = new Address("서울");
Address b = a;
위의 코드가
Address a = new Address("서울");
Address b = new Address("서울");
이 코드보다 메모리와 성능상 더 효율적이다. 인스턴스가 하나이기 때문에 메모리가 절약되고 성능 향상이 되기 때문이다.
자바에서 여러 참조형 변수가 하나의 객체를 참조하는 공유 참조 문제는 피할 수 없다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 변경했기 때문이다.
이를 위해 불변 객체가 도입되었다.
불변 객체 : 객체의 상태가 변하지 않는 객체를 불변 객체(Immutable Object)라고 한다. 앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어 보자
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 + '\'' +
'}';
}
}
Address 클래스를 ImmutableAddress로 만들었다.
value를 final로 바꾸어서 생성자에서 초기화 하면 더이상 value를 변경할 수 없고
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("부산"); setValue 메서드 자체가 없고, value가 final이므로 변경이 불가함
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
ImmutableAddress를 사용함으로써 value 값의 변경이 불가능해졌다. 따라서 개발자는 에러를 확인하고 새로운 인스턴스를 생성하여 부산으로 변경하는 방법을 모색할 것이다.
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다. 불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단된다.
Mutable Object vs Immutable Object
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻이다.
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻이다.
Address는 가변 클래스이다. 이 클래스로 객체를 생성하면 가변 객체가 된다.
ImmutableAddress는 불변 클래스이다. 이 클래스로 객체를 생성하면 불변 객체가 된다.
불변 객체에서 값을 변경해야하는 경우가 있다면 어떻게 해야할까?
예를 들어 기존 값에 새로운 값을 더하는 add()와 같은 메서드가 있다고 하자.
public class MutableObj {
private int value;
public MutableObj(int value) {
this.value = value;
}
public void add(int addValue){
value = value + addValue;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class MutableMain {
public static void main(String[] args) {
MutableObj mutableObj = new MutableObj(10);
mutableObj.add(20);
//계산 이후의 기존 값은 사라진다.
System.out.println("mutableObj = " + mutableObj.getValue());
}
}
지금 코드를 보면 계산 이후의 기존 10의 값은 사라지고 30이 되는 메서드이다.
이번에는 불변 객체에서 add()메서드를 어떻게 구현하는지 알아보자.
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);
}
}
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());
}
}
불변객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있다.
참고 - withXxx()
부랴벼ㅛㄴ 객체에서 값을 변경하는 경우 withYear()처럼 with로 시작하는 경우가 많다.
불변 객체의 메서드가 with으로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다.
정리하자면 with는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.
'공부 > Java' 카테고리의 다른 글
자바 열거형 ENUM 복습하기 (0) | 2024.09.20 |
---|---|
자바 String 클래스 복습하기 (1) | 2024.09.19 |
자바 Object 클래스 복습하기 (1) | 2024.09.18 |
자바 예외처리 2 (5) | 2024.09.16 |
자바 예외처리 1 (0) | 2024.09.15 |