
객체의 복사
Java에서는 객체를 복사하는 방식에 따라 두 가지로 나뉘는데, 이 두 가지 개념을 이해하면 객체를 다룰 때 예상치 못한 부작용을 방지할 수 있다.
얕은 복사 (Shallow Copy)
얕은 복사는 객체의 최상위 레벨 속성만 복사하고, 중첩된 객체는 참조를 복사한다. 따라서 원본 객체와 복사된 객체는 중첩된 객체를 공유하게 된다. 예제로 살펴보면 이해가 쉬울 것이다.
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Person implements Cloneable {
String name;
Address address;
Person(String name, Address address) {
this.name = name;
this.address = address;
}
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 얕은 복사
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("Seoul");
Person person1 = new Person("John", address);
Person person2 = (Person) person1.clone();
person2.address.city = "Busan";
System.out.println(person1.address.city); // 출력: Busan
}
}
Q,객체의 최상위 레벨 속성만 복사한다는 것이 어떤 뜻일까?
객체의 최상위 레벨 속성만 복사한다는 것은, 객체의 직접적인 속성들만 복사하고, 그 속성들이 참조하는 다른 객체들은 복사하지 않는다는 의미이다. 이를 좀 더 구체적으로 설명해보겠다.
위 예시에서 Person 객체는 name과 address라는 두 개의 속성을 가지고 있는데, name은 String 타입의 속성이고, address는 Address 타입의 객체를 참조하는 속성이다.
Person 객체를 얕은 복사하면 name 속성은 새로운 String 객체로 복사되지만, address 속성은 원본 Person 객체와 동일한 Address 객체를 참조하게 된다. 따라서 person2의 address 속성을 변경하면 person1의 address 속성도 영향을 받게 되는 것이다.
name과 address는 Person 객체의 필드이지만, address 는 객체이므로 얕은복사시에 같은 주소를 공유하게 된다. 얕은 복사는 이 최상위 레벨 속성들만 복사한다는 사실을 기억하자. 이처럼 얕은 복사시에 복사되지 않는 객체를 중첩된 객체라고 한다.
깊은 복사(Deep Copy)
깊은 복사는 최상위 레벨 속성뿐만 아니라 중첩된 객체들도 모두 복사한다. 따라서 원본 객체와 복사된 객체는 완전히 독립적인 객체가 된다.
class Address implements Cloneable {
String city;
String street;
int zipCode;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable {
String name;
Address address;
int age;
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // 깊은 복사 수행
return cloned;
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person original = new Person();
original.name = "John";
original.address = new Address();
original.address.city = "New York";
original.address.street = "5th Avenue";
original.address.zipCode = 10001;
original.age = 30;
Person deepCopy = (Person) original.clone(); // 깊은 복사 수행
// deepCopy와 original은 서로 다른 Address 객체를 참조
System.out.println(original.address == deepCopy.address); // false
// deepCopy의 Address 객체를 변경해도 original에는 영향 없음
deepCopy.address.city = "Los Angeles";
System.out.println(original.address.city); // New York
System.out.println(deepCopy.address.city); // Los Angeles
}
}
Q, String 타입은 왜 왜 얕은복사시에도 객체끼리 값을 공유하지 않을까?
String 객체가 얕은 복사에서 다르게 동작하는 이유는 Java의 String이 불변(immutable) 객체이기 때문이다.
불변 객체란 한 번 생성되면 그 상태를 변경할 수 없는 객체를 말하는데, String은 대표적인 불변 객체이다.
String 객체의 값을 변경하려고 하면, 실제로는 새로운 String 객체가 생성되고, 기존 객체는 변경되지 않는다.
얕은 복사에서는 객체의 참조를 복사한다. 하지만 String은 불변 객체이기 때문에, 복사된 참조를 통해 값을 변경할 수 없는 것이다. 따라서 String 객체는 얕은 복사와 깊은 복사의 개념이 크게 의미가 없다.
참고로, StringBuffer 타입으로 name 필드를 사용하면, 얕은 복사를 할 때 clone된 객체의 name 필드를 변경하면 원본 객체의 name 필드도 변경된다. 이는 StringBuffer가 가변(mutable) 객체이기 때문이다. 이는 List나, Set에서도 동일하게 작용한다.
여기서 중요한 부분은 얕은 복사 이슈는 최상위 객체의 필드가 가변 객체일 때 발생한다 라는 사실이다. 따라서 얕은복사 이슈를 피하려면 가변 객체를 참조할 때 주의해야 한다.
동시성 이슈 VS 얕은 복사
이쯤 되면 기존에 알고 있던 필드 공유 이슈 지식과 헷갈리기 시작하는데, 동시성 이슈와 얕은 복사는 뭐가 다를까?
얕은 복사
얕은 복사는 객체의 필드가 복사될 때 참조형 필드가 동일한 객체를 참조하게 되는 문제. 이는 런타임 시점에 발생한다.
필드 공유 이슈: 얕은 복사로 인해 두 객체가 동일한 참조형 필드를 공유하게 되면, 한 객체에서 필드를 변경할 때 다른 객체에도 영향을 미칠 수 있다.
동시성 이슈
동시성 이슈는 여러 스레드가 동시에 동일한 객체나 자원을 접근할 때 발생하는 문제인데, 이는 런타임 시점에 발생한다.
필드 공유 이슈: 동시성 이슈는 여러 스레드가 동일한 필드를 동시에 읽거나 쓸 때 발생할 수 있다. 적절한 동기화가 없으면 데이터 불일치나 예기치 않은 동작이 발생할 수 있다.
정리
정확히 무엇이 다른지 눈치채지 못할 수 있는데 경중을 따지자면 얕은복사 이슈가 좀 더 본질적인 문제를 발생시킨다고 생각하면 된다.
동시성 이슈는 멀티 스레드가 동일한 객체에 접근할때만 발생하지만 (타이밍도 맞아야 한다..)
얕은복사 이슈는 싱글 스레드라도 이전 스레드가 동일한 객체에 접근하여 값을 바꾼다면 경우에도 발생할 수 있다는 것이다.
예를 들어 생각해보자면 얕은 복사의 심각성이 더 잘나타나는데
상황 - 클라이언트가 Person1 객체의 (Address 객체)주소를 요청하는 상황
동시성 이슈 : 클라이언트1이 Person1 객체의 주소를 요청하는 순간에 클라이언트2가 Person1 객체의 주소를 변경하여야 발생함
얕은 복사 : 클라이언트1이 Person1 객체의 주소를 요청하면, 클라이언트1은 클라이언트2가 일주일전에 변경했었던 Person2 객체의 주소를 보게됨
'Java' 카테고리의 다른 글
Gradle - Task (1) | 2025.01.19 |
---|---|
JVM 가비지 컬렉터의 내부 동작 원리 (1) | 2024.09.10 |
JVM 내부 구조와 동작 원리 - Runtime Data Area (1) | 2024.09.07 |
JVM 내부 구조와 동작 원리 - Class Loader, Execution Engine (3) | 2024.09.04 |
Lambda Expression 기초 (3) | 2024.09.02 |

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!