본문 바로가기

문법

[Swift]클래스와(class)와 (struct) 구조체를 알아보자

주의 : 영문서 번역 중 오역이 있을 수 있으며 정확하지않는 정보가 있을 수 있습니다.

 

 

구조체와 클래스

 

- 구조체
- 클래스
- 클래스 인스턴스 생성과 초기화
- 클래스 인스턴스 소멸
- 구조체와 클래스의 차이
- 값 타입과 참조타입
- 스위프트의 기본 데이터 타입은 모두 구조체
- 구조체와 클래스 선택해서 사용하기

 

Swift Laguage Guide

공식문서에서 소개하는 구조체와 클래스

" Structures and classes are general-purpose, flexible constructs that become the building blocks of your program’s code "

=> 구조체와 클래스는 프로그램코드의 빌딩 블록이 되는 유연한 범용구조이다.

 

구조체와 클래스는 프로그래머가 데이터를 용도에 맞게 묶어 표현하고자할때 유용하다. 구조체와 클래스는 프로퍼티와 메서드를 사용하여

구조화된 데이터와 기능을 가질 수 있다. 하나의 새로운 사용자 정의 데이터 타입을 만들어 주는것이다.

객체 지향 프로그래밍 패러다임을 안다면 클래스라는 용어를 들어봤을것이다. 그리고 객체지향 프로그래밍 패러다임이 아니더라도 데이터를 구조화 하여 관리하는데 구조체를 사용해 봤을것이다. 스위프트에서는 구조체와 클래스의 모습과 문법이 거의 흡사하다. 다만 구조체의 인스턴스는 값 타입이고, 클래스의 인스턴스는 참조타입이라는 것이 이둘을 구분하는 가장 큰 차이점이다.

 

이제까지 우리가 알아봄 스위프트의 데이터타입과 열거형은 모두 값타입이다. 그러나 구조체와 함께 배워볼 클래스는 참조타입이다. C언어

와 Objective-C의 포인터와 유사한 개념이다.

일부 프로그래밍 언어는 소스파일 하나에 구조체 또는 클래스 하나만 선언하고 구현할 수 있는 반면, 스위프트는 그런 제약사항이 전혀없다.

소스파일 하나에 여러개의 구조체와 여러개의 클래스를 정의하고 구현해도 문제가 없다. 또 중첩 함수와 마찬가지로 구조체안에 구조체 클래스 안에 클래스등과 같이 중첩타입의 정의 및 선언이 가능하다.

 

 

구조체

구조체를 어떻게 정의하고 인스턴스를 어떻게 생성하는지 그리고 구조체를 어떻게 활용하는지 알아보자.

struct 키워드로 정의해준다.

 

구조체를 정의한다는것은 새로운 타입을 생성해주는 것과 마찬가지이므로 기본 타입이름(Int, String, Bool 등등)처럼 대문자 카멜케이스를 사용하여 이름을 지어준다. 프로퍼티와 메서드는 소문자 카멜케이스를 사용하여 이름을 지어준다.

 

struct Animal {
    var name: String
    var hasMale: Bool
}

 

구조체 정의를 마친후, 인스턴스를 생성하고 초기화하고자 할때는 기본적으로 생성되는 멤버와이즈 이니셜라이저를 사용한다.[클릭가능]

구조체에 기본생성된 이니셜라이저의 매개변수는 구조체의 프로퍼티 이름으로 자동지정된다.

 

인스턴스가 생성되고 초기화된 후 프로퍼티 값에 접근하고 싶다면 마침표(.)를 사용하면된다. 구조체를 상수 let으로 선언하면

인스턴스 내부의 프로퍼티 값을 변경할 수 없고, 변수 var로 선언하면 내부의 프로퍼티가 var로 선언된 경우에 값을 변경할

수 있다.

 

 

var quokka: Animal = Animal(name: "quokka", hasMale: true)
quokka.name = "쿼카"
quokka.hasMale = false

let tiger: Animal = Animal(name: "tiger", hasMale: true)
//tiger.name = "호랑이" //오류발생 - Cannot assign to property: 'tiger' is a 'let' constant
//tiger.hasMale = false //오류발생 - Cannot assign to property: 'tiger' is a 'let' constant

