본문 바로가기
TIL (Today I Learned)

[Swift] Class vs Struct

by Bokoo14 2024. 2. 10.

참고

더보기

 

가장 큰 차이는 "Class 참조타입이고 ARC 메모리 관리를 한다. Struct 타입이다."

 

Class와 Struct의 공통점

  • 값을 저장할 프로퍼티를 선언할 수 있다.
  • 메서드를 선언할 수 있다.
  • 내부 프로퍼티에 .를 사용하여 접근할 수 있다.
  • 생성자(init)를 사용해 초기 상태를 설정할 수 있다.
  • extension을 사용하여 기능을 확장할 수 있다.
  • Protocol을 채택하여 기능을 설정할 수 있다.

 

Class의 특징 (차이점)

  • Reference Type
  • ARC로 메모리 관리를 한다.
  • 같은 클래스 인스턴스를 만들고 값을 변경하면 모든 변수에 영향을준다. (참조타입이라서)
  • 상속 가능
  • 타입 캐스팅을 통해 런타임에서 클래스 인스턴스의 타입을 확인할 수 있다.
  • 인스턴스가 소멸될때 deinit 메서드가 호출된다. (참조타입이라서)
  • 참조가 어디서 어떻게 될지 모르기 떄문에 Heap영역에 할당

Struct의 특징 (차이점)

  • Value Type
  • 여러 인스턴스를 만들고 값을 변경해도, 각 인스턴스의 값은 다르다. (값 타입이라서) -> 즉 같은 구조체를 여러 개의 변수에 할당한 뒤 값을 변경시키더라도 다른 변수에 영향을 주지 않음(값 자체를 복사)
  • 언제 생기고 사라질지 컴파일 단계에서 알 수 있어서 메모리의 Stack 공간에 할당
  • 상속 불가
  • 구조체는 생성자를 구현하지 않아도 default initalizer 를 사용할 수 있다.
  • init은 사용 가능하다 deinit은 사용 불가능하다.
  • Swift 표준 라이브러리의 기본 타입은 모두 구조체이다.(String, Bool, Int, Array, Dictionary, Set .....)

 

클래스는 참조 타입이라 같은 클래스 객체를 할당한 변수의 값을 변경시키면 참조된 객체의 값이 변경되고, 구조체는 타입이기 때문에 같은 구조체 객체를 할당하더라도 매번 새로운 메모리가 할당되어 값을 변경하더라도 다른 구조체 변수에 영향을 주지 않음


Class와 ARC, Retain Cycle

클래스 인스턴스를 여러 곳에서 참조하게 되면, 원래 인스턴스를 해제하더라도 참조 카운트가 남아있어 deinit이 실행되지 않음.

참조되는 모든 값들을 해제해줘야 deinit이 실행됨

-> 이런 특징때문에 retain Cycle이 발생

 

// ratain cycle
class StrongRefClassA {
    var classB: StrongRefClassB?
    deinit {
        print("ClassA 할당 해제")
    }
}

class StrongRefClassB {
    var classA: StrongRefClassA?
    deinit {
        print("ClassB 할당 해제")
    }
}

var classA: StrongRefClassA? = StrongRefClassA()
var classB: StrongRefClassB? = StrongRefClassB()

print(CFGetRetainCount(classA)) // reference count = 2(기본값)
print(CFGetRetainCount(classB)) // reference count = 2(기본값)

classA?.classB = classB
classB?.classA = classA

print(CFGetRetainCount(classA)) // reference count = 3
print(CFGetRetainCount(classB)) // reference count = 3

classA = nil
print(CFGetRetainCount(classB?.classA)) // reference count = 2(기본값)
classB = nil // <- 더 이상 classA, classB의 데이터에 접근 할 수 없지만 deinit 실행되지 않았음 -> 메모리 누수 발생
출처: https://icksw.tistory.com/256 [PinguiOS:티스토리]

