Swift 정리 - 9. 구조체와 클래스


Swift의 구조체와 클래스에 대해 알아보자.


Swift.org:구조체와 클래스

지금까지 (1~8) 공부한 내용은 Swift를 이해하는 데 꼭 필요한 최소한의 문법이다. 지금부터 볼 내용은 Swift의 객체지향 프로그래밍 패러다임이 적용되는 요소들이다. Swift는 함수형 프로그래밍 패러다임을 강조하지만, 객체지향 프로그래밍 패러다임도 매우 중요하다.

Structure와 class는 데이터를 용도에 맞게 묶어 표현하고자 할 때 유용하다. 구조체, 클래스는 프로퍼티와 메서드를 사용해서 구조화된 데이터와 기능을 가질 수 있다.

다른 프로그래밍 언어와는 다르게 Swift에서는 struct나 class의 인터페이스와 구현체를 별도로 생성하지 않아도 된다.

Comparing Structures and Classes

Structure와 class의 공통점:

  • 값을 저장할 수 있는 프로퍼티가 있다.
  • 기능을 제공하는 메서드가 있다.
  • 자신의 값에 접근할 수 있는 subscript 문법이 존재한다.
  • 초기 상태를 설정할 수 있는 이니셜라이저가 있다.
  • 기본 기능에서 확장할 수 있다. (extension)
  • 특정 종류의 기본 기능들을 제공하기 위해 프로토콜을 채택할 수 있다.

Class에서는 가능하지만 structure에서는 불가능한 것:

  • 상속
  • 런타임에 수행되는 타입 캐스팅
  • 할당된 자원을 해제하는 디이니셜라이저
  • Reference count(클래스 인스턴스에 하나 이상의 참조를 가능하게 한다.)

위와 같이 클래스에서 지원하는 추가기능은 복잡성을 키웠다. 일반적인 가이드라인은 구조체를 선호하고, 클래스를 사용하기 적절한 경우에만 클래스를 사용한다. 즉 정의하는 커스텀 데이터 타입들은 보통 구조체나 열거형으로 정의하게 될 것이다.

구조체와 클래스의 가장 큰 차이는 구조체의 인스턴스는 값 타입이고, 클래스의 인스턴스는 참조 타입이라는 점이다. 지금까지 공부했던 Swift의 데이터 타입과 열거형은 모두 값 타입이다. 하지만 클래스는 참조 타입으로, C언어에서의 포인터와 유사한 개념이다.

Structure

Definition Syntax

image

새로운 구조체나 클래스를 정의하게 되면 새로운 타입을 정의하게 되는 것이다. 따라서 구조체나 클래스의 이름은 UpperCamelCase로 짓고, 프로퍼티나 메서드 이름은 lowerCamelCase로 짓는다.

struct BasicInformation {
  var name: String
  var age: Int
}

struct Resolution {
    var width = 0
    var height = 0
}

위 구조체들은 각각 두 개의 stored property를 가지고 있다. 이 Stored property는 구조체나 클래스에 같이 묶여 저장되는 상수나 변수를 의미한다.

Structure Instances

구조체를 정의하고 인스턴스를 생성할 때는 기본적으로 생성되는 멤버 와이즈 이니셜라이저를 사용하는데, 이 이니셜라이저의 파라미터는 구조체의 프로퍼티 이름으로 자동으로 지정된다.

// 기본적으로 생성되는 이니셜라이저
var someInformation: BasicInformation = BasicInformation(name: "YJ", age: 100)

// 빈 괄호로 초기화하면 프로퍼티를 기본 값으로 설정해서 초기화 하겠다는 뜻이다.
let someResolution = Resolution()

다만 구조체를 정의했을 때 프로퍼티 기본 값을 지정하지 않았다면 인스턴스를 생성할 때 프로퍼티의 기본 값을 어떤 것으로 설정해야 할 지 모르니 아래와 같이 인스턴스를 생성하게 되면 컴파일 에러가 발생한다.

