라이브러리

java deep copy 비교

탄생 2022. 5. 19. 21:09

● 개요

deep copy의 종류와 장단점을 확인 하고 실행시간을 체크한다.


● git 주소

https://bitbucket.org/technology-team/deep-copy/src/master/


● deep copy 종류

더 많은 라이브러리가 deep copy지원하지만 개인적으로 자주 사용하는 라이브러리로 추려보았다.

java 지원 deep copy library를 deep copy
- Cloneable Interface
- Copy Constructor
- Copy Factory
- lombok
- ObjectMapper

deep copy 시나리오

account안에 company object가 존재하며 deep copy 실행 후 account와 company가 모두 deep copy가 되는지 확인한다.

 

1. Cloneable Interface

 

- Cloneable 을 상속받아 clone() 메소드를 재정의하여 구현한다.
- 하위의 object까지 deep copy가 되지 않는다. (company의 deep copy를 따로 해주어야 한다.)
 추천되지 않는 방법이며 Copy Constructor 나 Copy Factory 사용을 권장한다.
https://kkimsangheon.github.io/2019/03/31/effective/

public final class CloneableAccountDto {

    private CloneableAccountDto() {}

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Account implements Cloneable {

        private int id;

        private String name;

        private Company company;

        @Override
        public Account clone() throws CloneNotSupportedException {
            return (Account) super.clone();
        }
    }

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Company implements Cloneable {

        private int id;

        private String name;

        private String address;

        @Override
        public Company clone() throws CloneNotSupportedException {
            return (Company) super.clone();
        }
    }
}

-----

@Slf4j
public final class DeepCopy {

    public static void main(String[] args) throws CloneNotSupportedException {
        final var account = Account.builder()
                                   .id(1)
                                   .name("name")
                                   .company(Company.builder().id(1).name("companyName").build())
                                   .build();
        final var cloneAccount = account.clone();
        cloneAccount.setCompany(account.getCompany().clone());
        
        cloneAccount.setName("changeAccount");
        cloneAccount.getCompany().setName("changeCompanyName");

        log.info(account.toString());
        log.info(cloneAccount.toString());
    }
}

◆ 실행결과

20:19:34.827 [main] INFO net.suby.deepcopy.cloneable.DeepCopy - CloneableAccountDto.Account(id=1, name=name, company=CloneableAccountDto.Company(id=1, name=companyName, address=null))
20:19:34.830 [main] INFO net.suby.deepcopy.cloneable.DeepCopy - CloneableAccountDto.Account(id=1, name=changeAccount, company=CloneableAccountDto.Company(id=1, name=changeCompanyName, address=null))

 

2. Copy Constructor, Copy Factory

- Copy Constructor, Copy Factory 는 어떻게 메소드를 구성하느냐의 차이만 있을 뿐이다.

- 생성자에 자기자신의 객체를 파라미터로 받아 필드를 재정의한다.

- 하위의 object도 deep copy 되도록 설정해야 하위 object(company)까지 deep copy 된다.

public final class ConstructorAccountDto {

    private ConstructorAccountDto() {}

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Account {

        // Copy factory
        public static Account newInstance(Account student) {
            return new Account(student);
        }
        
        private int id;
        private String name;
        private Company company;

	// Copy Constructor
        public Account(Account account) {
            id = account.getId();
            name = account.getName();
            company = new Company(account.getCompany());
        }

    }

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Company {

        private int id;

        private String name;

        private String address;

        public Company(Company company) {
            id = company.getId();
            name = company.getName();
            address = company.getAddress();
        }
    }
}


----------

@Slf4j
public final class DeepCopy {

    public static void main(String[] args) {
        final var account = Account.builder()
                                   .id(1)
                                   .name("name")
                                   .company(Company.builder().id(1).name("companyName").build())
                                   .build();
        final var cloneAccount = new Account(account);
        cloneAccount.setName("changeAccount");
        cloneAccount.getCompany().setName("changeCompanyName");

        log.info(account.toString());
        log.info(cloneAccount.toString());
    }
}​

 

◆ 실행결과

20:42:49.360 [main] INFO net.suby.deepcopy.constructor.DeepCopy - ConstructorAccountDto.Account(id=1, name=name, company=ConstructorAccountDto.Company(id=1, name=companyName, address=null))
20:42:49.363 [main] INFO net.suby.deepcopy.constructor.DeepCopy - ConstructorAccountDto.Account(id=1, name=changeAccount, company=ConstructorAccountDto.Company(id=1, name=changeCompanyName, address=null))

 

3. Lombok의 ToBuilder를 통한 deep copy

- lombok 라이브러리가 필요하다.

