[Kotlin] 얕은 복사, 깊은 복사, 방어적 복사

boogi-woogi·2023년 3월 9일
0
post-thumbnail

얕은 복사 vs 깊은 복사

얕은 복사란?

  • 주소값 자체를 복사하는 것
  • 복사된 객체의 인스턴스는 원본 객체의 인스턴스와 같은 메모리 주소를 참조한다.
  • 복사된 인스턴스 값을 변경하면 원본 인스턴스의 값 또한 변경이 된다.

깊은 복사란?

  • 원본 객체의 인스턴스가 존재하는 주소와 다른 주소지만 값이 모두 같은 새로운 인스턴스를 생성하는 것
  • 참조하고 있는 주소가 다르기 때문에 복사된 객체가 변경되어도 원본 객체는 영향을 받지 않는다.

얕은 복사의 문제점

  • 이전에 언급했듯이 복사된 객체의 인스턴스가 변경될 경우 원본 객체의 인스턴스의 값이 변경될 수 있다.
  • 코틀린에서는 = 를 사용하면 기본적으로 얕은 복사가 수행된다.
data class Car(
    val name: String,
    var position: Int,
){

	fun move(){
			position++
	}
}

fun main() {
    val car = Car("롤스로이스", 3)
    val copyCar = car

	copyCar.move()
	// 원본 객체의 값 역시 변경된다.
}


carcopyCar의 주소 값이 같은 것을 알 수 있다! copyCarmove에 영향을 받아 원본 객체의 position까지 변경되는 모습

깊은 복사를 수행하는 방법

data class에서는 copy라는 메소드를 제공한다. copy는 원본 객체의 인스턴스 주소와 다른 새로운 주소에 원본 객체의 값과 같은 인스턴스를 생성한다.

data class가 아닌 경우 메소드를 직접 구현해서 깊은 복사를 수행할 수 있다.

class Car(
    val name: String,
    var position: Int,
){

	fun move(){
			position++
	}
    
    fun copy(): Car = Car(name, position)
}

fun main() {
    val car = Car("롤스로이스", 3)
    val copyCar = car.copy()

	copyCar.move()
}

💡 참조하고 있는 주소가 다르기 때문에 복사된 인스턴스의 값이 변경되도 원본 인스턴스는 변경되지 않는다.

방어적 복사?

방어적 복사란 무엇일까?

생성자로 받은 가변 데이터들을 외부에서 변경하는 것을 막기 위해 복사본을 이용하는 방법

Collection에 대한 방어적 복사

일급 컬렉션 객체 내부로 들어오는 List에 대한 방어적 복사

class Cars(val cars: List<Car>)

fun main() {
    val cars = Cars(
        listOf(
            Car("롤스로이스", 0),
            Car("아반떼", 0)
        )
    )
}

인자로 들어오는 cars를 조작하면 일급 컬렉션 내부의 cars에 대해서 영향을 줄 수 있다!

class HyundaiCars(val cars: List<Car>)

fun main() {
    val cars = (
            mutableListOf(
                Car("제네시스", 0),
                Car("아반떼", 0)
            )
     )

    val hyundaiCars = HyundaiCars(cars)

    cars.add(Car("소나타", 0))

	println("현대차 개수: %d".format(hyundaiCars.cars.size))
}

위와 같은 상황을 예방하는 방법

class HyundaiCars(cars: List<Car>){
		
	//인자로 들어온 cars가 참조하고있는 주소와 다른 주소를 참조하는 list를 넣어주자!
    val cars = cars.toList()
}

fun main() {
    val cars = (
            mutableListOf(
                Car("제네시스", 0),
                Car("아반떼", 0)
            )
     )

    val hyundaiCars = HyundaiCars(cars)

    cars.add(Car("소나타", 0))

    println(hyundaiCars.cars.size)
}

인자로 들어오는 carstoList를 사용해 원본 List가 참조하고 있는 주소와 다른 주소를 참조하는 새로운 List를 생성한다!

원본 carsadd를 해주어도 HyundaiCars 내부에 존재하는 cars에 영향을 주지 못한다.


컬렉션의 내부 객체가 가변객체인 경우 컬렉션 내부 객체에 대한 방어적 복사

data class Car(
    val name: String,
    var position: Int,
) {

    fun move() {
        position++
    }
}

class HyundaiCars(cars: List<Car>) {

    val cars = cars.toList()
}

fun main() {
    val genesis = Car("제네시스", 0)
    val avante = Car("아반떼", 0)
    val cars = mutableListOf(genesis, avante)
    
    val hyundaiCars = HyundaiCars(cars)
    
	//외부에서 genesis를 조작하면 hyundaiCars안에 있는 gensis의 position또한 변경될까? 그렇다...
    genesis.move()
    genesis.move()

    hyundaiCars.cars.forEach { hyundaiCar ->
        println("%s: %d".format(hyundaiCar.name, hyundaiCar.position))
    }
}

왜 이런 결과가 나올까?

