[Java] Cloneable 상속 없이 깊은 복사(deep copy) 해주는 library 소개

최대한·2021년 12월 28일
0
post-custom-banner

서론


최근 운영업무를 보던 중 dto 객체의 값을 수정해줘야 할 경우가 생겨서 작업을 했는데, 배포 후 사이드 이펙트가 여럿 터지는 이슈가 생겼다. 당연히 해서는 안되는 일이었지만 dto 의 값을 변경하게 되면서 연관되어있던 다른 로직들까지 함께 영향을 받아 이슈가 발생한 것. 당시 빠르게 수정을 하기 위해 기존 dto 를 그대로 두고 ModelMapper 를 이용하여 깊은 복사 후 그 객체의 값을 변경해주려고 하였다.

하지만 웬걸? ModelMapper 를 사용하니 원본객체를 그대로 반환하는게 아니겠는가? 그래서 어쩔 수 없이 cloneable 을 상속받고 clone() 을 통해 원래 새로 추가하고자 했던 로직에 사용함으로써 이슈를 해소했었다.

얕은 복사(shallow copy)깊은 복사(deep copy)는 개발자에게 친숙한 개념이다. (아니라면 유감..) 친숙한 개념이니만큼 이 넓은 지구에 많은 선배 개발자분들께서 이미 쉽게 구현해 놓은 방법이 있을 줄 알았다!
라고 생각해서 여러 글들을 살펴보니, 일일이 생성자를 통해 새로운 객체를 반환, 혹은 Gson 이나 jackson 라이브러리로 직렬화/역직렬화를 통해 깊은 복사를 구현하고 있었다.

이는 필자의 귀차니즘을 씻어주기에는 부족했고.. 더욱 구글링해본 결과 메소드 한번 호출하는 것으로 쉽게 구현해놓은 라이브러리가 있어서 공유하고자 한다.

본론


Libraries (한 세트)

1. Person.java

  • Cloneable 은 수행속도 비교를 위해 상속해줌
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 직업 {
    학생,
    개발자
}

2. 테스트

2.1) ModelMapper 비교
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, 직업=개발자}

2.2) Cloneable, Gson 속도 비교
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
    수행 속도를 보면 단연 cloneable 이 압도적이었고, 그다음 Cloner, Gson 순이었다. 사용하는 dto자신의 프로젝트일 경우 Cloneable 을 상속받아서 깊은 복사를 수행하는 것이 가장 효율적이기 때문에 추천하지만,
    특정 이유로 Cloneable 을 상속받아선 안되거나, 외부 클래스의 객체를 복사할 필요가 있을 경우 Gson 보다는 Cloner 라이브러리를 사용하는 것이 시간 상 효율적이라고 판단된다.

라이브러리에 대한 결론만 내리자면 딱히 코멘트 달 것은 없지만, 라이브러리를 이용해 간단히 객체의 깊은 복사를 할 수 있음을 알게 되었다.

그 이외에 느낀 점으로, 최근 함수형 프로그래밍에 관심을 가지고 공부를 하고 있는데 개발의 안전성을 높히기 위해 불변성이라는 것이 얼마나 중요한지 느끼고 있다. 예전에 롬복의 @Data@Setter 의 사용을 가급적 삼가하라는 조언을 들은 적이 있는데 그때는 왜 그런지 몰랐었지만, 현재 직접 경험을 해 보니 왜 그런지 조금 더 알 수 있었고, 불변성의 중요성에 대해서도 다시금 생각해볼 수 있게 되었다.

profile
Awesome Dev!
post-custom-banner

0개의 댓글