[Java] 김영한의 실전 자바 - 중급편 섹션2 불변 객체
○ 기본형과 참조형의 공유
기본형: 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
참조형: 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
기본형 예제
package lang.immutable.address;
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);
b = 20;
System.out.println("20 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
a = 10
b = 10
20 -> b
a = 10
b = 20
참조형 예제
package lang.immutable.address;
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 "Address{" +
"value='" + value + '\'' +
'}';
}
}
package lang.immutable.address;
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);
}
}
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='부산'}
b = Address{value='부산'}
참조형 변수들은 같은 참조값을 통해 같은 인스턴스를 참조할 수 있다.
참조 값을 복사해서 전달하므로 결과적으로 a,b는 같은 인스턴스(x001)를 참조한다.
○ 공유 참조와 사이드 이펙트
사이드 이펙트(Side Effect): 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것
b.setValue("부산"); // b의 값을 부산으로 변경해야 함
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);
위에서 b의 주소값을 (서울→부산) 바꾸려했지만 a,b가 같은 인스턴스를 참조해서 a의 값도 함께 부산으로 변경되었다.
이에 대한 해결방안은 아래처럼 서로 다른 인스턴스를 참조하는 것이다.
Address a = new Address("서울");
Address b = new Address("서울");
결국 사이드이펙트 문제는 객체를 공유하지 않으면 문제가 해결된다.이렇게 하려면 서로 다른 객체를 참조하면 된다.
하지만 여러 변수가 하나의 객체를 공유하는 것을 막을 방법은 없다.
그리고 이는 복잡한 상황에서 함께 값이 변경되었을때 원인을 찾기 어렵게 만든다.
○ 불변 객체 - 도입
사실 문제의 직접적인 원인은 객체를 공유하는 것이아니라, 공유된 객체의 값을 변경한 것에 있다.
즉, 진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생한다.
불변 개체를 도입하여 이러한 문제를 해결해보자.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
package lang.immutable.address;
public class ImmutableAddress {
private final 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 "Address{" +
"value='" + value + '\'' +
'}';
}
}
package lang.immutable.address;
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("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
}
}
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
value필드를 final로 선언후, 값을 변경할 수 있는 setValue()를 제거했다.
이렇게하면 후에 개발자는 값을 변경하려고 시도하다가, 값변경이 불가능하다는 것( setValue() 없음 )을 알고 객체가 불변 객체인 사실을 깨닫게 된다. 따라서 새 인스턴스를 생성해 b에 대입한다.
정리해보면, 불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.
사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다.
이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않는다.
○ 불변 객체 - 예제
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해보자.
package lang.immutable.address;
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 "Member{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
package lang.immutable.address;
public class MemberMainV1 {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A", address);
MemberV1 memberB = new MemberV1("회원B", address);
// 회원A, 회원B의 처음 주소는 모두 서울
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
// 회원B의 주소를 부산으로 변경해야함
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
memberA = Member{name='회원A', address=Address{value='서울'}}
memberB = Member{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = Member{name='회원A', address=Address{value='부산'}}
memberB = Member{name='회원B', address=Address{value='부산'}}
위를보면 회원A 와 회원B 는 같은 Address 인스턴스를 참조하고 있다.
회원B 의 주소를 부산으로 변경하는 순간 회원A 의 주소도 부산으로 변경된다.
불변 클래스를 사용해보자.
MemberV2 는 주소를 변경할 수 없는, 불변인 ImmutableAddress 를 사용한다.
private ImmutableAddress address;
package lang.immutable.address;
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 "Member{" +
"name='" + name + '\'' +
", address=" + address +
'}';
}
}
package lang.immutable.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);
}
}
memberA = Member{name='회원A', address=Address{value='서울'}}
memberB = Member{name='회원B', address=Address{value='서울'}}
부산 -> memberB.address
memberA = Member{name='회원A', address=Address{value='서울'}}
memberB = Member{name='회원B', address=Address{value='부산'}}
회원B 의 주소를 중간에 부산으로 변경하려고 시도한다. 하지만 ImmutableAddress 에는 값을 변경할 수 있는 메서드가 없다. 따라서 컴파일 오류가 발생한다. 결국 memberB.setAddress(new ImmutableAddress("부산")) 와 같이 새로운 주소 객체를 만들어서 전달하도록한다.
○ 불변 객체 - 값 변경
불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까?
아래예시를 보자.
package lang.immutable.change;
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;
}
}
package lang.immutable.change;
public class MutableMain {
public static void main(String[] args) {
MutableObj obj = new MutableObj(10);
obj.add(20);
// 계산 이후 기존 값은 사라짐
System.out.println("obj = " + obj.getValue());
}
}
obj = 30
계산 이후 기존 10은 사라지고 30이 출력된다.
이제 불변 객체에서 add()를 구현해보자. 불변 객체는 변하지 않아야 한다.
package lang.immutable.change;
public class ImmutableObj {
private final int value;
public ImmutableObj(int value) {
this.value = value;
}
public ImmutableObj add(int addValue) {
int result = value + addValue;
return new ImmutableObj(result);
}
public int getValue() {
return value;
}
}
package lang.immutable.change;
public class ImmutableMain1 {
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());
}
}
obj1 = 10
obj2 = 30
위 핵심은 add()이다. 불변 객체는 기존 값 변경 없이 새로운 객체를 만들어 반환한다.
이렇게 하면 불변도 유지하면서 새로운 결과도 만들 수 있다.
불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭! 반환 값을 받아야 한다.
참고 - withXxx()
불변 객체에서 값을 변경하는 경우 withYear() 처럼 "with"로 시작하는 경우가 많다. 예를 들어 "coffee with sugar"라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미한다. 이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 "with"로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다. 정리하면 "with"는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.
'Java' 카테고리의 다른 글
[Java] 김영한의 실전 자바 - 중급편 섹션5 열거형 - ENUM (0) | 2024.05.09 |
---|---|
[Java] 김영한의 실전 자바 - 중급편 섹션3,4 String ,래퍼,Class 클래스 (0) | 2024.05.09 |
[Java] 김영한의 실전 자바 - 중급편 섹션1 Object 클래스 (0) | 2024.05.05 |
[Java의 정석] chapter09 java.lang패키지와 유용한 클래스 - 요약정리 (0) | 2024.05.05 |
[Java] 김영한의 실전 자바 - 기본편 섹션9,10 상속,다형성 (0) | 2024.04.24 |