아래 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
public class Cars {
private List<Car> cars;
public Cars(List<Car> cars) {
this.cars = cars;
}
public Collection<Car> getCars() {
return Collections.unmodifiableCollection(cars);
}
}
Car를 저장하는 일급 컬렉션 객체이다.
이 컬렉션을 가지고 오기 위해 getCars()라는 메서드를 만들었고, 불변 컬렉션을 반환하도록 하였다.
그리고 컬렉션에 담겨있는 Car 클래스를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Car implements Comparable<Car> {
private RandomUtil randomUtil;
private String name;
private Integer moveDistance;
public Car(String name, Integer moveDistance, RandomUtil randomUtil) {
this.name = name;
this.moveDistance = moveDistance;
this.randomUtil = randomUtil;
}
public void move() {
if (canMove()) {
this.moveDistance += MOVE_DISTANCE;
}
}
/...
}
Car 내부에 Car의 상태를 변경할 수 있는 메서드가 존재한다.
그렇다면 Cars를 불변 컬렉션으로 만들었으니 내부의 Car도 바꿀 수 없을까?
이 포스트에서 다룬 내용이다. 처음 객체를 만들었을 때 메모리 주소와, 불변 컬렉션으로 반환한 컬렉션 내부에 있는 같은 객체의 메모리 주소가 동일하다.
즉 복사한 조회용 값이 원본의 메모리 주소를 향하고 있는 것이다. 따라서 복사한 조회용 값임에도 그 값을 변경하면 원본 객체의 데이터도 바뀌게 되는 문제가 발생한다.
해결 방법
위와 같은 문제를 막기 위해서는 getter 메서드를 호출해 복사한 조회용 값이 원본의 메모리 주소를 향하지 않도록, 원본과 내용만 같은 새로운 객체를 만들어주어야 한다.
1
2
3
4
5
6
public Car move() {
if (canMove()) {
return new Car(this.name, this.moveDistance + MOVE_DISTANCE);
}
return this;
}
그런데 아래 한 가지 의문이 들었습니다.
이렇게 move()가 호출될 때 마다 객체를 새로 생성하면 메모리 문제 같은 것은 없을까?
만약 사용자가 시도 횟수에 매우 큰 수를 입력한다면, 그 수 만큼 move() 메서드가 실행되면서 그 때 마다 새 객체가 생성되는데 문제가 없을까?
찾아보니 문제가 충분히 발생할 수 있다고 한다.
이럴 때는 아래의 내용들을 고려해보아야 한다고 한다.
- 가비지 컬렉터 오버헤드 문제
- 빈번한 개체 생성 및 삭제는 가비지 컬렉터에 부담을 준다.
- 객체 생성 오버헤드 문제
- 새 객체를 생성하면 메모리 할당 및 초기화를 포함하여 일부 오버헤드가 발생할 수 있다. 최신 JVM은 최적화가 되어있지만 그럼에도 과도한 객체 생성은 문제가 될 수 있다.
- 불변 객체의 이점
- 각 상태 변경에 대해 새로운 불변 인스턴스를 생성하는 위와 같은 방식은 불변 객체를 다룰 때 일반적인 패턴이다.
- 이러한 불변 객체는 상태에 대한 추론을 단순화하는 장점 그리고 동시성 및 스레드 안정성에도 도움을 줄 수 있다.
즉 빈번한 상태 변경이 예상되어 메모리 또는 성능에 대해 우려되는 시나리오에서는 개발자가 불변성과 기타 최적화 기술의 균형을 판단하여 잘 사용해야 한다는 것이다.
결론은 아직 저에게 어려운 부분인 것 같습니다. 이러한 부분은 충분한 경험이 쌓이면 해결할 수 있을 것이라고 생각합니다. 명확한 답은 아직 내릴 수 없을 것 같습니다.