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

Heeel·2022년 5월 6일
0

Study-Swift 5.6

목록 보기
6/22

참고사이트:
The Swift Language Guide
boostcourse


🥯 Structures and Classes

class와 structure는 프로그램의 코드를 조직화하기 위해 사용한다. 상수, 변수, 그리고 함수를 정의하는 것처럼 같은 문법을 사용하여 프로퍼티와 메서드를 정의하고 클래스와 구조체에 기능을 추가한다.

다른 프로그래밍 언어와 다르게 swift는 interface 파일과 implementation 파일을 분리해서 만들지 않아도 된다. 하나의 파일에 structure나 class를 정의하면 Swift가 자동으로 해당 class와 structure를 사용할 수 있는 인터페이스를 생성하기 때문이다.

class의 인스턴스는 전통적으로 객체(object)라고 알려져 있다. 그러나 Swift의 sturctures와 class는 다른 언어에 비해 훨씬 더 기능적으로 가깝다. (structure가 class처럼 메서드를 가짐) 따라서 구조체와 클래스에 일반적으로 사용될 수 있는 인스턴스라는 용어를 사용한다.


🍞 Comparing Classes and Structures

🥐 class와 structure의 공통된 기능

  • 값을 저장하기 위한 프로퍼티 정의.
  • 기능을 제공하기 위한 메서드 정의.
  • subsript 문법을 이용해 특정 값을 접근할 수 있는 subscript 정의.
  • 초기 상태를 설정할 수 있는 initializer 정의.
  • 기본 구현에서 기능 확장.
  • 특정한 종류의 표준 기능을 제공하기 위한 프로토콜 순응(conform).

🥨 class만 가지는 고유 기능

  • 상속(Inheritance): 클래스의 여러 속성을 다른 클래스에서도 사용할 수 있다.
  • 타입 캐스팅(Type casting): 런타임에서 클래스의 인스턴스 타입을 해석.
  • 소멸자(Deinitializers): 인스턴스에 할당된 자원을 해제 시킴.
  • 참조 카운트(Reference counting): 클래스 인스턴스에서 1개 이상의 참조가 가능.

NOTE
structure, enumeration는 데이터가 전달될 때 복사하여 전달되고, 참조 카운트(Reference counting)을 사용하지 않는다.

classes가 가지는 추가적인 기능들은 복잡성을 증가시킨다. 그래서 일반적인 가이드라인에서는 structures가 쉽게 추론 되기 때문에 더 선호되고 classes는 필요하거나 적절한 상황에 사용한다. 실제로 Swift에서 대부분의 데이터 타입은 structures이거나 enumeration이다.

자세한 사항은 애플 공식 문서에서 확인할 수 있다. (추후 작업할 예정)
https://developer.apple.com/documentation/swift/choosing_between_structures_and_classes


🧀 문법 정의

classes와 Structures는 비슷한 선언 문법을 가지고 있다.
클래스는 class 키워드, 구조체는 struct 키워드를 앞에 적어 선언한다.

struct SomeStructure {
    // structure definition goes here
}
class SomeClass {
    // class definition goes here
}

NOTE
새로운 class나 structure을 선언할 때마다 Swift에서 새로운 타입을 정의하는 것이다. 그래서 이름을 다른 Swift 표준 타입(String, Int, Bool)과 같이 UpeerCameCase(SomeClass, SomeStructure)로 사용한다. 반대로 프로퍼티나 메서드는 lowerCamelCase(frameRate, incrementCount)로 선언한다.

아래는 구조체 선언과 클래스 선언의 예시다.

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

이 구조체는 2개의 프로퍼티를 가지고 있다. 이 프로퍼티들은 0으로 초기값이 설정되었기 때문에 Int형으로 추론된다.

