본문 바로가기
프로그래밍/Swift

[Swift] Structures and Classes (구조체와 클래스)

by 별준 2021. 12. 24.

References

Contents

  • 구조체와 클래스 비교
  • Structures and Enumerations are Value Types
  • Class are Reference Types

구조체(Structures)와 클래스(Classes)는 범용적(general-purpose)이고 유연한 구성 요소입니다. 상수나 변수, 함수를 정의하는 문법을 동일하게 사용하여 속성(프로퍼티, property)와 메소드(method)를 정의하여 구조체나 클래스에 기능을 추가할 수 있습니다.

 

다른 프로그래밍 언어와 달리 Swift는 커스텀 구조체나 클래스를 위해 인터페이스와 구현 파일을 분리하여 만들 필요가 없습니다. Swift에서 구조체 또는 클래스를 단일 파일에 정의하면 해당 구조체와 클래스에 대한 외부 인터페이스를 다른 코드에서 사용할 수 있도록 자동으로 설정됩니다.

클래스의 인스턴스(instance)는 일반적으로 object로 알려져있습니다. 그러나 Swift의 구조체와 클래스는 다른 언어보다 기능적인 면에 훨씬 더 가깝고, 설명의 대부분은 구조체나 클래스 타입의 인스턴스에 적용되는 기능에 대해서 다룹니다.

Comparing Structures and Classes

Swift의 구조체와 클래스는 많은 부분에서 동일합니다.

  • 값을 저장하기 위한 속성(property) 정의
  • 기능(functionality)를 제공하기 위한 메소드(method) 정의
  • subscript 문법을 사용하여 특정 값에 접근할 수 있는 subscript 정의
  • 초기 상태를 설정할 수 있는 이니셜라이저(initializer) 정의
  • 기본 구현에서 기능의 확장
  • 특정 종류의 표준 기능을 제공하기 위한 프로토콜(protocol) 준수

이에 대한 더 자세한 내용은 추후에 다른 포스트를 통하여 알아보겠습니다.

 

구조체에서는 가능하지 않고 클래스에서만 가능한 기능은 다음과 같습니다.

  • 상속(Inheritance) : 클래스의 특성을 다른 클래스에 물려줌
  • 타입 캐스팅(Type casting) : 런타임에 클래스 인스턴스의 타입을 확인
  • 소멸자(Deinitializers) : 클래스의 인스턴스에 할당된 리소스를 해제
  • 참조(Reference counting) : 클래스의 인스턴스에 하나 이상의 참조가 가능

이에 대한 자세한 내용도 추후의 포스트에서 알아보겠습니다.

 

클래스에서만 제공되는 추가적인 기능은 복잡성이 증가하기 때문인데, 일반적으로는 구조체가 더욱 쉽기 때문에 구조체를 선호하고, 필요할 때만 클래스를 사용하도록 권장합니다. 실제로 사용자 정의 타입의 대부분은 구조체나 열거형으로 정의됩니다.

 

Definition Syntax

구조체와 클래스는 유사한 정의 문법을 사용합니다. 구조체는 struct 키워드로 정의하고, 클래스는 class 키워드로 정의합니다.

struct SomeStructure {
    // structure definition goes here
}
class SomeClass {
    // class definition goes here
}
새로운 구조체나 클래스를 정의하는 것은 새로운 swift 타입을 정의하는 것입니다. 따라서 구조체나 클래스의 타입 이름은 표준 swift 타입인 String과 Int와 처럼 UpperCamelCase로 명명하는 것을 권장합니다 (SomeStructure나 SomeClass처럼).
속성(properties)나 메소드(method)는 frameRate나 incrementCount처럼 lowerCamelCase로 명명하여 타입 이름과 차별시켜주는 것이 좋습니다.

 

아래는 구조체와 클래스를 정의하는 예제 코드입니다.

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

Resolution이라 불리는 새로운 구조체를 정의했고, 이 구조체는 pixel에 기반한 해상도 정보를 갖고 있습니다. 이 구조체는 width와 height라는 두 개의 저장 속성(stored properties)를 가지고 있습니다. 저장 속성은 구조체 또는 클래스의 일부로 저장되는 상수 또는 변수입니다. 두 속성은 초기 정수 값 0으로 설정되어 Int 타입이라고 추론됩니다.

 

그리고 VideoMode라는 새로운 클래스도 정의하였습니다. 이 클래스에는 4개의 변수인 저장 속성을 가지고 있는데, 먼저 resolution은 새로운 Resolution 구조체의 인스턴스로 초기화되고, 이 변수의 타입은 Resolution으로 추론됩니다. 다른 3개의 속성인 interlaced는 false로, frameRate는 0.0으로 설정되고, name은 optional String으로 설정됩니다. name 속성은 기본값 nil이 자동으로 설정됩니다.

 

Structure and Class Instances

Resolution 구조체 정의와 VideoMode 클래스 정의는 오직 Resolution과 VideoMode가 무슨 역할을 하는지에 대해서만 묘사합니다. 그 자체로는 특정 해상도나 비디오 모드에 대해서 묘사하지 않습니다. 즉, 타입만 정의했을 뿐 사용하지는 않고 있다는 의미입니다. 정의한 사용자 구조체나 클래스를 사용하기 위해서는 구조체나 클래스의 인스턴스를 먼저 생성해야 합니다.

 

