[이 글은 NextStep Kotlin TDD, Refactoring, Clean Code 수업 중 공부한 것을 복습하고자 정리한 글입니다.]
얕은 복사란?
주소값
자체를 복사하는 것이다.
복사된 객체의 인스턴스는 원본 객체의 인스턴스와 같은 메모리주소를 참조합니다.
-> 따라서, 같은 메모리 주소값을 참조하기 때문에 복사된 객체의 값이 변경되면 원본 객체의 값도 변경됩니다.
깊은 복사란?
새로운 메모리 공간에 객체의 모든 값
을 복사하는 것이다.
원본 객체는 그대로 두고, 새로운 메모리공간에 원본 객체의 값들을 모두 복사합니다.
-> 따라서, 다른 메모리 주소값을 참조하기 때문에 복사된 객체가 변경되어도 원본 객체는 영향을 받지 않습니다.
얕은복사의 문제점
자바(코틀린)에서는 기본적으로 '=
'를 사용하여 다른 인스턴스를 넣어주면 기본적으로 얕은 복사가 수행됩니다.
얕은 복사를 수행하게 되면 같은 메모리 주소값을 참조하기 때문에 복사된 객체가 변경되어도 원본 객체의 값이 변경됩니다.
아래 코드를 통해 보도록 하겠습니다.
import org.junit.jupiter.api.Test
data class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class DeepCopyTest {
@Test
fun shallowCopyTest() {
val car = Car("car")
val copyCar = car
copyCar.move()
}
}
car객체를 생성하고, copyCar객체는 car를 넣어줍니다.
실행 결과
이렇게 구현을 하면 얕은복사가 실행되고 같은 메모리 주소값을 참조하게 됩니다.
결국, copyCar의 position을 변경 했음에도, car의 position이 변하게 됩니다.
깊은복사를 하는 방법
1. data class의 copy() 사용 (기본 클래스의 경우 copy()를 오버라이딩해서 직접 구현 후 사용)
코틀린을 이용한다면 data class의 copy()메소드를 이용하여 깊은 복사를 수행할 수 있습니다.
import org.junit.jupiter.api.Test
data class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class DeepCopyTest {
@Test
fun deepCopyTest() {
val car = Car("car")
val copyCar = car.copy()
copyCar.move()
}
}
copy 메소드를 사용하면 깊은 복사가 수행됩니다.
실행 결과
따라서, 주소값이 다르므로 copyCar를 move()한다고 해도 car의 position이 변하지 않습니다.
2. 깊은복사를 수행하는 메소드를 직접 구현
data class를 사용하지 않을 경우 깊은 복사를 수행하는 메소드를 직접 구현해주면 됩니다.
copy 메소드 그대로 깊은 복사를 수행하는 메소드를 구현해 주도록 하겠습니다.
import org.junit.jupiter.api.Test
class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
fun copy(name: String = this.name, position: Int = this.position): Car = Car(name, position)
}
class DeepCopyTest {
@Test
fun deepCopyTest() {
val car = Car("car")
val copyCar = car.copy()
copyCar.move()
}
}
실행 결과
위에서 data class의 copy 를 사용한 방법과 마찬가지로 car의 주소값이 다르므로 원본 객체가 변하지 않습니다.
코틀린 Collection의 방어적 복사
Collection의 깊은복사를 하는방법은 대표적으로 일급 컬렉션을 만들어서 방어적 복사를 구현해주는 것입니다.
방어적 복사란?
생성자로 받은 가변 데이터들을 외부에서 변경하는 것을 막기 위해 복사본을 이용
하는 방법.
Collection에 방어적 복사를 할 때에는 3가지를 모두 수행해주어야 완벽한 방어적 복사가 됩니다.
- 일급 컬렉션 객체 내부로 들어오는 List에 대한 방어적 복사 수행하기
- 컬렉션의 내부 객체가 가변객체라면 컬렉션 내부 객체에 대한 방어적 복사 수행하기
- 일급 컬렉션 객체 외부로 나가는 List에 대한 방어적 복사 수행하기
List에 대한 방어적 복사
import org.junit.jupiter.api.Test
class Car(val name: String = "", var position: Int = 0)
class Cars(val cars: List<Car>)
class DeepCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList) // carA, carB
carList.add(Car("carB")) // carA, carB
}
}
먼저, 위의 코드처럼 Car객체와 일급컬렉션 Cars를 정의했습니다.
그 후, 아래 순서대로 실행 하였습니다.
- carList에 Car("carA")를 추가한다.
- 일급 컬렉션 Cars 객체에 carList를 넣어준다.
- carList.add(Car("carB")) 해준다.
위 코드를 실행시키면 cars의 값을 조작하지 않았음에도 carList에 add를 해주어도 cars의 값이 변경됩니다.
이유는, Cars에 List<Car>가 들어갈 때 얕은 복사로 같은 메모리 주소를 참조하기 때문
에
carList와 Cars 내부의 cars가 같고 결국에는 carList를 조작하면 Cars의 값 또한 변경이 되기 때문입니다.
실행결과를 보면 carList에 carB를 넣어주었음에도 cars에 있는 값이 변경되었습니다.
List가 동일한 주소값을 바라보고 있기 때문에 이런일이 일어난 것입니다.
문제점을 해결하기 위해 List에 대한 방어적 복사를 하기 위해 Cars를 아래 코드처럼 바꾸겠습니다.
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.toList()
}
생성자에 있는 val cars를 cars로 바꿔서 생성자에서는 파라미터의 역할만 하게 만들고 value를 생성하지 않게합니다.
그리고 클래스 내부로 cars 프로퍼티를 옮깁니다. 그리고 cars를 toList()를 하면 새로운 List를 반환하여 List에 대한 깊은 복사가 수행됩니다.
자바로 치면 new List(cars)를 수행하는 것과 비슷하고, 새로운 List에 담는 효과를 가집니다.
즉, Cars를 생성할 때 생성자의 파라미터로 받은 cars와의 관계가 끊어지고 새로운 List를 만들어서 담게 됩니다.
import org.junit.jupiter.api.Test
data class Car(val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.toList()
}
class DeepCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList)
println(cars.cars) // carA
carList.add(Car("carB")) // carA, carB
}
}
실행결과
List에 대한 방어적 복사를 했기 때문에 carList의 값을 변경해도 cars는 변하지 않는 것을 확인할 수 있습니다.
List 내부 객체들에 대한 방어적 복사
Car는 가변객체입니다. 따라서, carList[0].move()를 수행해주면 cars의 내부에 있는 Car의 값도 바뀌게 됩니다.
불변객체라면 위에서 끝났겠지만 가변객체이기 때문에 내부 객체들에게도 깊은 복사를 해주어야 하는 것입니다.
carList[0].move()를 수행해보도록 하겠습니다.
import org.junit.jupiter.api.Test
data class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.toList()
}
class DeepCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList) // carA(position: 1)
carList[0].move() // carA(position: 1)
}
}
실행 결과
carList[0]의 position을 바꾸었음에도 cars의 값이 바뀐 것을 알 수 있습니다.
해결방법
List 내부 객체들에 대한 깊은 복사를 해주기 위해 map { it.copy }를 추가해줍니다.
이렇게 해주면, map API를 통해 각각의 Car들이 깊은 복사가 수행될 것입니다.
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.map { it.copy() }
}
전체 코드
import org.junit.jupiter.api.Test
data class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.map { it.copy() }
}
class DeepCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList)
println(cars.cars) // carA(position: 0)
carList[0].move()
println(cars.cars) // carA(position: 0)
}
}
실행결과
실행결과를 보면 carList[0].move()를 실행해도 Cars의 position은 변경되지 않은 것을 확인할 수 있습니다.
BackingProperty를 이용해 외부로 나가는 것에 대한 방어적 복사 구현
위에서 했었던 List에 대한 깊은 복사는 생성자로 들어오는 List, 즉, 밖에서 들어오는 것에 대한 방어적 복사였습니다.
마찬가지로 외부에서 Cars 내부의 List에 직접 접근해서 조작하는 것, 즉, 나가는 것에 대한 방어적 복사를 구현
을 해주어야 합니다.
그렇지 않으면 Cars의 List 원소인 Car인 cars.cars[0]에 접근해서 cars.cars[0].move()를 하게 되면 position이 이동하게 될 것입니다.
import org.junit.jupiter.api.Test
data class Car(private val name: String = "", var position: Int = 0) {
fun move() {
position++
}
}
class Cars(cars: List<Car>) {
val cars: List<Car> = cars.map { it.copy() }
}
class DeepCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList) // carA(position: 1)
cars.cars[0].move() // carA(position: 1)
}
}
실행 결과
그런데, 여기서 인자로 받은 cars와 프로퍼티인 cars가 이름이 같습니다. 이렇게 하면 잘못하면 StackOverFlowException(재귀에러)가 발생할 가능성이 많습니다.
그러므로 Backing Property를 이용하여 아래와 같은 코드로 개선시킬 수 있습니다.
class Cars(cars: List<Car>) {
private val _cars: List<Car> = cars.map { it.copy() }
val cars: List<Car>
get() = _cars.map { it.copy() }
}
외부에서 받은 인자는 _cars에 담아서 내부 Car까지 다른 List를 담아주고
외부로 나가는 cars 또한 방어적 복사를 통해서 내보내게 해주면 Collection에 대한 개선된 방어적 복사가 완성됩니다.
import org.junit.jupiter.api.Test
data class Car(val name: String = "", var position: Int = 0)
class Cars(cars: List<Car>) {
private val _cars: List<Car> = cars.map { it.copy() }
val cars: List<Car>
get() = _cars.map { it.copy() }
}
class DefensiveCopyTest {
@Test
fun listCopyTest() {
val carList = mutableListOf(Car("carA"))
val cars = Cars(carList)
println(cars.cars) // carA(position: 0)
cars.cars[0].move()
println(cars.cars) // carA(position: 0)
}
}
실행 결과
실행결과를 보면 Cars 내부의 List를 조작해도 Cars 내부 List안에있는 Car 원본 객체는 변하지 않는 것을 확인할 수 있습니다.