프로그래밍 언어/JAVA

[JAVA] immutable 객체?방어적 복사?

딤섬뮨 2022. 11. 15. 13:16
728x90

 

immutable 객체?방어적 복사?

immutable한 객체...?? 클린코드 교육을 들으며 정말 많이 들었지만 무언가 겉핥기로 아는 것 같아서 정리해보려고 한다!!!!

Immuable object 정의

객체 지향 프로그래밍에 있어서 불변객체(immutable object)는 생성 후 그 상태를 바꿀 수 없는 객체를 말한다. 반대 개념으로는 가변(mutable) 객체로 생성 후에도 상태를 변경할 수 있다. 객체 전체가 불변인 것도 있고, C++에서 const 데이터 멤버를 사용하는 경우와 같이 일부 속성만 불변인 것도 있다. 또, 경우에 따라서는 내부에서 사용하는 속성이 변화해도 외부에서 그 객체의 상태가 변하지 않은 것 처럼 보인다면 불변 객체로 보기도 한다. 예를 들어, 비용이 큰 계산의 결과를 캐시하기 위해 메모이제이션(Memoization)을 이용하더라도 그 객체는 여전히 불변하다고 볼 수있다. 불변 객체의 초기 상태는 대개 생성 시에 결정되지만 객체가 실제로 사용되는 순간까지 늦추기도 한다.
불변 객체를 사용하면 복제나 비교를 위한 조작을 단순화 할 수 있고, 성능 개선에도 도움을 준다. 하지만 객체가 변경 가능한 데이터를 많이 가지고 있는 경우엔 불변이 오히려 부적절한 경우가 있다. 이 때문에 많은 프로그래밍 언어에서는 불변이나 가변 중 하나를 선택할 수 있도록 하고 있다.
-위키백과

 

 

1. 객체가 한번 생성되면 안에 있는 값을 변경할 수 없다는 말이다.

+ 추가 : 불변 객체는 멀티 스레드 환경에서도 안전하게 사용할 수 있다는 신뢰성을 보장하며, 대표적인 불변 객체로 String 등이 존재한다. 이외에도 프로그래머가 커스텀 객체를 생성하여 내부 상태가 변경되지 않게 만들면, 그것도 불변 객체가 된다.

 

String,Integer,Boolean도 불변 객체이다

String name = "sienna"

name = "Alice"

이런식으로 값을 바꿔도 값이 바뀌는게 아니라 재할당되는 것

즉 Alice라는 새로운 객체를 만들고 , 그 객체를 name이 참조하게끔 하는 것이다

class Car {
   public string name;
   public int position;
    
   public Car(string name, int position) {
    	this.age = age;
        this.name = name;
    }
    
   public void move(){
   	this.position += 1
   }
}

위의 코드는 불변객체가 아니다. move()만 봐도 생성된 객체의 position을 1 증가시킬 수 있기 때문이다.

class Car {
   private final string name;
   private final int position;
    
   public Car(string name, int position) {
    	this.age = age;
        this.name = name;
    }

}

위 코드는 final로 선언했기에 , 불변 객체가된다.

 

 

불변객체 만드는 법?

필드가 모두 원시타입인 경우 : 원시타입은 참조값이 존재하지 않기 때문에  값을 그대로 외부로 내보내는 경우에도 내부 객체는 불변이므로 setter가 없고 final로 설정했다면 불변 객체

참조타입인 경우 :

class Car {
   private final string name;
   private final Position position;
    
   public Car(string name, Position position) {
    	this.age = age;
        this.name = name;
    }

}

 만약 원시값을 포장해서  다음과 같이 Position이란 reference Type이 있다면 불변 객체일까?

 Position안에 있는 필드들이 가변이라면 결국 Car도 가변 객체가 된다.

 만약 원시값을 포장해서  다음과 같이 Position이란 reference Type이 있다면 불변 객체일까?

 Position안에 있는 필드들이 가변이라면 결국 Car도 가변 객체가 된다.

 

reference type collection인 경우:

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 = monthlyMileages;
    }

    // 필요하다면 getter만 사용. setter는 금지
}

monthlyMileages.add(1000);

이렇게 조작 가능하다.

어떻게? 이것은 처음에 Car 객체의 생성자로 monthlyMileages를 넘길 때 주소가 공유되기 때문에 발생한 것이다.

따라서 생성자로 monthlyMileages를 그냥 넘겨 주면 안 되고 방어적 복사를 거치고 넘겨야 한다.

 

방어적 복사가 먼데?

방어적 복사는 new ArrayList<>() 와 같이 메모리를 새로 할당하여 기존 List와의 참조 주소를 끊어 내는 복사를 말한다.

간단히 말해서 생성자의 매개변수로 객체가 넘어온다면 복사본을 만든 뒤 복사본으로 검증을 해준다.

다음과 같이 주소가 공유되지 않도록, new ArrayLisr<>()를 하나 생성해서 새로 넘겨준다

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);
    }
}

 

