티스토리 뷰

불변객체와 캐싱

불변 인스턴스의 정보는 고정되어 생성~파괴까지 값이 달라지지 않는다.

불변 객체의 다섯가지 규칙

  1. 객체의 상태를 변경하는 메서드(변경자)를 제공하지 않는다.
  2. 클래스를 확장할 수 없도록 한다.
    불변 객체를 상속받지 못하게 하려면 모든 생성자를 private 혹은 package-private으로 설정하고, public 정적 메소드를 제공할 수 있다.
  3. 모든 필드를 final 선언한다.
  4. 모든 필드를 private 선언한다.
  5. 자신 외에는 내부의 가변 컴포넌트에 접근할 수 없도록 한다.

불변객체의 장점

근본적으로 스레드 안전하며 따로 동기화할 필요가 없다. 불변 객체 자체로 실패 원자성을 제공하기 때문이다. 안심하고 공유할 수 있다. 이것은 방어적 복사를 사용하지 않아도 됨을 의미한다. 가변 클래스보다 구현하고 사용하기 쉬우며 오류의 여지가 적고 안전하다.

불변객체의 타입을 사용하나 값이 변경되어야 할 경우?

간단하다! 새로운 값을 가진 객체를 생성하면 된다. 메모리와 시간을 많이 사용할 수 있다는 단점이 될 수 있지만 특수한 경우(객체생성에 지나치게 많은 메모리를 사용하거나 시간이 오래걸리는 경우)를 제외하고는 이 단점을 넘어서는 장점이 강력하다.

캐싱하여 사용하기

매번 같은 값을 가지는 객체를 자주 참조해야한다면 같은 값을 가지는 불변 객체를 계속 생성해 내는 것은 낭비가 된다. 이럴 때, 캐싱하여 값을 사용할 수 있다. 아래와 같이 Position은 항상 0의 값으로 초기화 된다면, ZERO라는 상수 값(public static final)으로 캐싱해놓고 사용이 가능하다.

public static final Position ZERO = new Position(0);

정리 : 불변객체가 되어야 한다.

클래스는 꼭 필요한 경우가 아니라면 불변으로 만들어야 한다. 불변으로 만들수 없다면, 변경 가능성을 최소화하여야 한다. 객체가 가지는 상태와 변경가능성을 줄여야 훨씬 예측가능한 객체가 된다. 이것은 오류의 가능성이 줄어듦을 의미한다. 가능한 필드는 private final로 설정되어야 한다. 또한 특별한 이유가 없다면 생성자와 정적팩토리메소드 이외에는 초기화 메소드를 public으로 오픈하지 않는다.

왜 private이기까지 해야하는 걸까?

정보 은닉을 하기 위해서 private으로 정보를 감춘다.

✨공개되는 정보의 범위를 축소함으로써 컴포넌트의 개별성을 갖게한다.✨

이것으로 파생되는 장점은 엄청나다. 컴포넌트를 병렬로 개발할 수 있으며, 각 컴포넌트의 파악이 빨라진다. 개별적인 컴포넌트의 성능개선 및 재사용성을 높여준다. 각 컴포넌트를 검증할 수 있게 해준다.

따라서 모든 클래스와 멤버의 접근성을 가능한 좁혀야 하고, 가장 낮은 수준인 private 인 것이 가장 좋다.

길이가 1이상인 배열은 모두 변경 가능하다.

private static final 이라고 해도 길이가 1이상인 배열은 모두 변경가능하다. 주소값의 모음으로 이루어져 있기 때문에 배열은 불변이 이나다. 따라서 배열을 다룰 때 불변의 특성을 주고 싶다면 2가지 방법이 있다.

 

  1. 불변 리스트로 다룬다. (unmodifiableList를 사용)
  2. 방어적 복사를 사용한다. (복사본을 활용)

방어적 복사

방어적 복사는 불변이 아닌 객체에서 사용된다. 객체의 값을 set, get을 할 때, 방어적복사는 외부에서 주입된 객체 정보의 복사본을 사용함으로써 외부와 객체 간의 연결고리를 끊어주는 역할을 한다. 객체의 허락없이 해당 객체의 값을 외부에서 수정하는 일을 허락해서는 안 된다. 책에서는 불변이어 보이지만 불변이 아닐 수 있는 객체인 Date 클래스를 예시로 든다. 하지만 배열의 경우를 예로 들어 정리하겠다.

// src 코드
public class ArrayStudy {
    private static List<String> names;

    public ArrayStudy(List<String> names) {
        this.names = names;
    }

    public List<String> getNames(){
        return names;
    }

}

// test 코드
class ArrayStudyTest {

    @Test
    @DisplayName("배열 값 불변 테스트")
    void arrayTest() {
        List<String> names = new ArrayList<>();
        names.add("nana");
        names.add("gold");

        ArrayStudy arrayStudy = new ArrayStudy(names); // names로 초기화

        names.add("better"); // 외부 배열에 새로운 이름 추가
        assertThat(arrayStudy.getNames()).isEqualTo(Arrays.asList("nana", "gold"));
        // 객체 내부 배열에도 추가됨
    }
    
    /* fail
    expected: ["nana", "gold"]
    but was : ["nana", "gold", "better"]
    */
}