기본 제공되는 멤버와이즈 이니셜라이저 외에 사용자 정의 이니셜라이저 구현이 가능하다.

 

 

클래스

클래스를 어떻게 정의하고 인스턴스를 어떻게 생성하는지 그리고 클래스를 어떻게 활용하는지에 대해 알아보자.

스위프트의 클래스는 부모클래스가 없더라도 상속 없이 단독으로 정의가 가능하다.

 

클래스를 정의한다는것은 새로운 타입을 생성해주는 것과 마찬가지이므로 기본 타입이름(Int, String, Bool 등등)처럼 대문자 카멜케이스를 사용하여 이름을 지어준다. 프로퍼티와 메서드는 소문자 카멜케이스를 사용하여 이름을 지어준다.

 

class Guitar {
    var brand: String
    var modelName: String
    var color: Color
    // 오류 발생 - Class 'Guitar' has no initializers
}

 

클래스는 값 타입인 구조체처럼 자동으로 이니셜라이저가 생성이 되지않고 만들어 주어야한다.

 

클래스 인스턴스 생성과 초기화

클래스를 정의한 후 인스턴스를 생성하고 초기화하고자 할때는 기본적인 이니셜라이저를 사용합니다. Guitar클래스에서는 프로퍼티의 기본값이 지정되어 있지않으면 위와같은 오류 메세지가 나온다. => 이니셜라이저가 없다고 생성해야한다는 오류이며 class는 초기값을 지정해주거나 인스턴스 생성시 초기값을 할당할 수 있도록 이니셜라이저를 생성하거나 둘 중에 하나는 의무사항인데 쉽게말해 초기값을 지정하거나 인스턴스 생성을 도와주는 이니셜라이저를 생성하라는 에러 메세지이다.

 

// 초기값 지정

class Guitar {
    var brand: String = ""
    var modelName: String = ""
    var color: Color = .red
}

 

NOTE

인스턴스와 객체

Swift Language Guide

해석)

한 클래스의 인스턴스는 전통적으로 객체라고 알려져있다 그러나 스위프트에서는 구조체와 클래스는 기능에 더 가깝다. 그런데 다른언어에서는 이 장 대부분에 클래스 또는 구조유형의 인스턴스에 적용되는 기능에 대해 설명한다. 이 때문에 더 일반적인 용어 인스턴스가 사용된다.

 

 

흔히 다른 프로그래밍 언어에서는 클래스의 인스턴스를 객체라고 부른다. 물론 스위프트에서도 객체라고 부르는것이

틀린것은 아니지만, 스위프트. 공식문서에는 좀 더 한정적인 인스턴스라는 용어를 사용한다 이책의 초반부에는

다른 프로그래밍 언어를 사용했던 독자를 위해 객체라는 용어를 사용했지만 지금부터는 인스턴스라는 용어를 사용한다.

보통 객체지향 프로그래밍 패러다임을 지향하는 언어에서는 클래스의 인스턴스를 객체라고 통칭한다.

 

인스턴스가 생성되고 초기화된 후(이니셜라이즈된 후)프로퍼티 값에 접근하고 싶다면 마침표를 사용하면 된다.

구조체와는 다르게 클래스의 인스턴스 참조 타입이므로 클래스의 인스턴스를 상수 let으로 선언해도 내부 프로퍼티

값을 변경할 수 있다!!

var guitar = Guitar()
guitar.brand = "Yamaha"
guitar.modelName = "c-1234"
guitar.color = .gray

let guitar2 = Guitar()
guitar2.brand = "Crafter"
guitar2.modelName = "c-4321"
guitar2.color = .blue

 

 

 

클래스 인스턴스 소멸(deinit)

클래스의 인스턴스는 참조 타입이므로 더는 참조할 필요가 없을 때 메모리에서 해제*됩니다.

이 과정을 소멸이라고 하는데 소멸되기 직전 deinit라는 메서드가 호출됩니다. 클래스 내부에

deinit 메서드를 구현해주면 소멸되기 직전 deinit 메서드가 호출됩니다. 이렇게 호출되는

deinit 메서드는 디이니셜라이저라고 부릅니다. deinit 메서드는 클래스당 하나만 구현할 수 있으며

매개변수와 반환값을 가질 수 없다. deinit메서드는 매개변수를 위한 소괄호도 적어주지않는다.