class VideoMode {
    var resolution = Resolution() // 위 구조체 사용
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

이 클래스는 4개의 프로퍼티를 가지고 있다. 첫번째 프로퍼티는 새로운 Resolution structure instance로 초기화되고 이는 Resolution 타입이다. 마지막 프로퍼티는 Optional type이므로 default value인 nil을 가진다.


🥖 Class and Structure 인스턴스

classes와 Structures의 인스턴스를 만들기 위해서 initializer를 사용한다. initializer의 문법은 클래스의 타입 이름과 괄호를 사용한다. 이렇게 새로 만들어진 인스턴스는 default values로 초기화된 프로퍼티를 가진다.

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

🧇 Properties 접근

점(dot)문법을 통해 class, structure 인스턴스의 프로퍼티에 접근할 수 있다.

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

이 예제에서 someResolution 인스턴스의 width 프로퍼티는 default value로 0을 가지므로 0을 출력한다.

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

예제에서와 같이 인스턴스 프로퍼티의 프로퍼티, 즉 하위레벨의 프로퍼티도 점(.)문법을 이용하여 접근할 수 있다.

그리고 다음과 같이 점문법을 이용하여 직접 값을 할당할 수 있다.

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

NOTE
Objective-c와 다르게 Swift에서는 하위레벨의 구조체 프로퍼티도 직접 값을 설정할 수 있다. 위의 예제처럼 resolution 전체의 값을 설정하지 않고, width 프로퍼티만 직접 설정할 수 있다.


🧈 Structure Types의 멤버 초기화

모든 structures들은 자동적으로 생성된 memberwise initializer를 가지고 있고 이는 새로운 structure 인스턴스의 멤버 프로퍼티를 initialize한다. 새 인스턴스의 poperties에 대한 초기값은 memberwise initializer에 이름으로 전달할 수 있다.

let vga = Resolution(width: 640, height: 480)
print("The width is \(vga.width) and height is \(vga.height)")
// Prints The width is 640 and height is 480)

structures와 다르게 class는 자동적으로 memberwise initializer를 가지지 않는다.


🍔 Structures and Eumerations Are Value Types

값 타입(value type)이란 함수, 변수, 상수에 값이 전달될 때 그 값이 복사되어 전달 된다는 의미이다.

실제로 값 타입은 Swift에서 많이 사용된다.(Integer, floating numbers, boolean, strings, arrays, dictionaries)는 값 타입이고 structure로 구현된다.

모든 Swift에서 structures와 enumerations는 모두 값 타입이고 이들의 인스턴스는 코드에서 전달될 때 모두 복사되어 복사본이 전달된다.(Class는 reference type이고 주소가 전달됨)

NOTE
배열, 딕셔너리 문자열 등과 같이 표준 라이브러리에서 정의된 컬렉션들은 복사를 할 때 성능 비용을 줄이기 위해 최적화를 한다. 즉시 복사본을 만드는 대신에 이들 컬렉션은 원래 인스턴스와 복사하고자 하는 인스턴스가 데이터가 저장된 주소를 공유한다. 만약 이들 중 한 개의 인스턴스가 수정이 이루어진다면 수정이 이루어지기 직전에 그 데이터에 대해 복사본을 만들고 수정된 인스턴스가 이를 값으로 가지게 된다. 실제로 이는 코드상에서 바로 복사가 되는 것처럼 보이나 최적화가 진행되는 것이다.(이를 copy-on-write optimization이라고 하고 value-type에서 사용된다.)


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

위 코드의 첫 줄에서 Resoultion 구조체의 인스턴스 hd를 선언한다. 그리고 hd를 cinema 변수에 할당한다. 그러면 이 cinema와 hd는 할당되는 순간 복사 되기 때문에 완전히 다른 인스턴스다. 계속해서 예제를 살펴보자.

cinema 인스턴스의 width 프로퍼티에 2048을 할당한다.

cinema.width = 2048

그 후 cinema의 width값은 2048이다.

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

하지만 원본(원래 데이터를 가지고 있는 인스턴스)hd는 원래 값인 1920을 갖고 있는 것을 확인할 수 있다. 이는 두 인스턴스가 완전히 다른 주소공간에 저장되어 사용된다는 것을 보여준다.

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

enumration에서도 확인할 수 있다.

enum CompassPoint {
    case north, south, east, west
}
var currentDirection = CompassPoint.west
let rememberedDirection = currentDirection
currentDirection = .east
if rememberedDirection == .west {
    print("The remembered direction is still .west")
}
// "The remembered direction is still .west" 출력

currentDirection에 west를 할당하고, rememberedDirection에 currentDirection을 저장한다. 그러고 나서 currentDirection을 east로 변경한다. rememberedDirection를 살펴보면 여전히 원본을 복사할 때의 값 west를 갖고 있는 것을 확인할 수 있다. 즉, 다른 인스턴스의 변화는 그 인스턴스에만 영향을 끼치고 그것과 다른 인스턴스에는 아무런 영향도 없다는 것을 알 수 있다.


🥓 Classes Are Reference Types

value types와 달리, reference types은 변수나 상수에 할당되거나, 함수에 인자로 전달될 때 그 값이 복사되지 않고 참조 된다. 참조 된다는 의미는 그 값을 갖고 있는 메모리를 바라보고 있다는 뜻이다.

다음 예제를 보자.

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

tenEighty라는 VideoMode클래스 인스턴스를 생성하고 각 프로퍼티에 값을 할당한다.

let alsoTenEighty = tenEighty
alsoTenEighty.frameRate = 30

다음은 tenEighty는 새로운 상수인 alsoTenEighty라는 상수에 할당되었고 asloTenEighty의 frameRate를 수정한다.

아래 예제의 출력을 통해 두 상수의 frameRate 프로퍼티의 값을 확인한다.

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

두 상수는 모두 frameRate의 값으로 30을 가지고 있는 것을 확인할 수 있다.tenEighty의 값이 alsoTenEighty에 복사된 것이 아니라 바라보고 있는 메모리의 주소가 전달된 것이다. 2개의 상수가 하나의 메모리를 동일하게 바라보고 있다는 뜻이다.
다음 그림을 보면 이해가 더 쉽게 된다.

이 예제는 reference types(참조 타입)이 추론하기 어려운 이유를 보여준다. 만약 tenEighhtyalsoTenEighty가 코드에서 멀리 떨어져 있다면 두 상수를 사용할 때 서로를 고려해야 한다.
그런데 두 상수 모두 변수가 아닌 값을 바꿀 수 없는 constant인데 frameRate 프로퍼티를 어떻게 바꿨을까? 이것은 사실 두 상수 자체의 값이 바뀐 것이 아니라 바라보는 값(VideoMode 자체의 frameRate)이 바뀐 것이기 때문에 가능하다.

그렇다면 tenEighty가 상수이기 때문에 상수 자체의 값, 즉 상수가 가리키는 메모리의 주소를 변경해 주면 에러가 발생하는지 확인을 해본다.

let testMode = VideoMode()
testMode.resolution = hd
testMode.interlaced = false
testMode.name = "2016i"
testMode.frameRate = 30.0

tenEighty = testMode
// error: error: cannot assign to value: 'tenEighty' is a 'let' constant
// tenEighty = testMode
// ^~~~~~~~~

새로운 상수 testMode를 만들고 상수인 tenEighty가 새로운 데이터 주소를 가리키게 하였다. 그 결과 let의 값을 바꿀 수 없다는 에러 메시지를 확인할 수 있다.


🍟 Identity Operators

classes는 reference types이기 때문에 상수나 변수가 하나의 동일 인스턴스를 참조하는 것이 가능하다. (value types인 structure나 enumeration은 상수나 변수, 함수의 인자에 전달될 때 복사되기 때문에 다르다.)

이 Identity Operators(식별 연산자)는 2개의 상수나 변수가 class의 동일 인스턴스를 참조하고 있는지 확인할 떄 유용하다.