image

Accessing Properties

구조체 인스턴스의 프로퍼티에 접근하고 싶으면 마침표(.)를 사용하면 된다. 프로퍼티를 변경할 때 인스턴스를 var로 선언 되어 있으면 값을 변경할 수 있고, let으로 선언되어 있으면 값을 변경할 수 없다.

print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0"

// 프로퍼티의 프로퍼티에도 접근할 수 있다.
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0"

Memberwise Initializers for Structure Types

위에서 구조체의 이니셜라이저는 기본적으로 멤버 와이즈 이니셜라이저가 생성된다고 했다. 이 이니셜라이저는 새로운 구조체 인스턴스를 만들기 위해 멤버 프로퍼티를 초기화하는데 쓸 수 있다. 뒤에서 보겠지만 클래스 인스턴스는 구조체와는 다르게 멤버 와이즈 이니셜라이저를 자동으로 생성하지 않는다.

let vga = Resolution(width: 640, height: 480)

Class

Definition Syntax

클래스를 정의할 때는 class 키워드를 사용한다.

image

class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

Instance

클래스의 인스턴스를 생성해서 초기화할 때는 기본 이니셜라이저를 사용한다.

Instance / Object 다른 프로그래밍 언어에서는 주로 클래스의 인스턴스를 객체라고 한다. 하지만 Swift 문서에서는 좀 더 한정적인 인스턴스라는 용어를 사용한다.

// 위에서 프로퍼티에 기본값을 할당했으므로 전달인자를 통해서 초깃값을 따로 전달하지 않아도 된다.
let someVideoMode = VideoMode()

Accessing Properties

구조체와 마찬가지로 마침표를 사용해서 접근한다. 구조체와는 달리 클래스의 인스턴스는 참조 타입이므로 상수로 선언해도 내부의 프로퍼티를 변경할 수 있다.

let mode = VideoMode()
mode.name = "modeName" // ok

기본 이니셜라이저 외에 사용자가 직접 이니셜라이저를 정의할 수도 있는데, 이는 뒤에서 더 공부할 것이다.

클래스 인스턴스의 소멸

클래스의 인스턴스는 참조 타입이므로 더 이상 참조될 필요가 없을 때 메모리에서 해제된다. 이 과정을 소멸이라고 하는데, 소멸하기 직전에 deinit 메서드가 호출된다. 클래스 내부에 deinit 메서드를 구현하면 인스턴스가 소멸하기 직전 우리가 구현한 deinit 메서드가 호출된다.

deinit 메서드를 deinitializer라고 한다. 인스턴스를 생성하는게 이니셜라이저니, 인스턴스를 해제하는 애는 디이니셜라이저가 되는 것이다. deinit 메서드는 다음의 특징이 있다.

  1. 클래스당 하나만 구현할 수 있다. (이니셜라이저는 클래스당 여러 개가 있을 수 있다.)
  2. 매개변수와 값을 가질 수 없다.(이니셜라이저는 다양한 매개변수를 가질 수 있다.)
  3. 매개변수가 없기 때문에 소괄호를 작성하지 않는다.

image

보통 디이녓ㄹ라이저에는 인스턴스가 메모리에서 해제되기 직전에 처리할 코드를 작성한다.

값 타입과 참조 타입

구조체는 값 타입, 클래스는 참조 타입이다. 이 둘의 차이는 상수나 변수에 할당될 때, 함수에 전달될 때 무엇이 전달되느냐이다.

  • 값 타입 : 값이 복사된다.
  • 참조 타입 : 참조(주소)가 전달된다.

값 타입 : 구조체와 열거형

Swift의 모든 기본 타입(정수, 실수, 불리언, 문자열, 배열, 딕셔너리)는 값 타입이고, 구조체를 기반으로 구현되었다. 구조체가 값 타입이나 당연히 이를 기반으로 구현된 타입들도 모두 값 타입이다. 또한 구조체와 열거형도 값 타입이다. 이는 내가 생성하는 구조체나 열거형 인스턴스나, 프로퍼티를 가지는 값 타입들이 코드 내에서 전달될 때 항상 복사됨을 의미한다.