구조체와 클래스의 인스턴스를 생성하는 문법 또한 서로 유사합니다.

let someResolution = Resolution()
let someVideoMode = VideoMode()

위에서 구조체와 클래스는 둘 다 이니셜라이저(initializer) 문법을 사용하여 새로운 인스턴스를 생성하고 있습니다. 이니셜라이저 문법의 매우 간단한 형태는 클래스나 구조체의 타입 이름 뒤에 빈 괄호를 넣어주는 것입니다. 이것은 클래스나 구조체가 새로운 인스턴스를 생성하고, 생성된 인스턴스의 속성들의 값은 기본값으로 설정됩니다. 

 

Accessing Properties

인스턴스의 속성에 액세스하기 위해서는 dot 문법을 사용하면 됩니다. dot 문법은 인스턴스 이름 바로 뒤에 dot(.)을 찍고 속성의 이름을 붙이면 됩니다.

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

위 예제 코드에서 someResolution.width는 someResolution의 width 속성을 참조하고, 설정된 기본값 0을 반환합니다.

 

속성의 서브속성 또한 참조할 수 있습니다. 아래 코드는 VideoMode의 속성인 resolution의 width 속성에 액세스하고 있습니다.

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

 

또한, dot 문법을 사용하여 변수 속성에 새로운 값을 할당할 수도 있습니다.

