오늘의 잔디
잔디 밭이 점점 채워지고 있다.
오늘의 공부
지금까지 발생한 문제를 잘 생각해보면 공유하면 안되는 객체를 여러 변수에서 공유했기 때문에 발생한 문제이다.
하지만 앞서 살펴보았듯이 객체의 공유를 막을 수 있는 방법은 없다.
그런데 사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 아니다. 객체를 공유한다고 바로 사이드 이펙트가 발생하지는 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.
앞의 예를 떠올려보면 a , b 는 처음 시점에는 둘다 "서울" 이라는 주소를 사용해야 한다. 그리고 이후에 b 의 주소를 "부산" 으로 변경해야 한다.
Address a = new Address("서울");
Address b = a;
따라서 처음에는 b = a 와 같이 "서울" 이라는 Address 인스턴스를 a , b 가 함께 사용하는 것이, 다음 코드와 같이 서로 다른 인스턴스를 사용하는 것 보다 메모리와 성능상 더 효율적이다. 인스턴스가 하나이니 메모리가 절약되고,
인스턴스를 하나 생성하지 않아도 되니 생성 시간이 줄어서 성능상 효율적이다.
Address a = new Address("서울");
Address b = new Address("서울");
여기까지는 Address b = a 와 같이 공유 참조를 사용해도 아무런 문제가 없다. 오히려 더 효율적이다.
진짜 문제는 이후에 b가 공유 참조하는 인스턴스의 값을 변경하기 때문에 발생한다.
b.setValue("부산"); //b의 값을 부산으로 변경해야함
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);
자바에서 여러 참조형 변수가 하나의 객체(인스턴스)를 참조하는 공유 참조 문제는 피할 수 없다.
기본형과 다르게 참조형인 객체는 처음부터 여러 참조형 변수에서 공유될 수 있도록 설계되었다. 따라서 이것은 문제가 아니다.
문제의 직접적인 원인은 공유될 수 있는 Address 객체의 값을 어디선가 변경했기 때문이다.
만약 Address 객체의 값을 변경하지 못하게 설계했다면 이런 사이드 이펙트 자체가 발생하지 않을 것이다.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
앞서 만들었던 Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어보자.
package lang.immutable.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 + '\'' +
'}';
}
}
value 의 필드를 final 로 선언했다.setValue() 를 제거했다.불변 클래스를 만드는 방법은 아주 단순하다. 어떻게든 필드 값을 변경할 수 없게 클래스를 설계하면 된다.
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);
}
}
ImmutableAddress 의 경우 값을 변경할 수 있는 b.setValue() 메서드 자체가 제거되었다.ImmutableAddress 인스턴스의 값을 변경할 수 있는 방법은 없다.ImmutableAddress 를 사용하는 개발자는 값을 변경하려고 시도하다가, 값을 변경하는 것이 불가능하다는 사실을 알고, 이 객체가 불변 객체인 사실을 깨닫게 된다.b.setValue("부산") 을 호출하려고 했는데, 해당 메서드가 없다는 사실을 컴파일 오류를 통해 인지한다.ImmutableAddress("부산") 인스턴스를 생성해서 b 에 대입한다.a , b 는 서로 다른 인스턴스를 참조하고, a 가 참조하던 ImmutableAddress 는 그대로 유지된다.실행 결과
a = Address{value='서울'}
b = Address{value='서울'}
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}
실행 결과를 보면 a 의 값은 그대로 유지되는 것을 확인할 수 있다.