이상 classA, classB 접근할 방법이 없는데도 불구하고 참조 카운트가 0 되지 않아 deinit 실행되지 않음

-> 메모리 누수 발생

-> weak, unowned 참조를 사용하면 해결가능


메모리 구조

1) 코드(code) 영역: 실행할 프로그램의 코드
2) 데이터(data) 영역: 전역 번수, 정적(static) 변수
3) 스택(stack) 영역: 컴파일 타임에 크기가 결정됨
4) 힙(heap) 영역: 런타임시 크기가 결정됨(동적할당)

 

 

 

Heap 영역 할당

컴파일 단계에서 생성과 해제를 알 수 없는 참조 타입(Class)의 객체 할당

메모리 할당과 해체가 하나의 명령어로 처리되지 않기 때문에 관리하기 어려움 (참조 계산)

 

또한 Heap 스레드가 공유하는 메모리 공간이기 때문에 스레드로부터 안전하지 않음

이를 관리해주기 위한 lock 같은 자원도 필요하게 되고 이는 오버 헤드로 이어지게 됨 

 

Stack 영역 할당

컴파일 단계에서 언제 생성되고 해제되는지 알 수 있는 구조체와 같은 값들이 스택에 저장

 

Stack: LIFO(Last In First Out) 형태의 자료구조로 가장 마지막에 들어간 객체가 가장 먼저 나오게 되는 자료구조

자료구조 특성상 하나의 명령어로 메모리를 할당, 해제할 수 있음

 

스레드들은 각각 독립적인 Stack 공간을 가지고 있기 때문에 상호 배제를 위한 자원이 필요하지 않아 스레드로부터 안전함

 

Stack 값을 사용하는 것이 Heap 값을 사용하는 것보다 빠르다.

 

구조체와 클래스가 중첩된 경우

1. 값 타입을 포함하는 참조 타입 (클래스 내부에 구조체 프로퍼티가 있는 경우)

참조타입이 소멸되기 전에 값 타입도 힙에 저장

클로저 내부에 사용하는 값 타입도 이러한 경우에 포함됨

2. 참조 타입을 포함하는 값 타입 (구조체 내부에 클래스 프로퍼티가 있는 경우)

내부에 참조타입이 있기 때문에 참조 카운팅을 처리해줌


Copy-on-assignment, Copy-on-write

copy-on-assignment

타입을 다른 변수에 할당하면 복사를 하게 됨

새로운 메모리 공간에 같은 값을 복사

Copy-on-write

다른 변수에 할당하면 일단은 메모리를 할당하지 않고 같은 곳을 보고 있다가, 해당 값을 변경할 실제로 메모리에 값을 복사하고 값을 변경. 

이는 메모리를 최적화해주기 위함이며 Swift에서는 Int, Double, String, Array, Set, Dictionary에서만 사용하고 있음

참조 타입을 포함하고 있는 타입은 이러한 메모리 최적화를 없음 (억지로 만들 있지만 이는 많은 오버헤드를 발생시킴)


언제 Struct를 쓰고 언제 Class를 써야 할까?

Struct를 사용하는게 좋은 경우

  • 연관된 간단한 값의 집합을 캡슐화하는 것만이 목적일 경우
  • 캡슐화한 값을 참조하는 것보다 복사하는 것이 합당할때
  • 구조체에 저장된 프로퍼티가 값 타입이며, 참조하는 것보다 복사하는것이 합당할때
  • 다른 타입으로부터 상속받거나, 자신을 상속할 필요가 없을때

Class를 사용하는게 좋은 경우

  • Objective-C와 상호 운용성이 필요할 때
 

'TIL (Today I Learned)' 카테고리의 다른 글

[Swift] Swift란?  (0) 2024.02.12
[CS] 메모리 구조  (1) 2024.02.10
[Swift] 메모리 관리  (1) 2024.02.08
[SwiftUI] SwiftUI의 View란?  (0) 2024.01.30
ObservableObject , @ObservedObject, @StateObject  (0) 2024.01.21