외부 배열 값에 새로운 값을 넣으면 객체 안에 있는 배열에도 값이 추가된다. 따라서 테스트는 실패가 되고 외부의 값이 변경되면 객체 내부의 값도 변경된다.

 

// src 코드
public class ArrayStudy {
    private static List<String> names;

    public ArrayStudy(List<String> names) {
        this.names = new ArrayList<>(names); // 주입받은 값의 복사본으로 셋팅
    }

    public List<String> getNames(){
        return names;
    }

}

// test 코드
class ArrayStudyTest {

    @Test
    @DisplayName("배열 값 불변 테스트")
    void arrayTest() {
        List<String> names = new ArrayList<>();
        names.add("nana");
        names.add("gold");

        ArrayStudy arrayStudy = new ArrayStudy(names);

        names.add("better"); // 외부 배열에 값 추가
        assertThat(arrayStudy.getNames()).isEqualTo(Arrays.asList("nana", "gold"));
        // 객체 내부 배열에는 추가되지 않음
    }
    // pass
}

위와 같이 방어적 복사로 초기화를 해주면 외부의 값과 객체 내부의 값이 분리된다.

class ArrayStudyTest {

    @Test
    @DisplayName("배열 값 불변 테스트")
    void arrayTest() {
        List<String> names = new ArrayList<>();
        names.add("nana");
        names.add("gold");

        ArrayStudy arrayStudy = new ArrayStudy(names);
        arrayStudy.getNames().add("better"); // getName을 하여 값을 추가
        assertThat(arrayStudy.getNames()).isEqualTo(Arrays.asList("nana", "gold"));
        // 객체 내부 배열에도 값이 추가됨
    }
    /* fail
    expected: ["nana", "gold"]
    but was : ["nana", "gold", "better"]
    */
}

위와 같은 src 코드로 셋팅을 방어적 복사를 해주어도 getName에서 방어적 복사를 사용하지 않으면 외부에서 값을 넣으면 또 객체 내부 값에 값이 들어간다.

 

// src 코드
public class ArrayStudy {
    private static List<String> names;

    public ArrayStudy(List<String> names) {
        this.names = new ArrayList<>(names); // 초기화에 방어적 복사
    }

    public List<String> getNames(){
        return new ArrayList<>(names); // getter에도 방어적 복사
    }

}

// test 코드
class ArrayStudyTest {

    @Test
    @DisplayName("배열 값 불변 테스트")
    void arrayTest() {
        List<String> names = new ArrayList<>();
        names.add("nana");
        names.add("gold");

        ArrayStudy arrayStudy = new ArrayStudy(names);
        arrayStudy.getNames().add("better"); // getName하여 값을 추가
        assertThat(arrayStudy.getNames()).isEqualTo(Arrays.asList("nana", "gold"));
        // 하지만 객체 내부의 배열 값에는 영향을 주지 않음
    }
    // pass
}

이렇게 getter에도 방어적 복사를 사용해야 외부에서 값을 변경해도 객체 내부의 값에 영향을 미치지 않는다. 그렇다면 방어적 복사와 unmodifiableList의 차이는 무엇일까?

// src 코드
public class ArrayStudy {
    private static List<String> names;

    public ArrayStudy(List<String> names) {
        this.names = new ArrayList<>(names);
    }

    public List<String> getNames(){
        return Collections.unmodifiableList(names); // unmodifiableList로 불변처리
    }

}

// test 코드
class ArrayStudyTest {

    @Test
    @DisplayName("배열 값 불변 테스트")
    void arrayTest() {
        List<String> names = new ArrayList<>();
        names.add("nana");
        names.add("gold");

        ArrayStudy arrayStudy = new ArrayStudy(names);
        arrayStudy.getNames().add("better"); // 여기서 exception발생 add 자체가 안 됨
        assertThat(arrayStudy.getNames()).isEqualTo(Arrays.asList("nana", "gold"));
    }
}

unmodifiableList로 처리해주면 add 자체가 되지 않는다. unmodifiableList는 불변을 반환한다. 하지만 방어적 복사는 외부와 객체 내부의 연결고리를 끊어주는 것으로 그 자체가 불변을 의미하는 것은 아니다. 이 둘은 이런 점에서 다른 목적성을 가지고 있다. 

 

방어적 복사에서의 유효성 체크

방어적 복사를 사용할 때는 복사본에 대해서 유효성을 검사한다. 검사~외부의 값을 복사 사이에 공격에 위험할 수 있기 때문이다. 

 

마무리

길이가 1 이상인 배열은 무조건 가변이다. 따라서 객체 내부에서 사용하는 배열을 반환할 때는 항상 방어적 복사를 해서 외부와의 연결고리를 끊어주어야 한다.


정리

예측가능한 객체관리를 위해서 불변 객체로 만드는 것이 중요하다. 더불어 왜 정보은닉을 해야하는지도 살펴보았다. 예측 가능한 객체로 만드는 것이 오류 발생도를 낮추고 관리하기에 훨씬 편리해진다. 해당 내용과 관련하여 배열을 예시로 들어 방어적 복사 내용까지 정리를 해보았다. 불변과 방어적복사가 같은 내용은 아니지만, 외부에서 조작 값에 영향을 받지 않게 한다는 것에서 공통점을 가진다. 

참고

이펙티브 자바(책) - 아이템 15, 17, 50

우아한테크코스 수업

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함