java deep copy 비교
● 개요
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를 추천한다.