배열을 복사해서 새 변수에 담았습니다. 복사본만 고쳤는데 원본까지 바뀌어 있네요. 프로그래밍하다 한 번은 꼭 만나는 미스터리입니다.
범인은 “복사”라는 말의 두 얼굴입니다. 값을 통째로 베끼는 복사가 있고, 같은 데이터를 가리키는 이름표만 하나 더 만드는 복사가 있어요.
얕은 복사와 깊은 복사를 가르는 기준을 짚고, Swift에서는 이 문제가 어떻게 달라지는지 정리합니다. 값 타입과 Copy-on-Write 덕분에 사정이 달라지지만, 그래도 함정은 남습니다.
핵심 요약입니다.
- 참조 복사: 데이터는 그대로 두고 가리키는 이름표만 복제. 한쪽을 고치면 같이 바뀐다
- 얕은 복사: 겉 껍데기는 새로 만들지만, 안에 든 참조들은 같은 대상을 가리킨다
- 깊은 복사: 안에 든 것까지 재귀적으로 전부 새로 만든다. 완전히 독립
- Swift 값 타입은 대입만으로 독립 복사처럼 동작하지만, 그 안에 클래스가 들어 있으면 얕은 복사 문제가 그대로 돌아온다
세 단계로 나눠야 정확합니다
흔히 얕은/깊은 두 종류로 말하지만 참조 복사까지 세 단계로 나눠야 그림이 맞습니다.
참조 복사는 복사가 아니라 별명 붙이기입니다. 클래스 인스턴스를 다른 변수에 대입하면 객체는 하나이고 가리키는 손가락만 둘이 돼요.
class Profile { var name = "김개발" }
let a = Profile()
let b = a // 참조 복사: 객체는 하나
b.name = "이코딩"
print(a.name) // "이코딩" — 원본도 바뀜
얕은 복사는 껍데기 한 겹만 새로 만드는 겁니다. 새 상자를 만들긴 했는데, 상자 안에 든 게 참조라면 그 참조가 가리키는 대상은 공유돼요.
깊은 복사는 상자 안의 상자, 그 안의 상자까지 전부 새로 만드는 겁니다. 두 구조는 완전히 독립이라 어느 쪽을 고쳐도 서로 영향이 없습니다.
Swift 값 타입: 대입이 곧 복사
Swift의 구조체·열거형·기본 타입은 값 타입이라, 대입하는 순간 값이 복제됩니다.
var original = [1, 2, 3]
var copy = original
copy.append(4)
print(original) // [1, 2, 3] — 안전
다른 언어에서 배열 대입이 참조 복사인 것과 비교하면 큰 차이입니다. 파이썬에서 리스트를 대입하면 같은 리스트를 가리키죠.
“매번 통째로 복사하면 느리지 않나?” 싶은데, 여기서 Copy-on-Write(COW)가 등장합니다. Array·Dictionary·String은 대입 시점엔 내부 버퍼를 공유하다가, 어느 한쪽이 수정하는 순간에만 실제 복사를 수행해요. 읽기만 하면 복사 비용이 없고, 의미상으로는 항상 독립 값처럼 동작합니다. 값 타입의 안전함과 참조 공유의 효율을 같이 챙긴 설계예요.
그런데 함정이 남아 있습니다
값 타입 안에 참조 타입이 들어가는 순간, 얕은 복사 문제가 부활합니다.
class Attachment { var filename = "a.png" }
struct Mail {
var title: String
var attachment: Attachment // 구조체 안의 클래스
}
var mail1 = Mail(title: "원본", attachment: Attachment())
var mail2 = mail1 // 구조체는 복사되지만...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — 원본도 바뀜!
구조체 Mail은 분명 복사됐습니다. title은 독립이에요. 하지만 attachment 프로퍼티에 들어 있던 건 “참조”라서 복사된 것도 참조입니다. 두 Mail이 같은 Attachment를 공유하게 된 거죠. 정확히 얕은 복사의 정의입니다.
해결하려면 깊은 복사를 직접 만들어야 합니다. Attachment를 새로 만들어 넣는 복사 로직을 작성하거나, 애초에 Attachment를 구조체로 바꾸는 방법이 있어요. Objective-C 계열에서는 NSCopying 프로토콜의 copy(with:)를 구현하는 게 전통적인 방식이고 클래스 안에 또 클래스가 있다면 그 안까지 재귀적으로 복사해야 진짜 깊은 복사가 됩니다.
실무 관점의 결론은 Swift가 값 타입을 권장하는 이유와 만납니다. 모델을 구조체와 값 타입 프로퍼티로만 구성하면 이 문제 자체가 사라져요.
면접에서 한 문장으로
“얕은 복사는 최상위 껍데기만 새로 만들어 내부 참조가 공유되는 복사이고, 깊은 복사는 내부까지 재귀적으로 복제해 완전히 독립시키는 복사입니다. Swift 값 타입은 대입만으로 독립 복사처럼 동작하지만, 값 타입 안의 참조 타입 프로퍼티는 여전히 공유되므로 주의해야 합니다.”
꼬리 질문 대비: “COW가 뭔가요?”(수정 시점까지 복사를 미루는 최적화), “깊은 복사는 어떻게 구현하나요?”(NSCopying 또는 수동 복제, 재귀 적용)까지 답하면 충분합니다.
정리
- 복사는 참조 복사 → 얕은 복사 → 깊은 복사 세 단계로 나눠 이해한다
- 참조 복사는 이름표만 복제(객체 공유), 얕은 복사는 껍데기만 복제(내부 참조 공유), 깊은 복사는 전부 복제(완전 독립)
- Swift 값 타입은 대입이 곧 복사이고, Array·String은 COW로 수정 시점까지 실제 복사를 미룬다
- 값 타입 안에 클래스 프로퍼티가 있으면 얕은 복사 문제가 그대로 재현된다
- 깊은 복사가 필요하면 NSCopying이나 수동 복제 로직을 재귀적으로 구현한다
- 근본 해법은 모델을 값 타입으로 구성하는 것 — Swift가 구조체를 권하는 또 하나의 이유
