일급컬렉션의 불변객체, unmodifiable, 방어적 복사

June·2022년 2월 20일
2

우테코

목록 보기
13/84
post-thumbnail

학습 동기 1

        Cars cars = new Cars();
        String[] carNames = InputView.requestCarNames().split(",");
        for (String carName : carNames) {
            Car car = new Car(carName);
            cars.participateInRacing(car);
        }
        return cars;

Car 객체들을 모두 생성하고 new Cars(cars)와 같이 생성하면 Cars 객체를 불변 객체로 만들 수 있지 않을까요?

네 맞네요! 일급 컬렉션을 쓰는 이유에서 불변 객체 장점을 놓쳤네요. 감사합니다!

일급컬렉션을 써야하는 이유에 대해서는 위의 블로그에서 정리가 잘되어 있어 간단하게만 요약한다.

  1. 비즈니스 규칙을 담은 자료구조를 만들 수 있다.
  2. 컬렉션의 불변을 보장할 수 있다.
  3. 상태와 행위를 한 곳에서 관리 가능하다.
  4. 컬렉션에 이름을 붙일 수 있다.

1차 수정한 코드

    public Cars(List<Car> cars) {
        this.cars = cars;
    }
    ...
    
    public List<Car> getParticipantCars() {
        return Collections.unmodifiableList(cars);
    }

두 가지 질문을 드릴 수 있을 거 같아요.

  • 만약 생성자에서 넘어온 cars를 외부에서 수정하면 어떤 일이 생길까요?
  • unmodifiableList()는 불변일까요?

방어적 복사와 불변에 대한 글을 읽어보세요.

불변이란

불변 객체는 생성된 후 값이 변하지 않는 객체를 말한다.
final을 붙이면 불변일까?

final은 정확히는 한번만 선언될 수 있음을 나타내는 키워드이다. 즉 재할당이 불가능한 것이다.

원시 데이터일 때는 재할당이 불가능하므로 변경이 불가능한 것과 같지만, 객체를 참조하고 있을 때는 다르다. 속성은 변경 가능하다. immutable 하지 않다는 뜻이다.

불변이란 속성 변경이 불가능해야하는 것이다.

불변 객체를 사용해야 하는 이유

1. Thread-safe하다

멀티스레딩 환경에서 여러 스레드가 접근해서 값이 변경하다면 객체의 상태가 훼손된다. 다른 스레드에서 안심하고 쓸 수 없다.

2. 예측하지 못한 부작용이 줄어든다.

다른 곳에서 변경이 불가능하니 안심하고 사용할 수 있다.

불변 객체를 만드는 방법

  1. 객체의 상태를 변경할 수 있는 메서드를 제공하지 않는다. (ex. setter)
  2. 클래스를 확장할 수 없게 한다.
  3. 클래스를 final 선언
  4. 생성자 private 선언
  5. 모든 필드를 final로 선언
  6. 모든 필드를 private으로 선언
  7. 자신 외에 내부의 가변 컴포넌트에 접근할 수 없게 한다.

참고: https://velog.io/@injoon2019/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-17.-%EB%B3%80%EA%B2%BD-%EA%B0%80%EB%8A%A5%EC%84%B1%EC%9D%84-%EC%B5%9C%EC%86%8C%ED%99%94%ED%95%98

현재 코드에서 문제점 1

RacingController

    private Cars enrollCars() {
        List<Car> cars = new ArrayList<>();
        String[] carNames = InputView.requestCarNames();
        for (String carName : carNames) {
            cars.add(new Car(carName));
        }

        return new Cars(cars);
    }

Cars

public class Cars {

    private final List<Car> cars;

    public Cars(List<Car> cars) {
        //this.cars = new ArrayList<>(cars);
        this.cars = cars;
    }
    ...
}

ListCars의 생성자 인자로 전달해주고 있다. 하지만 문제는 외부(여기서는 List)에서 add를 해도 Cars에도 추가가 된다.. 참조가 그대로 유지되고 있기 때문이다.

방어적 복사

생성자의 인자로 받은 객체의 복사본을 만들어 내부 필드를 초기화하거나,
getter메서드에서 내부의 객체를 반환할 때, 객체의 복사본을 만들어 반환하는 것이다.

방어적 복사를 사용할 경우, 외부에서 객체를 변경해도 내부의 객체는 변경되지 않는다.