그런데 나중에 monthlyMileages를 반환하는 경우 getter를 써야하는 경우가 있다.만약 이 getter로 받은 monthlyMileages를 변경한다면?

 List<Integer> monthlyMileagesOfCar = car.getMonthlyMileages();
        monthlyMileagesOfCar.add(10000);

이 코드는 상태가 변경이 된다.

생성자에서 방어적 복사를 수행하였으므로 다른 객체는 맞지만, getter로 반환한 monthlyMileages는 여전히 주소를 공유하고 있으므로 mothlyMileageOfCar가 변경되면 원래의 monthlyMileages의 상태가 변경이 된다.

 

그래서 getter도 방어적 복사를 해줘야한다.

public List<Integer> getMonthlyMileages() { return new ArrayList<>(monthlyMileages); }

일급컬렉션이 변수로 있어도 방어적 복사 했으니깐 immutable일까?

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 position;

    public Car(String name, int position) {
        this.name = name;
        this.position = position;
    }
}

다시 말해List<Car> getter도 방어적 복사로 해결했는데 , 안에 있는 Car의 상태를 변경하면 바뀔까?

정답 : 바뀐다.

carList는 불변객체로 참조를 끊은 건 맞지만 ,  그 안에 있는 Car의 참조는 유지된다.

 

왜?? ( 여긴 살짝 어려워서 혼자 이해하고 끄적여보았다 정확하지 않아요)

new ArrayList<>()안에 이유가 있다. 안에 있는 arraycopy()라는 메서드가 요소에 대해 얕은 복사를 수행한다고 한다.

위 메소드를 사용하면 A배열의 요소를 B배열로 복사하되 ,  같은 주소를 가르치게한다.

그래서 새로운 메모리를 할당하긴하는데 , 인자로 들어온 arrayList의 요소들은 같은 주소를 참조한다. 따라서 상태가 이어진다.

 

정리

가변 객체

가변 객체는 Java에서 Class의 인스턴스가 생성된 이후에 내부 상태가 변경 가능한 객체이다. 가변 객체는 멀티 스레드 환경에서 사용하려면 별도의 동기화 처리가 필요하며, 대표적인 가변 객체로 ArrayList, HashMap, StringBuilder, StringBuffer 등이 존재한다. 이외에도 프로그래머가 커스텀 객체를 생성하여 내부 상태를 변경할 수 있게 만든다면, 그것도 가변 객체가 된다.

 

불변 객체

불변 객체는 가변 객체와 반대로 Java에서 Class의 인스턴스가 생성된 이후에 내부 상태를 변경할 수 없는 객체이다. 불변 객체는 멀티 스레드 환경에서도 안전하게 사용할 수 있다는 신뢰성을 보장하며, 대표적인 불변 객체로 String 등이 존재한다. 이외에도 프로그래머가 커스텀 객체를 생성하여 내부 상태가 변경되지 않게 만들면, 그것도 불변 객체가 된다.

 

불변 객체의 장점을 설명하라

  • Thread-safe하여 병렬 프로그래밍에 유용하며, 동기화를 고려하지 않아도 된다.
  • 실패 원자적인(Failure Atomic) 메소드를 만들 수 있다.
  • Cache, Map, Set 등의 요소로 활용하기에 적합하다.
  • 부수 효과(Side Effect)를 피해 오류 가능성을 최소화할 수 있다.
  • 다른 사람이 작성한 함수를 예측 가능하며 안전하게 사용할 수 있다.

불변 객체를 만드는 방법을 설명하라

  • 모든 필드에 대해 final을 설정한다.
  • 필드에 참조 타입이 있을 경우, 해당 객체도 불변성을 보장해야 한다.
  • 필드에 컬렉션이 존재할 경우, 생성자 및 getter에 대해 방어적 복사를 수행해야 한다.

방어적 복사와 Unmodifiable의 차이점은?

방어적 복사는 A 리스트와 B 리스트 사이의 참조를 끊는 행위이지만, Unmodifiable은 참조를 끊지 않고 단순히 특정 리스트에서 요소의 변경이 일어날 경우 예외를 던진다. 그래서 생성자 단계에서 방어적 복사를 취하지 않고, getter에서 Unmodifiable만 취할 경우 초기 생성자로 주입한 컬렉션의 변화가 생기면 불변성이 깨진다.

 

방어적 복사를 사용하면 항상 불변성을 유지하는가?

그렇지 않다. 방어적 복사는 컬렉션의 요소에 대해 얕은 복사를 수행하므로 컬렉션의 참조 타입이 가변 객체라면, 복사하려는 컬렉션의 요소가 변경될 경우 불변성이 깨진다.

[출처]

[Java] 가변 객체 vs 불변 객체 (tistory.com)

728x90

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

JAVA - 스트림 가공하기  (0) 2022.10.06
JAVA - 스트림 생성하기  (0) 2022.10.06
객체 간 협력 - 기본 클래스 구현  (0) 2021.04.11