Java

불변객체

hongdangmoo 2024. 10. 31. 18:06

◎ 기본형과 참조형

-> 자바의 기본적인 데이터 타입에는 기본형과 참조형이 있다.

-> 기본형 : 하나의 값을 여러 변수에서 절대로 공유하지 않는다.

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);

        System.out.println();
        b = 20;
        System.out.println("b = 20으로 변경");
        System.out.println("a = " + a);;
        System.out.println("b = " + b);;
    }
}

-> 결과

-> 기본형 변수 a와 b는 하나의 값을 공유하지 않는다.

-> int b = a; 에서 a에 있는 값 10을 복사하여 변수 b에 저장한다. a와 b는 각각 별도의 변수로 단지 두 변수 모두 10이라는 값을 가지고 있을 뿐이다.

-> 따라서 b = 20;으로 b의 값을 변경해도 a와는 별도의 값을 가지고 있는 것이기 때문에 b의 값만 변경된다.

 

-> 참조형 : 하나의 객체를 참조값을 가지고 해당 참조값을 저장하고 있는 여러 변수에서 공유할 수 있다.

public class Address {
    private String value;

    public Address(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

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

    @Override
    public String toString() {
        return "Address{" +
                "value='" + value + '\'' +
                '}';
    }
}
public class RefMain1_1 {
    public static void main(String[] args) {
        // 참조형 변수는 하나의 인스턴스를 공유할 수 있다.
        Address a = new Address("서울");
        Address b = a; // a와 b는 같은 참조값을 저장한다.
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        b.setValue("인천");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

-> 결과

-> a와 b는 같은 주소를 가지고 있다. 따라서 b.setValue()를 통해 value의 값을 변경하면, a의 value값도 변경된다.

 

 

◎ 공유 참조와 사이드 이펙트

-> 사이드 이펙트는 프로그래밍에서 어떤 계산이 주된 작업 외에 추가적인 부수 효과를 일으키는 것을 의미한다.

-> 위에 있는 참조형 변수 예시에서 원래 의도는 b의 값만 서울에서 인천으로 변경하려 한 것인데, a의 값도 인천으로 변경되었다. 이것이 사이드 이펙트다.

-> 사이드 이펙트는 특정 부분에서 발생한 변경이 의도와 다르게 다른 부분에 영향을 미치는 것으로 이로 인해 디버깅이 어려워지고 코드의 안정성이 떨어질 수 있다.

-> 참조형 변수 예시에서 사이드 이펙트를 해결하려면 a와 b가 같은 인스턴스가 아닌 서로 다른 인스턴스를 참조하게 해야 한다.

public class RefMain1_2 {
    public static void main(String[] args) {
        // 참조형 변수는 하나의 인스턴스를 공유할 수 있다.
        Address a = new Address("서울");
        Address b = new Address("서울");
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        b.setValue("인천");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

-> 결과

-> 이제 a와 b는 서로 다른 인스턴스를 참조하고 있기 때문에 b.setValue()를 통해 값을 변경하면 b의 값만 변경된다.

-> 이렇게 각기 다른 인스턴스를 생성하여 참조하도록 하면 문제가 되지 않는다. 하지만 애초에 자바에서는 여러 변수가 하나의 객체를 공유하는 것 자체는 문제가 되지 않기 때문에 실수로 하나의 객체에 여러 변수가 공유하도록 코드를 작성해도 실행할 때 까지 문제를 발견하기 어렵다. 자바에서는 하나의 객체를 여러 변수가 공유하는 것은 문법상 정상이다.

 

◎ 불변 객체

-> Address 객체를 여러 변수에서 공유했을 떄 문제가 되는 부분은 Address 객체의 값을 어디선가 변경했다는 것이다.

-> 만약 Address 객체의 값을 변경하지 못하게 한다면 사이드 이펙트가 발생하지 않을 것이다.

-> 이 때 객체의 상태가 변하지 않는 불변 객체(Immutable Object)를 이용하여 문제를 해결할 수 있다.

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

-> value를 final로 설정하여 생성자를 통해서만 값을 설정하고, 이후에는 값을 변경하지 못하도록 했다.

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("인천"); // 새로운 객체를 만들어 새로운 객체의 참조값을 b에 저장
        System.out.println("인천 -> b");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

-> 결과

-> ImmutableAddress에서 value의 값을 변경하는 setValue()자체를 없애 ImmutalbeAddress 인스턴스의 값을 변경할 수 있는 방법이 없다.

-> 만약 b.setValue()로 값을 변경하려 한다면 컴파일 오류가 발생하여 개발자는 오류를 인지할 수 있다.

-> b는 새로운 ImmutableAddress 인스턴스를 생성하여 참조값을 저장한다. 이에 따라 a와 b는 서로 다른 인스턴스를 참조하게 된다.

 

* 가변(Mutable) 객체 & 불변(Immutable) 객체

가변 객체는 처음 만든 이후 상태가 변할 수 있는 객체다. 

불변 객체는 처음 만든 이후 상태가 변하지 않는 객체다.

위에서 봤던 예제를 기준으로 Address 클래스는 가변 객체, Immutable 클래스는 불변 객체다.

 

-> 가변 객체 예제

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 "MemberV1{" +
                "name='" + name + '\'' +
                ", address=" + address +
                '}';
    }
}
public class MemberMainV1 {
    public static void main(String[] args) {
        Address address = new Address("서울");

        MemberV1 memberA = new MemberV1("memberA", address);
        MemberV1 memberB = new MemberV1("memberB", address);

        // memberA, memberB 첫 주소는 서울
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);

        memberB.getAddress().setValue("인천");
        System.out.println("인천 -> memberB.setAddress");
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);
    }
}

-> 결과

-> 위 코드는 memberA와 memberB 모두 같은 Address 인스턴스를 참조하기 때문에 둘 중 하나만 setValue()를 통해 값을 바꿔도 둘다 변경된다.

 

-> 불변 객체 예제

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 "MemberV2{" +
                "name='" + name + '\'' +
                ", address=" + address +
                '}';
    }
}
public class MemberMainV2 {
    public static void main(String[] args) {
        ImmutableAddress address = new ImmutableAddress("서울");
        MemberV2 memberA = new MemberV2("memberA", address);
        MemberV2 memberB = new MemberV2("memberB", address);

        // memberA, memberB 첫 주소는 서울
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);

//        memberB.getAddress().setValue("인천"); -> 컴파일 에러
        memberB.setAddress(new ImmutableAddress("인천"));
        System.out.println("인천 -> memberB.setAddress");
        System.out.println("memberA = " + memberA);
        System.out.println("memberB = " + memberB);
    }
}

