Mutable과 Immutable

개발자 춘식이·2023년 7월 20일
0
post-thumbnail
  • Mutable: adj. 변할 수 있는, 잘 변하는
  • Immutable: adj. 변경할 수 없는, 불변의

Mutable 변수는 값을 재할당할 수도 있고, 값을 바꿀 수도 있습니다. 그와 반대로 immutable 변수는 값을 바꿀 수가 없고 재할당을 할 수 없습니다. Immutable 변수는

  • 멀티스레드일 때 thread safety를 보장해주고,
  • 값을 바꿀 수 없기 때문에 보안성이 높아집니다.

대표적인 예로 자바의 String이 있습니다.

String is Immutable

String은 immutable

만약 new 연산자를 사용해서 String 객체를 생성하면 String constant pool 영역 밖 Heap에 생성됩니다. 하지만 모두 Heap 영역에 생기기 때문에 GC의 영역 대상이 됩니다.(Java7~)

String strA = "a";
String strB = "b";
System.out.println(System.identityHashCode(strA)); //1651191114
System.out.println(System.identityHashCode(strB)); //1579572132

strA = strA + strB;
System.out.println(System.identityHashCode(strA)); //783286238   

System.identityHashCode 메소드는 hashCode 메소드로 오버라이딩 하지 않은 객체의 해시코드값을 10진수로 변환해주는 메소드입니다. strA와 strB 그리고 strA와 strB를 더한 strA 변수의 메모리 주소가 다른 것을 확인할 수 있습니다.
String은 + 연산을 할 때 strA가 참조하고 있는 힙의 값이 재할당되는 것이 아니라, heap에 새로 만들어서 그 주소를 재참조합니다.

String의 hashCode() 메소드로 호출했을 때는 ASCII 코드랑 같은 값이 나왔다..!

String strA = "a";
System.out.println(System.identityHashCode(strA)); //1651191114

strA = "hello";
System.out.println(System.identityHashCode(strA)); //1579572132

Mutable -> Immutable

Mutable을 immutable로 변경하려면 아래의 규칙들을 따라야 합니다.

  • 모든 필드들을 private final 로 지정합니다.
  • setter 메서드가 있으면 안됩니다.
  • mutable 필드를 리턴하는 getter 메서드가 있으면 안됩니다.
    - 객체를 리턴하는 getter 메서드가 있다면, 이 객체는 반드시 immutable 해야 합니다.
    - 레퍼런스 타입의 필드에 적용됩니다.
    - 또는 mutable 필드를 get해야 한다면 방어적 복사를 해야 합니다.
  • 자녀 클래스에서 메서드 override를 금지시켜야 합니다. 즉, 클래스 상속이 불가능하도록 final 클래스로 지정해줍니다.

아래의 Person 클래스는 name이라는 필드를 갖고, get과 set 모두 가능한 mutable한 클래스 입니다.

public class Person {
    private String name;

	public Person(String name) {
    	this.name = name;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
} 

Immutable하게 바꾸려면 우선 생성자를 제외하고 상태를 바꿀만한 메서드는 모두 제거해야합니다. 여기서는 setName()이 해당됩니다.

public class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

필드가 현재 private으로만 되어있으므로, final 키워드를 붙여줌으로써, Person 클래스에서만 name 필드를 접근 가능하도록 수정해줍니다.

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

이렇게 변경된 클래스 Person을 상속 받음으로써 아래와 같이 mutable처럼 사용될 수 있습니다.

public class NewPerson extends Person {

    private String newName;

    public NewPerson(String name) {
        super(name);
        newName = name;
    }

    @Override
    public String getName() {
        return this.newName;
    }

    public void setName(String newName) {
        this.newName = newName;
    }
}

public class Main {
	public static void main(String[] args) {
        Person person = new NewPerson("choonsik");
        System.out.println("person.getName() = " + person.getName()); //choonsik

        NewPerson newPerson = (NewPerson) person;
        System.out.println("newPerson.getName() = " + newPerson.getName()); //choonsik

        newPerson.setName("Java");
        System.out.println("person.getName() = " + person.getName()); //Java
	}
}

분명히 newPerson에서 setName을 해주었는데, person의 name이 변경되었습니다. 왜 그럴까요? 그 이유는 바로, Person 클래스를 상속받은 NewPerson 클래스에서 getName() 메소드를 NewPerson의 name을 리턴하도록 재정의했기 때문입니다. 따라서 person.getName()을 하게되면 실제로는 NewPerson 클래스의 재정의된 getName()을 호출합니다. 따라서 상태가 바뀐 것 처럼 보이게 됩니다.
이를 막기 위해, Person 클래스를 상속받지 못하도록 클래스 또한 final 클래스가 되도록 final 키워드를 붙여줍니다.

public final class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

그런데 만약 객체의 필드 중에 mutable 객체를 가리키는 레퍼런스가 있다면 어떻게 될까요?

public class RGB {
    public int r;
    public int g;
    public int b;

    public RGB(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }
} 

위와 같은 mutable한 RGB 클래스가 있다고 가정을 합니다. 그리고, Person의 클래스는 다음과 같습니다.

public final class Person {
    private final String name;
    private final RGB rgb;

    public Person(String name, RGB rgb) {
        this.name = name;
        this.rgb = rgb;
    }

    public String getName() {
        return name;
    }

