
불변 객체 (Immutable Object)
가변 객체는 Java에서 Class의 인스턴스가 생성된 이후에 내부 상태가 변경 가능한 객체이다.(ex ArrayList, HashMap, StringBuilder, StringBuffer )
그 반대로, 불변 객체는 Java에서 Class의 인스턴스가 생성된 이후에 내부 상태를 변경할 수 없는 객체이다. 불변 객체는 멀티 스레드 환경에서도 안전하게 사용할 수 있다는 신뢰성을 보장하며, 대표적인 불변 객체로 String 등이 존재한다. 이외에도 프로그래머가 커스텀 객체를 생성하여 내부 상태가 변경되지 않게 만들면, 그것도 불변 객체가 된다.
불변 객체 사용시 장점
1. 쓰레드에 안전하여 멀티 쓰레드 환경에서 동기화를 고려하지 않아도 된다.
2. 불변객체를 필드로 사용할 때 방어적 복사가 필요없다.
3. 불변객체는 내부상태가 변경되지 않으므로, Map Key 와 Set 요소로 사용하기에 적합하다.
4. 불변객체를 한번 메모리에 할당하게 되면 같은 객체를 계속 호출하여도, 새롭게 할당하지 않아도 되므로 GC 의 성능을 높힐 수 있다.
불변 객체를 만드는 방법
불변 객체는 Setter 메서드를 제공하지 않으며, 객체의 내부 상태를 알려주는 메소드를 제공하지 않거나 제공할 경우 방어적 복사 혹은 Unmodified 라이브러리를 통해 제공한다.
또한, 객체의 필드는 모두 final을 사용하여 처음 할당된 이후 상태가 바뀌지 않도록 설정해야 한다. 다만 무조건 final만 필드에 붙인다고 해당 객체를 불변 객체라고 부를 수 있는 것은 아니다. 객체가 가질 수 있는 상태의 종류에 따라 불변 객체를 만드는 방법이 다르다.
필드가 모두 primitive type(원시 타입) 인 경우
public class Car {
private final String name;
private final int engine;
public Car(String name, int engine) {
this.name = name;
this.position = engine;
}
}
원시 타입은 참조 값이 존재하지 않기 때문에 값을 그대로 외부로 내보내는 경우에도 내부 객체는 불변이므로
setter가 없고, 원시 타입 필드에 대해 final을 설정하였다면 해당 객체인 Car는 불변 객체가 된다.
필드에 reference type(참조 타입)이 있는 경우
public class Car {
private final String name;
private final Engine engine;
public Car(String name, Engine engine) {
this.name = name;
this.engine = engine;
}
}
engine 필드를 포장하여 참조 객체로 수정한 경우이다. 이때 Engine 필드에 단순히 final만 붙이면 Car 객체는 불변 객체가 되는 것처럼 보이지만 그렇지 않다.
public class Engine {
private String brand;
public Engine(String brand) {
this.brand = brand;
}
public void setBrand(String brand) {
this.brand = brand;
}
}
Engine 이 불변 객체가 아니라면, Car 객체의 getEngine() 메소드를 통해 Engine을 가져 오고 Engine객체의 setValue() 메소드를 사용하면 Engine 의 상태를 바꿀 수 있기 때문이다. 이 말은 즉, Car 객체가 불변객체가 이나라는 의미로 필드에 참조 타입이 있는 경우, 해당 참조 타입 객체도 불변객체로 만들어야 한다.
필드에 불변 reference type 컬렉션이 있는 경우
public class Car {
private final String name;
private final int engine;
private final List<Integer> tires;
public Car(String name, int engine, List<Integer> tires) {
this.name = name;
this.engine = engine;
this.tires = tires;
}
public List<Integer> getMonthlyMileages() {
return monthlyMileages;
}
}
Car객체에 tires 필드를 추가했다. 필드에 final을 추가하고, setter를 사용하지 않으니까 불변 객체라고 생각할 수 있다.
그러나 생성자로 tires를 그냥 넘겨 주면 안 되고 방어적 복사를 거치고 넘겨야 한다.
public class Car {
private final String name;
private final int engine;
private final List<Integer> tires;
public Car(String name, int engine, List<Integer> tires) {
this.name = name;
this.engine = engine;
this.tires = new ArrayList<>(tires);
}
public List<Integer> getMonthlyMileages() {
return new ArrayList<>(monthlyMileages);
}
}
방어적 복사는 new ArrayList<>() 와 같이 메모리를 새로 할당하여 기존 List와의 참조 주소를 끊어 내는 복사를 말한다.
생성자에서뿐만 아니라 car의 getter에 대해서도 방어적 복사를 취해야 한다.
참고로 getter에서는 방어적 복사 외에 Collections.unmodifiableList() 를 사용하는 방법도 있다. 이 메소드는 요소의 수정이 발생하면 예외를 던짐으로써 상태의 변화를 막는다.
public class Car {
private final String name;
private final int position;
private final List<Integer> monthlyMileages;
public Car(String name, int position, List<Integer> monthlyMileages) {
this.name = name;
this.position = position;
this.monthlyMileages = new ArrayList<>(monthlyMileages);
}
public List<Integer> getMonthlyMileages() {
return Collections.unmodifiableList(monthlyMileages);
}
}
Collections.unmodifiableList() 는 예외 핸들링을 추가로 해 주어야 하므로 getter에서도 방어적 복사를 수행하는 것이 좋다고 한다.
방어적 복사와 Unmodifiable의 차이점은?
방어적 복사는 A 리스트와 B 리스트 사이의 참조를 끊는 행위이지만, Unmodifiable은 참조를 끊지 않고 단순히 특정 리스트에서 요소의 변경이 일어날 경우 예외를 던진다. 그래서 생성자 단계에서 방어적 복사를 취하지 않고, getter에서 Unmodifiable만 취할 경우 초기 생성자로 주입한 컬렉션의 변화가 생기면 불변성이 깨진다.
필드에 가변 reference type 컬렉션이 있는 경우
public class Cars {
private final List<Car> cars;
public Cars(List<Car> cars) {
this.cars = new ArrayList<>(cars);
}
public List<Car> getCars() {
return new ArrayList<>(cars);
}
}
public class Car {
private final String name;
public int engine;
public Car(String name, int engine) {
this.name = name;
this.engine = engine;
}
}
이 경우도 마찬가지다. 참조 타입 컬렉션을 필드로 사용할 때는 방어적 복사를 맹신하지 말고, 참조 타입 자체를 불변 객체로 보장해 주어야 한다.
정리하면
- 모든 필드에 대해 final을 설정한다.
- 필드에 참조 타입이 있을 경우, 해당 객체도 불변성을 보장해야 한다.
- 필드에 컬렉션이 존재할 경우, 생성자 및 getter에 대해 방어적 복사를 수행해야 한다.
참고
'Java' 카테고리의 다른 글
| [ Java ] 정적 팩토리 메서드 (Static factory method) (0) | 2022.12.22 |
|---|---|
| [ Java ] Checked Exception, Unchecked Exception (0) | 2022.12.04 |
| [Java] Optional (0) | 2022.12.02 |
| [Java] 함수형 인터페이스 (0) | 2022.12.02 |
| [ Java ] Enum (0) | 2022.11.23 |