본문 바로가기

ComputerScience/Java

[Java] immutable(불변) 객체란?

이미지 출처: https://miro.medium.com/v2/resize:fit:1400/1*XY-4dQf4EZuYCuG71nEOJQ.jpeg

자바에서 불변(Immutable) 객체에 대한 이해

  • 가변 객체는 상태 정보를 변경할 수 있어 불안정하고, 작업 전후에 일일이 코드를 확인하거나 DB를 호출해야한다.
  • 하지만 불변 객체는 한번 만들어지면 상태가 변경되지 않아 안정적인 개발이 가능하다.
  • 불변(Immutable)객체는 생성 이후에 상태가 변하지 않는 객체이다.
  • 불변 객체는 중복 제거의 장점을 가지며, 안전한 서비스 개발에 도움이 된다.
  • 예를 들어 DB에서 객체 정보를 받아와 작업할 때, 의도치 않게 불변 객체의 정보를 변경하려는 경우 불변 객체를 사용하면 이를 방지할 수 있다.
  • 또한 map, set, cache에 쓰기에 적절하다.
  • 불변 객체를 사용하면 Thread-safe의 장점이 있다.
  • 데이터 불일치 역시 없어, 안전하게 여러 셀에서 상태정보를 공유할 수 있다.
  • _변하지 않는 객체_이기 때문에 방어적 복사가 불필요하다는 장점도 있다.
  • 하지만 모든 것을 불변 객체로 표현할 수 없지만, 가능하다면 적극적으로 사용하는 것이 권장된다.

Java에서 String

  • Java에서 String은 불변 객체이며, 객체는 할당되는 값에 따라 새로운 객체를 생성한다.

  • 따라서 새로운 값을 할당하면 해당 값으로 기존 객체를 변경하는 것이 아니라, 새로운 객체를 생성한다.

    String str1 = "Test String1";
    String sameStr = str1;
    System.out.println(str1 == sameStr) // 같은 메모리 주소를 가르키고 있으므로 true (동일성)
    str1 = "Test String2"; // 새 객체가 만들어져 메모리 주소가 바뀜
    System.out.println(str1 == sameStr); // 서로 다른 메모리 주소에 존재하므로 false

Java에서 불변 객체 만들기

  • 자바에서 불변 객체를 만드는 방법은 생성자로만 설정해주고 setter메서드를 제공하지 않는 것이다.
  • final 키워드를 사용하여 클래스의 모든 필드를 final 지정해주면 더 이상 변경이 불가능한 것으로 설정 가능함.
  • final로 지정된 변수는 값을 한번 저장하면 변경이 불가능하기 때문에, private와 함께 사용하여 해당 변수가 차후에 바뀔 여지가 없도록 할 수 있음.

final 변수를 클래스 상속과 오버라이드로 내부적으로 _이름 바꾸기_가 가능

클래스 상속과 오버라이드로 final 변수의 수정이 가능하다.

public class Person {
  private final String name;

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

  public String getName() {
      return name;
  }
}

public class NewPerson extends Person {
    private String newName;

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

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

    public String getName() { //override
        return this.newName;
}
Person person = new NewPerson("messi");
System.out.println(person.getName()); // messi

NewPerson newPerson = (NewPerson) person;
newPerson.setName("wow");

System.out.println(person.getName()); // wow
  • 클래스 'NewPerson'는 'Person'을 상속받아 클래스를 정의하고, 생성자의 파라미터로 이름(name)을 받을 수 있도록 하였다.
  • 'getName' 메서드를 오버라이드하고 'newName'에 저장하여 리턴한다.
  • Person 객체를 상속한 NewPerson 객체를 생성하고, 타입 캐스팅을 활용하여 'Person' 클래스의 이름을 'wow'로 바꾼 후 출력하면 바뀐 이름이 출력된다.
  • 결과적으로 Person 클래스의 name은 final 이지만, 상속받은 클래스의 setter 메서드에 의해 값이 변하였다.
  • 이를 방지하려면 'final' 키워드를 클래스 앞에 붙여서, 클래스를 상속할 수 없게하는 방법이 있다.

'final' 클래스 내부에 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;
    }
}
public class Person {
  private final String name;
  private final RGB rgb; // RGB is mutable

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

  public String getName() {
      return name;
  }
  public RGB getRGB() {
      return rgb;
  }
}

위 코드에서 RGB rgb를 final로 선언했더라도, 완전히 immutable하다고 볼 수 없다.

RGB green = new RGB(0, 128, 0);
Person person = new Person("messi", green);
System.out.println(person.getRGB().g); // 128
green.g = 0;
System.out.println(person.getRGB().g); // 0

