객체 세계인 자바 세상에서 여러분은 객체에 대한 복사를 해보신 적이 있나요?
아마 원시타입과 문자열의 복사의 경우는 많이 해보셨을겁니다.
그렇다면 객체에 대한 복사는 어떻게 처리될까요? 오늘은 이에 대해 알아보겠습니다.
자바 뿐 아니라 수많은 언어에선 복사를 크게 깊은 복사와 얕은 복사로 나눕니다.
여기서 깊은 복사란 원본에 대한 참조를 공유하지 않고 복사하는 것을 말합니다. 즉 바꿔 말하면 수정 사항이 발생할 경우 원본 객체와는 상관없는 다른 객체가 됩니다.
반면에 얕은 복사는 원본에 대한 참조를 공유한 채로 복사하는 것을 의미합니다.
우리가 아는 자바는 어떠한 상황에서 깊은 복사와 얕은 복사를 진행할까요?
원시타입과 불변객체의 복사
/* 원시타입의 복사 */
int origin = 0;
int copy = origin;
origin = 999; //값 수정
System.out.println(origin); //999
System.out.println(copy); //0
/* 불변객체의 복사 */
String origin = "origin";
String copy = origin;
origin = "modify"; //값 수정
System.out.println(origin); //modify
System.out.println(copy); //origin
원시타입은 객체가 아니기 때문에 참조값이 존재하지 않아 원본과 상관없는 변수가 됩니다.
여기서 특이한 점은 String Class입니다.
문자열은 불변객체이므로 원본과 참조를 공유합니다. 따라서 새로운 값을 할당할 때 새로운 객체가 생성됩니다.
때문에 copy 변수는 새로 만들어진 변수가 아닌 원래 객체에 대한 참조가 유지됩니다.
그렇기 때문에 원시타입과 같이 깊은 복사를 하는 것처럼 동작하게 됩니다.
결론적으로 원시타입은 깊은 복사를 수행하고, 불변 객체의 경우 깊은 복사처럼 동작한다라고 이해하시면 되겠습니다.
객체에 대한 복사
일단 이름과 나이 주소를 가지는 Person Class를 만들어보겠습니다.
여러 어노테이션이 붙어있는데 단순히 Getter/Setter 메서드를 만들어주고, 모든 필드에 대한 생성자를 만들어줄 뿐인 단순한 어노테이션입니다.
이때 세번째 멤버변수는 원시타입이 아닌 또 다른 객체 주소를 만들었습니다.
@Getter
@Setter
@AllArgsConstructor
public class Person {
private String name;
private int age;
private Address adress;
@Override
public String toString() {
return "Person ["
+ "name=" + name
+ ", age=" + age
+ ", adress.city=" + adress.getCity()
+ ", adress.street=" + adress.getStreet() + "]";
}
}
@Getter
@Setter
@AllArgsConstructor
public class Address {
private String city;
private String street;
}
Address address = new Address("Seoul", "Sindorim");
Person origin = new Person("Jax", 999, address);
Person copy = origin;
/* 값 수정 */
origin.setName("modify");
origin.setAge(0);
origin.getAdress().setCity("Incheon");
origin.getAdress().setStreet("JungGu");
System.out.println(origin); //Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]
System.out.println(copy); //Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]
위와 같이 기본적으로는 얕은 복사를 합니다.
하지만 자바에선 최상위 객체인 Object Class에서 clone이란 메서드를 지원합니다.
이 clone이란 메서드는 깊은 복사일까요 아님 얕은 복사일까요?
해당 메서드를 오버라이딩하기 위해선 Cloneable Interface를 구현해주어야 합니다.
@Getter
@Setter
@AllArgsConstructor
public class Person implements Cloneable{
...
@Override
protected Person clone(){
Person clonePerson = null;
try {
clonePerson = (Person)super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clonePerson;
}
}
Address address = new Address("Seoul", "Sindorim");
Person origin = new Person("Jax", 999, address);
Person copy = origin.clone(); //clone 메서드 사용
/* 값 수정 */
origin.setName("modify");
origin.setAge(0);
origin.getAdress().setCity("Incheon");
origin.getAdress().setStreet("JungGu");
System.out.println(origin); //Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]
System.out.println(copy); //Person [name=Jax, age=999, adress.city=Incheon, adress.street=JungGu]
값이 좀 이상하죠? 그럼 깊은 복사가 잘 된걸까요?
구체적으로 설명을 드리자면 멤버변수 중 원시 타입은 깊은 복사가 되었고 참조 타입의 경우 얕은 복사가 되었습니다.
소스를 자세히 보시면 주소 객체의 경우는 원본 객체와 복사본 동일하신게 보일겁니다.
clone 메서드는 객체에 대한 필드를 단순히 복사하기만 합니다. 때문에 멤버변수가 객체일 경우 얕은 복사를 진행합니다. 만약 객체가 원시타입의 변수만 있다면 깊은 복사가 됬겠죠.
따라서 clone 메서드는 얕은 복사입니다.
다른 분의 예제를 보고 싶다면 https://stackoverflow.com/questions/5066427/how-to-clone-object-in-java 참고하세요.
배열과 컬렉션에 대한 복사
배열과 컬렉션 또한 객체이기 때문에 기본적으로는 자바는 깊은 복사를 지원하지 않습니다. 그렇다고 깊은 복사를 할수 없는 것은 아니지만 메서드 자체는 모두 얕은 복사를 합니다.
Address address = new Address("Seoul", "Sindorim");
Person person = new Person("Jax", 999, address);
Person[] origin = {person};
/* 배열의 다양한 복사 방법 */
Person[] copy1 = origin.clone();
Person[] copy2 = Arrays.copyOf(origin, origin.length);
Person[] copy3 = Arrays.stream(origin).toArray(Person[]::new);
Person[] copy4 = Arrays.stream(origin).map(p -> p.clone()).toArray(Person[]::new);
/* 수정 */
origin[0].setAge(0);
origin[0].setName("modify");
origin[0].getAdress().setCity("Incheon");
origin[0].getAdress().setStreet("JungGu");
System.out.println(Arrays.toString(origin)); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(Arrays.toString(copy1)); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(Arrays.toString(copy2)); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(Arrays.toString(copy3)); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(Arrays.toString(copy4)); //[Person [name=Jax, age=999, adress.city=Incheon, adress.street=JungGu]]
마지막의 값이 다른 이유는 Person 객체 내부의 clone 메서드를 사용했기 때문에 원시 타입의 경우는 깊은 복사가 됬지만 객체의 경우 원본 객체와 복사본이 같은 값을 가지는 얕은 복사가 수행됬음을 주의하셔야합니다.
정리하자면 어떤 방식을 사용하던 모두 얕은 복사를 합니다.
컬렉션의 경우는 어떨까요? ArrayList를 예시로 살펴봅시다.
Address address = new Address("Seoul", "Sindorim");
Person person = new Person("Jax", 999, address);
ArrayList<Person> origin = new ArrayList<>();
origin.add(person);
/* ArrayList의 다양한 복사방법 */
List<Person> copy1 = (List<Person>) origin.clone();
List<Person> copy2 = new ArrayList<>(origin);
List<Person> copy3 = new ArrayList<>();
copy3.add(null); //Collections.copy는 size가 같아야 한다.
Collections.copy(copy3, origin);
List<Person> copy4 = new ArrayList<>();
copy4.addAll(origin);
List<Person> copy5 = origin.stream().map(p -> p.clone()).collect(Collectors.toList());
/* 값 수정 */
person.setName("modify");
person.setAge(0);
person.getAdress().setCity("Incheon");
person.getAdress().setStreet("JungGu");
System.out.println(copy1); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(copy2); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(copy3); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(copy4); //[Person [name=modify, age=0, adress.city=Incheon, adress.street=JungGu]]
System.out.println(copy5); //[Person [name=Jax, age=999, adress.city=Incheon, adress.street=JungGu]]
배열과 마찬가지로 컬렉션의 경우에도 얕은 복사임을 알 수 있습니다.
그렇다면 객체에 대한 완벽한 깊은 복사는 어떻게 할까요? 생성자를 새로 만들거나 직렬화를 사용하면 깊은 복사가 가능합니다.
이들에 대해 다루기엔 분량이 많기 때문에 다음에 기회가 된다면 객체에 대한 깊은 복사까지 다루어 보겠습니다.
참고
https://www.baeldung.com/java-array-copy
https://www.delftstack.com/ko/howto/java/copy-arraylist-java/
'Java > Grammer' 카테고리의 다른 글
입출력 스트림 (2) - File I/O (0) | 2022.05.24 |
---|---|
입출력 스트림 (1) - System.out.println와 I/O (0) | 2022.05.22 |
객체의 중복/정렬 그리고 Collection (0) | 2022.04.24 |
Simple Data Structure (2) - List/Set/Map (0) | 2022.04.23 |
Simple Data Structure (1) - 공통 메서드 (0) | 2022.04.23 |