someVideoMode.resolution.width = 1280
print("The width of someVideoMode is now \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is now 1280"

 

Memberwise Initializers for Structure Types

모든 구조체는 자동으로 memberwise intializer를 생성합니다. 이 생성자로 새로운 구조체 인스턴스의 멤버 속성를 초기화할 수 있습니다. 새로운 인스턴스의 속성의 값은 memberwise initializer에 속성 이름에 전달되어 해당 속성의 초기값을 설정할 수 있습니다.

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

위 코드는 구조체의 인스턴스를 생성하면서 width와 height 속성의 초기값을 설정해주고 있습니다.

 

구조체와 달리, 클래스 인스턴스는 기본 memberwise initializer를 생성하지 않습니다.

 


Structures and Enumerations Are Value Types

value type은 변수나 상수에 할당되거나 함수의 파라미터로 전달될 때, 그 값이 복사되는 타입을 의미합니다.

사실 Swift의 모든 기본 타입(정수, 부동소수점, Boolean, 문자열, 딕셔너리)는 Value Type이고, 그 이면에는 구조체로 구현됩니다.

 

모든 구조체와 열거형도 Swift에서 value type입니다. 이는 사용자가 만드는 모든 구조체와 열거형 인스턴스가 코드에서 전달될 때 항상 복사가 발생합니다.

배열이나 딕셔너리, 문자열과 같은 표준 라이브러리에 정의된 콜렉션(collection)은 복사 비용을 줄이기 위해서 최적화를 사용합니다. 이러한 콜렉션들은 즉시 복사본을 만드는 대신, 원본 인스턴스와 복사본 간에 요소가 저장되는 메모리를 공유합니다. 만약 콜렉션의 복사본 중의 하나가 수정되면 수정 되기 직전에 그 값은 복사가 됩니다. 따라서, 코드에서 볼 때에는 항상 복사가 바로 발생하는 것처럼 보입니다.

 

위에서 살펴본 Resolution 구조체를 사용한, 아래 코드를 살펴봅시다.

let hd = Resolution(width: 1920, height: 1080)
var cinema = hd

hd라는 상수를 선언하고 1920(width) x 1080(height)의 해상도(FHD)로 초기화를 했습니다. 그리고 cinema라는 변수를 선언하고 hd의 현재 값으로 설정합니다. Resolution은 구조체이기 때문에 현재 인스턴스의 복사본이 생성되고, 새로운 복사본이 cinema로 할당됩니다. 비록 hd와 cinema가 같은 width와 height를 가지고 있지만, 둘은 완전히 다른 인스턴스 입니다.

 

이제 cinema의 width 속성 값을 변경해보고, cinema와 hd의 width 속성 값을 확인해보겠습니다.

cinema.width = 2048

print("cinema is now \(cinema.width) pixels wide")
// Prints "cinema is now 2048 pixels wide"

print("hd is still \(hd.width) pixels wide")
// Prints "hd is still 1920 pixels wide"

cinema의 widht 속성 값은 2048로 변경되었지만, 원본인 hd 인스턴스의 width 속성 값은 여전히 1920인 것을 확인할 수 있습니다.

 

cinema에 hd의 값이 할당될 때, hd에 저장된 값들은 새로운 cinema 인스턴스로 복사됩니다. 그 결과 같은 값을 갖는 두 개의 완전히 분리된 인스턴스가 생깁니다. 따라서 cinema의 width 속성 값을 2048로 변경해도, hd에 저장된 width에는 영향을 끼치지 않습니다.

 

열거형(enumerations)도 동일합니다.

enum CompassPoint {
    case north, south, east, west
    mutating func turnNorth() {
        self = .north
    }
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection.turnNorth()

print("The current direction is \(currentDirection)")
print("The remembered direction is \(rememberedDirection)")
// Prints "The current direction is north"
// Prints "The remembered direction is west"

rememberDirection에 currentDirection의 값을 할당하여, 값의 복사본이 할당됩니다. 따라서 currentDirection의 값을 변경해도 rememberDirection에 저장된 값은 변경되지 않습니다.

 


Classes Are Reference Types

Value Type과 달리, reference types는 변수나 상수에 할당되거나, 함수로 전달될 때 복사되지 않습니다. 복사하는 대신, 동일한 인스턴스의 참조가 사용됩니다.

 

위에서 정의한 VideoMode 클래스를 사용한 예제 코드를 살펴보겠습니다.

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

tenEighty라는 새로운 상수를 선언하고 VideoMode 클래스의 새로운 인스턴스를 참조하도록 설정하였습니다. 전에 정의한 해상도가 1920x1080인 hd의 복사본이 tenEighty.resolution에 할당되며, interlaced는 true, name은 "1080i", frameRate는 25.0으로 설정됩니다.

이제 tenEighty를 새로운 상수 alsoTenEighty에 할당하고, alsoTenEighty의 frameRate를 수정해보겠습니다.

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30.0

클래스는 reference type이기 때문에 tenEighty와 alsoTenEighty는 동일한 VideoMode 인스턴스를 참조하고 있습니다. 즉, 아래 그림처럼 동일한 하나의 인스턴스를 두 개의 다른 이름으로 참조하고 있는 것과 같습니다.

tenEighty의 frameRate 속성을 확인하면, frameRate의 값이 변경된 것을 확인할 수 있습니다.

print("The frameRate property of tenEighty is now \(tenEighty.frameRate)")
// Prints "The frameRate property of tenEighty is now 30.0"

 

이 예제에서는 reference type이 얼마나 어려운지 보여줍니다. 코드 상에서 tenEighty와 alsoTenEighty가 멀리 떨어져 있다면 video mode를 변경하는 모든 방법을 찾기가 어려울 수 있습니다. tenEighty를 사용하는 곳에서는 alsoTenEighty를 사용하는 코드에 대해서 고려해야하고, 반대의 경우도 고려해야 합니다.

 

예제 코드에서 tenEighty와 alsoTenEighty는 상수로 선언되었습니다. 그러나 우리는 tenEighty.frameRate와 alsoTenEighty.frameRate의 값을 변경할 수 있었는데, 이는 tenEight와 alsoTenEighty 상수의 값 자체는 실제로 변경되지 않았기 때문입니다.

tenEighty와 alsoTenEight 자체는 VideoMode 인스턴스를 "저장"하지 않고, 백그라운드에서 VidoeMode 인스턴스를 "참조"합니다. 변경되는 것은 VideoMode의 frameRate 속성일 뿐, VideoMode에 대한 상수 참조의 값은 변경되지는 않습니다.

 

Idenity Operators (식별 연산자)

클래스가 reference types이기 때문에 여러 상수나 변수가 동일한 단일 인스턴스를 참조할 수 있습니다. 따라서 두 상수 또는 변수가 정확하게 어느 클래스의 동일한 인스턴스를 참조하는지에 대한 여부를 확인하는 것이 유용할 수 있습니다. 이를 위해서 swift는 두 가지 식별 연산자를 제공합니다.

  • === (Identical to) : 두 상수나 변수가 같은 인스턴스를 참조하고 있는 경우 true
  • !== (Not identical to) : 두 상수나 변수가 다른 인스턴스를 참조하고 있는 경우 true

이 연산자는 두 상수나 변수가 동일한 인스턴스를 참조하고 있는지 확인할 때 사용할 수 있습니다.

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

'==='(Identical to)는 '=='(Equal to)와 동일하지 않으니 주의해서 사용해야 합니다. 식별 연산자는 참조를 비교하는 것이고, 비교 연산자는 값을 비교합니다.

 

Pointers

C, C++에 익숙하다면 해당 언어에서 메모리의 주소를 참조하기 위해 포인터를 사용한다는 것을 알고 있습니다. 어떤 reference type의 인스턴스를 참조하고 있는 swift의 상수나 변수는 C의 포인터와 유사하지만, 메모리의 주소를 직접 가리키는 것이 하니기 때문에 참조를 의미하는 별표(*)를 사용할 필요가 없습니다. Swift에서 이러한 참조는 다른 상수나 변수처럼 정의됩니다. 표준 라이브러리에서는 만약 직접적으로 포인터와 상호작용해야 하는 경우 사용할 수 있는 포인터 및 buffer type을 제공합니다.

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Methods  (0) 2021.12.26
[Swift] Properties  (0) 2021.12.25
[Swift] Enumerations (열거형, 열거자)  (0) 2021.12.22
[Swift] Closures (클로저)  (0) 2021.12.22
[Swift] Functions (함수)  (0) 2021.12.18

댓글