iOS/iOS 앱 개발 올인원 패키지 Online

[iOS] 패스트캠퍼스 - Class

듀IT 2021. 9. 16. 21:27

Class


Class는 Reference type이다. 그리고 heap 메모리 영역에 생성된다. 

Stack Heap
Fast: 시스템에서 당장 실행해야하거나, tight 하게 컨트롤하고 관리되어야 하는 경우. 함수 내에 선언된 지역변수. 효율적이고 빠르다 Slow: reference type을 저장한다. 큰 메모리 풀을 가지고 있어서 시스템에 동적으로 메모리 할당을 요청할 수 있다. 대신 스택처럼 자동으로 데이터를 제거하지 않기 때문에, 개발자가 신경써서 쓰지 않는 데이터는 제거해줘야 한다. 

Structure vs. Class

Structure를 Class로 바꿔보자.

다음과 같이 PersonStruct 구조체가 있다. 

struct PersonStruct {
    var firstName: String
    var lastName: String
    
    var fullName: String {
        return "\(firstName) \(lastName)"
    }
    
    mutating func uppercaseName() {
        firstName = firstName.uppercased()
        lastName = lastName.uppercased()
    }
}

이 구조체를 Class로 바꾸면 다음과 같다.

class PersonClass {
    var firstName: String
    var lastName: String
    
    var fullName: String {
        return "\(firstName) \(lastName)"
    }
    
    func uppercaseName() {
        firstName = firstName.uppercased()
        lastName = lastName.uppercased()
    }
    
    init(firstName: String, lastName: String){
        self.firstName = firstName
        self.lastName = lastName
    }
}

class는 structure와 코드가 비슷하지만, 다른 부분이 2가지 있다. 

  1. stored property를 fucntion 내에서 바꿀 때 func 앞에 mutating 키워드를 붙이지 않는다.
  2. init()이라는 생성자를 통해 선언된 stored property의 값을 초기화 해줘야 한다.

Struct vs. Class 언제, 무엇을 쓸까?

이럴때 Struct를 쓰자!

  1. 두 object를 "같다, 다르다"로 비교해야 하는 경우
  2. Copy된 각 객체들이 독립적인 상태를 가져야 하는 경우
  3. 코드에서 오브젝트의 데이터를 여러 스레드 걸쳐 사용할 경우
    1. 각 쓰레드가 유니크한 인스턴스를 가지고 있어서, 한 객체에 여러 쓰레드가 접근했을 때 발생할 수 있는 잠재적인 위험을 피할 수 있다.

이럴 때 Class를 쓰자

  1. 두 object의 인스턴스 자체가 같음을 확인해야 할 때
  2. 하나의 객체가 필요하고, 여러 대상에 의해 접근되고 변경이 필요한 경우

단순하게 이야기 해보자

  1. 일단 Struct로 쓰자

Swift는 structure를 많이 사용한다. Swift에서는 앞서 배웠던 string, array, dictionary 모두 struct 타입이다.

 

2. StackOverflow보다 apple 공식 문서를 먼저 확인하자. 습관을 들이자. 그러면 개발에 대한 이해도를 높일 수 있다. 

 

상속


A is B(A는 B에 포함)

해당 명제가 논리적으로 맞으면 A는 B를 상속받을 수 있다.

예를 들면, Student is Person (Student는 Person에 포함) 는 논리적으로 맞는 명제이므로, Student는 Person을 상속받을 수 있다.

이런 경우 Person의 속성을 Student에게 물려주는 것이므로 Person을 Super class(Parent class)라 부른다. 그리고 Student는 Parent의 속성을 물려받는 입장이기 때문에 SubClass (Child Class)라고 부른다.

 

Person: Super class (Parent Class)

Student: SubClass (Child Class)

 

상속의 규칙

  1. 자식은 한개의 super class만 상속받을 수 있다.
  2. 부모는 여러 자식들을 가질 수 있다.
  3. 상속의 깊이는 상관이 없다.

1. 자식은 한개의 super class 즉 한개의 부모 클래스만 가질 수 있다.
2. 부모 클래스는 여러 자식 클래스를 가질 수 있다.
3. 상속의 깊이는 상관이 없다. 깊이는 얼마든지 깊어져도 괜찮다는 것이다.

 

Student is Person이므로 Student 는 Person class를 상속받을 수 있다. 이를 Swift 코드로 구현해보면 다음과 같다.

struct Grade {
    var letter: Character
    var points: Double
    var credits: Double
}

class Person {
    var firstName: String
    var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }

    func printMyName() {
        print("My name is \(firstName) \(lastName)")
    }
}

class Student: Person {
    var grades: [Grade] = []
}

Student class는 Person 클래스를 상속받으므로, Person class의 firstName, lastName, printMyName() 에 접근할 수 있다.