- @Builder(toBuilder = true) 의 annotation을 붙여주면 끝난다. (구현체를 따로 구현하지 않아 편리하다.)

- 하위의 object까지 deep copy가 되지 않는다. (company의 deep copy를 따로 해주어야 한다.)

dependencies {
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
}
public final class ToBuilderAccountDto {

    private ToBuilderAccountDto() {}

    @Setter
    @Getter
    @Builder(toBuilder = true)
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Account {

        private int id;

        private String name;

        private Company company;
    }

    @Setter
    @Getter
    @Builder(toBuilder = true)
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Company {

        private int id;

        private String name;

        private String address;
    }
}

----------

@Slf4j
public final class DeepCopy {

    public static void main(String[] args) {
        final var account = Account.builder()
                                   .id(1)
                                   .name("name")
                                   .company(Company.builder().id(1).name("companyName").build())
                                   .build();
        final var cloneAccount = account.toBuilder().build();
        cloneAccount.setCompany(account.getCompany().toBuilder().build());
        cloneAccount.setName("changeAccount");
        cloneAccount.getCompany().setName("changeCompanyName");

        log.info(account.toString());
        log.info(cloneAccount.toString());
    }
}

◆ 실행결과

20:47:59.844 [main] INFO net.suby.deepcopy.tobuilder.DeepCopy - ToBuilderAccountDto.Account(id=1, name=name, company=ToBuilderAccountDto.Company(id=1, name=companyName, address=null))
20:47:59.847 [main] INFO net.suby.deepcopy.tobuilder.DeepCopy - ToBuilderAccountDto.Account(id=1, name=changeAccount, company=ToBuilderAccountDto.Company(id=1, name=changeCompanyName, address=null))


4. ObjectMapper를 통한 deep copy

- jackson-databind 라이브러리가 필요하다.

- dto에 정의하는 부분이 없다.

- 4.1 mapper 선언

    final ObjectMapper mapper = new ObjectMapper();

  4.2 deep copy 할 객체와 데이터 타입을 선언해 준다.

    final var cloneAccount = mapper.readValue(mapper.writeValueAsString(account), Account.class);

- 유일하게 하위의 object까지 deep copy가 된다. 따로 하위 company를 deep copy하지 않아도 된다.

dependencies {
	implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3'
}
public final class MapperAccountDto {

    private MapperAccountDto() {}

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Account {

        private int id;

        private String name;

        private Company company;
    }

    @Setter
    @Getter
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    public static class Company {

        private int id;

        private String name;

        private String address;
    }
}

----------

@Slf4j
public final class DeepCopy {

    public static void main(String[] args) throws JsonProcessingException {
        final ObjectMapper mapper = new ObjectMapper();
        final var account = Account.builder()
                                   .id(1)
                                   .name("name")
                                   .company(Company.builder().id(1).name("companyName").build())
                                   .build();
        final var cloneAccount = mapper.readValue(mapper.writeValueAsString(account), Account.class);
        cloneAccount.setName("changeAccount");
        cloneAccount.getCompany().setName("changeCompanyName");

        log.info(account.toString());
        log.info(cloneAccount.toString());
    }
}

● deep copy 속도 체크

deep copy를 5,000,000 반복

> Task :SpeedCheck.main()
21:00:08.695 [main] INFO net.suby.deepcopy.SpeedCheck - cloneable execute time : 45
21:00:08.745 [main] INFO net.suby.deepcopy.SpeedCheck - constructor execute time : 45
21:00:08.794 [main] INFO net.suby.deepcopy.SpeedCheck - toBuilder execute time : 48
21:00:13.198 [main] INFO net.suby.deepcopy.SpeedCheck - mapper execute time : 4276

deep copy를 5,000,000 반복

> Task :SpeedCheck.main()
21:02:21.592 [main] INFO net.suby.deepcopy.SpeedCheck - cloneable execute time : 1488
21:02:23.048 [main] INFO net.suby.deepcopy.SpeedCheck - constructor execute time : 1450
21:02:24.376 [main] INFO net.suby.deepcopy.SpeedCheck - toBuilder execute time : 1325


Cloneable Interface, Copy Constructor, Copy Factory, toBuilder는 비슷한 속도가 발생하였지만 데이터가 많아졌을 경우 toBuilder의 속도가 더 빠르게 처리되었다. mapper의 경우 처리속도는 굉장히 느린것이 확인되었다.


● 결론

구현방법이나 성능부분을 확인했을때 Copy Constructor, Copy Factory, toBuilder 셋 중 하나를 활용하여 deep copy 를 구현하는 것이 best라고 생각한다. 개인적으로 구현부 없이 어노테이션으로 편리하게 사용할 수 있고 성능도 좋은 toBuilder를 추천한다.