class와 struct의 차이점은?

클래스과 구조체 (Classes and Structures)

The Swift Language Guide(한국어) - 클래스와 구조체

class와 struct의 공통점

  • 값을 저장하기 위한 프로퍼티를 정의할 수 있다.
  • 기능을 제공하기 위한 메서드를 정의할 수 있다.
  • .를 사용하여 특정 값에 접근할 수 있다.
  • 초기 상태를 설정할 수 있는 initializer를 정의할 수 있다.
  • 기본 구현에서 기능을 확장할 수 있다.
  • 프로토콜을 사용할 수 있다.

class와 struct의 차이점

class

  • 참조 타입이다.
  • ARC로 메모리를 관리한다.
  • 상속이 가능하다.
  • 타입 캐스팅: 런타임에서 클래스 인스턴스의 타입을 확인할 수 있다.
  • 소멸자: deinit을 사용하여 메모리 할당을 해제시킬 수 있다.
  • 참조 카운트: 클래스 인스턴스에 하나 이상의 참조가 가능하다.

struct

  • 값 타입이다.
  • 구조체 변수를 새로운 변수에 할당할 때마다 새로운 구조체가 할당된다.

class 참조 예제

NewClass 클래스를 작성한다.

class NewClass {
    var count = 0
}

class1을 생성하고 class1을 이용해 class2를 생성한다.

var class1 = NewClass()
var class2 = class1

class2의 count값을 변경하였지만 class1의 count값도 변경된 것을 확인할 수 있다. 클래스는 참조 타입이기 때문이다.

class2.count = 1
print(class1.count) // 1

struct 값 타입 예제

NewStruct 구조체를 작성한다.

struct NewStruct {
    var count = 0
}

struct1을 생성하고 struct1을 이용해 struct2를 생성한다.

var struct1 = NewStruct()
var struct2 = struct1

struct2의 count값을 변경하였지만 struct1의 count값은 변경되지 않았다. 구조체는 값 타입이기 때문이다.

struct2.count = 1
print(struct1.count) // 0

class ARC 예제

NewClass에 소멸자를 추가한다.

class NewClass {
    var count = 0
    deinit {
        print("할당 해제")
    }
}

참조 카운트의 기본값은 2이다. classARC2는 classARC1도 참조하고 있기에 참조 카운트가 1 추가된다.

var classARC1: NewClass? = NewClass()
print(CFGetRetainCount(classARC1)) // 2

var classARC2: NewClass? = classARC1
print(CFGetRetainCount(classARC2)) // 3

참조되는 모든 값들을 해제하였기 때문에 deinit이 실행된다. "할당 해제"가 출력된 것을 확인할 수 있다.

classARC1 = nil
print(CFGetRetainCount(classARC2)) // 2
classARC2 = nil // deinit 실행됨

class retain cycle 예제

클래스 StrongRefClassA와 StrongRefClassB를 작성한다.

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

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

클래스 classA와 classB를 생성한다. 참조 카운트의 기본값인 2가 출력된다.

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

print(CFGetRetainCount(classA)) // 2
print(CFGetRetainCount(classB)) // 2

classA의 classB는 classB를 참조하도록 하고, classB의 classA는 classA를 참조하도록 한다. 참조 카운트가 증가한다.

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

print(CFGetRetainCount(classA)) // 3
print(CFGetRetainCount(classB)) // 3

classA와 classB를 지웠지만 deinit이 실행되지 않았다. 이렇게 되면 메모리 누수가 발생하게 된다.

classA = nil
print(CFGetRetainCount(classB?.classA)) // 2
classB = nil // deinit 실행되지 않음

class 메모리 누수 해결

weak 참조를 사용하면 retain cycle을 방지할 수 있다.

class StrongRefClassA {
    weak var classB: StrongRefClassB?
    deinit {
        print("A 할당 해제")
    }
}

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

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

print(CFGetRetainCount(classA)) // 2
print(CFGetRetainCount(classB)) // 2

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

print(CFGetRetainCount(classA)) // 2
print(CFGetRetainCount(classB)) // 2

classA = nil // deinit 실행됨
classB = nil // deinit 실행됨

메모리 할당 차이

이러한 특징들 때문에 구조체와 클래스는 메모리에 저장되는 위치가 다르다. 구조체는 언제 생기고 사라질 지 컴파일 단계에서 알 수 있기에 메모리의 stack 공간에 할당된다. 반면 클래스는 참조가 어디서 어떻게 될 지 모르기에 Heap 공간에 할당된다.

Stack 할당

Stack은 LIFO(Last In First Out)로 가장 마지막에 들어간 객체가 가장 먼저 나오는 자료구조이다. 하나의 명령어로 메모리를 할당, 해제할 수 있다. 또한 컴파일 단계에서 언제 생성되고 해제되는지 알 수 있는 구조체와 같은 값 타입이 저장되게 된다.

스레드들은 각각 독립적인 Stack 공간을 가지고 있기 때문에 상호 배제를 위한 자원이 필요하지 않는다. 즉 스레드로부터 안전하다. 이러한 특징 때문에 Stack의 값을 사용하는 것이 Heap의 값을 사용하는 것보다 빠르다.

*스레드란 프로세스의 작업흐름의 단위를 말한다.

*컴파일이란 사람이 보기 편하게 만든 소스코드를 컴퓨터가 이해할 수 있는 기계어 구조로 변환하는 과정을 의미한다.

Heap 할당

Heap에는 컴파일 단계에서 생성돠 해제를 알 수 없는 참조 타입의 객체가 할당된다. 클래스 객체가 힙에 할당된다. Heap은 참조 계산도 해야 하기에 메모리 할당과 해제가 하나의 명령어로 처리되지 않는다.

Heap은 스레드가 공유하는 메모리 공간이기에 스레드로부터 안전하지 않는다. 이를 관리하기 위한 lock과 같은 자원도 필요하게 되고 이는 곧 오버 헤드로 이어지게 된다.

*오버헤드란 어떤 처리를 위한 간접적인 처리 시간, 메모리 등을 의미한다.

언제 클래스를 쓰고 언제 구조체를 쓸까?

단순한 데이터 값을 보유하기 위해서는 구조체를 쓴다. 그리고 메모리의 스택은 크기가 크지 않기에 작은 값을 갖는 데이터를 처리할 때 구조체를 사용한다. 반면 Object-C와 상호 운용성을 원할 때는 클래스를 사용한다.

참고 사이트 주소

참고 사이트

좋은 웹페이지 즐겨찾기