# 클래스

클래스는 타입이다.

class Dog{
    var name = "강아지"
    var weight = 0.0

    func sit(){
        print("\(self.name) 앉기")
    }
}

var bori = Dog() // Dog 클래스의 실제 데이터

bori.name = "보리"
bori.weight = 15.0
bori.sit()

클래스 내에 만들어진 각 변수와 함수들을 통칭 멤버라고 한다.

이때 클래스 내의 변수를 속성(property), 메서드(method)라고 한다.

struct라는 키워드로 생성하는 구조체라는 개념도 있는데 이는 클래스와 동일한 기능을 한다고 보면 된다.

클래스와 구조체 모두 메모리에 찍어낸 것을 인스턴스라고 한다. 클래스 인스턴스만 스위프트에서 특별히 객체라고 함.

구조체도, 열거형도 모두 인스턴스를 갖는다.

구조체와 클래스의 가장 큰 차이점은 메모리 저장 방식에서 나타남

  1. 구조체
    1. 값 형식
    2. 인스턴스 데이터를 모두 스택에 저장
  2. 클래스
    1. 참조 형식
    2. 인스턴스 데이터는 힙에 저장, 힙을 가리키는 변수는 스택에 저장
    3. 메모리 주소값이 힙을 가리킨다.

# 1. 클래스 저장 방식

데이터 영역에 클래스가 저장되고 여기로부터 생성된 인스턴스가 힙에 저장됨. 각 인스턴스는 데이터 영역의 자신을 생성한 클래스 메모리 주소를 갖고 있고 참조함.

힙에 저장된 인스턴스는 특정 변수에 저장되어 활용되는데, 이 변수는 스택 영역에 저장된다.

클래스의 값 복사

class Person{
    var name = "사람"
}

var person1 = Person()

person1.name = "박경준" // 인스턴스 프로퍼티 변경
print(person1.name) // 박경준

var person2 = person1
print(person2.name) // 박경준
// person2와 person1은 동일한 인스턴스를 가리키고 있다.

person2.name = "김경준"
print(person1) // 김경준
// person2가 가리키는 인스턴스 프로퍼티값이 바뀌었음에도
// person1이 가리키는 인스턴스 프로퍼티값이 바뀐다
// 따라서, person1과 person2는 동일한 인스턴스를 가리킨다

위 코드를 보면 Person이라는 클래스가 데이터 영역에 저장되어 있고 Person() 생성자를 통해 만들어진 인스턴스는 힙 영역에 저장된다.

이 힙 영역에 저장된 데이터는 다시 person1라는 변수에 저장되게 되는데 데이터 자체가 저장되는 것이 아닌 힙 영역에 저장된 인스턴스의 주소를 저장하게 된다.

person2라는 변수를 하나 생성하여 person1에 저장된 값을 할당하면 어떻게 될까?

person1에는 힙 영역에 저장된 인스턴스 데이터의 메모리 주소가 저장되어 있기 때문에 person2도 동일한 메모리 주소를 가리키게 된다.

# 2. 구조체 저장 방식

데이터 영역에 구조체가 만들어지고 이 구조체 생성자를 통해 만들어진 데이터는 변수에 저장된 후 스택에 쌓인다.

함수 내에서 구조체를 통해 생성된 데이터는 함수 종료와 함께 사라진다.

구조체의 값 복사

struct Animal{
    var name = "동물"
}

var animal1 = Animal();
var animal2 = animal1;

animal1.name = "고양이"
print(animal2.name) // 동물

let선언

구조체의 let 선언은 스택에 쌓인 인스턴스 내부 속성 모두가 let선언이 된다는 것이고, 클래스의 let선언은 힙 메모리 주소의 변경이 불가능하다는 것을 의미한다.

# 생성자 (initializer)

init키워드로 함수를 셍성한다.

class Dog{
    var name: String
    var weight: Double

    init(n: String, w: Double){
        name = n
        weight = w
    }
}

var bori = Dog(n: "보리", w: 15.0)
class Dog{
    var name: String
    var weight: Double

    init(name: String, weight: Double){
        self.name = name
        self.weight = weight
    }
}