💡 우리는 toList로 원본 List의 head가 가리키고 있는 주소만 변경했을 뿐 HyudaiCars의 프로퍼티 cars안에 존재하는 각각의 Car인스턴스 주소는 여전히 원본 Car인스턴스 주소와 같기 때문이다.

일급 컬렉션 내부에서 관리하고 있는 객체가 가변 객체일때 외부에서의 변경을 막으려면 어떻게 해야할까? List의 head를 바꾸는 것 뿐만 아니라 List에 존재하는 각각의 인스턴스에 대해서 깊은 복사를 수행해주어야 한다.

data class Car(
    val name: String,
    var position: Int,
) {

    fun move() {
        position++
    }
}

class HyundaiCars(cars: List<Car>) {

    val cars = cars.map{
		//list에 존재하는 각각의 인스턴스에 대해서 깊은 복사 수행! 
        it.copy()
    }
}

fun main() {
    val genesis = Car("제네시스", 0)
    val avante = Car("아반떼", 0)
    val cars = mutableListOf(genesis, avante)

    val hyundaiCars = HyundaiCars(cars)

    genesis.move()
    genesis.move()

    hyundaiCars.cars.forEach { hyundaiCar ->
        println("%s: %d".format(hyundaiCar.name, hyundaiCar.position))
    }
}

genesismove해도 HyundaiCars내부의 cars에는 영향을 미치지 못한다.


일급 컬렉션 객체 외부로 나가는 list에 대한 방어적 복사

data class Car(
    val name: String,
    var position: Int,
) {

    fun move() {
        position++
    }
}

class HyundaiCars(cars: List<Car>) {

    val cars = cars.map{
        it.copy()
    }
}

fun main() {
    val genesis = Car("제네시스", 0)
    val avante = Car("아반떼", 0)
    val cars = mutableListOf(genesis, avante)

    val hyundaiCars = HyundaiCars(cars)

	//이렇게 hyndaiCars안의 cars를 꺼내서 move를 수행하면 cars들이 영향을 받을 것이다.
    hyundaiCars.cars.forEach { car ->
        car.move()
    }

    hyundaiCars.cars.forEach { hyundaiCar ->
        println("%s: %d".format(hyundaiCar.name, hyundaiCar.position))
    }
}

만약 HyundaiCars 내부의 cars들을 외부에서 꺼내 사용할 필요가 있지만 외부에서는 cars에게 영향을 줄 수 없도록 하기 위해서는 어떻게 해야할까?

data class Car(
    val name: String,
    var position: Int,
) {

    fun move() {
        position++
    }
}

class HyundaiCars(cars: List<Car>) {

	// 이런식으로 내부에서 사용하는 컬렉션에는 언더바를 붙여서 관리한다. backing property라는 이름으로 불린다! 
    private val _cars = cars.toList().map { it.copy() }
		
	// 외부로 노출되는 cars는 내부의 _cars에 존재하는 인스턴스들을 대상으로 깊은 복사를 수행한다.
    val cars: List<Car>
        get() = _cars.map { it.copy() }

    fun myCars() {
        _cars.forEach { hyundaiCar ->
            println("%s: %d".format(hyundaiCar.name, hyundaiCar.position))
        }
    }
}

fun main() {
    val genesis = Car("제네시스", 0)
    val avante = Car("아반떼", 0)
    val cars = mutableListOf(genesis, avante)

    val hyundaiCars = HyundaiCars(cars)

    hyundaiCars.cars.forEach { car ->
        car.move()
    }

    hyundaiCars.myCars()
}

외부에서 아무리 hyundaiCarscars 를 조작해도 영향을 주지 못한다!


정리하면서

내가 적용한 부분

블랙잭 미션을 진행하면서 다음과 같이 Card를 관리하는 클래스 Cards를 생성했는데 Card가 가변 객체가 아니기 때문에 외부에 노출되는 cards에 대해서는 깊은 복사를 수행하지 않았다.

class Cards(
    cards: List<Card> = listOf(Card.draw(), Card.draw()),
) {

    private val _cards: MutableList<Card> = cards.toMutableList()
    val cards: List<Card>
        get() = _cards.toList()
...
}

공부하면서 든 나의 생각

원본 객체의 인스턴스들에 의해서 원하지 않은 변경이 일어날 수 있는 경우가 굉장히 많은 것을 보았다. 이때까지 코드를 구현하면서 크게 생각하지 않았던 부분이지만 앞으로는 깊은 복사를 잘 이용해 의도치 않은 상황이 일어나지 않도록 주의해야겠다.

profile
https://github.com/boogi-woogi

1개의 댓글

comment-user-thumbnail
2023년 3월 13일

제가 방어적 복사를 잘 알고 있다고 생각했는데, 이 글을 읽고 방어적 복사를 할 때, 리스트 내 객체에 대해서도 깊은 복사를 수행해야 한다는 것을 처음 알았네요 ! 감사합니다 ~ 👻

답글 달기