Class와 Struct
새로운 데이터 구조를 정의할 때 구조체와 클래스 중에 무엇을 사용할지 고민합니다.
클래스와 구조체의 차이에 대해 알아보고 어떤 상황에서 무엇을 쓰면 좋을지 알아보겠습니다.
공통점
Swift에서 Class와 Struct는 공통점이 꽤 많습니다.
property와 메서드를 정의할 수 있고 init 함수를 통해 초기화할 수 있습니다.
또한, 둘 다 extension을 통해 기능을 확장할 수 있으며 protocol 채택도 할 수 있습니다.
이러한 공통점 덕에 구조체와 클래스 중 무엇을 사용해도 어느정도 구현은 할 수 있죠.
하지만 최적화된 구현을 하기 위해서는 차이점을 파악하여 적절한 선택을 해야합니다.
지금부터는 차이점에 대해 알아봅시다.
타입 종류
클래스는 참조(reference) 타입이고 구조체는 값(value) 타입입니다.
변수에 대입되거나 매개변수로 전달될 때
클래스 인스턴스는 인스턴스 참조 값이 전달되고 구조체는 복사되어 새로운 인스턴스가 생성됩니다.
클래스는 data의 reference가 2개 생기고 구조체는 data가 2개가 생긴다는 말인데요.
이렇게 되면 클래스는 1개만 내용을 바꿔도 2개가 모두 바뀌고 구조체는 두 개가 영향이 없습니다 ㅎ
struct PersonS {
var name: String = "시리"
}
class PersonC {
var name: String = "시리"
}
let personS1: PersonS = PersonS()
let personC1: PersonC = PersonC()
print("personS1 name: \(personS1.name)")
print("personC1 name: \(personC1.name)\n")
var personS2 = personS1
var personC2 = personC1
personS2.name = "팀쿡"
personC2.name = "팀쿡"
print("personS1 name: \(personS1.name)")
print("personS2 name: \(personS2.name)\n")
print("personC1 name: \(personC1.name)")
print("personC2 name: \(personC2.name)")
직접 확인을 해보면,
Struct는 personS1과 personS2가 서로에게 영향을 주지 않지만
Class는 personC1과 personC2가 서로에게 영향을 줍니다.
이런 차이점이 있기 때문에 클래스를 사용한다면 property 변경에 신중해야겠죠?
메모리 비교
타입 종류의 차이로 인해 클래스와 구조체는 사용하는 메모리도 다릅니다.
클래스
클래스는 참조 타입이기 때문에 인스턴스가 Heap 영역에 저장됩니다.
Stack에 메모리 주소를 저장하고 Heap 메모리에 접근하는 방식입니다.
Heap 영역에 저장되는 데이터들은 모두 런타임에 크기가 결정됩니다.
따라서 런타임에 추가적인 연산을 해야하고 이는 성능 저하로 이어집니다.
마지막으로 Heap 영역은 하나의 프로세스의 Thread들이 공유하기 때문에 Thread-Safe를 고려하여 개발해야 합니다.
구조체
구조체는 값 타입으로 인스턴스가 Stack 영역에 저장됩니다.
Stack 영역은 자료구조 특성상 pop, push 명령어 하나로 메모리 할당/해제를 할 수 있습니다.
이 타이밍을 컴파일 단계에서 알 수 있기 때문에 런타임에 성능 저하가 발생하지 않습니다.
Stack 영역에 저장되면 가장 큰 장점이 Thread-Safe 하다는 것입니다.
Thread도 각자 독립적인 스택 영역을 가지고 있기 때문입니다.
참조 타입은 ARC 관리 대상
클래스는 reference 타입이므로 ARC에 의해 메모리가 관리됩니다.
따라서 클래스는 reference counting에 유의하며 개발을 해야합니다.
일반적인 상황에서는 신경쓰지 않아도 되지만 강한 순환 참조가 발생하면 메모리 leak이 발생하므로 주의해야 하는데요.
이런 상황에서는 weak, unowned 참조를 사용해 강한 순환 참조를 해결해야 합니다.
ARC에 대한 내용은 위 포스팅에 자세히 작성되어 있습니다.
값 타입은 Copy-on-write로 최적화
Swift의 값(Int, Double, String, Array, Set, Dictionary) 타입은 복사될 때 COW(Copy-on-write) 방식을 사용합니다.
위에서 알아봤듯이 값 타입은 복사하면 두 개의 데이터가 생성됩니다.
이때 복사한 값에 변화가 없다면 굳이 새로운 메모리 영역에 복사하지 않고 기존 영역을 참조하고 있다가
값에 변화가 생기면 그 때 새로운 메모리에 옮기면 메모리 효율을 높일 수 있겠죠?
이 방식을 COW 방식이라고 합니다.
let arr: [Int] = Array(repeating: 0, count: 100)
var arr2 = arr //같은 메모리를 가리킴
arr2[0] = 3 //이때 메모리 복사가 발생
위 코드를 보면 arr2에 arr를 대입만 했을 때는 같은 메모리 공간을 가리키고 있다가
arr2가 변경될 때 새로운 메모리에 복사가 되는 것입니다.
반대되는 개념은 Copy-on-assignment 인데요.
복사가 발생할 때마다 새로운 메모리 공간에 무조건 복사를 하는 것입니다.
Copy-on-assignment 보다 Copy-on-write를 사용하면 불필요한 메모리 사용을 줄일 수 있습니다.
클래스와 구조체의 혼합
개발을 하다보면 클래스 안에 구조체를 넣거나 구조체 안에 클래스를 넣는 경우도 있습니다.
이럴 때는 타입, 메모리, 복사 등은 어떻게 이루어 질까요??
구조체 안에 클래스
구조체 안에 클래스가 있는 상황은 값 타입이 참조 타입을 포함한다고 말할 수 있습니다.
이 경우 값 타입은 Stack에 할당되기는 하지만 내부에 참조 타입이 있기 때문에 Reference Count를 처리해야 합니다.
또한 복사가 이루어질 때 데이터가 두 개 생성되는 것은 동일하지만, 참조 타입 property는 주소 값이 복사가 됩니다.
즉, 값 타입 안의 참조 타입은 다른 데이터의 영향을 받는다는 의미입니다.
struct PersonS {
var name: String = "시리"
var personC: PersonC = PersonC()
}
class PersonC {
var name: String = "시리"
}
let personS1: PersonS = PersonS()
var personS2 = personS1
var personS3 = personS1
personS2.personC.name = "팀쿡"
print("personS1 name: \(personS1.personC.name)")
print("personS2 name: \(personS2.personC.name)")
print("personS2 name: \(personS3.personC.name)")
코드로 살펴보겠습니다. 이 코드의 마지막 실행 결과는 어떨까요?
하나의 데이터 변경이 다른 인스턴스에도 영향을 주는 모습을 볼 수 있습니다.
값 타입만 존재하는 구조체는 "시리 시리 팀쿡"으로 출력이 될 것입니다.
하지만 위 예시는 값 타입 안의 참조 타입을 변경하였기 때문에 다른 인스턴스에도 영향을 주는 것입니다.
값 타입 안에 참조 타입을 사용한다면 이러한 케이스를 잘 고려해야 합니다.
클래스 안에 구조체
클래스 안에 구조체는 참조 타입이 값 타입을 포함했다고 말할 수 있습니다.
이 경우는 일반적인 클래스 인스턴스와 동일하게 동작합니다.
struct PersonS {
var name: String = "시리"
}
class PersonC {
var name: String = "시리"
var person: PersonS = PersonS()
}
let person1: PersonC = PersonC()
var person2 = person1
var person3 = person1
person3.person.name = "팀쿡"
print("person1 name: \(person1.person.name)")
print("person2 name: \(person2.person.name)")
print("person3 name: \(person3.person.name)")
구조체는 값 타입이지만 클래스 인스턴스에 포함되어 있으므로 클래스 인스턴스처럼 동작됩니다.
따라서 값이 복사될 때 데이터가 복사되는게 아니라 데이터의 reference가 증가하는 것입니다.
클래스와 구조체의 혼합하여 사용할 때의 차이점을 아시겠죠?
구조체 안에 클래스를 넣을 때는 주의가 필요하다는 것도 잊지 마세요.
Class vs Struct
공통점과 차이점은 이제 아실겁니다.
그럼 어떤 상황에서 무엇을 사용하는 게 좋을까요?
공식문서에서는 기본적으로 구조체를 사용하는 것을 권장합니다.
위에서 봤듯이 Swift에서 클래스와 구조체는 많은 공통점이 있기 때문입니다.
클래스에는 구조체에 없는 더 많은 기능을 지원하는데 이런 기능이 필요 없다면 굳이 클래스로 안 써도 되는 것이죠.
클래스의 고유 기능만큼 자원이 절약 되는 것입니다.
따라서 기본으로 구조체를 사용하되 아래 상황일 때는 클래스를 사용하면 좋습니다.
Objective-C와 상호 이용을 해야 할 때
Objective-C에서 지원하는 API를 사용해야 할 때 클래스를 사용합니다.
요즘에는 Objective-C가 거의 없어졌지만 그럼에도 필요한 경우가 있다면 클래스를 사용하면 됩니다.
객체 비교가 필요할 때
값 비교가 아닌 객체가 동일한지 비교해야 하는 경우가 있습니다.
이는 공식 문서에서 설명하는 기준이기도 합니다.
=== 연산자나 !== 연산자를 이용해서 두 객체가 동일한 메모리에 위치하는지(완전히 동일한 객체인지) 비교하려면
클래스를 이용해야 합니다.
상속을 필요할 때
상속이 필요할 때도 클래스를 이용해야 합니다.
구조체는 상속이 불가능하기 때문입니다.
하지만 상속 대신 프로토콜을 이용하면 상속을 하지 않고도 기능을 확장할 수 있습니다.
프로토콜은 구조체, 열거형에서도 가능하므로 만약 프로토콜로 가능한 기능이라면 클래스 + 상속 대신
구조체 + 프로토콜이 좋을 수 있습니다.
마무리
오늘은 클래스와 구조체에 대해 알아보았습니다.
많이 닮았지만 다른 개념이므로 정확히 알고 사용하면 더 효율적인 개발을 할 수 있을 것입니다.
iOS 면접 단골 질문이기도 하고요 ㅎㅎ...
이어서 보면 좋은 글
감사합니다!
참고
https://jeonyeohun.tistory.com/178?category=874083
https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.