์์ ๋ณต์ฌ(Shallow Copy)
- ๊ฐ์ฒด์ 1์ฐจ ๋ ๋ฒจ ํ๋ ๊ฐ๋ง ์๋ก ๋ด๋๋ค. ์ฐธ์กฐ ํ์
ํ๋๋ ์ฃผ์(์ฐธ์กฐ)๋ง ๋ณต์ฌํ๋ค.
๊น์ ๋ณต์ฌ(Deep Copy)
- ์ค์ฒฉ ๊ฐ์ฒด๊น์ง ์ ์ธ์คํด์ค๋ฅผ ๋ง๋ค์ด ์ ์ฒด ๊ทธ๋ํ๋ฅผ ๋ณต์ ํ๋ค.
๊ฒฐ๊ณผ์ ์ผ๋ก ์์ ๋ณต์ฌ๋ ๋ด๋ถ ์ฐธ์กฐ๊ฐ ๊ณต์ ๋๊ณ ๊น์ ๋ณต์ฌ๋ ๊ณต์ ๋์ง ์๋๋ค. ์๋ฅผ ๋ค์ด ์๋์ ๊ฐ์ ๋๋ฉ์ธ์ด ์์ ๋
class Address {
private String city;
public Address(String city) { this.city = city; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
}
class Person {
private String name; // ๋ถ๋ณ(๋ฌธ์์ด ์์ฒด๋ ๋ถ๋ณ)
private Address address; // ๊ฐ๋ณ ์ฐธ์กฐ ํ๋
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() { return name; }
public Address getAddress() { return address; }
}
์์ ๋ณต์ฌ๋ ์์ฑ์๋ ํฉํ ๋ฆฌ์์ ๋ด๋ถ ์ฐธ์กฐ๋ฅผ ๊ทธ๋๋ก ๋๊ธฐ๋ ๊ฒฝ์ฐ, clone()์ ๊ธฐ๋ณธ ๋์ ๋ฑ์ด ์๋ค.
Person p1 = new Person("Min", new Address("Seoul"));
// ์ ๊ฐ์ฒด๋ฅผ ์์ฑํ์์ง๋ง ๋ด๋ถ ์ฐธ์กฐ ๊ณต์
Person p2 = new Person(p1.getName(), p1.getAddress());
p1.getAddress().setCity("Busan");
System.out.println(p2.getAddress().getCity()); // Busan (๊ฐ์ด ๋ฐ๋)
๋ฐ๋ฉด ๊น์ ๋ณต์ฌ๋ ์ค์ฒฉ ๊ฐ์ฒด๊น์ง ์๋ก ๋ง๋๋ ๊ฒฝ์ฐ์ด๋ค.
Person p1 = new Person("Min", new Address("Seoul"));
// ๊น์ ๋ณต์ฌ
Person p3 = new Person(
p1.getName(),
new Address(p1.getAddress().getCity())
);
p1.getAddress().setCity("Busan");
System.out.println(p3.getAddress().getCity()); // Seoul (์ํฅ ์์)
์๋ฐ์ Cloneable / Object.clone()์ ์ญ์ฌ์ ์ผ๋ก ์ค๊ณ๊ฐ ๋ํดํ๋ค.
- ์์ ๋ณต์ฌ๊ฐ ๊ธฐ๋ณธ super.clone()์ ํ๋ ๋จ์ ๊ฐ ๋ณต์ฌ๋ฅผ ์ํํ๋ค. ์์ ํ์
์ ๊ฐ์ด ๋ณต์ฌ๋๊ณ ์ฐธ์กฐ ํ์
์ ์ฐธ์กฐ๋ง ๋ณต์ฌ๋๋ฏ๋ก ์์ ๋ณต์ฌ๊ฐ ๋๋ค. ๊ฒ๋ค๊ฐ ์์ฑ์๋ฅผ ๊ฑฐ์น์ง ์์ ๋ถ๋ณ์์ด ๊นจ์ง ์ํ์ด ์๋ค.
- ์์ธ/๊ฐ์์ฑ ์ด์
๋จ์ ๋ง์ปค ์ธํฐํ์ด์ค์ธ Cloneable์ ๊ตฌํํ์ง ์์ผ๋ฉด CloneNotSupportedException์ ๋์ง๋ฉฐ ๊ธฐ๋ณธ์ ์ผ๋ก protected๋ผ์ public์ผ๋ก ์ค๋ฒ๋ผ์ด๋ ํ์ง ์์ผ๋ฉด ์ธ๋ถ ์ฝ๋์์ ํธ์ถํ ์ ์๋ค. ๋ํ ํธ์ถํ๋ ์ธก์์๋ ๋ถํ์ํ๊ฒ ์ฒดํฌ ์์ธ๋ฅผ ์ฒ๋ฆฌํด์ผ ํ๋ค.
- ์์ ํธ๋ฆฌ ๋ชจํธ์ฑ
์ด๋ ๋ ๋ฒจ์์ ๊น๊ฒ ํน์ ์๊ฒ ๋ณต์ฌํ ์ง๋ฅผ ๊ฐ์ ํ์ง ์์ ํ์
๋ฐ ํ์ฅ ๊ณผ์ ์์ ์ผ๊ด์ฑ์ด ์ฝ๊ฒ ๊นจ์ง๋ค.
๋ฐ๋ผ์ clone()์ ์ฌ์ฉํ ๋๋ ๋ชจ๋ ๊ฐ๋ณ ์ฐธ์กฐ ํ๋๋ฅผ ์ง์ ๋ณต์ ํด์ผ ๊น์ ๋ณต์ฌ๊ฐ ๋๋ค.
// Cloneable ์์
class Address implements Cloneable {
private String city;
public Address(String city) { this.city = city; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
// public์ผ๋ก ์ค๋ฒ๋ผ์ด๋
@Override
public Address clone() {
// ์์ธ ์ฒ๋ฆฌ
try {
return (Address) super.clone(); // ๋ถ๋ณ/๋ฌธ์์ด ํ๋๋ง ์์ผ๋ฉด ์์
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
class Person implements Cloneable {
private String name;
private Address address; // ๊ฐ๋ณ ์ฐธ์กฐ
public Person(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
public Person clone() {
try {
Person copy = (Person) super.clone(); // ํ๋ ๋จ์ ๋ณต์ฌ
copy.address = address == null ? null : address.clone(); // ๊น์ ๋ณต์ฌ
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
์์ ๊ฐ์ ์ด์ ๋ก ์๋ฐ์์๋ ๊ฐ์ฒด ๋ณต์ฌ ์ ๋ ๊ถ์ฅ๋๋ ๋ฐฉ๋ฒ๋ค์ด ์กด์ฌํ๋ค. ์ฒซ ๋ฒ์งธ๋ก๋ ๋ณต์ฌ ์์ฑ์/์ ์ ํฉํ ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๋ฐฉ์์ด๋ค.
class Person {
private final String name;
private final Address address;
public Person(String name, Address address) {
this.name = name;
this.address = address; // ์ฃผ์: ๊ทธ๋๋ก ๋์ ํ๋ฉด ์์ ๋ณต์ฌ
}
// ๋ณต์ฌ ์์ฑ์ (๊น์ ๋ณต์ฌ)
public Person(Person other) {
this.name = other.name; // String์ ๋ถ๋ณ
this.address = other.address == null ? null : new Address(other.address.getCity());
}
public static Person copyOf(Person other) {
return new Person(other);
}
}
๊ฐ์ฅ ๋ช
์์ ์ด๊ณ ์์ ํ ๋ฐฉ์์ด๋ค.
๋ ๋ฒ์งธ๋ ์ง๋ ฌํ ๊ธฐ๋ฐ์ ๊น์ ๋ณต์ฌ์ด๋ค. Jackson, Gson, Java ์ง๋ ฌํ ๋ฑ์ผ๋ก ๊ฐ์ฒด๋ฅผ ๋ฐ์ดํธ๋ JSON์ผ๋ก ๋ณํํ ๋ค ๋ค์ ์ญ์ง๋ ฌํํ๋ฉด ์๋ก์ด ๊ฐ์ฒด๊ฐ ์์ฑ๋๋ฏ๋ก ๊น์ ๋ณต์ฌ ํจ๊ณผ๋ฅผ ์ป์ ์ ์๋ค.
ObjectMapper om = new ObjectMapper();
// original ๊ฐ์ฒด๋ฅผ JSON ๋ฐ์ดํธ๋ก ์ง๋ ฌํ
byte[] bytes = om.writeValueAsBytes(original);
// JSON ๋ฐ์ดํธ๋ฅผ ๋ค์ Person.class๋ก ์ญ์ง๋ ฌํ -> ์๋ก์ด ๊ฐ์ฒด ์์ฑ
Person copy = om.readValue(bytes, Person.class);
๋น ๋ฅด๊ฒ ๊ตฌํ์ด ๊ฐ๋ฅํ๋ค๋ ์ ์ ์ฅ์ ์ด์ง๋ง ์ฑ๋ฅ ๋น์ฉ์ด ํฌ๋ค๋ ๋จ์ ๊ณผ ์ํ ์ฐธ์กฐ ๊ตฌ์กฐ์ ๊ฒฝ์ฐ JSON ์ง๋ ฌํ ์ StackOverflow๋ ๋ฌดํ ๋ฃจํ๊ฐ ๋ฐ์ํ ์ ์๋ค.
๋ํ ํ๋๊ฐ transient, @JsonIgnore ๋ฑ์ผ๋ก ์ง๋ ฌํ ๋์์์ ์ ์ธ๋๋ ๊ฒฝ์ฐ ๋ณต์ฌ ๊ฒฐ๊ณผ๊ฐ ๋ฌ๋ผ์ง ์ ์์ด ์ฃผ์ํด์ ์ฌ์ฉํด์ผ ํ๋ค.
์ธ ๋ฒ์งธ๋ก๋ ๋งคํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด๋ค. MapStruct, ModelMapper ๋ฑ์ผ๋ก DTO์ ๋๋ฉ์ธ ๊ฐ์ ๋ณต์ฌ ๊ท์น์ ์ปดํ์ผ/๋ฐํ์์ ์์ฑํ๋ค.
@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);
PersonDto toDto(Person entity);
Person toEntity(PersonDto dto);
}
// ์ฌ์ฉ ์
Person p1 = new Person("Min", new Address("Seoul"));
PersonDto dto = PersonMapper.INSTANCE.toDto(p1);
// dto ์์ Address ์ญ์ ๋งคํ ๊ท์น์ ๋ฐ๋ผ ์๋ก์ด ๊ฐ์ฒด๋ก ๋ณํ๋๋ค.
๋ฐ๋ณต๋๋ ๋งคํ ์ฝ๋๋ฅผ ์ค์ฌ ์์ฐ์ฑ๊ณผ ์ ์ง ๋ณด์์ฑ์ ๋์ผ ์ ์๊ณ ๊น์ ๋ณต์ฌ ๊ท์น์ ์ค์์์ ๊ด๋ฆฌ๊ฐ ๊ฐ๋ฅํ๋ค. ๊ทธ๋ฌ๋ ๋จ์ ๋ณต์ฌ๋ง ํ์ํ ๊ฒฝ์ฐ์๋ ์ค๋ฒ ์์ง๋์ด๋ง์ด ๋ ์ ์๋ค.
new ArrayList<>()๋ ์์ ๋ณต์ฌ์ด๋ค. ๊น์ ๋ณต์ฌ๋ฅผ ์ํด์๋ ์์ ์์ฒด๋ฅผ ๋ณต์ฌํด์ผ ํ๋ค.
List<Person> src = ...; // ์๋ณธ ๋ฆฌ์คํธ
List<Person> deep = src.stream()
.map(Person::new) // Person ๋ณต์ฌ ์์ฑ์๋ฅผ ์ด์ฉํด ์ ๊ฐ์ฒด ์์ฑ
.toList(); // ์ ๋ฆฌ์คํธ๋ก ์์ง
List.copyOf()/Set.copyOf()๋ ๋ถ๋ณ ๋ฆฌ์คํธ๋ฅผ ๋ฐํํ๋ค. ๋ฆฌ์คํธ ์์ฒด๋ ์๋ก ๋ง๋ค์ด์ง์ง๋ง ๋ด๋ถ ์์๋ ๊ทธ๋๋ก ์ฐธ์กฐ๋ง ๋ณต์ฌ๋๋ค. (์์ ๋ณต์ฌ)
List<String> list = new ArrayList<>();
list.add("Blue");
list.add("Cool");
List<String> copy = List.copyOf(list); // ๋ถ๋ณ ๋ฆฌ์คํธ + ์์ ๋ณต์ฌ
copy.add("c"); // UnsupportedOperationException ๋ฐ์
๋ฐฐ์ด์ arr.clone()์ ์ ๋ฐฐ์ด์ ๋ง๋ค์ง๋ง ์์๊ฐ ์ฐธ์กฐ ํ์
์ด๋ผ๋ฉด ์์ ๋ณต์ฌ์ด๋ค. ํ์ง๋ง ์์(primitive) ํ์
๋ฐฐ์ด์ ๊ฐ ๋ณต์ฌ์ด๋ฏ๋ก ๊ฒฐ๊ณผ์ ์ผ๋ก ๊น์ ๋ณต์ฌ์ฒ๋ผ ๋์ํ๋ค.
Address[] a1 = { new Address("Seoul") };
Address[] a2 = a1.clone(); // ๋ฐฐ์ด ๊ฐ์ฒด๋ ์๋ก ์์ฑ, ์์๋ ๊ฐ์ ์ฐธ์กฐ
์ธ๋ถ์ ๊ฐ๋ณ ๊ฐ์ฒด๋ฅผ ๋ ธ์ถํ ๋๋ ๋ฐ๋์ ๋ฐฉ์ด์ ๋ณต์ฌ๋ฅผ ๊ณ ๋ คํด์ผ ํ๋ค.
class Order {
private final Date createdAt; // Date๋ ๊ฐ๋ณ ํด๋์ค
public Order(Date createdAt) {
this.createdAt = new Date(createdAt.getTime()); // ์ ๋ ฅ ๋ฐฉ์ด ๋ณต์ฌ
}
public Date getCreatedAt() {
return new Date(createdAt.getTime()); // ๋ฐํ ๋ฐฉ์ด ๋ณต์ฌ
}
}
Instant, LocalDateTime ๊ฐ์ java.time ๊ณ์ด์ ๋ถ๋ณ์ด๊ธฐ ๋๋ฌธ์ ๋ฐฉ์ด ๋ณต์ฌ๊ฐ ๋ถํ์ํ๋ค.
๋๋ฉ์ธ ๊ฐ์ฒด๋ฅผ ์ต๋ํ ๋ถ๋ณ์ผ๋ก ๋ง๋ค๋ฉด ์์/๊น์ ๋ณต์ฌ์ ๋ํ ๊ณ ๋ฏผ ์์ฒด๊ฐ ์ค์ด๋ ๋ค. ๋ด๋ถ ์ํ๋ฅผ ๋ณ๊ฒฝํ ์ ์์ด ์ฐธ์กฐ๋ฅผ ๊ณต์ ํด๋ ์์ ํ๊ธฐ ๋๋ฌธ์ด๋ค.
์๋ฐ์ record๋ JDK 16๋ถํฐ ์ ์ ๋์
๋ ๋ฐ์ดํฐ ์ ์ฉ ํด๋์ค์ด๋ค. ๋ชจ๋ ํ๋๊ฐ ์๋์ผ๋ก final์ด ๋๋ฉฐ ์์ฑ์๋ก๋ง ๊ฐ์ด ์ค์ ๋๋ค. ๋ฐ๋ผ์ record ์์ฒด๊ฐ ๋ถ๋ณ์ ์ด๋ค.
public record PersonR(String name, AddressR address) {}
public record AddressR(String city) {}
// ์ฌ์ฉ ์
PersonR p1 = new PersonR("Min", new AddressR("Seoul"));
PersonR p2 = p1; // ๊ฐ์ ์ฐธ์กฐ๋ฅผ ์จ๋ ์์ ํ๋ค. (๋ถ๋ณ)
์ฃผ์ํ ์ ์ record์ ํ๋ ์์ฒด๋ final์ด์ง๋ง ์ฐธ์กฐ ํ์
ํ๋๊ฐ ๊ฐ๋ณ ๊ฐ์ฒด๋ผ๋ฉด ๋ด๋ถ์ ์ผ๋ก ์ฌ์ ํ ๋ฐ๋ ์ ์๋ค. ๋ฐ๋ผ์ record๋ฅผ ๋ถ๋ณ์ ์ผ๋ก ์ฐ๋ ค๋ฉด ํ๋ ํ์
๋ ๋ถ๋ณ ๊ฐ์ฒด๊ฐ ๋์ด์ผ ํ๋ค.
๋ ๊ฐ์ฒด๊ฐ ๊ฐ์ ์ค์ฒฉ ์ฐธ์กฐ๋ฅผ ๊ณต์ ํ๋ฉด ๊ทธ ๋ด๋ถ ์ํ๊ฐ ๋ณํ ๋ equals()๋ hashCode()์ ๊ฒฐ๊ณผ๊ฐ ๋ฐ๋ ์ ์๋ค.
class Address {
String city;
Address(String city) { this.city = city; }
// equals/hashCode city ๊ธฐ์ค์ผ๋ก ๊ตฌํํ๋ค๊ณ ๊ฐ์
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
class Person {
String name;
Address address;
// equals/hashCode: name + address ๊ธฐ์ค์ผ๋ก ๊ตฌํํ๋ค๊ณ ๊ฐ์
...
}
Person p1 = new Person("Min", new Address("Seoul"));
Person p2 = new Person(p1.name, p1.address); // ์์ ๋ณต์ฌ
Set<Person> set = new HashSet<>();
set.add(p1);
// ์ค์ฒฉ ๊ฐ์ฒด(Address) ๋ณ๊ฒฝ
p1.address.city = "Busan";
// p2.equals(p1)๋ ์ฌ์ ํ true (๊ฐ์ ์ฐธ์กฐ ๊ณต์ )
// set.contains(p1) -> false (hashCode๊ฐ ๋ฌ๋ผ์ง)
ํนํ HashSet, HashMap ๊ฐ์ ํด์ ๊ธฐ๋ฐ ์ปฌ๋ ์
์์ ์น๋ช
์ ์ธ ๋ฒ๊ทธ๋ก ์ด์ด์ง ์ ์๊ธฐ ๋๋ฌธ์ ๊น์ ๋ณต์ฌ๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ด ์ข๋ค.
