Struct 와 Class, Enum 의 차이점
개요
Swift 에서 지원하는 자료형인 class 와 struct 의 차이점에 대해서 알아보자. 또한 enum 에 대해서도 알아보자.
객체지향 언어를 다루는 많은 개발자들은 이미 class 의 개념에 대해서 익숙하다. 하지만 필자는 struct 라는 자료형을 swift 에서 처음으로 접해봤는데 (c언어를 제외하고), 이 두 가지 타입의 차이점이 극명하고 장단점이 명확하다보니, 다양한 상황에서 적절한 타입을 사용하는 것이 성능 개선을 위해 필요한 능력이 되었다.
Class, Struct 의 공통점
- 관련있는 값을 저장하거나 계산할 수 있는 프로퍼티를 선언할 수 있다.
- 함수를 선언할 수 있다.
- 내부에 선언된 프로퍼티나 메소드에 (타입이름).(프로퍼티 이름) 과 같이 . 을 사용해 접근할 수 있다.
- Protocol 을 채택할 수 있다 (인터페이스의 구현).
- 생성자를 사용해 초기 상태를 지정할 수 있다.
- extension 을 사용해 기능을 확장할 수 있다.
Class 의 특징
위에서 Class 와 Struct 의 공통점을 알아보았으니, 이제 Class 만이 가지고 있는 특징에 대해 알아보자.
참조 타입이다.
Class가 가지고 있는 가장 큰 특징이다. 참조 타입을 가지고 있는 프로퍼티엔 실제 값 대신 해당 값을 저장하고 있는 메모리 주소가 저장된다. 아래 그림을 보도록 하자.
iOS 메모리의 구조는 위와 같이 생겼다. 참조 타입에 해당하는 타입은 인스턴스가 생성될 때, 메모리의 Heap 영역 에 할당된다. 그리고 Heap 영역에 저장된 실제 데이터는 스택 영역에서 참조 프로퍼티 를 통해 접근할 수 있다. 즉 Stack 영역에 존재하는 프로퍼티들은 Heap 영역에 할당된 실제 데이터를 가리키고 있는 주소값을 가지고 있다는 뜻이다.
이것이 무슨 의미를 가지고 있을까? 아래 코드를 통해 계속해서 알아보자.
class Person {
var name: String = "호종"
var age: Int = 27
}
let A: Person = Person() // A라는 프로퍼티에 새로운 Person 인스턴스를 할당!
let B = A // B라는 프로퍼티는 A가 가리키고 있는 주소값을 저장한다.
print(A.name) // 호종
print(B.name) // 호종
A.name = "호종이"
print(A.name) // 호종이
print(B.name) // 호종이
A 프로퍼티에 새로운 Person 인스턴스를 할당했다. 그러나 class 는 참조 타입이기 때문에, 실제 데이터 대신 Heap 영역의 메모리 주소를 가리키고 있다. 그 다음 B 프로퍼티에 A 프로퍼티를 대입했는데, 이 때 B 프로퍼티엔 A 프로퍼티의 주소값이 할당되어 두 프로퍼티는 똑같은 메모리 주소를 가리키고 있다. 즉 다음과 같이 되는 것이다.
A 와 B 가 같은 인스턴스를 가리키고 있기 때문에, A.name 의 변경이 B.name 에도 영향을 미친다. 왜냐면 둘은 같은 인스턴스를 가리키고 있기 때문이다.
ARC로 메모리를 관리한다.
Class 의 경우 ARC 를 활용해 인스턴스를 관리한다. ARC는 인스턴스의 참조 횟수에 따라 인스턴스를 메모리에서 해제할 지, 아니면 계속해서 유지하고 있을지를 결정한다. 아래 코드를 살펴보자.
class Person {
var name: String = "호종"
var age: Int = 27
deinit {
print("메모리에서 해제되었다.")
}
}
var A: Person? = Person() // A라는 프로퍼티에 새로운 Person 인스턴스를 할당!
print(CFGetRetainCount(A)) // 2
var B: Person? = A // B라는 프로퍼티는 A가 가리키고 있는 주소값을 저장한다.
print(CFGetRetainCount(A)) // 3
A = nil
print(CFGetRetainCount(B)) // 2
B = nil
// 메모리에서 해제되었다.
// A와 B가 가리키고 있던 인스턴스가 메모리에서 해제되어 deinit 블록이 실행된다!
A와 B가 동일한 Person 인스턴스를 참조하여 참조 횟수는 3이 되었다. 이후 두 프로퍼티를 nil 으로 만들어 더 이상 해당 인스턴스를 참조하지 않게 만들고, B 가 nil 이 되어 비로소 아무 프로퍼티도 해당 인스턴스를 참조하지 않을 때 class 의 deinit 블록 (class 가 메모리에서 해제될 때 실행될 코드 블록) 이 실행되는 것을 볼 수 있다.
위와 같이 메모리를 관리하기 때문에 주의할 점이 존재한다. 바로 retain cycle, 즉 참조 순환 문제가 발생할 수 있다. 참조 순환 문제란 두 개의 클래스가 서로를 참조하고 있어, 두 클래스 모두 메모리에서 해제되지 않는 상황을 말한다.
A 와 B라는 두 개의 클래스가 있고, 두 클래스는 프로퍼티를 통해 서로를 참조하고 있는 상황이다. 이것을 코드로 표현하면 아래와 같다.
class A {
var b: B?
deinit {
print("A 인스턴스 해제")
}
}
class B {
var a: A?
deinit {
print("B 인스턴스 해제")
}
}
var a: A? = A()
var b: B? = B()
a?.b = b
b?.a = a
print(CFGetRetainCount(a))
print(CFGetRetainCount(b))
a = nil
b = nil
// 두 인스턴스의 참조 횟수는 각각 2로 줄어들었다. 하지만 참조 횟수가 1이 아니기 때문에 두 인스턴스 모두 메모리에서 해제되지 않는다.
이 때 두 인스턴스의 참조 횟수는 3으로, 다음으로 메모리에서 두 인스턴스를 해제하기 위해 a 와 b 프로퍼티를 nil 로 만들었다. 하지만 deinit 문이 호출되지 않는다. 왜냐하면 두 인스턴스의 참조값이 1이 되지는 않기 때문이다. 두 인스턴스의 내부 프로퍼티가 모두 서로를 가리키고 있기 때문이다.
이런 경우엔 다시 각 인스턴스에 접근해 a.b = nil 혹은 b.a = nil 과 같은 코드를 작성해 참조 횟수를 줄여줘야 하는데, 문제는 두 인스턴스의 주소값을 더 이상 모른다. 따라서 이 두 개의 인스턴스는 사용하지도 않는데 영원히 Heap 영역을 차지한다. 이것을 메모리 누수 (memory weak) 라고 부른다.
물론 swift 에선 이러한 상황을 위해 weak, unowned 키워드를 제공한다. Retain cycle 을 해결하기 위한 방법은 다음 링크에서 공부해보자!
상속이 가능하다.
class 는 상속이 가능해 Subtype 다형성을 구현할 수 있다. 또한 class 는 항상 상속 가능성이 존재하기 때문에, 하위 타입에서의 메소드 호출등을 대비하기 위해 런타임에 메소드 호출 테이블을 생성한다. 이 때문에 성능상 단점이 존재할 수 있고, 만약 절대 상속할 수 없는 class 라는 것을 알면 final class 로 성능상 이점을 가져가도록 하자!
deinit 을 사용할 수 있다.
인스턴스가 메모리에서 해제될 때 실행할 코드 블록을 작성할 수 있다.
Struct 의 특징
위에서 class 의 특징을 알아보았다. 이제 struct (구조체) 의 특징을 알아보자!
값 타입이다.
위에서 클래스는 참조 타입이라고 하였다. 그렇다면 값 타입은 무엇일까? 아래 코드를 통해 알아보자.
struct Person {
var name: String = "호종"
var age: Int = 27
}
var a: Person = Person()
var b: Person = a // a의 값을 복사한 아예 새로운 인스턴스가 할당된다!
var c: Person = b // b의 값을 복사한 아예 새로운 인스턴스가 할당된다!
print(a.name) // 호종
print(b.name) // 호종
print(c.name) // 호종
a.name = "호종이"
print(a.name) // 호종이
print(b.name) // 호종
print(c.name) // 호종
Class 를 설명할 때 작성한 코드를 거의 그대로 가져왔다. 달라진 점이 있다면 Person 이 클래스에서 구조체로 변경되었다는 점이다.
클래스 예제와 달리 a 프로퍼티에서 name 을 변경하더라도, b 와 c 의 name 에는 전혀 영향이 가지 않은 것을 알 수 있다. 왜냐하면 구조체는 값 타입이기 때문에, 다른 변수에 값을 할당하거나 매개변수로 넘길 때 아예 새로운 인스턴스가 새로 생성되기 때문이다.
이것이 struct 의 가장 큰 특징이다. 따라서 여러 프로퍼티나 변수가 동일한 인스턴스를 바라봐야 하는 상황에서는 클래스를 사용하는 것이 좋다.
Stack 과 Heap 메모리
이전에 ios 의 메모리 영역으로 Stack 영역과 Heap 영역이 존재하는 것을 살펴보았다. Stack은 유명한 자료구조로 LIFO (Last In First Out) 형태다. 즉 가장 마지막에 들어간 데이터가 가장 먼저 나오게 되는 자료구조이다.
구조체의 경우 컴파일 단계에서 언제 생성되고 해제되는지 알 수 있기 때문에, 구조체와 같은 것들은 스택에 저장된다. 구조체는 참조 타입이 아니라서 여러 변수나 프로퍼티가 같은 인스턴스를 참조하는 경우가 없으므로, 멀티 쓰레드 환경에서 상호 배제 (mutual exclusion) 를 위한 자원이 필요없다.
Heap 영역의 경우 컴파일 단계에서 생성과 해제를 알 수 없는 참조 타입의 객체가 할당되는, 즉 class 타입의 인스턴스들이 할당되는 영역으로, Stack 보다 관리하기 까다로운 영역이다. ARC 를 사용한 메모리 관리와, Heap 은 모든 쓰레드가 공유하는 메모리 영역이기 때문에 데이터 경합을 방지하기 위해 추가적인 자원이 소모된다. 즉 Heap 영역이 Stack 보다 상대적으로 느린 퍼포먼스를 보여준다.
값 타입의 Copy on assignment, Copy on write
값 타입을 다른 변수나 매개벼수로 넘겨주면 값을 복사한 새로운 인스턴스가 할당된다고 했다. 하지만 단순히 변수에 할당하거나 매개변수로 넘겨줄 때마다 새로운 인스턴스가 생성되고 할당되는 것은 성능을 낭비할 수 있다. 이것을 Copy on assignment 라고 하는데, 단어 뜻 그대로 할당 시 바로 인스턴스를 복사하는 것을 의미한다.
하지만 swift 의 Int, Double, String, Array, Set, Dictionary 에서는 할당 시 새로운 인스턴스를 생성하지 않고 같은 인스턴스를 참조한다. 이는 성능을 최적화해주기 위함이다. 그렇다면 새로운 인스턴스는 언제 할당될까? 기존 인스턴스의 값을 변경하려고 할 때 새로운 인스턴스가 할당되는데, 이것을 Copy on write 라고 한다.
Enumeration (열거형)
Enum (열거형) 은 컴파일 타임에서 개발자가 선택할 수 있는 값을 제한하기 위해, 임의의 관계를 맺는 값들을 하나의 타입으로 묶어서 안전하게 하기위해 사용한다. 이를테면 요일을 생각해보자. 월,화,수,목,금,토,일요일이 존재한다. 요일을 나타내는 프로퍼티가 문자열의 형태로 존재한다고 생각해보자.
var today: String = "월요일"
물론 문자열이나 Int 등을 사용해서 요일을 표현할 수 있다. 하지만 개발자가 항상 잘못된 값을 넣을 여지가 존재한다. 즉 today 변수에 "이요일", "안녕하세요" 와 같이 요일에 전혀 무관한 값을 넣어도 컴파일러는 에러를 인식하지 못한다. 즉 개발자가 런타임에 정확한 값이 들어왔는지 검증해야 하는 하나의 수고로움이 더해진 셈이다.
하지만 이것을 enum 으로 표현해보자.
enum Weekday {
case monday
case tuesday
case wednesday
case thursday
case friday
case saturday
case sunday
}
var today: Weekday = .monday // 개발자는 Weekday enum 의 각 case 를 제외한 값들을 넣을 수 없다!
Weekday 타입의 각 case 를 제외한 값은 절대 넣을 수 없기 때문에, 런타임에 잘못된 값이 들어갔는지 체크할 필요도 없고, 또한 가독성이 훨씬 좋아지는 장점이 있다. 또한 열거형의 각 case 가 비트단위의 고유값을 갖고 있기에, 비교문에서의 성능 이점도 가져갈 수 있다. 하지만 열거형이 수정되면 열거형이 사용된 모든 소스들을 다시 빌드해야 하는 경우가 생기므로, 확장성 측면에서의 단점도 가지고 있다.
또한 열거형은 구조체와 마찬가지로 값 타입이다.
References
[Swift] Class와 Struct의 차이점?
안녕하세요 Pingu입니다.🐧 오늘은 iOS 개발에 쓰이는 Swift 언어에서 Class, Struct의 차이점이라는 주제를 가지고 글을 써보려고 합니다. iOS 개발자로 면접을 준비하다 보면 Class, Struct의 차이점이라
icksw.tistory.com
class 와 struct 그리고 enum
안녕하세요! 오늘은 매일매일 사용하면서도 헷갈리는 친구들이죠?? class(클래스)와 struct(구조체)를 알아보도록 할게요! 추가적으로 enum(열거형)도 같이 비교해보도록 하겠습니다~ 우선 가장 많은
h4njun.tistory.com