Java Object clone() 메서드 – Java에서의 복제

복제는 객체의 복사본을 생성하는 과정입니다. 자바 Object 클래스에는 기본적으로 clone() 메서드가 제공되며, 기존 인스턴스의 복사본을 반환합니다. 자바에서 Object는 기본 클래스이므로 모든 객체는 기본적으로 복제를 지원합니다.

자바 객체 복제

자바 Object clone() 메서드를 사용하려면 java.lang.Cloneable 마커 인터페이스를 구현해야 합니다. 그렇지 않으면 실행 중에 CloneNotSupportedException 예외가 발생합니다. 또한 Object clone은 protected 메서드이므로 오버라이드해야 합니다. 예제 프로그램으로 자바에서 Object 복제를 살펴보겠습니다.

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class Employee implements Cloneable {

	private int id;

	private String name;

	private Map<String, String> props;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Map<String, String> getProps() {
		return props;
	}

	public void setProps(Map<String, String> p) {
		this.props = p;
	}

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

}

Object clone() 메서드를 사용하므로 Cloneable 인터페이스를 구현했습니다. 상위 클래스의 clone() 메서드인 Object clone() 메서드를 호출합니다.

Object clone() 메서드 사용

테스트 프로그램을 작성하여 객체 clone() 메서드를 사용하여 인스턴스의 사본을 만들어 봅시다.

package com.journaldev.cloning;

import java.util.HashMap;
import java.util.Map;

public class CloningTest {

	public static void main(String[] args) throws CloneNotSupportedException {

		Employee emp = new Employee();

		emp.setId(1);
		emp.setName("Pankaj");
		Map props = new HashMap<>();
		props.put("salary", "10000");
		props.put("city", "Bangalore");
		emp.setProps(props);

		Employee clonedEmp = (Employee) emp.clone();

		// emp와 clonedEmp 속성이 같은지 다른지 확인해 봅시다
		System.out.println("emp and clonedEmp == test: " + (emp == clonedEmp));
		
		System.out.println("emp and clonedEmp HashMap == test: " + (emp.getProps() == clonedEmp.getProps()));
		
		// 기본 복제 사용의 효과를 살펴봅시다
		
		// emp 속성 변경
		emp.getProps().put("title", "CEO");
		emp.getProps().put("city", "New York");
		System.out.println("clonedEmp props:" + clonedEmp.getProps());

		// emp 이름 변경
		emp.setName("new");
		System.out.println("clonedEmp name:" + clonedEmp.getName());

	}

}

출력:

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: true
clonedEmp props:{city=New York, salary=10000, title=CEO}
clonedEmp name:Pankaj

런타임에서 CloneNotSupportedException

만약 우리의 Employee 클래스가 Cloneable 인터페이스를 구현하지 않으면, 위의 프로그램은 CloneNotSupportedException 런타임 예외가 발생할 것입니다.

Exception in thread "main" java.lang.CloneNotSupportedException: com.journaldev.cloning.Employee
	at java.lang.Object.clone(Native Method)
	at com.journaldev.cloning.Employee.clone(Employee.java:41)
	at com.journaldev.cloning.CloningTest.main(CloningTest.java:19)

객체 복제 이해하기

위의 출력을 살펴보고 객체 clone() 메서드의 작동 방식을 이해해 봅시다.

  1. emp and clonedEmp == test: false: 이는 emp와 clonedEmp가 두 개의 다른 객체임을 의미하며, 동일한 객체를 참조하지 않음을 나타냅니다. 이는 자바 객체 복제 요구 사항과 일치합니다.
  2. emp 및 clonedEmp HashMap == test: true: 따라서 emp 및 clonedEmp 객체 변수는 동일한 객체를 참조합니다. 기본 객체 값을 변경하면 심각한 데이터 무결성 문제가 발생할 수 있습니다. 값이 변경되면 cloned 인스턴스에도 반영될 수 있습니다.
  3. clonedEmp props:{city=뉴욕, salary=10000, title=CEO}: clonedEmp 속성을 변경하지 않았지만 emp 및 clonedEmp 변수가 동일한 객체를 참조하기 때문에 변경되었습니다. 이는 기본적으로 Object의 clone() 메서드가 얕은 복사를 생성하기 때문입니다. 복제 프로세스를 통해 완전히 분리된 객체를 생성하려는 경우 문제가 발생할 수 있으므로 Object의 clone() 메서드를 적절히 재정의해야 합니다.
  4. clonedEmp name:Pankaj: 여기서 무슨 일이 일어났을까요? emp 이름을 변경했지만 clonedEmp 이름은 변경되지 않았습니다. 이는 String이 불변하기 때문입니다. 따라서 emp 이름을 설정할 때 새로운 문자열이 생성되고 emp 이름 참조가 this.name = name;로 변경됩니다. 따라서 clonedEmp 이름은 변경되지 않습니다. 원시 변수 유형에 대해서도 유사한 동작을 찾을 수 있습니다. 따라서 객체에 기본적으로 자바 객체 복제를 사용할 때 원시 및 불변 변수만 있는 경우에는 잘 작동합니다.

