이 글에서는
1. 얕은 복사(Shalldow Copy)
2. 깊은 복사(Deep Copy)
3. Call by Reference
4. Call by Value
5. 깊은 참조?
의 개념이 나옵니다. 단어가 매우 많이 나오니 헷갈리지 마시길 !
기존에 나는 ! 위에 나오는 개념들에 엄청난 혼동이 왔었다.
Call by Value는 원본 값을 바꾸지 않기 때문에 얕게 복사하는 것이다 ! 라고 생각했고,
Call by Reference는 메모리 주소로 접근하기 때문에 원본을 건드린다 즉 깊게 참조하는 것이다. 라고 생각했다
검색하니 모든게 틀렸다 ㅋㅋ
나의 문제점
* 얕은 복사, call by reference 등의 개념을 다 !제대로 몰랐다
1. 얕은 복사와 깊은 복사의 의미부재
2. 얕은 복사(shallow Copy)와 Call by Reference는 같은 개념이라고 생각했다
그래서 이번 블로그로 제대로 짚고 가려고한다.
자 그렇다면 내가 헷갈렸던 개념들을 차례대로 알아가보자 ( with Dart )
개념정리
Call by Value
- 직역하자면 값에 의한 호출이다. 인수를 전달 할 때 값 자체를 복사해서 전달한다는 것
- 다트에서는 int, double, bool, string등 기본타입이라고 불리는 것들이 Call by Value이다.
우리의 기존 상식을 깨부수는 예시를 보겠다
void changeValue(int value) {
value = 10;
}
void main() {
int x = 5;
changeValue(x);
print(x); // 출력: 5
}
당연히 10이 나와야한다고 생각이 되는 이 예시에서 값이 5가 나온다.
왜일까 !!
x 가 다녀오는게 아니라 x의 복사본이 다녀오는거기 때문에
더 정밀히 말하면 x는 int고 int는 Call by Value니까! ChangeValue(x)일때는 x의 값만 복사했다 오는거다.
잘 동작하게 바꾸려면
int changeValue(int value) {
value = 10;
return value;
}
void main() {
int x = 5;
x = changeValue(x);
print(x); // 출력: 10
}
이런식으로 함수에 리턴 값을 만들어서 x에 대입하던가,
class Wrapper {
int value;
Wrapper(this.value);
}
void changeValue(Wrapper wrapper) {
wrapper.value = 10;
}
void main() {
Wrapper x = Wrapper(5);
changeValue(x);
print(x.value); // 출력: 10
}
참조를 사용하여 원본 값을 건들 수 있게해야한다.
Call by Reference
- 직역하자면 참조에 의한 호출이다.
- 클래스 인스턴스 참조에 의한 호출로 동작한다
- 함수 내부에서 객체의 속성을 변경하면 원본도 변경된다.
코드 예시를 봐야겠죵
class Point {
int x;
int y;
Point(this.x, this.y);
}
void moveToOrigin(Point point) {
point.x = 0;
point.y = 0;
}
void main() {
Point myPoint = Point(10, 10);
moveToOrigin(myPoint);
print(myPoint.x); // 출력: 0
}
요렇게 되는거다 원본 주소를 가지고 바꿔왔기 때문에 원본 값 역시도 변경되어있다.
그렇다면
Class 안에 있는 기본타입 프로퍼티들은 Call by Value야 Reference야 라는 고민이 들 수 있는데, Class라는 틀 안에 있는거니 힙에 저장됨 -> Call by Reference 가 됩니다.
너무 헷갈렸던건
Swift에서는 애초에 값타입, 참조타입이라는게 존재했다. 근데 다트엔 그런거 없고요 모든 클래스 객체가 참조타입이며 다트에선 모든게 객체이다.. 라고 했다.
무..슨 말이야~! 이해하기에 시간이 꽤 걸릴듯하다.
일단 요약을 해보자면
- 값 타입의 범위:
- Dart: 기본 타입(int, double, bool, String 등)만 값 타입이다.
- Swift: 기본 타입 외에도 Struct와 Enum도 값 타입으로 작동한다.
- 값 타입의 사용:
- Dart: 값 타입은 주로 기본 데이터 저장에 사용된다.
- Swift: 값 타입인 Struct는 데이터 모델링에 많이 사용되며, 성능이 뛰어나다는 장점이 있다.
- 값 타입이기에 Stack을 쓰고 그래서 요즘 새로나온 프레임워크인 SwiftUI는 Struct를 위주로 쓴다. 스택이 가볍기 때문에, 근데 다트는 Struct라는 개념이 아예 부재해서 놀랐다.
얕은 복사
- 객체의 복사본을 생성할 때 원본 객체와 같은 메모리 주소를 공유하는 경우를 말한다.
즉, 객체 내의 참조형 필드는 복사되지 않고 참조만 복사된다.
뭔소린가 싶을땐
예시를 바로 봐야합니다.
class Person {
String name;
List<String> hobbies;
Person(this.name, this.hobbies);
}
void main() {
var original = Person('John', ['reading', 'swimming']);
var copy = Person(original.name, original.hobbies);
copy.name = 'Bob';
copy.hobbies.add('running');
print(original.name); // 출력: John
print(original.hobbies); // 출력: [reading, swimming, running]
print(copy.name); // 출력: Bob
print(copy.hobbies); // 출력: [reading, swimming, running]
}
copy 객체와 original 객체가 있다.
둘은 동일한 name과 hobbies를 가지지만 hobbies는 동일한 List객체를 참조하고 있다.
따라서 ! copy.hobbies.add('running') 로 인해 original 객체의 hobbies도 변경된다.
깊은 복사
- 원본 객체와 별개로 메모리 주소를 가진다. 복사본은 원본가 완전히 독립적인 객체가 된다
(하위 객체를 재귀적으로 복사한다고 하는데 이해할 수 없다. 재귀적 복사가 무엇이란 말인가 )
다트는 기본적으로 깊은 복사가 제공되지 않아서, 수동으로 구현해야한다고 한다.
예시를 보자.
class Address {
String city;
String street;
Address(this.city, this.street);
// 깊은 복사를 위한 메서드
Address copy() {
return Address(this.city, this.street); // 아 이렇게 다 복사하니 재귀적 복사라고 하는군 ㅎㅎ
}
}
class Person {
String name;
Address address;
Person(this.name, this.address);
// 깊은 복사를 위한 메서드
Person copy() {
return Person(this.name, this.address.copy());
}
}
void main() {
Address address = Address('Seoul', 'Gangnam');
Person person1 = Person('Alice', address);
Person person2 = person1.copy(); // 깊은 복사
person2.address.city = 'Busan';
print(person1.address.city); // 출력: Seoul
print(person2.address.city); // 출력: Busan
}
Person 클래스와 Address 클래스에 copy 메서드를 추가하여 깊은 복사를 구현하였다. person2는 person1과 독립적인 객체가 되며, person2의 address 필드도 person1의 address 필드와 독립적이다.
그치만 보시다시피 굉장히 복잡하다.
Source of Truth(데이터 무결성)가 유지되기 위해서 쓰는것 같은데, 이렇게 까지 해야하나!? 싶은 생각이 든다. 그렇다가도, 클래스 안의 클래스를 쓰는 경우가 자주 있다면 그래야 할 수도 있겠다는 생각이 들고
다트에서는 중첩된 클래스를 많이 쓰나 궁금해졌다
추가적으로, 깊은참조, 얕은참조들은 뭘까
일단 기본적으로 참조는 본질적으로 얕다. 단순히 메모리를 가리키는것이기에.
그래서 깊은 참조라는 개념은 애초에 없는거였다..
자 아무튼
지금까지 나온 개념들을 명확히 정리하자면
Call by Reference & Call by Value
- 얘네는 함수 호출 시 값이 어떻게 전달되는지, 함수 내부에서 원본 값을 변경할 수 있는지 없는지에 대한 이야기고
얕은 복사 & 깊은 복사
- 객체를 복사 할 때 중첩된 객체를 어떻게 처리하는지에 대한 이야기다
댓글 언제나 환영합니다
'Flutter > Dart' 카테고리의 다른 글
멤버 변수 vs 상태 (0) | 2024.10.14 |
---|---|
fromMap을 사용해 map데이터 쉽게 사용하기 (0) | 2024.09.19 |
Stream, Future 둘의 차이점은 뭐지? (2) | 2024.08.12 |
Enum이 뭐고, Enhanced Enum은 왜 쓸까? (0) | 2024.08.12 |
자꾸 헷갈리는 Const & Final / Var & Dynamic 정리 (0) | 2024.08.07 |