Swift에서 class와 struct의 공통점은 '데이터를 캡슐화'하고, '사용자 정의 데이터 타입을 생성'하는 것이다.
그러나 메모리 관리 측면에서 중요한 차이점이 있다.
1. Value Type과 Reference Type
Struct (Value Type):
- struct는 Swift에서 값 타입(value type)이다.
- 값 타입은 변수에 할당되거나 함수에 전달될 때 그 값이 복사된다.
- 각각의 인스턴스는 고유한 데이터 복사본을 가지며, 다른 인스턴스와 독립적이다.
- 메모리에서는 스택(stack) 영역에 저장되는 경우가 많다. 스택은 컴파일 시간에 크기가 결정되는 변수들을 저장하며, 메모리 할당 및 해제가 빠르다.
Class (Reference Type):
- class는 참조 타입(reference type)이다.
- 참조 타입은 변수에 할당되거나 함수에 전달될 때 실제 데이터가 아닌 메모리 주소(즉, 참조)가 전달된다.
- 이러한 방식은 여러 변수가 메모리상의 동일한 데이터 인스턴스를 공유할 수 있게 한다.
- 클래스 인스턴스는 힙(heap) 영역에 저장된다. 힙은 런타임에 크기가 결정되는 변수들을 저장하며, 스택에 비해 메모리 관리가 더 복잡하고 느릴 수 있다.
2. 메모리 관리: Stack vs Heap
Struct의 메모리 관리
- 구조체 인스턴스는 스택에 할당되어 빠르고 자동으로 메모리가 관리된다.
- 스택은 LIFO(Last In, First Out) 방식으로 작동한다.
- 변수가 범위(scope)를 벗어나면 자동으로 메모리에서 제거된다.
- 스택 기반 메모리 할당은 작은 데이터 구조에 적합하다.
Class의 메모리 관리
- 클래스 인스턴스는 힙에 저장되어 더 유연하지만, 복잡하고 비용이 많이 드는 메모리 관리가 필요하다.
- Swift에서는 ARC(Automatic Reference Counting)를 사용하여 클래스 인스턴스의 메모리를 관리한다.
- 객체에 대한 참조가 더 이상 없을 때 시스템이 자동으로 메모리를 해제하는 방식
3. 성능 측면
Struct:
- 값 타입의 복사는 대체로 간단한 데이터에 대해서는 매우 빠르지만, 크기가 큰 구조체의 경우 비용이 많이 들 수 있다.
- 따라서 작은 데이터 구조에 적합하다.
Class:
- 참조 타입은 메모리 주소만 전달되므로, 일반적으로 크기에 관계없이 복사 비용이 적다.
- 그러나 참조 카운팅으로 인한 오버헤드가 있으며, 힙 할당/해제로 인해 성능 저하가 발생할 수 있다.
4. 크기 결정: Compile-time vs Runtime
Struct (Compile-time):
- struct는 컴파일 시간에 크기가 결정된다.
- 이는 struct가 값 타입이며, 그 값이 복사될 때 전체 내용이 메모리에 복사되기 때문이다.
- 컴파일 시간에 struct의 크기를 결정하면, 스택 메모리에 효율적으로 할당하고 관리할 수 있다.
Class (Runtime):
- class는 런타임에 크기가 결정된다.
- 클래스 인스턴스는 힙에 저장되며, 인스턴스의 크기는 런타임에 객체가 생성될 때 결정된다.
- 이는 클래스 인스턴스의 크기가 동적으로 변할 수 있고, 상속, 다형성 등으로 인해 컴파일 시점에서 정확한 크기를 결정하기 어렵기 때문이다.
더보기
컴파일:
- 소스 코드가 실행 가능한 코드(바이너리)로 변환되는 과정.
- 이 시점에는 프로그램이 실행되지 않고, 컴파일러는 소스 코드를 분석하고 최적화하여 실행 파일을 생성한다.
- 컴파일 시간에는 실행 환경에 대한 정보가 제한적이므로, 주로 정적인 분석과 최적화가 이루어진다.
런타임:
- 컴파일된 프로그램이 실제로 실행되는 시간.
- 이때 메모리 할당, 사용자 입력 처리, 오류 관리 등 동적인 작업이 수행한다.
- 런타임에는 프로그램이 실제 작동 환경에서 실행되므로, 많은 동적 요소들이 관여한다.
5. 메모리 {할당 ~ 접근 ~ 해제} 과정
Struct (Value Type) - 스택 할당
- 메모리 할당:
- 구조체(Struct) 인스턴스가 생성될 때, 그 크기와 구조는 이미 컴파일 시점에 결정된다.
- 실행 시점에 해당 구조체 변수가 범위(scope)에 들어오면, 스택 메모리에 공간이 할당된다.
- 스택 메모리는 함수 호출과 함께 관리되며, 각 함수 호출에 대한 로컬 변수들이 순차적으로 스택에 쌓인다.
- 메모리 접근:
- 스택에 저장된 변수는 빠르게 접근 가능하다.
- 스택은 LIFO(Last In, First Out) 구조를 가지기 때문에 최근에 할당된 변수가 가장 먼저 접근된다.
- 메모리 해제:
- 변수의 범위(scope)를 벗어나면, 해당 변수에 할당된 메모리는 자동으로 해제된다.
- 예를 들어, 함수가 종료되면 그 함수의 로컬 변수들이 스택에서 제거된다.
- 이 과정은 빠르고 효율적이며, 개발자가 직접 관리할 필요가 없다.
Class (Reference Type) - 힙 할당
- 메모리 할당:
- 클래스(Class) 인스턴스가 생성될 때, 그 크기가 런타임에 결정되고 힙에 메모리가 할당된다.
- 힙은 동적 메모리 할당을 위한 공간으로, 실행 시간에 크기가 결정되는 데이터를 저장한다.
- 클래스 인스턴스에 대한 참조(주소)는 스택이나 다른 클래스의 일부로 저장될 수 있지만, 실제 데이터는 힙에 위치한다.
- 메모리 접근:
- 힙에 저장된 객체는 주소를 통해 접근된다.
- 이 주소는 변수가 스택에 저장되거나 다른 객체에 포함될 수 있다.
- 힙 메모리 접근은 스택에 비해 상대적으로 느릴 수 있다.
- 메모리 해제:
- Swift에서는 ARC(Automatic Reference Counting)를 사용하여 클래스 인스턴스의 메모리를 관리한다.
- 인스턴스에 대한 모든 참조가 사라지면, 즉 참조 카운트가 0이 되면 ARC는 자동으로 해당 인스턴스의 메모리를 해제한다.
- 메모리 누수를 방지하기 위해 강한 순환 참조(strong reference cycles)를 피하는 것이 중요하다.
- 힙에서의 메모리 할당과 해제는 스택에 비해 비용이 많이 들고 복잡할 수 있다.
6. ARC(Automatic Reference Counting)에 대한 추가 설명과 예시
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
reference1 = Person(name: "John") // "John is being initialized"
// 여기서 John에 대한 참조 카운트는 1.
reference2 = reference1 // John에 대한 참조 카운트가 2가 된다.
reference3 = reference1 // John에 대한 참조 카운트가 3이 다.
reference1 = nil // John에 대한 참조 카운트가 2로 감소한다.
reference2 = nil // John에 대한 참조 카운트가 1로 감소한다.
reference3 = nil // "John is being deinitialized"
// 모든 참조가 제거되었으므로 John에 대한 인스턴스가 메모리에서 해제된다.
ARC는 강한 순환 참조(strong reference cycles)를 방지하기 위해,
강한(strong), 약한(weak) 또는 미소유(unowned) 참조에 대한 개념을 이해하는 것이 중요하다.
이러한 메커니즘을 통해 메모리 누수를 방지할 수 있다.
7. ARC의 참조 유형
1. Strong 참조
class Person {
let name: String
init(name: String) { self.name = name }
}
var person1: Person? = Person(name: "Alice")
var person2 = person1 // Strong 참조
// 이 시점에서 'Alice'에 대한 참조 카운트는 2이다.
- 기본 참조 방식:
- Swift에서는 변수가 클래스의 인스턴스를 참조할 때 기본적으로 strong 참조를 사용한다.
- 이는 참조하는 인스턴스가 메모리에 유지되어야 함을 의미.
- 작동 원리:
- strong 참조가 있는 동안, ARC는 해당 인스턴스를 메모리에서 해제하지 않는다.
- 인스턴스에 대한 strong 참조가 하나라도 있으면, 해당 인스턴스는 메모리에 유지된다.
- 메모리 누수 위험:
- strong 참조는 순환 참조(두 객체가 서로를 strong으로 참조)를 생성할 수 있으며, 이는 메모리 누수로 이어질 수 있다.
2. Weak 참조
class Apartment {
let unit: String
weak var tenant: Person?
init(unit: String) { self.unit = unit }
}
var john: Person? = Person(name: "John")
var unit4A: Apartment? = Apartment(unit: "4A")
unit4A?.tenant = john
// 'John'에 대한 참조는 weak이므로 참조 카운트는 증가하지 않는다.
// 여기서 Apartment 클래스의 tenant 속성은 weak 참조이다.
// john이 unit4A.tenant에 할당되어도, 'John' 인스턴스의 참조 카운트는 증가하지 않는다.
// unit4A.tenant에 <- john을 할당 == unit4A.tenant가 -> john을 참조함
- 옵셔널 참조:
- weak 참조는 옵셔널 형태로 선언된다.
- 즉, 참조된 인스턴스가 메모리에서 해제될 수 있으며, 그 경우 참조는 자동으로 nil이 된다.
- 주 사용 시나리오:
- 순환 참조를 피하기 위해, 두 객체 간의 관계에서 한 쪽이 다른 쪽보다 "덜 중요할" 때 사용된다.
- 예를 들어, delegate 패턴에서는 delegate를 weak으로 선언하는 것이 일반적이다.
- 참조 카운트 영향:
- weak 참조는 참조 카운트를 증가시키지 않는다.
- 따라서, weak 참조만이 남아있는 객체는 ARC에 의해 메모리에서 해제될 수 있다.
3. Unowned 참조
class CreditCard {
let number: UInt64
unowned let customer: Person
init(number: UInt64, customer: Person) {
self.number = number
self.customer = customer
}
}
var bob: Person? = Person(name: "Bob")
var card: CreditCard? = CreditCard(number: 1234567890123456, customer: bob!)
// 'bob' 인스턴스는 CreditCard에 의해 unowned 참조된다.
// 이 경우 CreditCard 클래스는 customer 속성을 unowned로 참조한다.
// 'bob' 인스턴스는 CreditCard가 존재하는 한 메모리에 유지되지만, CreditCard 인스턴스는 'bob'이 메모리에서 해제되더라도 그 상태를 변경하지 않는다.
// 'bob'이 메모리에서 해제된 후에 card의 customer에 접근하려고 하면 런타임 오류가 발생할 수 있다.
- Non-Optional 참조:
- unowned 참조는 non-optional 타입.
- 참조된 인스턴스가 항상 존재한다고 가정한다.
- unowned 참조는 참조된 인스턴스가 해제된 후에도 nil로 바뀌지 않는다.
- 사용 상황:
- unowned 참조는 두 객체가 서로에 대해 동등한 생명주기를 가지고 있거나,
참조하는 객체가 참조된 객체보다 더 긴 생명주기를 가질 때 사용된다. - 순환 참조를 피하는 데 사용될 수 있으나, 참조된 객체가 해제된 후에는 사용할 수 없다.
- unowned 참조는 두 객체가 서로에 대해 동등한 생명주기를 가지고 있거나,
- 위험성:
- unowned 참조를 사용할 때는 주의가 필요하다.
- 참조된 객체가 해제된 후에 unowned 참조를 접근하면 런타임 오류가 발생할 수 있다.
더보기
'참조하다'와 '참조되다'의 개념
- 참조하다 (Referencing):
- 객체 A가 객체 B를 '참조한다'는 것은 A가 B를 가리키고 있음을 의미.
- 즉, A는 B의 데이터에 접근할 수 있으며, B의 생명주기에 영향을 줄 수 있음.
- 참조되다 (Being Referenced):
- 객체 B가 '참조된다'는 것은 하나 이상의 객체가 B를 가리키고 있음을 의미한다.
- B는 참조하는 객체들에 의해 메모리에 유지될 수 있다.
'참조된 객체가 해제되다'
- 의미:
- '참조된 객체가 해제된다'는 것은 그 객체가 더 이상 필요하지 않아 메모리에서 제거되었음을 의미한다.
- 개발자의 역할:
- Swift에서는 주로 ARC(Automatic Reference Counting)가 이 과정을 관리한다.
- 개발자는 객체의 참조를 제거함으로써 ARC가 객체를 메모리에서 해제하도록 유도할 수 있다.
- 예를 들어, 객체에 대한 모든 strong 참조를 nil로 설정하면, ARC는 참조 카운트가 0이 되었음을 감지하고 객체를 메모리에서 해제한다.
+ 24.02.04
컬렉션 타입(Array, Dictionary, Set)의 조금은 다른 점
대량의 데이터를 관리하는 경우, 값 타입은 매번 전체 값을 메모리에 저장해야 하는 문제가 있다. 느려진다.
특히 이런 문제를 마주할 가능성이 높은 컬렉션 타입은 값 타입이지만, 내부적으로 참조 타입을 사용한다.
즉, 메모리 관리를 위해 Stack과 Heap을 모두 사용한다.
1. 메모리 할당
- 스택에서의 할당
- 컬렉션 타입의 인스턴스가 생성될 때, 인스턴스의 메타 데이터는 스택에 할당된다.
- 메타 데이터에는 컬렉션의 크기, 용량 등의 정보가 포함된다.
- 기존 규칙에 따라, 스코프를 기준으로 즉시 할당된다.
- 힙에서의 할당
- 실제 데이터(예: 배열의 요소)는 힙에 할당된 참조 타입을 통해 관리된다.
- 즉, 컬렉션에 데이터가 추가, 수정, 삭제되는 경우 힙에 할당된다.
- ARC를 통해서도 관리된다.
2. 데이터 수정
- Copy-On-Write(COW)
- 컬렉션의 복사본이 수정되려 할 때, Swift는 해당 컬렉션이 다른 인스턴스와 내부 데이터를 공유하고 있는지 확인한다.
- 공유되고 있다면, 수정 작업 수행 전에, 내부 데이터의 실제 복사본을 생성한다. (힙에 새로운 데이터 할당)
- 불필요한 데이터 복사를 방지한다.
- 이때, ARC로 새로운 할당을 추적한다.
- Class의 그것과 같다.
3. 메모리 해제
- 스택에서의 해제
- 컬렉션 타입의 인스턴스가 스코프를 벗어나면, 스택에 저장된 메타 데이터는 자동으로 해제된다.
- 빠르다. ARC 필요없다.
- 힙에서의 해제
- 컬렉션 타입의 인스턴스에 연결된, 실제 데이터는 ARC에 의해 관리된다.
- 인스턴스에 대한 모든 참조가 사라지면(RC == 0), ARC는 힙에서 해당 데이터를 해제한다.
'Dev > Swift' 카테고리의 다른 글
Swift 문법 (4) - Optional Types (0) | 2024.04.11 |
---|---|
[Swift] 타입(Type)의 종류 (0) | 2024.02.04 |
Swift 문법 (3) - Tuple, Enumeration, Operators (1) | 2024.01.14 |
Swift 문법 (2) - Variables, Constants, Strings, Types (1) | 2024.01.14 |
Swift 문법 (1) - Swift 문법 특징 (0) | 2024.01.11 |