불변객체
◎ 기본형과 참조형
-> 자바의 기본적인 데이터 타입에는 기본형과 참조형이 있다.
-> 기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
public class PrimitiveMain {
public static void main(String[] args) {
// 기본형은 같은 값을 공유하지 않는다.
int a = 10;
int b = a; // a에 저장된 값을 복사 후에 b에 저장
System.out.println("a = " + a);
System.out.println("b = " + b);
System.out.println();
b = 20;
System.out.println("b = 20으로 변경");
System.out.println("a = " + a);;
System.out.println("b = " + b);;
}
}
-> 결과
-> 기본형 변수 a와 b는 하나의 값을 공유하지 않는다.
-> int b = a; 에서 a에 있는 값 10을 복사하여 변수 b에 저장한다. a와 b는 각각 별도의 변수로 단지 두 변수 모두 10이라는 값을 가지고 있을 뿐이다.
-> 따라서 b = 20;으로 b의 값을 변경해도 a와는 별도의 값을 가지고 있는 것이기 때문에 b의 값만 변경된다.
-> 참조형 : 하나의 객체를 참조값을 가지고 해당 참조값을 저장하고 있는 여러 변수에서 공유할 수 있다.
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; // a와 b는 같은 참조값을 저장한다.
System.out.println("a = " + a);
System.out.println("b = " + b);
b.setValue("인천");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
-> 결과
-> a와 b는 같은 주소를 가지고 있다. 따라서 b.setValue()를 통해 value의 값을 변경하면, a의 value값도 변경된다.
◎ 공유 참조와 사이드 이펙트
-> 사이드 이펙트는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 의미한다.
-> 위에 있는 참조형 변수 예시에서 원래 의도는 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);
}
}
-> 결과
-> 이제 a와 b는 서로 다른 인스턴스를 참조하고 있기 때문에 b.setValue()를 통해 값을 변경하면 b의 값만 변경된다.
-> 이렇게 각기 다른 인스턴스를 생성하여 참조하도록 하면 문제가 되지 않는다. 하지만 애초에 자바에서는 여러 변수가 하나의 객체를 공유하는 것 자체는 문제가 되지 않기 때문에 실수로 하나의 객체에 여러 변수가 공유하도록 코드를 작성해도 실행할 때 까지 문제를 발견하기 어렵다. 자바에서는 하나의 객체를 여러 변수가 공유하는 것은 문법상 정상이다.
◎ 불변 객체
-> Address 객체를 여러 변수에서 공유했을 떄 문제가 되는 부분은 Address 객체의 값을 어디선가 변경했다는 것이다.
-> 만약 Address 객체의 값을 변경하지 못하게 한다면 사이드 이펙트가 발생하지 않을 것이다.
-> 이 때 객체의 상태가 변하지 않는 불변 객체(Immutable Object)를 이용하여 문제를 해결할 수 있다.
public class ImmutableAddress {
private final String value; // final로 선언
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return "ImmutableAddress{" +
"value='" + value + '\'' +
'}';
}
}
-> value를 final로 설정하여 생성자를 통해서만 값을 설정하고, 이후에는 값을 변경하지 못하도록 했다.
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 = new ImmutableAddress("인천"); // 새로운 객체를 만들어 새로운 객체의 참조값을 b에 저장
System.out.println("인천 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
-> 결과
-> ImmutableAddress에서 value의 값을 변경하는 setValue()자체를 없애 ImmutalbeAddress 인스턴스의 값을 변경할 수 있는 방법이 없다.
-> 만약 b.setValue()로 값을 변경하려 한다면 컴파일 오류가 발생하여 개발자는 오류를 인지할 수 있다.
-> b는 새로운 ImmutableAddress 인스턴스를 생성하여 참조값을 저장한다. 이에 따라 a와 b는 서로 다른 인스턴스를 참조하게 된다.
* 가변(Mutable) 객체 & 불변(Immutable) 객체
가변 객체는 처음 만든 이후 상태가 변할 수 있는 객체다.
불변 객체는 처음 만든 이후 상태가 변하지 않는 객체다.
위에서 봤던 예제를 기준으로 Address 클래스는 가변 객체, Immutable 클래스는 불변 객체다.
-> 가변 객체 예제
public class MemberV1 {
private String name;
private Address address; // 가변 객체 사용
public MemberV1(String name, Address address) {
this.name = name;
this.address = address;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV1{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("memberA", address);
MemberV1 memberB = new MemberV1("memberB", address);
// memberA, memberB 첫 주소는 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
memberB.getAddress().setValue("인천");
System.out.println("인천 -> memberB.setAddress");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
-> 결과
-> 위 코드는 memberA와 memberB 모두 같은 Address 인스턴스를 참조하기 때문에 둘 중 하나만 setValue()를 통해 값을 바꿔도 둘다 변경된다.
-> 불변 객체 예제
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 ImmutableAddress getAddress() {
return address;
}
public void setAddress(ImmutableAddress address) {
this.address = address;
}
@Override
public String toString() {
return "MemberV2{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
public class MemberMainV2 {
public static void main(String[] args) {
ImmutableAddress address = new ImmutableAddress("서울");
MemberV2 memberA = new MemberV2("memberA", address);
MemberV2 memberB = new MemberV2("memberB", address);
// memberA, memberB 첫 주소는 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// memberB.getAddress().setValue("인천"); -> 컴파일 에러
memberB.setAddress(new ImmutableAddress("인천"));
System.out.println("인천 -> memberB.setAddress");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
-> 결과
-> memberB가 setValue()를 사용하면 컴파일 에러가 발생한다. setAddress()를 통해 새로운 ImmutableAddress를 생성해 해당 객체의 값에 변경된 값을 넣는다.
-> memberA와 memberB는 이제 서로 다른 ImmutableAddress 인스턴스를 참조한다.
-> 다음은 불변 객체를 사용하면서 값을 변경해야 하는 경우에 대한 예시이다.
-> 먼저 가변 객체에서의 예시다.
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 obj = new MutableObj(7);
obj.add(20);
// 계산 이후 기존 값은 사라진다.
System.out.println("obj = " + obj.getValue());
}
}
-> 결과
-> MutableObj를 7이라는 값으로 생성했다. 이후 add메서드를 이용해 20을 추가하면 기존 value에 value + 20이 되어 value에는 27이라는 값이 들어간다.
-> 다음은 불변 객체를 이용한 값 변경 예시다.
public class ImmutableObj { // 불변 객체
private final int value;
public ImmutableObj(int value) {
this.value = value;
}
public ImmutableObj add(int addValue) {
int result = value + addValue; // 새로운 필드 값에 계산 결과를 저장 따라서 변경될 수 없는 value를 건드리지 않고 계산을 할 수 있다.
return new ImmutableObj(result);
}
public int getValue() {
return value;
}
}
-> add메서드의 리턴타입을 ImmutableObj로 하여 연산 결과를 리턴할 때 새로운 ImmutableObj 객체를 생성하도록 했다. 또한 value를 final값을 만들고, add 메서드 연산을 할 때 기본 int형 result를 새로 생성하여 여기에 연산 결과를 저장하였다.
public class ImmutableMain {
public static void main(String[] args) {
ImmutableObj obj1 = new ImmutableObj(7);
ImmutableObj obj2 = obj1.add(20);
System.out.println("obj1 = " + obj1.getValue());
System.out.println("obj2 = " + obj2.getValue());
}
}
-> 결과
-> ImmutableObj에서 value를 final로 하고, add연산을 할 때 value에 변경 값을 넣지 않고 단지 연산을 할 때 사용만 했다.
-> 연산 결과는 새 변수를 생성하여 거기에 담고 이 값을 새로운 ImmutableObj객체에 담아 리턴을 했다.
-> 따라서 obj1과 obj2는 서로 다른 인스턴스를 참조하게 되어 obj1은 기존의 값을 유지하고, obj2에는 새로운 값이 들어가게 된다.
★ 참고 및 출처