티스토리 뷰

원본코드
값 변화 실험 : Java) Shallow Copy vs Deep Copy (얕은/깊은 복사), unmodifiableList의 값 변화 관찰

좌표계산기 코드를 작성하다가, Jason코치님의 피드백을 받고
원본값을 안전하게 보호하기 위해 3가지 방법을 적용하게 되었다.
(세 기법이 모두 각자의 관점이 다르긴 하지만, 일단은)

  1. Collections.unmodifiableList
  2. Deep Copy
  3. VO(Value Object)

  • 사전지식 : final은 재할당만 막지, 값 조작은 막지못한다.

아래와 같은 컨트롤러 코드가 있다.
원본값인 List를 받아들이고, 이를 조작해서 figure를 만들고 출력한다.

List를 갖고있는 도메인 모델은 다음과 같다.

전형적인 생성자와 전형적인 getter. 단순히 주소값을 그대로 받아 points에 복사하고 있다.

아무 문제없어 보이지만, 다음과 같이 컨트롤러에서
의도적으로 값을 조작해 원본값을 변경할수있다 !!

?!?!?!?!?!?

원래대로라면 (2,2)와 (5,5) 두점을 입력하였으므로 두점만 출력되야하지만,
points를 이용해 figure를 생성한 후에도, points에 값이 추가되며 오염되버린다.


대책

1. Collections.unmodifiableList

getter로 List를 외부에 노출할때, Collections.unmodifiableList로 만들어주면
간단히 리스트의 값 추가(add)/변경(set)/삭제(remove)를 막을수있다.
자세한 내용은 각자 찾아볼것ㄱ

figure.getPoints().add(new Point(5, 2)); // 컨트롤러 19라인을 막아냈다.

2. Deep Copy vs Shallow Copy

참고할만한 자료

깊은복사를 했더니, 컨트롤러에서 여전히 원본값에 (2,5)를 추가해보았지만 더이상 복사값엔 추가되지 않았다.

points.add(new Point(2,5)); // 컨트롤러 18라인을 막아냈다.

기존에는 생성자에서 인자로 들어온 points의 주소값을 그대로 받아서 사용하고 있었다. (shallow copy)
따라서 figure의 points를 건드리지않고, 인자로 사용되었던 원본 points를 조작해도
figure의 points가 변경되었던 것이다.

위의 참고자료를 참고해, 얕은 복사가 아닌 깊은 복사(deep copy)를 하게 되면
새롭게 생성해서 할당하기 때문에, 이전과 같은 문제가 발생하지않고 안전하다.

  • 깊은복사에서도, 오해하기쉬운 '보통 깊은복사'와 '진정한 깊은복사'가 있다.

    이부분은 이글의 초점이 아니므로 생략하고, 해당 링크를 참고한다.

  • 추가로 이부분이 언급되야해서, 전자를 '그냥깊은복사' 후자를 '더깊은복사'로 편의상 부른다.

3. VO (Value Object) (6.16 추가)

끝난줄 알았는데 추가 피드백이 왔다. 메일에 피드백 알림이 뜰땐 언제나 반갑다.
Point 클래스는 사실 클래스 이름부터가 자신을 VO로 만들어주라는 말을 계속 하고있었지만 난 애써 외면했던것 같다,,

이젠 안전한것 같았지만 다음과 같은 문제가 또 있다.
figure의 points에 대한, input경로인 생성자에선 깊은복사로 output경로인 getPoints()에선 unmodifiableList로 이미 리스트를 안전하게 만들었다.
리스트의 값 추가/삭제에 대해선 안전해졌지만, 리스트안에 들어가있는 바로 그 객체에 관해선 과연 안전한가 ?

public class CoordinateCalculator {
    public void run() {
        try {
            Figure figure = FigureFactory.create(InputView.inputCoordinates());

            // 악의적인 행동
            figure.getPoints().get(0).moveRight();

            OutputView.showCoordinatePlane(figure);
            OutputView.showArea(figure);
        } catch (Exception e) {
            System.err.println(e);
        }
    }
}

Point(2,2)가 moveRight 메서드로 인해 (3,2)로 상태가 변경되어 버렸다.
"moveRight메서드를 조심히 사용하면 되는거 아니야?" 물론 내가 의도적으로 바꿔준거긴 하지만,
객체외부에서 저렇게 상태를 바꿀수있다는건 언제든 위험성이 존재한다는 것이다.

따라서 Point를 불변으로 VO로 만들어주기위해,
setter는 없었지만 Point의 상태를 변경할수있는 메서드였던 hasRight를 다음과 같이 바꿨다. (메서드명도 바꿈)

public void moveRight() { this.x++; }
==>
public Point right() { return new Point(x + 1, y); }

자신의 상태를 변경하지않고, 새로운 (x+1,y)값의 객체를 반환하도록 했다.
이제 Point는 한번 생성되어 값이 할당되면 더이상 자신의 상태를 바꾸지않는 VO이다.

하지만 여전히 아까 그 질문에 대한 답은 그대로다. 그래도 내가 조심해서 저렇게 명시적으로 상태를 바꿔주지만 않으면 될것같다. 즉 VO적용유무에 상관없이 원본값-복사값의 영향에 대한 버그는 특별히 없어보인다. 굳이 VO로 바꿔야하나?

VO로 바꾸고난 후의 이점

오른쪽의 의도적인 원본 points값 변경은 복사한 figure의 points에 영향을 주지못함 (다행히 여기까진 안전한데...)
이전에 복사를 할때, 더깊은복사인 copy(points)와 같이 한 이유는
왼쪽처럼 new ArrayList<>(points)로, 그냥깊은복사로 단순히 주소값만 다르게 복사해올경우
오른쪽처럼 원본값의 변경이 복사값에는 영향을 주지 못하지만 (여기까진 안전..)
아래처럼 복사값의 변경이 원본값에 영향을 줄수있기 때문이다. (?!?!?!!!! 분명히 new ArrayList 했는데 ?!)

figure의points는 당연히 (3,2)로 바뀐다. 오해하지말자 unmodifiableList는 리스트의 변경을 막지, 리스트안의 값이 스스로 자신의 속의 상태까지 바꾸는건 어찌할 도리가 없다.
figure의 points는 내가 getPoints().get(0).moveRight()으로 일부러 바꿔주었으니 그렇다 쳐도
콘솔출력으로 확인해보니 원본 points도 (3,2)로 바뀌어있었다.

이런 문제 때문에 단순히 '그냥깊은복사'보다 '더깊은복사'를 했었다.

이때, Point를 VO로 바꾸면 더깊은복사를 하지 않아도 된다. 즉 moveRight와 같은 상태를 바꾸는 메서드가 더이상 존재하지 않으므로, 이제 우리는 Point의 상태변화에 대한 걱정을 하지 않아도 되고, 이에 따라 복사도 스트림과 Clone을 이용해 '더깊은복사'할 필요없이 단순히 new ArrayList로 주소값만 다르게 복사해와도 괜찮게 되었다.

즉, VO로 만드니 객체의 상태변화에 대한 걱정을 할 필요없어졌다. VO를 갖고있는 리스트의 값 조작만 안전하게 하면 된다.


공부과정을 의식의 흐름으로 쓰다보니, 글이 조금 복잡한데
결론은 단순하다. 맨처음에 언급한 '값변화실험'포스팅을 다시한번 읽어보자.

공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함