객체 복제 유형

물체 복제에는 두 가지 유형이 있습니다 – 얕은 복제와 깊은 복제. 각각을 이해하고 자바 프로그램에서 복제를 구현하는 가장 좋은 방법을 찾아보겠습니다.

1. 얕은 복제

자바 Object의 기본 복제 메서드 구현은 얕은 복사를 사용합니다. 이는 인스턴스의 사본을 만들기 위해 리플렉션 API를 사용합니다. 아래 코드 조각은 얕은 복제 구현을 보여줍니다.

@Override
 public Object clone() throws CloneNotSupportedException {
 
	 Employee e = new Employee();
	 e.setId(this.id);
	 e.setName(this.name);
	 e.setProps(this.props);
	 return e;
}

2. 깊은 복제

깊은 복제에서는 필드를 하나씩 복사해야 합니다. List, Map 등과 같은 중첩된 객체가 있는 경우 해당 필드도 하나씩 복사하는 코드를 작성해야 합니다. 이것이 깊은 복제 또는 깊은 복사라고 불리는 이유입니다. 깊은 복제를 위해 다음과 같이 Employee clone 메서드를 재정의할 수 있습니다.

public Object clone() throws CloneNotSupportedException {

	Object obj = super.clone(); //utilize clone Object method

	Employee emp = (Employee) obj;

	// 불변 필드의 깊은 복제
	emp.setProps(null);
	Map hm = new HashMap<>();
	String key;
	Iterator it = this.props.keySet().iterator();
	// 필드별 깊은 복사
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, this.props.get(key));
	}
	emp.setProps(hm);
	
	return emp;
}

이 clone() 메서드를 구현하면 테스트 프로그램은 다음 출력을 생성합니다.

emp and clonedEmp == test: false
emp and clonedEmp HashMap == test: false
clonedEmp props:{city=Bangalore, salary=10000}
clonedEmp name:Pankaj

대부분의 경우, 우리가 원하는 것은 이것입니다. clone() 메소드는 원래 인스턴스와 완전히 분리된 새로운 객체를 반환해야 합니다. 따라서 프로그램에서 Object clone과 클론을 사용하려고 한다면, 가변 필드를 잘 처리하고 적절히 재정의해야 합니다. 만약 클래스가 다른 클래스를 상속하고, 그 클래스가 또 다른 클래스를 상속한다면, 이는 어려운 작업이 될 수 있습니다. 모든 가변 필드의 깊은 복사에 대해 Object 상속 계층 구조 전체를 거쳐가야 할 것입니다.

직렬화를 이용한 복제?

깊은 복제를 쉽게 수행하는 한 가지 방법은 직렬화를 통해 하는 것입니다. 하지만 직렬화는 비용이 많이 드는 절차이며, 클래스는 Serializable 인터페이스를 구현해야 합니다. 모든 필드와 상위 클래스도 Serializable을 구현해야 합니다.

Apache Commons Util 사용하기

프로젝트에서 이미 Apache Commons Util 클래스를 사용하고, 클래스가 직렬화 가능한 경우, 아래의 메소드를 사용하세요.

Employee clonedEmp = org.apache.commons.lang3.SerializationUtils.clone(emp);

복제를 위한 복사 생성자

우리는 객체의 복사본을 생성하기 위한 복사 생성자를 정의할 수 있습니다. 왜 모두 Object clone() 메서드에 의존해야 할까요? 예를 들어, 다음과 같이 Employee 복사 생성자를 가질 수 있습니다.

public Employee(Employee emp) {
	
	this.setId(emp.getId());
	this.setName(emp.getName());
	
	Map hm = new HashMap<>();
	String key;
	Iterator it = emp.getProps().keySet().iterator();
	// 필드별 깊은 복사
	while (it.hasNext()) {
		key = it.next();
		hm.put(key, emp.getProps().get(key));
	}
	this.setProps(hm);

}