public class Cars {

    private final List<Car> cars;

    public Cars(List<Car> cars) {
        this.cars = new ArrayList<>(cars);
    }
    ...

이제 방어적 복사를 사용하는 경우를 살펴보자.

생성자에서 인자를 받으면서 new ArrayList<>()를 이용해 만든 복사본으로, 필드 cars 를 초기화하였다.

위의 테스트는 통과가 된다. 즉 testCars에서 추가를 해도 cars 내부의 cars에는 영향을 미치지 못한다.

new ArrayList<>()를 이용해 원본과의 주소 공유를 끊어냈기 때문이다.

그럼 이거 깊은 복사인가요?

깊은 복사가 아니다. 만약 깊은 복사였다면 내부 element 하나하나가 새로 만들어졌어야한다.

위에서 보다시피, 원본 testCars에서 요소의 속성을 바꾸면 cars에도 영향을 미친다. 컬렉션의 주소만 공유하지 않을 뿐, 내부 요소들의 주소는 그대로 사용하기 때문이다.

Unmodifiable Collection

기존 코드에는 unmodifiableList를 반환한다.
테스트를 위해 final 선언이 되어 있던 Name 속성에서 final을 지웠고
Car에는 setter 역할을하는 changeName()을 임시로 만들었다.

    public List<Car> getParticipantCars() {
        //return new ArrayList<>(cars);
        return Collections.unmodifiableList(cars);
    }

Cars에서 내부 컬렉션을 반환하는 getParticipantCars()이다.

실제로 받아쓰는 쪽에서 add를 하려고하니 에러가 난다.

하지만 마찬가지로 외부에서 변경을 하면 변경이 일어난다. unmodifiableList는 원본 객체와의 참조를 끊지 않기 때문에 외부에서 변경을 하면 영향을 미친다.

UnmodifiableImmutable은 다르다. Unmodifiable이라는 키워드가 불변을 보장해주지는 않는다.

원본 자체에 대한 수정이 일어나면 unmodifiableList() 메서드를 통해 리턴되었던 리스트 또한 변경이 일어난다.

그래서?

그러면 방어적 복사Unmodifiable Collection 각각을 언제 어떻게 사용해야 할까?

생성자의 인자로 객체를 받았을 때

외부에서 넘겨줬던 객체를 변경해도 내부의 객체는 변하지 않아야 한다.

따라서 방어적 복사가 적절하다.

getter를 통해 객체를 리턴할 때

이 상황에선 방어적 복사를 통해 복사본을 반환해도 좋고, Unmodifiable Collection을 이용한 값을 반환하는 것도 좋다.

남은 궁금증

블로그 정리를 하면서 추가적인 궁금증이 생겨서 질문 남깁니다.

setter를 닫고, 속성들에 final을 기본으로 붙여주고 거기에 이어 원천적으로 각 element들도 immutable로 만들어주기 위해 깊은 복사를 하는 것은 너무 비효율적일까요?

깊은 복사를 하게 되면 어떻게 할지 방법에 대해서 먼저 고민을 해야합니다.
이 경우에 성능적으로나 코드 작성상으로 깊은 복사를 해서 얻는 이득이 많지 않죠.

대신, 객체를 불변객체로 만드는 팀 내 룰을 정할 순 있을 것 같네요. 처음부터 객체가 변하지 않으면 깊은 복사를 할 이유도 없지 않을까요?

그리고 방어적 복사 등의 방법은 혹시 모를 실수를 방지하는 차원에서 사용하는 겁니다. 이를 넘어서는 범위는 같이 코드를 작성하는 팀원을 믿어도 되지 않을까 싶어요 😃

방어적 복사와 Unmodifiable Collection

추가

new

new를 통한 복사는 기존 파라미터로 넘어온 것과 참조를 끊어준다. 물론 참조만 끊어졌지 가변이다.

copyOf

copyOf는 참조를 끊지않고 immutable로 만들어준다.

static <E> List<E> copyOf(Collection<? extends E> coll) {
    return ImmutableCollections.listCopy(coll);
}

불변으로 만들어준다.

unmodifiableList

add, remove를 호출하면 예외가 발생하지만 원본과의 참조를 끊지는 않는다. 그래서 불변은 아니다.

List.of() vs Arrays.asList()

List.of()는 자바9부터 지원한다.

0개의 댓글