코드로 확인해보자

 

class Person {
    var heght: Float = 0.0
    var weight: Float = 0.0
    
    deinit {
        print("Person 클래스의 인스턴스가 소멸된다.")
    }
}

var hongildong: Person? = Person()
hongildong = nil // Person 클래스의 인스턴스가 소멸됩니다.

 

보통 deinit메서드에는 인스턴스가 메모리에서 해제되기 직전에 처리할 코드를 넣어줍니다.

예를들어 인스턴스 소멸 직전에 데이터를 저장한다거나 다른 객체에 인스턴스 소멸을 알려야할

때는 특히 deinit메서드를 구현해야한다.

 

 

구조체와 클래스의 차이

구조체와 클래스는 서로 비슷하거나 같은 점이 많다.

다음은 같은 점이다.

 

- 값을 저장하기 위해 프로퍼티를 정의할 수 있다.

- 기능 실행을 위해 메서드를 정의할 수 있다.

- 서브스크립트 문법을 통해 구조체 또는 클래스가 갖는 값(프로퍼티)에 접근하도록 서브스크립트를 정의할 수 있다.

- 초기화될 때의 상태를 지정하기 위해 이니셜라이저를 정의할 수 있다.

- 초기화구현과 더불어 새로운 기능 추가를 위해 익스텐션을 통해 확장할 수 있다.

- 특정 기능을 실행하기 위해 특정 프로토콜을 준수할 수 있다.

 

그러나 확연히 다른점도 존재한다.

- 구조체는 상속할 수 없다.

- 타입캐스팅은 클래스의 인스턴스에만 허용된다.

- 디이니셜라이저는 클래스의 인스턴스에만 활용할 수 있다.

- 참조횟수계산은 클래스의 인스턴스에만 적용된다.

 

구조체와 클래스는 겉보기엔 정의하는 방법도, 인스턴스화하는 방법도, 프로퍼티와 메서드를 갖는다는 점을 비롯해 많은

부분에서 비슷해보인다. 그러나 이 두타입을 구분짓는 가장큰 차이점은 값 타입과 참조 타입이라는 것이다. 그래서

참조횟수계산은 클래스의 인스턴스에만 해당됩니다.

 

 

값 타입과 참조 타입

구조체는 값타입이고 클래스는 참조타입이다. 값 타입과 참조 타입의 가장큰 차이는 '무엇이전달되느냐?'이다.

예를들어 어떤 함수의 전달인자로 값 타입의 값을 넘긴다면 전달된 값이 복사되어 전달된다. 그러나 참조타입이

전달인자로 전달될때는 값을 복사하지 않고 참조(주소)가 전달된다. 참조라는 것은 C언어, C++, Objective-C

등의 언어에서 사용되는 포인터와 매우 유사한 개념이다. 그러나 참조라는 것은 표현해주기 위해 애스터리스크를 사용하지않는다.

 

함수의 전달인자로 넘길때 참조가 전달된면 다른 변수 또는 상수에 할당될때로 마찬가지로 참조가 할당된다.

 

값 타입과 참조타입의 차이 예제 1)

// 값 타입과 참조타입의 차이

print("=========구조체========")
struct BasicInformation {
    let name: String
    var age: Int
}
var quokkaInfo = BasicInformation(name: "quokka", age: 25)
quokkaInfo.age = 99

// quokkaInfo의 값을 복사하여 할당합니다.!
var friendInfo = quokkaInfo

print("quokka age: \(quokkaInfo.age)") // 99
print("friend age: \(friendInfo.age)") // 99

friendInfo.age = 999
print("=================")
print("quokka age: \(quokkaInfo.age)") // 99
print("friend age: \(friendInfo.age)") // 999

print("=========클래스========")
class Person2 {
    var height: Float = 0.0
    var weight: Float = 0.0
}
var quokka2 = Person2()
var friend2 = quokka2

print("quokka2 height: \(quokka2.height)")
print("friend2 height: \(friend2.height)")

friend2.height = 170
print("quokka2 height: \(quokka2.height)")
// 170 - friend2 는 quokka2를 참조하기때문에 값이 변동된다

print("friend2 height: \(friend2.height)")
// 170- 이를 통해 quokka2가 참조하는 곳과 friend2가 참조하는곳이 같음을 알 수 있다.