배열, 딕셔너리, 문자열같이 표준 라이브러리에 의해 정의된 컬렉션들은 코드 내에서 전달 될 때마다 복사해서 성능이 저하되는 것을 줄이기 위해 최적화를 사용한다. 바로바로 복사본을 만드는 대신에, 이 컬렉션들은 원래 인스턴스와 이들의 복사본 사이에 요소들이 저장된 메모리를 공유한다. 만약 한 컬렉션의 복사본이 수정되면, 이 수정 전에 요소들은 복사된다.

let hd = Resolution(width: 1920, height: 1080)
// Resolution이 구조체이기 때문에 hd의 복사본이 만들어지고, cinema에 할당된다.
// hd와 cinema는 완전히 다른 인스턴스다.
var cinema = hd

// cinema의 width는 2048로 바뀌지만 hd의 width는 여전히 1920이다.
cinema.width = 2048

위 상황을 그림으로 자세히 표현하면 아래와 같다.

image

두 인스턴스는 완전히 별개의 인스턴스이기 때문에 cinema의 너비를 2048로 설정해도 원본 인스턴스의 너비에는 영향을 미치지 않는 것이다. 이는 열거형에도 똑같이 적용된다.

참조 타입 : 클래스

참조 타입은 상수나 변수에 할당되거나 함수에 전달될 때 복사되지 않는다. 대신 존재하는 인스턴스에 대한 참조가 전달된다.

let tenEighty = VideoMode()
tenEighty.resolution = hd
tenEighty.interlaced = true
tenEighty.name = "1080i"
tenEighty.frameRate = 25.0

// tenEighty에 할당된 인스턴스의 참조가 전달된다. alsoTenEigth는 tenEighty와 같은 인스턴스를 참조하고 있다.
let alsoTenEighty = tenEighty
// tenEighty의 frameRate도 30.0이 된다.
alsoTenEighty.frameRate = 30.0

위 상황을 그림으로 표현하면 아래와 같다.

image

이런 동작들을 보면 참조타입은 흔히 우리가 생각하는 일반적인 방법으로 동작하지는 않는다고 생각할 수도 있다.

Identity Operators

클래스가 참조 타입이기 때문에, 여러 상수와 변수들이 하나의 인스턴스를 동시에 참조하는 경우가 있을 수 있다.

두 상수나 변수가 같은 클래스 인스턴스를 참조하는지 확인하기 위해 식별 연산자를 사용한다.

  • === : identical to
  • !== : Not identical to
if tenEighty === alsoTenEighty {
    print("tenEighty and alsoTenEighty refer to the same VideoMode instance.")
}
// Prints "tenEighty and alsoTenEighty refer to the same VideoMode instance."

Pointer

C, C++, Objective-C를 안다면 이 언어들은 메모리의 주소를 가리키기 위해 포인터를 사용하고 있다는 것을 알 것이다. Swift에서 특정 참조 타입을 가리키는 상수나 변수는 C에서의 포인터와 비슷하지만, 메모리의 주소를 직접적으로 가리키지도 않고, 참조하고 있다는 뜻으로 * 기호를 붙이지도 않는다. 만약 포인터를 직접 사용해야 하는 경우에서는 표준 라이브러리에서 제공하는 포인터와 버퍼 타입을 사용할 수 있다. (https://developer.apple.com/documentation/swift/swift_standard_library/manual_memory_management)

구조체와 클래스 중 선택하기

구조체와 클래스는 모두 새로운 데이터 타입을 정의하고 기능을 추가한다는 점이 같다. 하지만 구조체는 값 타입이고, 클래스는 참조 타입이기 때문에 그 용도가 조금은 다르다.

애플은 가이드라인에서 다음 조건 중 하나 이상에 해당하면 구조체를 사용하는 것을 권장한다.

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