ImmutableAddress 는 불변 객체이다. 따라서 값을 변경할 수 없다.
ImmutableAddress 은 불변 객체이므로 b 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 생성해서 할당해야 한다.정리
불변이라는 단순한 제약을 사용해서 사이드 이펙트라는 큰 문제를 막을 수 있다.
참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻이다.)
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻이다.)
Address 는 가변 클래스이다. 이 클래스로 객체를 생성하면 가변 객체가 된다.
ImmutableAddress 는 불변 클래스이다. 이 클래스로 객체를 생성하면 불변 객체가 된다.
조금 더 복잡하고 의미있는 예제를 통해서 불변 객체의 사용 예를 확인해보자.
앞의 Address , ImmutableAddress 를 그대로 활용한다.
변경 클래스 사용
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 +
'}';
}
}
MemberV1 은 변경 가능한 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);
}
}
회원A 와 회원B 는 둘다 서울에 살고 있다.회원B 의 주소를 부산으로 변경해야 한다.회원A 와 회원B 는 같은 Address 인스턴스를 참조하고 있다.회원B 의 주소를 부산으로 변경하는 순간 회원A 의 주소도 부산으로 변경된다.실행 결과
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 뿐만 아니라 회원A의 주소도 부산으로 변경된다.
불변 클래스 사용
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 +
'}';
}
}
MemberV2 는 주소를 변경할 수 없는, 불변인 ImmutableAddress 를 사용한다.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);
}
}
회원B 의 주소를 중간에 부산으로 변경하려고 시도한다. 하지만 ImmutableAddress 에는 값을 변경할 수 있는 메서드가 없다. 따라서 컴파일 오류가 발생한다.memberB.setAddress(new ImmutableAddress("부산")) 와 같이 새로운 주소 객체를 만들어서 전달한다.실행 결과
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 는 기존 주소를 그대로 유지한다.
불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까?
예를 들어서 기존 값에 새로운 값을 더하는 add() 와 같은 메서드가 있다.
먼저 변경 가능한 객체에서 값을 변경하는 간단한 예를 만들어보자.
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
MutableObj 을 10 이라는 값으로 생성한다.obj.add(20) 을 통해서 10 + 20 을 수행한다.10 이라는 값은 사라진다.MutableObj 의 상태(값)가 10 -> 30 으로 변경되었다.obj.getValue() 를 호출하면 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;
}
}
add() 메서드이다.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
불변 객체를 설계할 때 기존 값을 변경해야 하는 메서드가 필요할 수 있다. 이때는 기존 객체의 값을 그대로 두고 대신에 변경된 결과를 새로운 객체에 담아서 반환하면 된다. 결과를 보면 기존 값이 그대로 유지되는 것을 확인할 수 있다.
실행 순서를 메모리 구조로 확인해보자.

1. add(20) 을 호출한다.
2. 기존 객체에 있는 10 과 인수로 입력한 20 을 더한다. 이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환한다.
3. 새로운 객체는 x002 참조를 가진다. 새로운 객체의 참조값을 obj2 에 대입한다.
만약 여기서 다음과 같이 새로 생성된 반환 값을 사용하지 않으면 어떻게 될까?
package lang.immutable.change;
public class ImmutableMain2 {
public static void main(String[] args) {
ImmutableObj obj1 = new ImmutableObj(10);
obj1.add(20);
System.out.println("obj1 = " + obj1.getValue());
}
}
실행 결과
obj1 = 10
실행 결과처럼 아무것도 처리되지 않은 것 처럼 보일 것이다.
불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭! 반환 값을 받아야 한다.
불변 객체에서 값을 변경하는 경우 withYear() 처럼 "with"로 시작하는 경우가 많다.
예를 들어 "coffee with sugar"라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미한다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 "with"로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다.
정리하면 "with"는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.
지금까지 왜 이렇게 불변 객체 이야기를 많이 했을까?
자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer , LocalDate 등 수 많은 클래스가 불변으로 설계되어 있다. (이후에 학습한다.)
따라서 불변 객체가 필요한 이유와 원리를 제대로 이해해야, 이런 기본 클래스들도 제대로 이해할 수 있다.
모든 클래스를 불변으로 만드는 것은 아니다.
우리가 만드는 대부분의 클래스는 값을 변경할 수 있게 만들어진다. 예를 들어서 회원 클래스의 경우 회원의 여러 속성을 변경할 수 있어야 한다. 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있다.
클래스를 불변으로 설계하는 이유는 더 많다.
지금은 이런 부분을 다 이해할 수 없다. 관련 내용을 학습하다 보면 자연스럽게 이번에 배운 불변 객체가 떠오르면서 관련된 내용을 본질적으로 더 잘 이해할 수 있을 것이다. 프로그래밍을 더 깊이있게 학습할 수 록 다양한 불변 클래스 이용 사례를 만나고 이해하게 된다. 따라서 지금은 불변 클래스가 어디에 사용되고, 어떻게 활용되는지 보다는 불변 클래스의 원리를 이해하는 정도면 충분하다.