func changeBasicInfo(_ info: BasicInformation) {
    var copiedInfo: BasicInformation = info
    copiedInfo.age = 1
}

func changePersonInfo(_ info: Person2) {
    info.height = 180.8
}

//changeBasicInfo(_:)로 전달되는 전달인자는 값이 복사되어 전달되기 때문에
//quokkaInfo의 값만 전달되는 것이다.
changeBasicInfo(quokkaInfo)
print("quokka2 height: \(quokkaInfo.age)")
//quokka2 height: 99

changePersonInfo(quokka2)
print("quokka2 height: \(quokka2.height)")
//quokka2 height: 180.8

 

값 타입과 참조타입의 차이 예제 2)

//======구조체======
struct PointStruct {
    var x, y: Double
    func draw() {}
}

let point1 = PointStruct(x: 0, y: 0)
var point2 = point1
point2.x = 5
print("x: \(point1.x), y: \(point1.y)") //x: 0.0, y: 0.0
print("x: \(point2.x), y: \(point2.y)") //x: 5.0, y: 0.0

// ======클래스======
class PointClass {
    var x = 0.0, y: Double = 0.0
    func draw() {}
}

let point3 = PointClass()
var point4 = point3
point4.x = 5
print("x: \(point3.x), y: \(point3.y)")// x: 5.0, y: 0.0
print("x: \(point4.x), y: \(point4.y)")// x: 5.0, y: 0.0

 

값 타입의 데이터를 함수의 전달인자로 전달하면 메모리에 전달인자를 위한 인스턴스가 새로 생성됩니다.

생성된 새 인스턴스에는 전달하려는 값이 복사되어 들어갑니다. 반면 참조 타입의 데이터는 전달인자로

전달할 때 기존 인스턴스의 참조를 전달하므로 새로운 인스턴스가 아닌 기존의 인스턴스 참조를 전달합니다.

함수의 전달인자뿐만 아니라 새로운 변수에 할당될 때 또 한 마찬가지입니다.

 

클래스의 인스턴스끼리 참조가 같은지 확인할 때는 식별연산자를 사용합니다.

식별연산자를 사용하여 두 참조가 같은 인스턴스를 가리키고 있는지 비교해보는 코드이다.

 

식별연산자를 사용한 참조확인

// 식별 연산자의 사용
var quokkaRC = Person2()
let friend = quokkaRC //quokkaRC의 참조를 할당한다.
let anotherFriend = Person() //새로운 인스턴스를 생성한다.

print(quokkaRC === friend) // true
print(quokkaRC === anotherFriend) // false
print(friend !== anotherFriend) // true

 

 

 

스위프트의 기본 데이터 타입은 모두 구조체

스위프트 표준 라이브러리에 포함되어 있는 스위프트의 String 타입의 기본정의 입니다.

스위프트의 String 타입의 기본정의

public struct String {
    // An empty 'String'
    public init()
}

 

public은 잠시 제처두고, struct 키워드는 익숙하지않으신가요 ? 네, 스위프트의 다른 기본타입

(Bool, Int, Array, Dictionary, Set등등)도 String 타입과 마찬가지로 모두 구조체로

구현되어 있다. 이는 기본 데이터타입은 모두 값타입이라는 뜻이다. 전달인자를 통해 데이터를

전달하면 모두 값이 복사되어 전달된 뿐, 함수내부에서 아무리 절달된 값을 변경해도 기존의 변수나

상수에는 전혀 영향을 미치지 못한다. 이런점을 더욱활실히 하기 위해 스위프트의 전달인자는 모두

상수로 취급되어 절달되는 것일지로 모른다.

 

견해)

만약에 기본 데이터타입이 class인 참조 타입이었다면 String타입을 가지는 모든 인스턴스의 값들이

변경될때마다 모두 같은 값을 가지게 되고 독립성이 없어지기때문에 값타입으로 할 수 밖에 없겠다라는

생각도 해볼 수 있을것같다.

 

구조체와 클래스 선택해서 사용하기

구조체와 클래스는 새로운 데이터타입을 정의하고 기능을 추가한다는 점이 같다. 하지만 구조체

인스턴스는 항상 값 타입이고, 클래스 인스턴스는 참조타입이다. 생긴 것은 비슷하지만 용도는 다르다는

