최근 운영업무를 보던 중 dto
객체의 값을 수정
해줘야 할 경우가 생겨서 작업을 했는데, 배포 후 사이드 이펙트
가 여럿 터지는 이슈가 생겼다. 당연히 해서는 안되는 일이었지만 dto 의 값을 변경하게 되면서 연관되어있던 다른 로직들까지 함께 영향을 받아 이슈가 발생한 것. 당시 빠르게 수정을 하기 위해 기존 dto 를 그대로 두고 ModelMapper
를 이용하여 깊은 복사 후 그 객체의 값을 변경해주려고 하였다.
하지만 웬걸? ModelMapper 를 사용하니 원본객체를 그대로 반환하는게 아니겠는가? 그래서 어쩔 수 없이 cloneable 을 상속받고 clone() 을 통해 원래 새로 추가하고자 했던 로직에 사용함으로써 이슈를 해소했었다.
얕은 복사(shallow copy)
와 깊은 복사(deep copy)
는 개발자에게 친숙한 개념이다. (아니라면 유감..) 친숙한 개념이니만큼 이 넓은 지구에 많은 선배 개발자분들께서 이미 쉽게 구현해 놓은 방법이 있을 줄 알았다!
라고 생각해서 여러 글들을 살펴보니, 일일이 생성자
를 통해 새로운 객체를 반환, 혹은 Gson
이나 jackson
라이브러리로 직렬화/역직렬화를 통해 깊은 복사를 구현하고 있었다.
이는 필자의 귀차니즘을 씻어주기에는 부족했고.. 더욱 구글링해본 결과 메소드 한번 호출하는 것으로 쉽게 구현해놓은 라이브러리가 있어서 공유하고자 한다.
class Person implements Cloneable {
String name;
int age;
직업 직업;
public Person(String name, int age, 직업 직업) {
this.name = name;
this.age = age;
this.직업 = 직업;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", 직업=" + 직업 +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
enum 직업 {
학생,
개발자
}
public class DeepCopyTest {
static ModelMapper mm = new ModelMapper();
static Cloner cloner = new Cloner();
public static void main(String[] args) {
Person original = new Person("비타맥스", 102, 직업.개발자);
Person cloneByModelMapper = mm.map(original, Person.class);
Person cloneByCloner = cloner.deepClone(original);
print("original == cloneByModelMapper : " + (original == cloneByModelMapper));
print("original == cloneByCloner : " + (original == cloneByCloner));
// 얕은 복사가 된 객체의 값 변경
cloneByModelMapper.age = 50;
cloneByModelMapper.직업 = 직업.학생;
print("original : " + original);
print("cloneByModelMapper : " + cloneByModelMapper);
print("cloneByCloner : " + cloneByCloner);
}
static private void print(String str) {
System.out.println(str);
}
}
original == cloneByModelMapper : true
original == cloneByCloner : false
original : Person{name='비타맥스', age=50, 직업=학생}
cloneByModelMapper : Person{name='비타맥스', age=50, 직업=학생}
cloneByCloner : Person{name='비타맥스', age=102, 직업=개발자}
public class DeepCopyTest2 {
static Cloner cloner = new Cloner();
static Gson gson = new Gson();
public static void main(String[] args) throws Exception {
final int NUMBER_OF_COPY = 5_000_000;
Person original = new Person("비타맥스", 102, 직업.개발자);
// 1. Cloneable 의 clone 수행 시간
long startAt = System.currentTimeMillis();
for (int i = 0; i < NUMBER_OF_COPY; i++) {
original.clone();
}
print("cloneable elapsed time : " + (System.currentTimeMillis() - startAt));
// 2. Cloner 의 clone 수행 시간
startAt = System.currentTimeMillis();
for (int i = 0; i < NUMBER_OF_COPY; i++) {
cloner.deepClone(original);
}
print("cloner elapsed time : " + (System.currentTimeMillis() - startAt));
// 3. Gson 의 clone 수행 시간
startAt = System.currentTimeMillis();
for (int i = 0; i < NUMBER_OF_COPY; i++) {
gson.fromJson(gson.toJson(original), Person.class);
}
print("gson elapsed time : " + (System.currentTimeMillis() - startAt));
}
static private void print(String str) {
System.out.println(str);
}
}
cloneable elapsed time : 66
cloner elapsed time : 813
gson elapsed time : 5386
Cloneable
> Cloner
> Gson
dto
가 자신의 프로젝트
일 경우 Cloneable 을 상속받아서 깊은 복사를 수행하는 것이 가장 효율적이기 때문에 추천하지만,외부 클래스
의 객체를 복사할 필요가 있을 경우 Gson
보다는 Cloner
라이브러리를 사용하는 것이 시간 상 효율적이라고 판단된다.라이브러리에 대한 결론만 내리자면 딱히 코멘트 달 것은 없지만, 라이브러리를 이용해 간단히 객체의 깊은 복사를 할 수 있음을 알게 되었다.
그 이외에 느낀 점으로, 최근 함수형 프로그래밍에 관심을 가지고 공부를 하고 있는데 개발의 안전성을 높히기 위해 불변성
이라는 것이 얼마나 중요한지 느끼고 있다. 예전에 롬복의 @Data
와 @Setter
의 사용을 가급적 삼가하라는 조언을 들은 적이 있는데 그때는 왜 그런지 몰랐었지만, 현재 직접 경험을 해 보니 왜 그런지 조금 더 알 수 있었고, 불변성의 중요성에 대해서도 다시금 생각해볼 수 있게 되었다.