var bori = Dog(name: "보리", weight: 15.0)

위의 코드에서 생성자 함수를 보면 파라미터 name, weight의 이름과 할당대상 변수의 이름이 동일한 것을 볼 수 있다.

이때 self키워드로 접근하면 클래스 인스턴스의 프로퍼티에 접근한다는 것을 나타낸다.

초기화 주의점

클래스, 구조체 생성자 함수에서는 모든 프로퍼티에 대해 초기화를 진행해야 한다.

생성자함수 실행 종료시점에는 모든 프로퍼티 초기값이 저장되어 있어야한다.

var dog = Dog.init(name: "강아지", weight:15.0)
// 위와 아래는 같은 코드이다
var dog = Dog(name: "강아지", weight: 15.0)

인스턴스의 초기화가 완료되면 메모리에 정상적으로 인스턴스가 생성되었다는 의미이다.

# 옵셔널 타입 속성

클래스 또는 구조체의 속성을 옵셔널 타입으로 선언하게 되는 경우를 생각해보자.

class Dog{
    var name: String?
    var weight: Int

    init(weight: Int){
        self.weight = weight
    }

    func sit(){
        print("\(name)이 앉았습니다.")
    }

    func layDown(){
        print("\(name)이 누웠습니다.")
    }
}

var myDog = Dog(weight: 15)

타입이 옵셔널이고 생성자 함수에서 모든 속성들을 초기화하지 않았더라도 옵셔널 선언된 변수는 nil값으로 초기화 되기 때문에 생성자 함수는 모든 속성을 초기화해야한다는 법칙에 저촉되지 않는다.

클래스 및 구조체 속성이 옵셔널 타입으로 생성되었다면 생성자 함수 파라미터를 통해 초기화 하거나 리터럴값으로 직접 초기화하더라도 인스턴스 생성 후 출력해보면 옵셔널 타입으로 출력된다. 따라서 옵셔널 타입을 한번 벗긴 후 값을 사용해야한다.

식별 연산자

=== 연산자는 클래스의 인스턴스가 같은 곳을 가리키고 있는지 판별하는 연산자

클래스는 기본적으로 메모리 주소에 참조 형태로 데이터를 저장하기 때문에 동작이 무거움. 꼭 써야만 하는 경우(상속 구조, 시리얼라이징, 파일로 전송 등의 작업)가 아닌 경우에는 구조체를 쓰는 것이 좋다.

# 객체지향의 4대 특징

  1. 추상화 - 실생활에서 관심있고 공통적 특싱을 뽑아내어 하나의 분류로 만든 것 (모델링)
  2. 캡슐화 - 속성과 메서드를 하나의 클래스로 묶어 활용한다는 개념(속성과 메서드 이들이 모두 하나의 목적을 가진 실체)
    1. 은닉화 - 캡슐화 이후 접근제어자를 (public, private 키워드 등)통해 객체 내외부 데이터 접근 통제가 가능
  3. 상속성 - 부모클래스 속성과 메서드를 자식클래스에서 물려받음(클래스 재활용)
  4. 다형성 - 한 객체가 여러 타입의 형태로 저장 가능. (나중에 이해)

# 속성과 메서드

# 1. 저장 속성 (Stored properties)

값이 저장되는 일반적인 속성(변수)을 저장 속성이라고 함 (클래스와 구조체 모두 동일하게 가질 수 있음)

// struct 구조체도 동일
class Person{
    var name: String? // 저장 속성
    var weight: Double? // 저장 속성
    // ...
}

저장속성 중에는 지연 저장 속성(lazy stored properties)이 있다. 저장속성 앞에 lazy 키워드만 붙이면 됨

class Person{
    var name:String
    lazy var weight: Double = 0.2

    init(name: String){
        self.name = name
    }
}

var me = Person(name: "경준")
// me.name [0x123]
// me.weight [ X ]

인스턴스 생성 시점에는 지연 저장 속성이 초기화되지 않는다. 접근연산자를 통해 접근할 때에 초기화된다. 생성자를 통해 인스턴스 생성 시에는 해당 속성을 초기화하지 않기 때문에 반드시 기본값을 가져야 한다는 것을 알 수 있다.