의미이다. 프로젝트의 성격에 따라, 데이터의 활용도에 따라, 특정 타입을 구현할때 구조체와 클래스

둘 중 하나를 선택해서 사용해야 한다.

 

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

애플 가이드라인 - https://goo.gl/6HYxfT

 

- 연관된 간단한 값의 집합을 캡슐화하는 것만이 목적일 때

- 캡슐화한 값을 참조하는 것보다 복사하는 것이 합당할 때

- 구조체에 저장된 프로퍼티가 값 타입이며 참조하는 것보다 복사하는것이 합당할 때

- 다른 타입으로 부터 상속받거나 자신을 상속할 필요가 없을 때

 

구조체로 사용하기에 가장 적합한 예로는 좌표계가 있습니다. x, y 좌표등을 표현하고 싶을 때

Int 타입으로 x, y프로퍼티를 생성할 수 있으며, 물건의 크기를 표현하고자 할때는 부동소수

표현인 Double 또는 Float타입을 사용하여 width, height, depth 등을 묶어 표현해 줄

수 있다.

 

이런 몇가지 상황을 제외하면 클래스로 정의하여 사용한다. 대다수 사용자 정의 데이터타입은

클래스로 구현할 일이 더 많을 것이다.

 

NOTE

똑똑한 스위프트의 복사처리

스위프트의 기본 데이터 타입이 모두 구조체라서 다수의 배열 또는 딕셔너리등의 데이터를 복사하고

이용할때 메모리를 비효율적으로 사용한다고 오해할 수 있다. 그렇지만 스위프트는 꼭 필요한 경우에만

'진짜 복사'를 한다. 컴파일러가 판단해서 꼭 복사를 할 필요가 없을 경우, 요소를 많이 갖는

큰 배열을 함수의 전달인자로 넘겨준다고 해서 꼭 모든값을 메모리의 다른 공간에 복사해 넣지 않을

수도 있다는 뜻이다. 스위프트가 적절히 알아서 효율적으로 처리해줄 것이다.

 

포인트정리

- 구조체와 클래스는 프로그래머가 데이터를 용도에 맞게 묶어 표현하고자할때 사용한다.

- 클래스와 구조체의 가장큰 차이점은 구조체의 인스턴스는 값 타입이고, 클래스의 인스턴스는 참조타입이라는 것이다.

- 스위프트의 데이터타입과 열거형은 모두 값타입이다.

- 구조체에 기본생성된 이니셜라이저의 매개변수는 구조체의 프로퍼티 이름으로 자동지정된다.

- class는 초기값을 지정해주거나 인스턴스 생성시 초기값을 할당할 수 있도록 이니셜라이저를 생성하거나 둘 중에 하나는 의무사항이다.

- 스위프트 공식문서에서는 객체라는 포괄적인 단어보단 좀 더 한정적인 인스턴스라는 용어를 사용한다.

- 구조체와는 다르게 클래스의 인스턴스 참조 타입이므로 클래스의 인스턴스를 상수 let으로 선언해도 내부 프로퍼티 값을 변경할 수 있다.

- 인스턴스는 참조 타입이므로 더는 참조할 필요가 없을 때 deinit메서드(디이니셜라이저)를 구현할 수 있고 클래스당 하나만 구현할 수 있     다.

- 스위프트는 꼭 필요한 경우에만'진짜 복사'를 한다. 컴파일러가 판단해서 꼭 복사를 할 필요가 없을 경우, 요소를 많이 갖는 큰 배열을 함수     의 전달인자로 넘겨준다고 해서 꼭 모든값을 메모리의 다른 공간에 복사해 넣지 않을 수도 있다.

 

궁금증

- 클래스는 값 타입인 구조체처럼 자동으로 이니셜라이저가 생성이 되지않고 만들어주어야할까 ?

 

Reference

- 참고책 - 야곰 Swift프로그래밍 3판

- https://docs.swift.org/swift-book/LanguageGuide/ClassesAndStructures.html

 

Structures and Classes — The Swift Programming Language (Swift 5.5)

Structures and Classes Structures and classes are general-purpose, flexible constructs that become the building blocks of your program’s code. You define properties and methods to add functionality to your structures and classes using the same syntax you

docs.swift.org