  • 식별 연산자(===)
  • 비교 연산자(==)

이 연산자를 사용한 예시를 살펴보자.

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

식별 연산자(===)와 비교 연산자(==)는 명백히 다르다. 식별 연산자는 2개의 상수나 변수가 클래스의 동일 인스턴스를 참조하는지 확인하는 것이고 비교 연산자(==)는 2개의 인스턴스가 같은 값을 가지고 있는지 확인하는 것이다.


🥐 Pointers

C, C++ 혹은 Objective-C를 사용해 본 사람은 이 참조라는 것이 포인터라고 생각할 수도 있다. Swift에서 상수나 변수가 특정 타입의 인스턴스를 참조하고 있다는 것은 위 언어의 포인터와 유사하나 실제로 메모리의 주소를 직접 가리키지 않는다. 그리고 표현에 다른 점이 있는데 Swift와 달리 위 언어의 포인터는 실제 메모리를 직접 가리키고 있고 키워드(*)로 표현하지만 Swift는 참조를 표현하기 위해 연산자를 사용하지 않고 대신 다른 상수와 변수처럼 정의해서 사용한다.


🥯 Classes와 Structures의 선택

클래스와 구조체 모두 프로그램의 코드를 조직화하고 특정 타입을 선언하는 데 사용된다. 그리고 class 인스턴스가 인자로 함수의 인자로 사용될 때 reference types은 참조(주소)가 넘어가고 structure은 value가 넘어간다고 했다. 그러면 언제 class와 structure을 적절하게 사용할까??

일반적으로 다음의 조건 중 1개 이상을 만족하면 structure를 사용하는 것을 고려한다.

  • structure의 주 목적이 관계된 간단한 값을 캡슐화(encapsulate) 하는 경우
  • structure의 인스턴스가 참조보다 복사되기를 기대하는 경우
  • 구조체에 의해 저장된 어떠한 프로퍼티가 참조되기 보다 복사되기를 기대하는 경우
  • 구조체가 프로퍼티나 메소드 등을 상속할 필요가 없는 경우

위의 기술된 경우를 제외한 다른 경우들은 클래스를 사용하는 것을 권장한다.

Swift는 Structure, Enumeration 사용을 선호한다. 그러나 Apple Framework는 대부분 Class를 이용해 구현되었다. 결국 어떤 것을 선택할 것인지는 적절하게 판단하여 사용하자.

0개의 댓글