lazy let으로는 초기화 불가능함.

지연 저장 속성을 사용하게 되는 경우는 다음과 같다.

class MYView{
    var a: Int

    // 1) 메모리 많이 먹을때
    lazy var view = UIImageView()

    // 2) 다른 속성을 이용할때
    // 클로저, a속성이 이미 존재해야 b가 사용 가능한 형태
    // 속성 a에 값이 할당된 이후 시점을 생각하자
    lazy var b: Int = {
        return a*10
    }()

    init(num:Int){
        self.a =  num
    }
}

메모리를 많이 차지하는 데이터가 속성에 저장되어야 할때 또는 클래스 및 속성 내의 다른 속성에 의존하는 경우 사용하게 된다.

# 2. 계산속성

class Person{
    var birth: Int = 0

    // 계산 속성
    var age: Int{
        get{
            return 2022 - birth
        }
        set(age){
            self.birth = 2021 - age
        }
    }
}

계산 속성은 getter와 setter 두 블록으로 나뉜다. getter는 필수이고 setter는 정의하지 않아도 된다.

위와 같이 계산 속성을 정의한 뒤에 해당 속성을 get하거나 set할때 각각의 블록이 실행된다. 메서드로 두 블록을 각각 구현해도 되지만 목적성을 속성 수정으로 묶어 활용하기 위해 사용된다.

계산 속성의 setter블록은 자동으로 newValue라는 파라미터를 할당해준다. instance.prop = 13이라는 코드가 있을때 인스턴스의 prop 계산속성 setter함수의 아규먼트로 13이 newValue 파라미터로 자동으로 들어가게 되는 것이다.

메서드 동작 방식

클래스가 데이터 영역에 저장된 뒤 인스턴스가 만들어지면 힙 영역에 저장된다. 데이터 영역의 메서드들은 다른 함수들과 동일하게 복귀 메모리 주소를 갖는다.

메서드 호출 후에 스택 영역에 프레임이 쌓이고 메서드 내에서 필요한 인스턴스 속성들을 참조하게 된다.

힙 영역에 메서드들의 메모리 주소를 함께 저장하지 않고 데이터 영역에 따로 빼놓은 이유는 메모리 관리의 효율성을 늘리기 위해서이다. (구조체는 추가 이해 필요)

계산 속성에 read-only특성을 적용하기 위해서는 getter만 구현하면 된다. 계산속성은 반드시 타입을 명시적으로 선언해야한다.

계산 속성의 메모리 구조

계산 속성은 메서드이다. 코드 묶음의 메모리주소를 갖고 있으며 getter또는 setter함수 호출과 동시에 인스턴스가 저장된 힙으로 이동하고 스택프레임 생성과 함께 힙에 저장되어 있는 인스턴스 주소를 참조하여 내부 저장속성등을 활용하게 된다.

# 3. 타입 속성

static키워드가 붙은 속성이다. 저장속성과 계산속성 모두 타입 속성으로 생성될 수 있다. (타입 저장 속성, 타입 계산 속성)

타입 속성은 타입 자체에 속한 속성이다. 인스턴스 생성과 함께 가는 속성이 아니라 클래스 및 구조체가 데이터 영역에 저장될 때 함께 불변적으로 저장되는 속성이다.

class Dog{
    static var species: String = "Dog"

    var name: String
    var weight: Double

    init(name: String, weight: Double){
        self.name = name
        self.weight = weight
    }
}

let bori = Dog(name: "초코", weight: 15.0)
print(bori.name)
print(bori.weight)
//print(bori.spicies) // Error!!
print(Dog.species) // Dog

Int.max 등의 속성들에 접근하는 것도 역시 타입 속성이다.

// 계산 타입 속성 예시
class Circle{
    static let pi: Double = 3.14

    static var multiPi: Double{
        return pi * 2
        // 타입 속성끼리는 굳이 타입을 통해 접근연산을 하지 않아도 됨
        // return Circle.pi * 2
    }

    var radius: Double

    init(radius: Double){
        self.radius = radius
        // 일반 메서드에서는 pi만으로 접근하면 안됨
        // Circle.pi로 접근
    }
}

