[Swift] Choosing Between Structures and Classes

Fezravien·2021년 10월 31일
0
post-thumbnail

WWDC 2016 - Understanding Swift Performance에서 Strcut, Class의 성능 부분을 보면서 이런 얘기가 있었다.

Struct안에 레퍼런스가 있는 경우, 레퍼런스 수에 비례하여 레퍼런스 카운팅 오버헤드를 지불하게 되며,
둘 이상의 레퍼런스가 있는경우, Class보다 레퍼런스 카운팅 오버헤드가 더 많이 유지된다.

이런 것을 보고 REST API 연동을 통한 프로젝트를 진행해보면서 네트워크 작업시 디코딩하기 위해 작성한 Model에서 String처럼 Heap에 저장되는게 2개 이상이면 이것 또한 Struct 오버헤드가 존재하는게 아닐까? 라는 생각이 들게 되었다.

struct Item {
    let id: UInt
    let title: String
    let descriptions: String?
    let price: UInt
    let currency: String
    let stock: UInt
    let discountPrice: UInt?
    let thumbnails: [String]
    let images: [String]?
    let registrationDate: Double
}

위 코드를 보고 String이 5개?? 이거 Class 써야되는거 아닌가? 라는 의문을 가졌었죠 ,,,
이제 이런 의문을 타파해보려고 합니다.

공식문서 아티클 Choosing Between Structures and Classes을 정리했습니다

Overview

구조체와 클래스는 데이터를 저장하거나 앱 내의 동작을 모델링하기 위해 사용하는데,
이 둘의 유사한 특징때문에 선택하기 어렵다.

이에 구조체, 클래스를 선택함에 있어서 아래와 같은 옵션을 고려해볼 수 있다.


Choose Structures by Default

Swift의 구조체는 다른 프로그래밍 언어의 클래스에는 없는 많은 기능들을 포함한다.

Swift의 구조체는 저장 프로퍼티, 계산 프로퍼티, 메소드를 포함할 수 있으며, 프로토콜을 채택할 수도 있으며,
표준 라이브러리와 Foundation 프레임 워크는 숫자형, 문자열, 배열, 딕셔너리 같은 여러 타입을 구조체를 통해 구현됐다.

구조체를 사용하면 앱의 전체 상태에 대해 고려할 필요가 없어진다.
구조체는 클래스와 달리 값 타입(value type)이기 때문에 구조체 내에 변경이 생기는 경우에도
변경 사항을 의도적으로 앱의 흐름에 끼워넣지 않는 이상 앱 내 다른 부분에서 신경쓰지 않아도 된다.

데이터 처리를 위해서 Obejctive-C API를 사용하거나,
데이터 모델을 Objective-C 프레임워크에 선언된 클레스 계층내에 적용해야 한다면
클래스클래스 상속을 사용해야한다.

Use Classes When You Need to Control Identity

Swift의 클래스는 참조 타입(reference types)이기 때문에, 동일성(identity)의 개념을 가진다.

서로 다른 두 클래스의 인스턴스가 저장 프로퍼티로 완전히 같은 값을 가지더라도
둘을 identity operator(===)로 비교하면 서도 다르다고 여겨지는 것을 의미한다.
또한, 앱 전체에 걸쳐 공유하는 클래스의 인스턴스를 변경하면,
변경사항은 해당 인스턴스에 대한 참조를 가지는 모든 코드에 영향을 준다.
따라서 인스턴스의 동일성을 위와 같은 방식으로 처리해야 한다면, 클래스를 사용하면 된다.

  • File Handling
  • Network Connection
  • Hardware intermeiaries (CBCentralManager - core bluetooth)

⚠️ 동일성(Identity)은 조심히 다뤄야한다.
클래스 인스턴스를 앱내에서 광범위하게 공유하면, 로직상의 에러가 나기 쉽다.
여기저기서 공유된 인스턴스에 대한 결과값을 예측하기 어렵기 때문에, 코드를 더 정확하게 작성해야 한다.


모델링한 데이터의 동일성을 관리할 필요가 없다면 구조체를 사용해라.

예를 들어 원격 데이터베이스를 사용하는 앱이라면, 인스턴스의 동일성을 완전히 외부 개체가 제어하고 식별자를 이용해 통신한다.
만약 앱 모델의 일관성(consistency)이 서버에 저장되어 있다면, 모델을 식별자들과 함께 구조체에 저장하기만 하면된다.

struct PenPalRecord {
    let myID: Int
    var myNickname: String
    var recommendedPenPalID: Int
}

var myRecord = try JSONDecoder().decode(PenPalRecord.self, from: jsonResponse)

로컬에서 위의 PenPalRecord와 같은 모델 타입을 변경하는 것으로 예를 들 수 있다.
사용자 피드백으로 여러 개의 펜팔을 보여줘야 하는 경우, PenPalRecord 구조체가 데이터베이스 개체의 동일성을 제어하지 않기 때문에
로컬 PenPalRecord 인스턴스에 대한 변경으로 인해 데이터베이스의 값이 실수로 변경될 위험이 없다.

만약 앱의 다른 부분에서 myNickName을 변경하고 서버로 변경 요청을 보낸다고 하더라도,
myID가 상수로 선언되어 있어 로컬에서 변경될 수 없고, 따라서 서버 데이터베이스의 기록도 변경될 수 없을 것이다.

의문을 가지고 시작한 서버로 부터 받은 데이터를 파싱(Codeable)하는 것은 동일성을 관리할 필요가 없기 때문에 Struct가 적합했다.
WWDC 2016 - Understanding Swift Performance에서 데이터가 Heap에 저장되는 레퍼런스 타입이 2개이상 일때의 오버헤드는
동일성을 관리할때 발생되는 Struct 오버헤드 였다.


구조체클래스는 모두 상속을 지원한다.
구조체는 프로토콜만 채택할 수 있고 클래스를 상속하지 못한다.
그러나 구조체와 프로토콜을 함께 사용하면 클래스의 상속과 같은 계층 구조를 만들어 사용할 수 있다.

개발 초기부터 상속 관계를 설계한다면 프로토콜 상속을 추천한다.
프로토콜은 클래스, 구조체, 열거형에서도 사용할 수 있지만, 클래스는 오직 클래스만 상속할 수 있기 때문이다.
데이터를 어떻게 모델링할지 결정해야 하는 상황이라면,
프로토콜 상속을 통해 데이터 타입의 계층 구조를 설계하고, 구조체가 해당 프로토콜을 채택하는 방식을 사용해라.


profile
꺼진 뷰도 다시보자.

0개의 댓글