기본형의 경우, 하나의 값을 여러 변수에서 절대 공유하지 않는다. 반면, 참조형은 하나의 객체를 참조값을 통해 여러 변수에서 공유할 수 있다.
Dog dog = new Dog();
int a = 10;
Dog dog1 = dog
int b = a
dog1은 dog의 주소를 그대로 가져온다. 만약 Dog 클래스에 wash()라는 메서드가 있다고 가정하고, dog1.wash()를 수행하면, dog와 dog1이 동일한 참조값을 가지므로 결국dog인스턴스의 wash()가 수행된다.
반면, 기본형인 int 타입에서 b = a;의 경우, a의 값(정수 10)을 복사하여 b에 넣는 것이기 때문에 두 변수는 독립적이다. 즉, b의 값을 변경하더라도 a에는 영향을 미치지 않는다.
사이드 이펙트(Side Effect)란 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 말하며, 보통은 부정적인 결과를 가져온다.
public class MemberV1Main {
public static void main(String[] args) {
Address address = new Address("서울");
MemberV1 memberA = new MemberV1("회원A" , address);
MemberV1 memberB = new MemberV1("회원B" , address);
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
memberB.getAddress().setValue("부산");
System.out.println("부산 -> memberB.address");
System.out.println("memberA = " + memberA);
System.out.println("memberB = " + memberB);
}
}
위 코드에서 사이드 이펙트를 일으킬 수 있는 함정이 존재한다.
MemberV1 클래스의 멤버 변수로 address 객체를 받는데, "서울"을 멤버 변수로 가지는 address 인스턴스가 MemberA와 MemberB에 동시에 들어간다. 즉, 실제로는 생성된 address의 참조값이 MemberA와 MemberB에 들어가는 것이다.
이로 인해, MemberB의 address를 수정하면 MemberA의 address도 수정된다. 이는 동일한 참조값을 두 개의 인스턴스가 공유하고 있기 때문이다.
우리는 MemberB의 지역 정보만 변경되기를 원했지만, MemberA의 지역 정보도 함께 변경되는 부작용이 발생했다. 이러한 현상을 사이드 이펙트라고 한다. 코드가 복잡해질수록 이러한 문제는 더욱 치명적일 수 있다.(우리가 의도한 바와 다르게 흘러가서 그렇지 코드의 문법적으로 이상은 없기 때문이다.)
사이드이펙트는 무의식적으로 나쁜 습관의 코딩할 때 발생할 가능성이 높으며, 컴파일러는 이를 에러로 인식하지 않으므로 개발자가 놓칠 수 있다. 따라서 처음부터 이러한 문제를 방지하기 위한 설계가 필요하다.
Address a = new Address("서울")
Address b = new Address("서울")
객체를 두 개 생성하여 참조값 공유 문제를 방지할 수도 있다.
이러한 방식은 참조값을 분리하여 문제를 해결하는 것처럼 보이지만,
개발자의 실수로 의도치 않게 하나의 객체만 생성하거나 참조값을 잘못 전달하는 실수로 인해 동일한 참조값을 공유하게 되는 경우가 있을 수 있다.
특히, 실무에서는 협업 과정에서 이러한 낮은 확률의 문제점이 현실적으로 나타날 가능성이 있다. 설계자가 놓친 부분이나 부주의로 인해, 다른 개발자가 기존 코드를 이어받아 수정하거나 확장하는 과정에서 사이드 이펙트가 발생할 수 있다.
이러한 문제는 프로그램이 크고 복잡해질수록 더 치명적이 될 수 있다. 컴파일러가 이러한 문제를 에러로 감지하지 않기 때문에, 문제가 발생했을 경우 직접 디버깅을 통해 원인을 찾아야 하는 부담이 생긴다.
발생 가능할 실수에 대해 컴파일 에러가 감지하지 못하는 영역이라면 애초에 이를 배제할 수 있어야한다.
따라서, 초기 설계 단계에서부터 이러한 가능성을 철저히 차단할 수 있는 설계 방식을 고려해야 하며, 참조값 공유로 인해 발생할 수 있는 부작용을 최소화하는 안전한 코딩 습관이 중요하다.
문제의 본질적인 원인은 여러 곳에서 객체를 공유하면서, 공유된 객체의 값을 변경할 수 있다는 것이다.
객체의 상태(내부 값이나 필드)가 절대 변하지 않는 객체를 불변 객체(Immutable Object)라고 한다. 불변 객체를 도입하면 이러한 문제(공유로 인한 변경 문제)를 원천적으로 차단할 수 있다.
setter 메서드를 제공하지 않는다.final로 선언하여 재할당을 막는다.불변 객체는 자바에서 별도로 지원하는 기능은 아니며, 설계자가 논리적으로 이러한 특성을 만족하도록 직접 구현해야 한다.
객체를 설계할 때, 생성자 작업 이후 객체의 내부 값을 변경할 수 없도록 만들면 된다. 예를 들어, 필드를 final로 선언하고, 값을 변경하는 setter 메서드를 제거하는 방식으로 구현할 수 있다.
불변 객체를 도입하면 객체 상태 변경으로 인한 부작용을 방지할 수 있으며, 특히 멀티스레드 환경에서 안정성을 높이는 데 유용하다.
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;
}
}
value는 final에 의해 생성자로 값을 받은 후 변경이 불가능하다. 그러므로 add메서드는 새로운 value를 가진 동일한 클래스로 만들어진 객체를 리턴하도록 하면된다. 이렇게 되면 본래의 불변객체는 불변객체로 남고 메서드 이후 변화한 value에 대한 새로운 불변객체가 생성된다.
withXxx()
불변 객체에서 값을 변경하는 경우 withYear, withNumber와 같이 "with"으로 시작하는 이름을 붙여주도록 하자.(관행) 불변 객체의 메서드가 with이름으로 만들어진 경우 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다.
당연히 모든 클래스를 불변으로 만들지는 않는다.
불변으로 설계할 때는 캐시 안정성, 멀티스레드 안정성, 엔티티의 값 타입등 추후 배울 내용에서 사용되므로 현재는 간단한 예시와 함께 불변객체를 만드는 방식과 값의 변경방식을 익혀놓도록 하자.