타입 속성은 반드시 디폴트값이 있어야한다.

타입 속성과 지연 저장

타입 속성은 기본적으로 지연 저장 속성의 성격을 가지므로 lazy로 선언할 필요가 없다.

계산 타입 속성은 함수가 호출되었을 때 스택프레임 생성과 동시에 데이터영역의 클래스 및 구조체에 직접 접근한다.

# 속성 감시자

속성 감시자는 willSetdidSet둘 중 하나만 구현하면 되고 저장속성을(계산속성은 상속한 뒤 재정의한 경우에만 가능) 관찰하는 메서드이다.

class Profile {
    // 일반 저장 속성
    var name: String = "이름"

    // 저장속성 + 저장 속성이 변하는 시점을 관찰하는 메서드
    var statusMessage: String = "기본 상태메세지" {
        willSet(message) {
            print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
        }
        didSet(message){
            print("메세지가 \(message)로 변경되었습니다!")
        }
    }
}

// .....
class Profile1 {

    // 일반 저장 속성
    var name: String = "이름"

    var statusMessage: String {
        willSet(message) {  // 바뀔 값이 파라미터로 전달
            print("메세지가 \(statusMessage)에서 \(message)로 변경될 예정입니다.")
            print("상태메세지 업데이트 준비")
        }
        didSet(message) {   // 바뀌기 전의 과거값이 파라미터로 전달
            print("메세지가 \(message)에서 \(statusMessage)로 이미 변경되었습니다.")
            print("상태메세지 업데이트 완료")
        }
    }

    init(message: String) {
        self.statusMessage = message
    }
}

리액트 useEffect와 유사한 역할을 하는 것 같다. 계산속성과 형태가 거의 유사하지만 근본은 저장속성임을 기억하자.

didSetoldValue파라미터를 기본적으로 갖고 willSetnewValue 파라미터를 기본적으로 갖는다. 일반적으로는 didSet 구현이 이루어진다.

계산속성은 set블록 자체에서 값 관찰이 가능하기 때문에 속성 감시자가 불필요하다. let키워드로 생성된 저장속성도 관찰하지 못한다. (값이 변하지 않기 때문 - 너무 당연한 이야기)

타입 저장 속성도 감시자 등록이 가능은 함. 굳이 안함

힙 영역에서의 데이터 변화가 감지된 후 데이터영역에 저장되어 있는 속성 감시자가 호출된다.

# 메서드

# 1. 인스턴스 메서드

클래스의 가장 기본적인 메서드를 인스턴스 메서드라고 한다.

class Dog{
    var name:String

    init(name: String){
        self.name = name
    }

    // 클래스 인스턴스의 메서드
    func sit(){
        print("\(name)이 앉았습니다.")
    }

    // 클래스 내의 또 다른 인스턴스 메서드 호출 가능
    func trainning(){
        self.sit()
        self.sit()
    }
}

var bori = Dog(name: "bori")
bori.sit() // 클래스 인스턴스의 메서드 호출
bori.trainning()

구조체에서의 인스턴스 메서드는 클래스 인스턴스 메서드와 조금 다르다.

struct Dog{
    var name:String
    var weight: Double

    init(name:String, weight: Double){
        self.name = name
        self.weight = weight
    }

    func sit(){
        print("\(name)이 앉았습니다")
    }

    // 구조체에서는 mutating 키워드 추가
    mutating func changeName(newName name:String){
        self.name = name
    }
}

계산속성을 통해 구조체 속성 값을 변화시키는 것은 가능하지만 기본적으로 구조체에서는 메서드를 통해 내부 속성 변경을 시키는 것은 원칙적으로 불가능하다. mutating키워드를 붙여줌으로써 메서드 내에서 인스턴스 내부 속성값을 변경시킬 수 있다.

인스턴스 메서드는 오버로딩이 가능하다. (같은 함수명에 다른 파라미터 구성)

# 2. 타입 메서드

함수정의 앞에 static키워드를 붙여 사용한다. 타입 메서드 내에서는 타입 저장 속성을 사용할 수 있다.

static으로 선언된 타입 메서드는 상속 시 재정의가 불가능하다.

