- Mutable: adj. 변할 수 있는, 잘 변하는
- Immutable: adj. 변경할 수 없는, 불변의
Mutable
변수는 값을 재할당할 수도 있고, 값을 바꿀 수도 있습니다. 그와 반대로 immutable
변수는 값을 바꿀 수가 없고 재할당을 할 수 없습니다. Immutable 변수는
대표적인 예로 자바의 String
이 있습니다.
만약 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
- 모든 필드들을
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;
}
}
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);
}
방어적 복사 : 객체를 복사해서 새로운 독립적인 객체를 생성하는 복사 기법. 기존의 객체와는 별개로 관리되어 기존 객체의 값이 변경되지 않는다.
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