Programming Language/Java

[Java] 18. 불변 객체

lumana 2024. 10. 15. 17:33

 

불변 객체

#Java


갑자기 java.lang에 있는 클래스를 배우다가 “불변 객체”라는 개념이 등장했다. 이 개념이 필요한 이유는 다음 챕터에서 배울 String 클래스가 “불변 객체”이기 때문이다. 먼저 불변 객체가 뭔지 알아보자.

공유 : 기본형 vs 참조형

  • 기본형: 하나의 값을 여러 변수에서 절대 공유하지 않는다.
  • 참조형: 하나의 객체를 참조값을 통해 여려 변수에서 공유할 수 있다.

공유 참조와 사이드 이펙트

사이드 이펙트 : 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것


공유 참조를 잘못하면 사이드 이펙트가 발생할 수 있다. 이를 방지할 수 있는 방법에 대해 알아보자.


사이드 이펙트 해결 방안

단순하게 서로 다른 객체를 참조해서, 같은 객체를 공유하지 않으면 문제가 해결된다. 그런데 이를 강제할 수 있는 방법이 없다.


참조값의 공유를 막을 수 있는 방법이 없다.


Address a = new Address("서울");
Address b = a; //참조값 대입을 막을 수 있는 방법이 없다. 

문법상에 오류가 없다. 만약 코드가 길어지거나, 다른 개발자가 실수로 참조값 대입을 하면 안되는 경우에 해버리면 알아챌 수 있는 방법이 문법상으로 없다. 단순히 개발자가 공유 참조 문제가 발생하지 않도록 조심해서 코드를 작성해야 할까?


불변 객체 - 도입

객체의 공유를 문법상으로 막을 수 있는 방법은 없다. 근데 엄밀히 따지면, 객체의 공유에는 문제가 없다. 객체를 공유했을 때 공유된 객체의 값을 바꿈으로써 사이드 이펙트가 터지는 거다.


어? 그러면 공유된 객체의 값을 바꾸지 못하게 만들면 되겠네요? 정답!


불변 객체 도입

객체의 상태(객체 내부의 값, 필드, 멤버 변수)가 변하지 않는 객체를 불변 객체(Immutable Object)라 한다. Address 클래스를 상태가 변하지 않는 불변 클래스로 다시 만들어보자.


package lang.immutable.address;

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 "Address{" +
                "value='" + value + '\'' +
                '}';
    }
}

  • 내부 값이 변경되면 안된다. 따라서 value 의 필드를 final 로 선언했다.
  • 값을 변경할 수 있는 setValue() 를 제거했다.
  • 이 클래스는 생성자를 통해서만 값을 설정할 수 있고, 이후에는 값을 변경하는 것이 불가능하다.

이제 개발자는, 값을 변경하려고 할 때 값 변경이 불가능함을 확인할 수 있다.


어? 그러면 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 ImmutableAddress getAddress() {
        return address;
    }

    public void setAddress(ImmutableAddress address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "MemberV1{" +
                "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);
    }
}

기존 같았으면, Address 참조 필드를 대입해버리거나, 아니면 세터를 통해서 값을 변경했을텐데, Address를 불변 객체로 생성했기 때문에 이와 같은 방법으로 값을 바꾸는 건 불가능하다.


대신 Member에 있는 address필드에 새로운 객체 인스턴스를 생성한 뒤 참조하는 식으로 회원의 주소를 바꿀 수 있도록 구현할 수 있다. 이렇게 하면 회원 B의 Address를 변경해도 회원 A의 Address는 변경되지 않는다.
(새로운 부품을 장착했다고 생각하면 이해가 잘 될거에요)


위 예시는 Address에 의존하는 참조 필드를 바꿔줌으로써 수정을 구현하였다. 만약 불변 객체의 값 자체를 바꿔야 한다면 어떻게 해야할까?


불변 객체 - 값 변경

package lang.immutable.change;

public class MutableObj {
    private int value;

    public MutableObj(int value) {
        this.value = value;
    }

    public void add(int addValue) {
        value += addValue;
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

위와 같은 가변 객체의 더하는 기능을 불변 객체로는 어떻게 구현할까?


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(value + addValue);
    }

    public int getValue() {
        return value;
    }
}

불변 객체의 값은 불변이다. 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.add(20)을 호출하면, 새로 생성된 인스턴스는 사라진다. 따라서 불변 객체에서 변경과 관련된 메서드들은 보통 객체를 새로 만들어서 반환하기 때문에 꼭! 반환 값을 받아야 한다.


보통 필드 값을 못바꾸게 되어있으면 불변인데, 값을 바꾼다고 한다면 새로운 객체를 만들어서 반환한다고 생각하면 된다.


참고 - withXxx()
불변 객체에서 값을 변경하는 경우 withYear() 처럼 메서드 이름이 "with"로 시작하는 경우가 많다.
예를 들어 "coffee with sugar"라고 하면, 커피에 설탕이 추가되어 원래의 상태를 변경하여 새로운 변형을 만든다는 것을 의미한다.
이 개념을 프로그래밍에 적용하면, 불변 객체의 메서드가 "with"로 이름 지어진 경우, 그 메서드가 지정된 수정사항을 포함하는 객체의 새 인스턴스를 반환한다는 사실을 뜻한다.
정리하면 "with"는 관례처럼 사용되는데, 원본 객체의 상태가 그대로 유지됨을 강조하면서 변경사항을 새 복사본에 포함하는 과정을 간결하게 표현한다.


정리

자바에서 가장 많이 사용되는 String 클래스가 바로 불변 객체이기 때문이다. 뿐만 아니라 자바가 기본으로 제공하는 Integer , LocalDate 등 수 많은 클래스가 불변으로 설계되어 있다.


가변 클래스가 더 일반적이고, 불변 클래스는 값을 변경하면 안되는 특별한 경우에 만들어서 사용한다고 생각하면 된다. 때로는 같은 기능을 하는 클래스를 하나는 불변으로 하나는 가변으로 각각 만드는 경우도 있다.