Kotlin Spring boot로 클린아키텍처를 적용하며 프로젝트를 만들어가고 있을때 RequestDto를 그냥 Dto로 변경해주고(toDto) 엔티티로 다시 변환할때(toEntity) copy() 메서드를 사용하여 password를 인코딩 시켰다. 이는 컨버터에서 패스워드 인코더를 의존하지 않고 서비스에서만 의존하는다는 이점이 있었다. (toDto에서 패스워드를 바로 인코딩시키면 패스워드 인코더를 컨버터가 의존하게 된다.)
여기서 copy()메서드는 새로운 객체를 만들어 다른 메모리공간에 할당한다는 점을 보아 성능상으로 차이가 많이 날까 궁금했다. 그렇게 성능 차이를 찍어본 결과 copy()를 사용하는 것이 조금 더 빨랐다.
그런데 갑자기 궁금해졌다 다른 메모리를 사용하여 공간을 할당한다해도 이게 프로젝트 전체를 보았을때 정말로 안전하고 또 빠를까? 그래서 copy()와 깊은 복사 얕은 복사에 대해서 조사를 해보았다.
copy()
얕은 복사
val originalList = mutableListOf(1, 2, 3)
val shallowCopyList = originalList
shallowCopyList[0] = 0
println(originalList) // [0, 2, 3]
println(shallowCopyList) // [0, 2, 3]
위의 코드처럼 얕은복사를 했을때 객체가 가지고 있는 리스트의 값을 복사된 객체에서 수정할 경우 원본 객체의 리스트의 값도 수정이 됩니다.
깊은 복사
val originalList = mutableListOf(1, 2, 3)
val deepCopyList = originalList.toMutableList()
deepCopyList[0] = 0
println(originalList) // [1, 2, 3]
println(deepCopyList) // [0, 2, 3]
따라서 얕은 복사와 깊은 복사는 객체의 복사 방법에 따라 복사된 객체와 원본 객체가 참조하는 객체가 같은 수도 있고 다를 수도 있습니다. 코틀린에서는 대부분의 객체가 얕은 복사와 깊은 복사를 모두 지원합니다.
data class Person(val name: String, val age: Int, val address: String, val phone: Phone)
data class Phone(val number: String)
val person1 = Person("hope", 18, "Korea", Phone("123-4567"))
val person2 = person1.copy(phone = Phone("555-5555"))
위의 코드처럼 두개의 데이터 클래스가 있고 copy를 사용해 다른 값 Phone 객체를 새로 생성해 넣으면 깊은 복사가 일어나며 서로 다른 객체가 됩니다. 그리고 값을 변경하지 않는 경우에는 얕은 복사가 발생합니다.
만약 객체의 내용을 변경해도 원본 객체를 변경하고 싶지 않은 경우, 깊은 복사를 수행해야합니다. 이 경우에는 copy메서드에 인자를 전달해 새로운 객체를 생성할 때, 필요한 객체를 새로 생성하여 전달해주어야합니다.
얕은 복사와 깊은 복사의 객체의 동등성과 동일성은 다릅니다.
얕은 복사 객체는 객체의 참조값만 복사합니다 그래서 복사된 객체와 원본 객체가 같은 객체를 참조하고 있게 됩니다. 그래서 객체의 비교 연산자인 ==, === 연산의 결과는 true가 됩니다. 한마디로 동등성과 동일성 둘다 만족합니다.
반면에 깊은 복사 객체는 원본 객체와 다른 객체이므로 동일성도 다르고 동등성도 다릅니다. 새로운 객체가 생성되기 때문에 깊은 복사 객체의 원본 객체는 서로 다른 객체를 참조합니다. 따라서 깊은 복사 객체와 원본 객체는 동일하지도 않고 동등하지 않습니다.
\==, ===의 연산자가 false가 됩니다.
얕은 복사 객체는 ==, === 전부 true
깊은 복사 객체는 ==, === 전부 false
다시 프로젝트 내용으로 돌아와 SignUpDto를 컨버팅하는 과정에서 copy를 사용하면 좋은 이유가 무엇일까?
-> 패스워드를 인코딩하는 과정은 상대적으로 오래걸리는 작업이다 그래서 엔티티 객체로 변환하면, 유저 엔티티 객체를 생성하는 작업과 패스워드 인코딩하는 작업이 함께 이루어지기 때문에 객체를 생성하는 작업과 패스워드 인코딩 작업이 함께 이루어진다. 따라서 변환 작업에 걸리는 시간이 오래걸릴 수 있다.
복사를 사용하여 새로운 객체를 생성한다면 원본 객체를 그대로 두고 새로운 객체를 생성하기 때문에 원본 객체의 상태를 그대로 유지할 수 있습니다. 따라서, 복사를 사용하여 새로운 객체를 생성하고 패스워드를 인코딩하는 작업을 따로 처리하면, 원본 객체의 상태를 그대로 유지할 수 있습니다. 따라서 복사를 사용하여 새로운 객체를 생성하고 패스워드를 인코딩하는 작업을 따로 처리하면 원본 객체의 상태를 변경하지 않고도 변환 작업을 수행할 수 있습니다.
여기서 의문이 원본 객체를 그대로 두면 이점이 뭐가있을까? 한번 생각해보았다
따라서 성능상의 이점을 고려한다면, dto to entity로 변환하는 과정에서 복사를 사용해 새로운 객체를 생성하고 패스워드를 인코딩하는 작업을 별도로 처리하는 것이 좋아보인다.
패스워드 인코딩과 엔티티로 변환하는 과정을 나누었을 때 성능이 더욱 좋아지는 이유는?
그렇게 프로젝트를 진행하면서 copy 성능에 관한 궁금증을 해결했다. 앞으로도 copy 함수를 애용하면서 성능 관련한 코드들을 연구하고 더욱 연구하며 더 나은 개발자가 될 수 있도록 노력할 것이다. 이렇게 길고 심오한 포스팅을 읽어주셔서 감사의 말씀을 전한다.