    public RGB getRGB() {
        return rgb;
    }
}

모든 필드가 private final로 되어있고, 상태를 변경시키는 setter 메서드도 없으며 클래스 또한 상속이 불가능한 final 클래스인데 완벽한 immutable 일까요? 아닙니다. 다음 코드를 보겠습니다.

public static void main(String[] args) {
	RGB green = new RGB(0, 128, 0);
        
    Person person = new Person("choonsik", green);
    System.out.println("person.getRGB().g = " + person.getRGB().g); //128

    green.g = 0;
    System.out.println("person.getRgb().g = " + person.getRGB().g); //0
}

Person 클래스에서는 분명히 RGB값을 변경하는 메소드가 없었음에도 불구하고 green 값이 바뀐 것을 확인할 수 있습니다. Person의 생성자를 보게 되면 RGB 라는 객체(레퍼런스)를 받게 됩니다. 따라서, RGB의 값이 변경되면 Person 또한 바뀐 RGB 객체를 바라보게 됩니다.

public Person(String name, RGB rgb) {
	this.name = name;
    this.rgb = new RGB(rgb.r, rgb.g, rgb.b);
}

이를 해결하기 위해서, Person 객체를 생성할 때 파라미터값으로 넘어오는 RGB 객체를 사용해 다시 새로운 RGB 객체를 생성하고 person의 rgb 멤버 변수는 이 새로운 객체를 가리키게 됩니다. 즉, green.g = 0;을 했을 때에도 변경되지 않은 값(128)을 리턴해주게 됩니다. 그래도! 문제가 있습니다.

RGB myRGB = person.getRGB();
myRGB.g = 0;
System.out.println("person.getRgb().g = " + person.getRGB().g); //0

여기서는 Person 클래스의 getRGB() 메소드에서 문제가 되는데, Person이 가지고 있는 RGB 객체를 그대로 리턴해주었기 때문에 문제가 발생한겁니다. 다시 말해, 리턴할 때의 RGB 객체는 값을 변경할 수 있는 mutable 객체로 값을 person에서 getRGB()를 하더라도 실질적으로는 Mutable한 객체 RGB를 전달해주기 때문에 값이 변경된 것입니다. 이를 해결하기 위해서, getRGB()를 할 때 다시 한 번 새로운 RGB를 리턴하도록 변경해주어야 합니다. -> 방어적 복사

public RGB getRGB() {
	return new RGB(rgb.r, rgb.g, rgb.b);
}

방어적 복사 : 객체를 복사해서 새로운 독립적인 객체를 생성하는 복사 기법. 기존의 객체와는 별개로 관리되어 기존 객체의 값이 변경되지 않는다.

Collection은 어떻게 Immutable 하도록 변경할 수 있을까?

RGB 클래스에서 toString()을 재정의해 주었습니다.

public class RGB {
    public int r;
    public int g;
    public int b;

    public RGB(int r, int g, int b) {
        this.r = r;
        this.g = g;
        this.b = b;
    }

    @Override
    public String toString() {
        return "RGB{" +
                "r=" + r +
                ", g=" + g +
                ", b=" + b +
                '}';
    }
}

Person 클래스의 RGB 필드 대신 List<RGB> rgbs를 새로 만들고, copy() 메소드가 추가되었습니다.

public final class Person {
    private final String name;
    private final List<RGB> rgbs;

    public Person(String name, List<RGB> rgbs) {
        this.name = name;
        this.rgbs = copy(rgbs);
    }

    public String getName() {
        return name;
    }

    public List<RGB> getRGBs() {
        return copy(rgbs);
    }

    private List<RGB> copy(List<RGB> rgbs) {
        List<RGB> cps = new ArrayList<>();
        rgbs.forEach(o -> cps.add(new RGB(o.r, o.g, o.b)));
        return cps;
    }
}

이 copy 메소드에서 List를 새로 생성하고, 이 새로 생성된 cps에 파라미터로 들어온 List rgbs의 각 객체를 꺼내 cps에 추가해주고, cps를 리턴해줍니다. 이렇게 함으로써 방어적 복사를 할 수가 있습니다.

public static void main(String[] args) {
	RGB red = new RGB(128, 0, 0);
    RGB green = new RGB(0, 128, 0);
    RGB blue = new RGB(0, 0, 128);

    List<RGB> rgbs = new ArrayList<>();
    rgbs.add(red);
    rgbs.add(green);
    rgbs.add(blue);
        
    Person person = new Person("choonsik", rgbs);
    List<RGB> rgbList = person.getRGBs();
    System.out.println("rgbList.toString() = " + rgbList.toString()); 
    //[RGB{r=128, g=0, b=0}, RGB{r=0, g=128, b=0}, RGB{r=0, g=0, b=128}]
    }

만약 List<RGB> rgbs 가 immutable이라면 copy() 메소드를 다음과 같이 변경하면 됩니다. 얕은 복사를 통해 List는 새로 만들지만 안에 있는 RGB 객체는 같은 레퍼런스를 복사하게 됩니다.

private List<RGB> copy(List<RGB> rgbs) {
    return new ArrayList<>(rgbs); //얕은 복사: 주소값 복사
}

또는 생성자를 변경해줄 수도 있습니다.

public Person(String name, List<RGB> rgbs) {
	this.name = name;
    this.rgbs = Collections.unmodifiableList(rgbs);
}

public List<RGB> getRGBs() {
	return this.rgbs;
}

참조
Java Strings are Immutable - Here's What That Actually Means
불변 객체(immutable object)는 안정적인 개발에 아주 도움이 됩니다! 불변 객체의 개념과 장점, 구현 방법을 자바 예제를 통해 배워보아요~! <- 강추👍🏻
Immutable Classes and Objects in Java

profile
춘식이를 너무 좋아하는 주니어 백엔드 개발자입니다.

0개의 댓글