호출시 스택프레임에 쌓인 뒤 데이터 영역의 클래스를 직접 참조한다.

# 3. 서브스크립트

배열의 arr[0], 딕셔너리의 dict["key"]와 같은 접근연산이 서브스크립트 문법이다.

서브스크립트도 메서드 호출의 한 형태이므로 직접 구현할 수도 있다. subscript키워드를 통해 구현한다.

class Data{
    var dummy = ["A", "B", "C"]

    subscript(index: Int) -> String{
        get{
            return dummy[index]
        }
        set(param){
            dummy[index] = param
        }
    }
}

var newData = Data()
newData.dummy[0] // 직접 접근 연산자를 통해 접근해도 됨
newData[0] // 서브스크립트 정의를 통해 접근해도 됨

계산속성과 유사한 형태를 갖는다.

// 유용한 예시
enum Planet: Int {   // 열거형의 원시값
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune

    static subscript(n: Int) -> Planet? {    // Self
        guard let result = Planet(rawValue: n) else {return nil}
        return result
    }
}

let mars = Planet[4]!
print(mars)

열거형에 원시값을 부여하여 rawValue를 통한 접근 후 옵셔널 벗기기로 열거형 케이스를 출력한다.

get블록만 구현하면 읽기전용으로 서브스크립트가 구현된다.set블록은 선택적 구현

static키워드를 붙이면 타입 서브스크립트로도 구현 가능하다.

서브스크립트는 인스턴스 내의 배열 저장 속성이 배열의 주소를 힙 영역에서 갖고 있게 되고 배열자체는 구조체이더라도 힙 영역에 함께 저장된다.(그림 참조)

# 접근제어 기초

클래스 내의 속성과 메서드의 사용 범위를 지정한다. 객체지향언어의 private등의 키워드를 사용하는 개념이 접근제어이다. (은닉화 - 코드 내부의 세부 구현 내용을 숨긴다)

접근 제어가 필요한 이유는 다음과 같다

  1. 원하는 코드를 감출 수 있다
  2. 코드 영역을 분리하여 효율적 관리가 가능
  3. 컴파일 시간이 줄어든다. (컴파일러에게 명확한 스코프를 전달함)

# 싱글톤(singleton) 패턴

유일하게 한개만 존재하는 객체가 필요한 경우에 사용한다. 앱 종료 전까지 지속적으로 메모리에 남음

class Singleton{
    // static으로 어디서든 접근 가능하게 하고
    // let으로 불변하도록 한 뒤
    // 객체를 생성하여 변수에 저장한다
    static let shared = Singleton()
    var userId: String = "123"

    // 생성자를 통해 싱글톤 객체가 더 생성되지 못하도록 막음
    // 중요!
    private init(){}
}

// 접근과 동시에 lazy할당이 되어 힙 메모리 상에 객체가 할당
let user1 = Singleton.shared
let user2 = Singleton.shared
user1.userId = "1234"
print(user2.userId) // 1234

클래스 인스턴스(객체)는 힙 메모리상에 주소값으로 원본 자체를 수정하기 때문에 어떠한 변수에 담아 내부 속성값을 수정하더라도 매번 해당 값들로 업데이트된다.

싱글톤패턴은 객체가 단 하나만 메모리 상에 존재해야 하는데, static let shared = Singleton()이 선언된 시점의 클래스 스코프 바깥에서 해당 타입으로 객체를 새로 생성하는 것을 막기 위해서 생성자를 private으로 접근제어해주는 것이다.

// 애플 내부 API를 사용할때 사용하게 된다
let screen = UIScreen.main    // 화면
let userDefaults = UserDefaults.standard    // 한번생성된 후, 계속 메모리에 남음!!!
let application = UIApplication.shared   // 앱
let fileManager = FileManager.default    // 파일
let notification = NotificationCenter.default   // 노티피케이션(특정 상황, 시점을 알려줌)

# Reference

  1. 인프런 - 앨런 swift 문법 마스터 스쿨 (opens new window)
  2. 큰돌의터전 - 싱글톤 패턴에 대해 자세히 알아보자 (opens new window)