'김영한의 실전 자바 - 중급 1편' 강의를 들으면서 복습할만한 내용을 정리하였다.
자바의 데이터 타입을 가장 크게 보면 기본형(Primitive Type)과 참조형(Reference Type)으로 나눌 수 있다.
기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.
참조형 : 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
하나의 값을 공유하거나 또는 공유하지 않는다는 것이 무슨 뜻인지 모르겠으면 이 링크의 글을 읽어보자
사이드 이펙트(Side Effect)는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말한다.
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;
}
}
Main
Address a = new Address("서울");
Address b = a;
b.setValue("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a); //사이드 이펙트 발생
System.out.println("b = " + b);
실행 결과
부산 -> b
a = Address{value='부산'}
b = Address{value='부산'}

개발자는 b 의 주소값을 서울에서 부산으로 변경할 의도로 값 변경을 시도했다. 하지만 a, b 는 같은 인스턴스를 참조한다. 따라서 a 의 값도 함께 부산으로 변경되어 버린다.
이렇게 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 사이드 이펙트라 한다. 프로그래밍에서 사이드 이펙트는 보통 부정적인 의미로 사용되는데, 사이드 이펙트는 프로그램의 특정 부분에서 발생한 변경이 의도치 않게 다른 부분에 영향을 미치는 경우에 발생한다. 이로 인해 디버깅이 어려워지고 코드의 안정성이 저하될 수 있다.
문제의 해결방안은 아주 단순하다. a 와 b 가 처음부터 서로 다른 인스턴스를 참조하면 된다.
Address a = new Address("서울");
Address b = new Address("서울");
쉽게 이야기해서 여러 변수가 하나의 객체를 공유하지 않으면 사이드 이펙트 문제가 발생하지 않는다. 그런데 여기서 문제가 있다. 하나의 객체를 여러 변수가 공유하지 않도록 강제로 막을 수 있는 방법이 없다는 것이다.
Address a = new Address("서울");
Address b = a; //참조값 대입을 막을 수 있는 방법이 없다.
b = a 로 여러 변수가 하나의 참조값을 공유하는 코드를 작성해도 아무런 문제가 없다. 자바 문법상 문제가 없기 때문에 컴파일 오류도 발생하지 않는다.
객체의 공유를 막을 수 있는 방법이 없다!
사이드 이펙트의 더 근본적인 원인을 고려해보면, 객체를 공유하는 것 자체는 문제가 되지 않는다. 문제의 직접적인 원인은 공유된 객체의 값을 변경한 것에 있다.
객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다.
ImmutableAddress
public class ImmutableAddress {
private final String value;
public ImmutableAddress(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
내부의 값이 변경되면 안되므로 value 의 필드를 final 로 선언했다.
값을 변경할 수 있는 setValue() 를 제거했다.
이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.
ImmutableMain
ImmutableAddress a = new ImmutableAddress("서울");
ImmutableAddress b = a; //참조값 대입을 막을 수 있는 방법이 없다.
b = new ImmutableAddress("부산");
System.out.println("부산 -> b");
System.out.println("a = " + a);
System.out.println("b = " + b);
실행 결과
부산 -> b
a = Address{value='서울'}
b = Address{value='부산'}

ImmutableAddress 은 불변 객체이므로 b 가 참조하는 인스턴스의 값을 서울에서 부산으로 변경하려면 새로운 인스턴스를 할당해야 한다.
객체의 공유 참조는 막을 수 없다. 그래서 객체의 값을 변경하면 다른 곳에서 참조하는 변수의 값도 함께 변경되는 사이드 이펙트가 발생한다. 사이드 이펙트가 발생하면 안되는 상황이라면 불변 객체를 만들어서 사용하면 된다. 불변 객체는 값을 변경할 수 없기 때문에 사이드 이펙트가 원천 차단된다.
불변 객체는 값을 변경할 수 없다. 따라서 불변 객체의 값을 변경하고 싶다면 변경하고 싶은 값으로 새로운 불변 객체를 생성해야 한다. 이렇게 하면 기존 변수들이 참조하는 값에는 영향을 주지 않는다.
참고 - 가변(Mutable) 객체 vs 불변(Immutable) 객체
가변은 이름 그대로 처음 만든 이후 상태가 변할 수 있다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 있다는 뜻이다.)
불변은 이름 그대로 처음 만든 이후 상태가 변하지 않는다는 뜻이다. (사전적으로 사물의 모양이나 성질이 달라질 수 없다는 뜻이다.)
불변 객체를 사용하지만 그래도 값을 변경해야 하는 메서드가 필요하면 어떻게 해야할까?
예를 들어서 기존 값에 새로운 값을 더하는 add() 와 같은 메서드가 있다.
ImmutableObj
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;
}
}
ImmutableObjMain
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(20) 을 호출한다.
기존 객체에 있는 10 과 인수로 입력한 20 을 더한다. 이때 기존 객체의 값을 변경하면 안되므로 계산 결과를 기반으로 새로운 객체를 만들어서 반환한다.
새로운 객체는 x002 참조를 가진다. 새로운 객체의 참조값을 obj2 에 대입한다.
꼭! 반환 값을 받아야 한다.
지금까지 왜 이렇게 불변 객체 이야기를 많이 했을까?
자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer, LocalDate 등 수 많은 클래스가 불변으로 설계되어 있다.
우리가 만드는 대부분의 클래스는 값을 변경할 수 있게 만들어진다. 예를 들어서 회원 클래스의 경우 회원의 여러 속성을 변경할 수 있어야 한다. 가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있따.
클래스를 불변으로 설계하는 이유는 더 많다.