프로그래밍을 하다 보면 컬렉션, 그중에서도 List를 참 많이 다루게 된다. 특히 리스트 안의 데이터가 비즈니스 로직적으로 중요해서 일급 컬렉션으로 다루는 경우 신경 써서 리스트 안의 데이터를 관리해야 한다.
일급 컬렉션 안에 데이터를 조회해야할 일이 생기는 경우 원본 데이터를 아래와 같이 바로 리턴하면 데이터를 보호할 수가 없다.
public class MemberRepository {
private final List<Member> members = new ArrayList<>();
public List<Member> getMembers() {
return members;
}
}
getMembers()를 호출한 클라이언트 쪽에서 리스트에 데이터를 변경하면 잘못된 정보가 전달돼버리고 만다. 그래서 원본이 아닌 복사 데이터를 넘겨줄 필요가 있다.
리스트 복사 실험 세팅하기
public class MemberRepository {
private final List<Member> members = Stream.of("lee", "dong", "kyu")
.map(Member::new)
.collect(Collectors.toList());
public List<Member> getOriginalMembers() {
return members;
}
public List<Member> getCopiedMembers() {
return 복사해서 전달하기
}
}
회원 리스트를 관리하는 클래스가 있다. 원래는 외부에서 데이터를 받아와야겠지만 예시의 단순화를 위해 필드에서 초기화를 했다. getOriginalMembers()는 원본 데이터를 그대로 전달하고 getCopiedMembers()는 실제로 쓰일 수 있는 복사 전달을 구현할 것이다.
public static void main(String[] args) {
MemberRepository memberRepository = new MemberRepository();
List<Member> originalMembers = memberRepository.getOriginalMembers();
List<Member> copiedMembers = memberRepository.getCopiedMembers();
/** 데이터 변경 발생 **/
// ...
/** ============ **/
System.out.println("원본 데이터 = " + originalMembers);
System.out.println("복사 데이터 = " + copiedMembers);
}
이처럼 원본 리스트와 복사 리스트를 가져온 뒤 데이터 변경을 발생시키고 원본이 어떻게 변화하는지 살펴볼 예정이다.
방법 1. new ArrayList<>()에 인자로 전달하기
public List<Member> getCopiedMembers() {
return new ArrayList<>(members);
}
이처럼 새로운 ArrayList를 생성하며 인자로 원본 리스트를 담으면 원본 리스트와는 다른 참조를 갖는 리스트에 데이터가 담겨 반환된다. 이러면 복사된 리스트가 변형되어도 원본 리스트는 바뀌지 않는다.
/** 데이터 변경 발생 **/
copiedMembers.remove(2);
copiedMembers.set(1, new Member("replacedMember"));
copiedMembers.add(new Member("addMember"));
/** ============ **/
이처럼 복사된 리스트의 리스트는 변경이 일어났으나 원본은 그대로다. 하지만 문제는 있다. 원본이 그대로라고 할지라도 변경된 리스트가 원본이라고 착각할 가능성이 있기 때문에 반환한 값이 변경되는 것을 아예 막을 필요가 있다. 리스트를 조회 전용으로 만드는 것이다.
방법2 Collections.unmodifiableList()
public List<Member> getCopiedMembers() {
return Collections.unmodifiableList(members);
}
unmodifiableList()를 사용하면 원본 리스트의 수정할 수 없는 리스트를 반환한다. (add, remove, set, sort 등 온갖 수정을 막는다.) 반환된 리스트를 수정하려고 하면 UnsupportedOperationException이 발생한다.
하지만 이 복사된 리스트가 원본 리스트와 참조를 아예 끊은 것은 아니다. 수정을 할 수 없을 뿐 참조는 같기 때문에 만약 원본 데이터가 수정이 일어나면 복사 리스트도 같이 수정된다.
originalMembers.add(new Member("addMember"));
현재 예제는 복사된 리스트가 원본 리스트를 그대로 반영해야 하기 때문에 원본의 변경에 복사 리스트의 변경이 자연스러울 수도 있다. 하지만 참조를 완전히 끊는 복사를 하고 싶을 경우 이 방법은 애플리케이션에 버그를 가져올 수도 있다. 원본과 참조를 아예 끊으려면 방법1과 방법2를 함께 사용하면 된다.
public List<Member> getCopiedMembers() {
return Collections.unmodifiableList(new ArrayList<>(members));
}
그러면 원본 데이터를 변경해도 복사된 데이터는 그대로다.
하지만 이 방법을 사용하면 인텔리제이가 노란 줄을 표시한다.
커서를 대고 설명을 읽어보면 List.copyOf()로 대체할 수 있다고 한다.
방법3. List.copyOf()
public List<Member> getCopiedMembers() {
return List.copyOf(members);
}
unmodifiableList()와 비슷하게 주어진 리스트의 수정 불가능한 리스트를 반환한다. 하지만 다른 점이 있다면 원본 리스트가 변경된다 하더라도 반환된 복사 리스트는 반영되지 않는다는 점이다. 원본 데이터에 아까와 같이 Member를 추가해도
사진과 같이 복사된 데이터는 그대로다.
List.copyOf()는 또 다른 특징이 있는데 null 값이 포함되는 걸 허용하지 않는다.
members.add(null);
return List.copyOf(members);
이처럼 null이 포함된 컬렉션을 사용하면 NullPointerException이 발생한다.
문제점
그럼 반환된 데이터가 변경되지 않길 원하면서 원본 데이터의 모습을 그대로 반영하고 싶을 땐 Collections.unmodifiableList()를 사용하고 원본과 참조를 아예 끊고 싶으면 List.copyOf()를 사용하면 되는 것인가? 하지만 문제가 하나 더 남아있다. 수정이 불가하고 컬렉션의 참조가 달라졌다고는 하지만 컬렉션이 담고 있는 데이터(Member)의 참조는 원본과 같다.
Member member = copiedMembers.get(0);
member.setName("nameChangedMember");
때문에 List.copyOf()를 사용하더라도 다음과 같은 수정이 일어나면
사진과 같이 복사는 물론 원본 리스트에도 변경이 일어난다.
데이터를 담는 그릇까지는 변경 불가하게 막을 수는 있으나 데이터 자체가 변경에 열려있다면 완벽한 불변은 보장해줄 수가 없다. 내부 객체의 변경까지 막고 싶다면 내부 객체를 불변으로 만들어야 할 것이다.
'JAVA' 카테고리의 다른 글
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |
---|---|
[AssertJ] Iterable and array assertions 활용 (컬렉션 테스트) (0) | 2022.03.03 |
자바 JDK, JRE, JVM 간단 정리 (0) | 2022.02.21 |
자바 스트림 사용 정리 (0) | 2022.02.20 |
자바 코드로 살펴보는 원시 값 포장에 대해 (0) | 2022.02.17 |