-> 결과

-> memberB가 setValue()를 사용하면 컴파일 에러가 발생한다.  setAddress()를 통해 새로운 ImmutableAddress를 생성해 해당 객체의 값에 변경된 값을 넣는다.

-> memberA와 memberB는 이제 서로 다른 ImmutableAddress 인스턴스를 참조한다.

 

-> 다음은 불변 객체를 사용하면서 값을 변경해야 하는 경우에 대한 예시이다.

-> 먼저 가변 객체에서의 예시다.

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;
    }
}
public class MutableMain {
    public static void main(String[] args) {
        MutableObj obj = new MutableObj(7);
        obj.add(20);

        // 계산 이후 기존 값은 사라진다.
        System.out.println("obj = " + obj.getValue());
    }
}

-> 결과

-> MutableObj를 7이라는 값으로 생성했다. 이후 add메서드를 이용해 20을 추가하면 기존 value에 value + 20이 되어 value에는 27이라는 값이 들어간다.

 

-> 다음은 불변 객체를 이용한 값 변경 예시다.

public class ImmutableObj { // 불변 객체
    private final int value;

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

    public ImmutableObj add(int addValue) {
        int result = value + addValue; // 새로운 필드 값에 계산 결과를 저장 따라서 변경될 수 없는 value를 건드리지 않고 계산을 할 수 있다.
        return new ImmutableObj(result);
    }

    public int getValue() {
        return value;
    }
}

-> add메서드의 리턴타입을 ImmutableObj로 하여 연산 결과를 리턴할 때 새로운 ImmutableObj 객체를 생성하도록 했다. 또한 value를 final값을 만들고, add 메서드 연산을 할 때 기본 int형 result를 새로 생성하여 여기에 연산 결과를 저장하였다. 

public class ImmutableMain {
    public static void main(String[] args) {
        ImmutableObj obj1 = new ImmutableObj(7);
        ImmutableObj obj2 = obj1.add(20);

        System.out.println("obj1 = " + obj1.getValue());
        System.out.println("obj2 = " + obj2.getValue());
    }
}

-> 결과

-> ImmutableObj에서 value를 final로 하고, add연산을 할 때 value에 변경 값을 넣지 않고 단지 연산을 할 때 사용만 했다.

-> 연산 결과는 새 변수를 생성하여 거기에 담고 이 값을 새로운 ImmutableObj객체에 담아 리턴을 했다.

-> 따라서 obj1과 obj2는 서로 다른 인스턴스를 참조하게 되어 obj1은 기존의 값을 유지하고, obj2에는 새로운 값이 들어가게 된다.

 

★ 참고 및 출처

김영한의 실전 자바 - 중급 1편