자바에서 불변(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)은 느슨한 개념을 대표하는 예시다.
참고
'ComputerScience > Java' 카테고리의 다른 글
[Java] Compile Time과 Runtime의 차이 (1) | 2024.03.08 |
---|---|
[Java] Logback - 로깅 라이브러리 (0) | 2024.02.22 |
[Java] JUnit5 (0) | 2024.02.22 |
[Java] Java에서 Queue와 구현체들 (+ ArrayList와 LinkedList의 차이) (0) | 2024.01.31 |
[Java] List와 Set의 차이, 그러면 Set은 어떻게 구현할까요? (0) | 2024.01.29 |