하지만 Person Class는 Student Class의 grades 속성에는 접근할 수 없다.

 

이를 좀 더 확장시켜보자. 아까 상속은 얼마든지 깊어질 수 있다고 했다.

// 학생인데 운동선수
class StudentAthlete: Student {
    var minimumTrainingTime: Int = 2
    var trainedTime: Int = 0

    func train() {
        trainedTime += 1
    }
}

// 학생이자 운동선인데 축구선수
class FootballPlayer: StudentAthlete {
    var footballTeam = "FC Swift"

    override func train() {
        trainedTime += 2
    }
}

위의 코드를 보면 정말 상속은 깊이가 없다는 걸 알 수 있다. Person > Student > StudentAthlete > FootballPlayer로 상속이 깊어지고 있다. 

 

그럼 이를 활용하여 객체를 생성해보자

var athelete1 = StudentAthlete(firstName: "Yuna", lastName: "Kim")
var athelete2 = FootballPlayer(firstName: "Heung", lastName: "Son")

athelete1은 StudentAthlete의 인스턴스이고, athelete2는 FootballPlayer의 인스턴스이다. FootballPlayer는 StudentAthelete의 SubClass이다.

이렇게 부모-자식 관계의 클래스는 서로의 클래스 타입으로 캐스팅 해줄 수 있다. 

캐스팅 방법엔 2가지가 있다.

  1. Upper Casting
  2. Down Casting

Upper casting은 SubClass를 SuperClass의 타입으로 캐스팅하는 것이다.

서브 클래스의 인스턴스를 슈퍼 클래스의 타입으로 참조하는 것을 의미한다.

athelete1 = athelete2 as StudentAthlete
athelete1.train()
athelete1.trainedTime

Down casting은 슈퍼 클래스의 인스턴스를 서브 클래스의 타입으로 참조하는 것이다.

if let son = athelete1 as? FootballPlayer {
    print("--> team:\(son.footballTeam)")
}

여기서 못보던 문자가 나온다. as? 는 뭔데 물음표가 붙는 걸까?

Swift는 안전한 언어이다. 슈퍼 클래스에서 서브 클래스로 캐스팅하는 것은 실패할 수 있기 때문에, optional 이 사용되는 것이다!

 

 as?  : "런타임 시점"에 타입 캐스팅(다운 캐스팅)을 하며, 실패할 경우 nil을 리턴

 

상속은 언제 하면 좋을까?

장점: 중복되는 코드를 제거할 수 있다.

단점: 상속의 깊이가 깊어지면 유지보수가 어려워진다.

 

짬에서 나오는 바이브가 필요한 영역인 것 같다.

즉 개발 철학이 필요하다.

 

  1. Single Responsibility(단일 책임)
    1. 클래스는 단 하나의 책임만 지면 된다. 한가지 일에 집중해야한다. 클래스가 너무 많은 일들을 책임지게 되면 정체성도 모호해지고  유지하기도 어려워진다.
  2. Type Safety (타입이 분명해야할 때)
    1. 학생이더라도 이 학생이 어떤 역할을 하는지 그 타입이 분명해야할 때 사용할 수 있다.
      1. 예를 들어 학생 중에서도 미술을 하는 학생, 운동을 하는 학생 등이 있는데 이들이 무엇을 하는지 그 타입을 알고 싶을 때 상속을 통해 그 타입을 구분할 수 있다.
  3. Shared Base Clases(다자녀가 있다!)
    1. 학생이라는 클래스가 학습한다()라는 행위를 구현하고 있다면, 운동선수, 미술을 하는 학생은 그 각각의 학습에 대한 행위가 다르다. 운동선수는 운동을 학습한다, 미술하는 학생은 미술을 학습한다로 학습의 행위가 각각 다르다.(다형성) 이렇게 내용 자체가 다를 때 상속을 고려해볼 수 있다.
  4. Extensibility (확장성이 필요한 경우)
    1. 캔이라는 클래스를 콜라캔, 사이다캔 등으로 확장이 필요할 때
  5. Identify (정체를 파악하기 위해)
    1. 어떤 객체인가, 클래스인가를 상속을 통해 검증할 수 있다.
      1. 체대생인지, 미대생인지 어떤 학생을 하는지 타입을 통해 정체를 검증할 수 있다.

 

생성자 이해하기

자식 클래스는 부모클래스와는 다른 형태의 생성자를 만들 수 있다.

 

stored property를 선언하면 객체를 생성하는 시점에 프로퍼티는 초기화가 되어있어야 한다.

따라서 선언할 때 초기화를 해주거나, 생성자를 통해서 초기화를 해줘야 한다.

 

생성자의 규칙이 있는데, 자식 클래스의 stored property를 초기화해주고 난 뒤에 부모 클래스의 init()을 호출하여 부모 클래스의 stored Property를 초기화 해줘야 한다는 것이다.