위 코드를 실행하면, 결국 rgb의 값을 바꿀 수 있게 된다. 이는 왜냐하면 RGB rgb는 객체를 가르키는 레퍼런스 변수이기 때문에, 영향을 받을 수 있기 때문이다.
따라서 다음과 같이 수정을 해야 한다.

public class Person {
  private final String name;
  private final RGB rgb; // RGB is mutable

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

  public String getName() {
      return name;
  }
  public RGB getRGB() {
      return rgb;
  }
}

하지만 이 코드도 완벽하지 않다. 아래 코드를 실행하면 RGB rgb 객체를 수정할 수 있다.

RGB green = new RGB(0, 128, 0);
Person person = new Person("messi", green);
System.out.println(person.getRGB().g); // 128
RGB myRGB = person.getRGB();
myRGB.g = 0;
System.out.println(person.getRGB().g); // 0

getRGB가 레퍼런스 변수를 리턴하기 때문이다. 따라서 getRGB 메서드를 아래와 같이 수정해야한다.

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

위와 같은 수정을 방어적 복사라고 한다.

완전한 불변 객체 만들기

  • 상태 변경 메서드 제거(setter)
  • 모든 필드를 private final로 지정
  • 클래스의 상속을 금지하기
  • mutable 객체 레퍼런스를 공유하지 않기
  • 기존 값 그대로 리턴하지 말고, _새로운 객체를 생성하여 값을 새 객체의 필드에 할당_해야 함 -> 방어적 복사
  • 아에 레퍼런스 객체 또한 immutable로 만든다면 방어적 복사를 할 필요가 없다.

리스트(Set, Array ...)객체 형을 필드로 가질 때 방어적 복사를 하는 방법.

  • 리스트는 값이 아니라 _주소 정보_를 가진다.
  • 방어적 복사를 하려면, _원본 객체를 변경하지 않고 새 객체를 생성 후 새로운 객체에 값을 복사_한다.
  • 따라서 리스트를 방어적 복사를 하기 위해서는 _새로운 리스트를 만든 후, 새로운 객체를 복사하여 그 새로운 객체를 새 리스트에 추가_한 후, 최종적으로 새로운 리스트를 리턴해야 한다.
public class Person {
    private final String name;
    private final List<RGB> rgbs;// RGB is mutable

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

    public String getName() {
        return name;
    }

    public RGB getRGBs() {
        return copy(rgbs);
    }

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

얕은 복사와 깊은 복사

얕은 복사(Shallow Copy):

int[] originalArray = {1, 2, 3};
int[] shallowCopy = Arrays.copyOf(originalArray, originalArray.length);

originalArray[0] = 10;

System.out.println("Original array: " + Arrays.toString(originalArray));
System.out.println("Shallow copy: " + Arrays.toString(shallowCopy));
Original array: [10, 2, 3]
Shallow copy: [10, 2, 3]
  • Arrays.copyOf는 얕은 복사를 수행한다. 또한 new ArrayList<>(originalArray);도 얕은 복사이다.
  • 얕은 복사는 원래 객체를 참조하는 객체를 만든다.
  • 따라서 참조하는 객체의 변경이 이루어지면, 얕은 복사를 한 객체도 영향을 받는다.

깊은 복사(Deep Copy)

class Point {
    int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

public class Example {
    public static void main(String[] args) {
        Point originalPoint = new Point(5, 7);
        Point deepCopy = new Point(originalPoint.x, originalPoint.y); // Deep copy

        originalPoint.x = 10;

        System.out.println("Original point: (" + originalPoint.x + ", " + originalPoint.y + ")");
        System.out.println("Deep copy: (" + deepCopy.x + ", " + deepCopy.y + ")");
    }
}
Original point: (10, 7)
Deep copy: (5, 7)
  • 깊은 복사는 복사하려는 객체와 똑같은 데이터를 가지고 새로운 객체를 생성하는 것이다.
  • 새로운 객체를 만들었기 때문에 메모리 주소가 다르고, 참조하는 객체의 수정에도 영향을 받지 않는다.

불변 객체 의미 차이?

  • 외부에 노출되는 상태 정보는 immutable 하지만, 내부에서만 관리되는 상태는 mutable한 경우에도 immutable 객체라고 부르기도 한다. 이 때 thread-safe하지 않을 수 있다. 왜냐하면 여러 thread들이 상태를 바꾸다 보면 race-condition이 발생할 수 있기 때문이다.
  • 파이썬에서 불변 객체는 'immutable container가 mutable 객체를 가지고 있을 때 mutable 객체의 값이 바뀌어도 이 immutable container는 immputable이라고 여겨진다'라고 명시되어 있다.
  • java에 불변 객체의 개념보다 훨씬 더 열려있는 개념이며, 파이썬의 튜플(tuple)은 느슨한 개념을 대표하는 예시다.

참고