사원 객체의 복사본이 필요할 때마다, Employee clonedEmp = new Employee(emp);를 사용하여 얻을 수 있습니다. 그러나 복사 생성자를 작성하는 것은 특히 기본형 및 불변 변수가 많은 경우 귀찮은 작업일 수 있습니다.

Java 객체 복제 최상의 방법

  1. 클래스에 기본형 및 불변 변수가 있는 경우 또는 얕은 복사가 필요한 경우에만 기본 Object clone() 메서드를 사용하십시오. 상속의 경우, Object 수준까지 확장된 모든 클래스를 확인해야 합니다.

  2. 클래스의 대부분이 변경 가능한 속성을 가진 경우 복사 생성자를 정의할 수도 있습니다.

  3. 오버라이드된 clone 메서드에서 super.clone()을 호출하여 Object clone() 메서드를 활용하고, 변경 가능한 필드의 깊은 복사를 위해 필요한 변경 사항을 수행합니다.

  4. 클래스가 직렬화 가능한 경우 직렬화를 사용하여 복제할 수 있습니다. 그러나 성능에 영향을 줄 수 있으므로 직렬화를 복제에 사용하기 전에 일부 벤치마킹을 수행하십시오.

  5. 만약 클래스를 확장하고 깊은 복사를 사용하여 clone 메서드를 올바르게 정의했다면 기본 clone 메서드를 활용할 수 있습니다. 예를 들어, 다음과 같이 Employee 클래스에 올바르게 정의된 clone() 메서드가 있습니다.

    @Override
    public Object clone() throws CloneNotSupportedException {
    
    	Object obj = super.clone();
    
    	Employee emp = (Employee) obj;
    
    	// 불변 필드에 대한 깊은 복제
    	emp.setProps(null);
    	Map<String, String> hm = new HashMap<>();
    	String key;
    	Iterator<String> it = this.props.keySet().iterator();
    	// 필드 별 깊은 복사
    	while (it.hasNext()) {
    		key = it.next();
    		hm.put(key, this.props.get(key));
    	}
    	emp.setProps(hm);
    
    	return emp;
    }
    

    다음과 같이 자식 클래스를 생성하고 상위 클래스의 깊은 복제를 활용할 수 있습니다.

    package com.journaldev.cloning;
    
    public class EmployeeWrap extends Employee implements Cloneable {
    
    	private String title;
    
    	public String getTitle() {
    		return title;
    	}
    
    	public void setTitle(String t) {
    		this.title = t;
    	}
    
    	@Override
    	public Object clone() throws CloneNotSupportedException {
    
    		return super.clone();
    	}
    }
    

    EmployeeWrap 클래스에는 가변 속성이 없으며, 상위 클래스의 clone() 메서드 구현을 활용하고 있습니다. 가변 필드가 있는 경우 해당 필드만 깊은 복사해야 합니다. 다음은 이 방법으로 클론이 잘 작동하는지 테스트하는 간단한 프로그램입니다.

    package com.journaldev.cloning;
    
    import java.util.HashMap;
    import java.util.Map;
    
    public class CloningTest {
    
    	public static void main(String[] args) throws CloneNotSupportedException {
    
    		EmployeeWrap empWrap = new EmployeeWrap();
    
    		empWrap.setId(1);
    		empWrap.setName("Pankaj");
    		empWrap.setTitle("CEO");
    		
    		Map<String, String> props = new HashMap<>();
    		props.put("salary", "10000");
    		props.put("city", "Bangalore");
    		empWrap.setProps(props);
    
    		EmployeeWrap clonedEmpWrap = (EmployeeWrap) empWrap.clone();
    		
    		empWrap.getProps().put("1", "1");
    		
    		System.out.println("empWrap mutable property value = "+empWrap.getProps());
    
    		System.out.println("clonedEmpWrap mutable property value = "+clonedEmpWrap.getProps());
    		
    	}
    
    }
    

    결과:

    empWrap mutable property value = {1=1, city=Bangalore, salary=10000}
    clonedEmpWrap mutable property value = {city=Bangalore, salary=10000}
    

    우리가 예상한 대로 완벽하게 작동했습니다.

자바에서 객체 복제에 관한 모든 내용입니다. Java 객체 clone() 메서드에 대해 어떤 아이디어를 얻었는지와 부작용 없이 제대로 재정의하는 방법에 대해 아마도 이해했을 것입니다.

프로젝트는 내 GitHub 저장소에서 다운로드할 수 있습니다.

참고: Object clone에 대한 API 문서

Source:
https://www.digitalocean.com/community/tutorials/java-clone-object-cloning-java