자바에서 객체를 복사하는 방법은 크게 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)로 나뉩니다. 이 둘은 객체의 참조 관계를 다르게 처리하므로 상황에 따라 신중하게 사용해야 합니다.
1. 얕은 복사 (Shallow Copy)란?
얕은 복사는 객체의 참조 값(주소)만을 복사합니다. 즉, 복사된 객체와 원본 객체는 동일한 참조를 공유하게 됩니다. 복사된 객체를 통해 변경이 이루어지면 원본 객체에도 영향을 미칩니다.
- 얕은 복사 관련 예시 코드
class User {
private String name;
public User(String name) {
this.name = name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{name='" + name + "'}";
}
}
public class ShallowCopyExample {
public static void main(String[] args) {
User user = new User("Edward");
User copy = user; // 얕은 복사: 참조값만 복사됨
System.out.println("Before change:");
System.out.println("Original user: " + user);
System.out.println("Copied user: " + copy);
copy.setName("Joshua"); // 복사본을 변경
System.out.println("\nAfter change:");
System.out.println("Original user: " + user);
System.out.println("Copied user: " + copy);
}
}
/*
// 출력 결과
Before change:
Original user: User{name='Edward'}
Copied user: User{name='Edward'}
After change:
Original user: User{name='Joshua'}
Copied user: User{name='Joshua'}
*/
User copy = user;
는 객체의 참조 값을 복사한 것입니다.copy
와user
는 동일한 객체를 참조하므로,copy
를 통해 객체를 변경하면user
도 영향을 받습니다.
2. 깊은 복사 (Deep Copy)란?
깊은 복사는 객체와 그 내부의 모든 객체를 새롭게 복사하는 방법입니다. 복사된 객체는 원본 객체와 완전히 독립된 객체가 됩니다. 따라서 복사본을 변경해도 원본에는 영향을 주지 않습니다.
- 깊은 복사 관련 예시 코드
class Author {
private String name;
public Author(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Author{name='" + name + "'}";
}
}
class Book {
private String title;
private Author author;
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
// 깊은 복사 메서드
public Book deepCopy() {
Author copiedAuthor = new Author(this.author.getName()); // 내부 객체도 새로 복사
return new Book(this.title, copiedAuthor);
}
public void changeAuthor(String name) {
this.author.setName(name);
}
@Override
public String toString() {
return "Book{title='" + title + "', author=" + author + "}";
}
}
public class DeepCopyExample {
public static void main(String[] args) {
Author author = new Author("Joshua Bloch");
Book book1 = new Book("Effective Java", author);
Book deepCopyBook = book1.deepCopy(); // 깊은 복사 실행
deepCopyBook.changeAuthor("Martin Fowler");
System.out.println("Original book: " + book1);
System.out.println("Deep copied book: " + deepCopyBook);
}
}
/*
// 출력 결과
Original book: Book{title='Effective Java', author=Author{name='Joshua Bloch'}}
Deep copied book: Book{title='Effective Java', author=Author{name='Martin Fowler'}}
*/
deepCopy()
는 새로운Author
객체를 생성하고 복사본Book
에 할당합니다.- 복사본
deepCopyBook
의author
를 변경해도 원본book1
에는 영향을 미치지 않습니다.
2.1 깊은 복사 구현 방법
1) 복사 생성자
- 개념
- 복사 생성자는 동일 클래스의 다른 객체를 입력으로 받아 복사본을 만드는 생성자입니다.
- 객체의 내부 필드 중 참조형 필드도 새롭게 복사해야 완전한 깊은 복사가 됩니다.
- 장점
- 구현이 명확하고 직관적입니다.
- 코드가 읽기 쉬워 유지보수에 용이합니다.
- 단점
- 클래스마다 일일이 복사 생성자를 만들어야 하므로 코드 중복이 발생할 수 있습니다.
- 클래스의 구조가 복잡하면 깊은 복사 로직도 복잡해질 수 있습니다.
- 복사 생성자를 이용한 깊은 복사 구현 예제 코드
class Author {
private String name;
public Author(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Author{name='" + name + "'}";
}
}
class Book {
private String title;
private Author author;
// 기본 생성자
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
// 복사 생성자
public Book(Book other) {
this.title = other.title; // 기본형 필드는 그대로 복사
this.author = new Author(other.author.getName()); // 참조형 필드는 새 객체 생성
}
public void changeAuthor(String name) {
this.author.setName(name);
}
@Override
public String toString() {
return "Book{title='" + title + "', author=" + author + "}";
}
}
public class CopyConstructorExample {
public static void main(String[] args) {
Author author = new Author("Joshua Bloch");
Book originalBook = new Book("Effective Java", author);
// 복사 생성자를 이용한 깊은 복사
Book copiedBook = new Book(originalBook);
copiedBook.changeAuthor("Martin Fowler");
System.out.println("Original book: " + originalBook);
System.out.println("Copied book: " + copiedBook);
}
}
/*
// 출력 결과
Original book: Book{title='Effective Java', author=Author{name='Joshua Bloch'}}
Copied book: Book{title='Effective Java', author=Author{name='Martin Fowler'}}
*/
2) 팩토리 메서드
- 개념
- 팩토리 메서드는 객체를 복제하는 기능을 제공하는 메서드입니다.
- 생성자를 사용하지 않고, 메서드를 통해 복사된 객체를 반환합니다.
- 장점
- 복사 로직을 외부 메서드로 분리할 수 있어 가독성이 높아집니다.
- 유지보수가 용이하고, 복사 과정의 변경이 필요하면 메서드만 수정하면 됩니다.
- 단점
- 클래스의 복사 메서드를 직접 정의해야 하므로 코드가 길어질 수 있습니다.
- 팩토리 메서드를 이용한 깊은 복사 구현 예제 코드
class Book {
private String title;
private Author author;
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
// 깊은 복사를 위한 팩터리 메서드
public Book deepCopy() {
return new Book(this.title, new Author(this.author.getName())); // 내부 객체도 복사
}
public void changeAuthor(String name) {
this.author.setName(name);
}
@Override
public String toString() {
return "Book{title='" + title + "', author=" + author + "}";
}
}
public class CopyFactoryMethodExample {
public static void main(String[] args) {
Book originalBook = new Book("Effective Java", new Author("Joshua Bloch"));
Book copiedBook = originalBook.deepCopy(); // 팩토리 메서드로 깊은 복사
// 복사 생성자를 이용한 깊은 복사
copiedBook.changeAuthor("Martin Fowler");
System.out.println("Original book: " + originalBook);
System.out.println("Copied book: " + copiedBook);
}
}
/*
// 출력 결과
Original book: Book{title='Effective Java', author=Author{name='Joshua Bloch'}}
Copied book: Book{title='Effective Java', author=Author{name='Martin Fowler'}}
*/
참고로 Collections
나 Map
의 경우 이미 복사 팩토리인 copy()
메소드를 구현하고 있습니다.
/**
* Copies all of the elements from one list into another. After the
* operation, the index of each copied element in the destination list
* will be identical to its index in the source list. The destination
* list's size must be greater than or equal to the source list's size.
* If it is greater, the remaining elements in the destination list are
* unaffected. <p>
*
* This method runs in linear time.
*
* @param <T> the class of the objects in the lists
* @param dest The destination list.
* @param src The source list.
* @throws IndexOutOfBoundsException if the destination list is too small
* to contain the entire source List.
* @throws UnsupportedOperationException if the destination list's
* list-iterator does not support the {@code set} operation.
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}
3) Serializable을 통한 복사
- 개념
- 객체를 직렬화(Serialization)하고 다시 역직렬화(Deserialization)하면 객체의 복사본이 생성됩니다.
- 모든 객체가 직렬화 가능해야 하므로
Serializable
인터페이스를 구현해야 합니다.
- 장점
- 코드 수정 없이 모든 객체를 복사할 수 있습니다.
- 복잡한 객체 구조도 깊은 복사가 가능합니다.
- 단점
- 성능 오버헤드가 발생합니다. (직렬화 및 역직렬화 과정이 비용이 많이 듭니다.)
- 클래스가
Serializable
을 구현해야 하며, 필드가 직렬화 가능한 객체여야 합니다.
- Serializable을 이용한 깊은 복사 구현 예제 코드
import java.io.*;
class Author implements Serializable {
private String name;
public Author(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public String toString() {
return "Author{name='" + name + "'}";
}
}
class Book implements Serializable {
private String title;
private Author author;
public Book(String title, Author author) {
this.title = title;
this.author = author;
}
// Serializable을 이용한 깊은 복사
public Book deepCopy() {
try {
// 객체 직렬화
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(this);
// 객체 역직렬화
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream in = new ObjectInputStream(bis);
return (Book) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return "Book{title='" + title + "', author=" + author + "}";
}
}
public class SerializationExample {
public static void main(String[] args) {
Book originalBook = new Book("Effective Java", new Author("Joshua Bloch"));
// 직렬화를 통한 깊은 복사
Book copiedBook = originalBook.deepCopy();
System.out.println("Original book: " + originalBook);
System.out.println("Copied book: " + copiedBook);
}
}
/*
// 출력 결과
Original book: Book{title='Effective Java', author=Author{name='Joshua Bloch'}}
Copied book: Book{title='Effective Java', author=Author{name='Joshua Bloch'}}
*/
3. 얕은 복사와 깊은 복사 비교
구분 | 얕은 복사 (Shallow Copy) | 깊은 복사 (Deep Copy) |
---|---|---|
복사 방식 | 참조 값(주소)만 복사 | 객체와 내부 객체까지 모두 새로 복사 |
원본과의 관계 | 원본 객체와 복사본 객체가 같은 객체를 참조 | 원본 객체와 복사본 객체는 완전히 독립적 |
성능 | 빠르지만 원본 객체에 영향을 줄 수 있음 | 느리지만 안전하게 원본과 분리됨 |
사용 사례 | 간단한 객체나 불변 객체 | 객체 내부에 다른 객체를 포함하고 있을 경우 |
4. 객체 형태의 따른 깊은 복사 주의사항
- 내부 객체 참조: 깊은 복사를 구현할 때는 객체 내부의 모든 중첩된 객체를 새로 복사해야 합니다.
- 성능 문제: 깊은 복사는 새로운 객체를 생성하므로 메모리 사용량이 증가하고 속도가 느려질 수 있습니다.
Cloneable
인터페이스:clone()
메서드를 사용하려면Cloneable
인터페이스를 구현해야 합니다. 그러나Cloneable
은 다음과 같은 문제점이 있습니다clone()
메서드는Object
클래스에 protected로 선언되어 있습니다.Cloneable
을 구현하는 것만으로는clone()
호출이 불가능합니다.- Effective Java에서는
Cloneable
을 확장하지 말고 복사 생성자나 팩토리 메서드를 사용할 것을 권장합니다.
4.1 Effective JAVA 13장: clone 재정의는 주의해서 진행하라
- 새로운 클래스를 설계할 때는
Cloneable
을 구현하지 않는 것이 좋습니다. - 복사 기능은 생성자나 팩터리 메서드를 이용하는 것이 더 안전하고 직관적입니다.
- 배열만은
clone()
방식이 예외적으로 효율적입니다.
5. 결론
- 얕은 복사는 참조만 복사하므로 간단하지만 원본 객체에 영향을 줄 수 있습니다.
- 깊은 복사는 독립된 객체를 생성하므로 안전하지만 성능에 주의해야 합니다.
- 복사 기능을 구현할 때는 Cloneable 인터페이스 대신 복사 생성자나 팩토리 메서드를 사용하는 것이 권장됩니다.
'BackEnd > JAVA' 카테고리의 다른 글
[JAVA] 익명 클래스 (Anonymous Class) 정리 (1) | 2025.02.20 |
---|---|
[JAVA] Lombok의 @UtilityClass 어노테이션 정리 (1) | 2025.02.13 |
[JAVA] 와일드카드 (Generic Wildcards) 정리 (1) | 2025.02.05 |
[JAVA] 제네릭 (Generic) 정리 (1) | 2025.01.26 |
[JAVA] 스레드 (Thread) 정리 (2) | 2025.01.25 |