[iOS] TypeCasting 타입캐스팅이 뭐야

Madeline👩🏻‍💻·2024년 1월 8일
0

iOS study

목록 보기
30/61
post-thumbnail

😶‍🌫️

오늘은 타입 캐스팅, 그리고 Class와 Structure에 대한 이야기를 해보자.

TypeCasting이 뭐야

지난주 TableView에 대해 공부하는 동안

let cell = tableView.dequeueReusableCell(withIdentifier: "MovieTableViewCell", for: indexPath) as! MovieTableViewCell
        // 💡 TypeCasting 타입캐스팅: 한 단계 구체화하는 역할 (씬 - 로직 매칭)

as라는 애를 처음 봤다.
타입 캐스팅이라는 애라는데, 이를 더 잘 알아보기 위해서 배경 지식 공부를 해보자.

let a = 3
// 이렇게 상수 a 에 3을 대입했다고 해보자
print(type(of:a))
// a의 타입을 확인해보면 Int라고 잘 뜬다.

타입 캐스팅, 타입 변환은 인스턴스 타입을 확인하거나, 인스턴스 자신의 타입을 다른 타입의 인스턴스인양 행세할 수 있는 방법으로 사용된다.

다시 코드로 예시를 들어보자.

let b = String(a)
print(type(of:b))

a를 String타입으로 변환해보자. String도 사실 구조체다.
엥?

진짜 구조체다. 그래서 위 let b = String(a) 는 초기화를 통해 새로운 인스턴스를 형성한거다.
흔히 타입 캐스팅이라고 부르지만 사실은 아니다.❌

struct User {
	let name: String
    let age: Int
}

var nickname = User(name: "쿠크다스", age: 10)
nickname.age
nickname.name

var nickname = "쿠크다스" 이런식으로 변수에 값을 넣을 수도 있지만,
User라는 struct를 만들어서
위와 같이 점 찍어서 내부 요소를 꺼내올 수 있다.
여기에서 nickname은 구조체의 인스턴스이다. 근데 이제 선언과 초기화를 한 것,,
그리고 nickname 인스턴스를 계속 만든다면, 각각의 공간을 차지하게 된다.
각 인스턴스의 별도의 공간이 생성되는거다.

그래서 let b = String(a) 이것도 마찬가지로 초기화하면서 인스턴스를 생성한 것일 뿐, a와 b가 차지하는 공간은 다르다.
일반적으로 다른 프로그래밍에서는 타입 캐스팅, 타입 변환이라고 말하긴 하는데,
우리가 다루고자 하는 건 다른 개념이다.

타입 캐스팅은 다른 공간이 아니라 같은 공간을 이용하는거다.
내 타입을 다른 인스턴스처럼, 나는 Int인데 String처럼 쓰고 싶을 때, 다른 타입의 인스턴스처럼 쓴다는 것을 타입 캐스팅이라고 한다!

인스턴스의 타입을 확인하는건 is,
다른 타입의 인스턴스로 쓰겠다는건 as로 표현한다.

타입 캐스팅은 타입에 힌트만 주는 것이고, 실제의 인스턴스 타입은 메모리에 남아있다.

또 클래스를 만들어보자.

class Mobile {
	let name: String
    init(name: String) {
    	self.name = name
    }
}
// 클래스는 초기화를 해줘야 한다.

class Apple {
}

class Google {
}

요렇게 만들어놨다. 애플과 구글이라는 클래스가 Mobile을 상속하게 해보자.

class Mobile {
	let name: String
    init(name: String) {
    	self.name = name
    }
}
// 클래스는 초기화를 해줘야 한다.

class Apple: Mobile {
}

class Google: Mobile {
}

let mobile = Mobile(name: "phone")
let apple = Apple(name: "apple")
let google = Google(name: "google")

요렇게 인스턴스까지 만들어줬다.
여기에서 이제 얘네가 어떤 클래스의 인스턴스 타입인지 확인해보자.
(당연히 각자 모바일, 애플, 구글이겠지만)
이럴 때 사용하는게 is 이다.

mobile is Mobile // true
mobile is Google // false
mobile is Apple // false

mobile의 타입은 Mobile 클래스임.

apple is Mobile // true
apple is Apple // true
apple is Google // false

apple의 타입은 Apple 클래스인데, Mobile 클래스도 true로 나오는 이유는?
상속 받고 있기 때문에! 부모 클래스인 Mobile의 특성을 물려받는다.
또 apple is Google 코드 라인에서는 Cast from 'Apple' to unrelated type 'Google' always fails 이런 에러가 뜨는데, 이걸 캐스팅 실패라고 한다.

업캐스팅, 다운캐스팅

let iphone = Apple(name: "iphone")

위 코드에서 이어지는 내용으로,
iphone의 타입은? Apple로 추론될거다.
그렇다면 iphone의 타입을 바꿔보자

let iphone : Mobile = Apple(name: "iphone")

type annotation을 부모 클래스로 확장한거다.
이렇게 하면 Mobile을 상속받는 모든 클래스를 가져올 수 있다.

부모 클래스인 Mobile의 내용을 추가해보자.

class Apple: Mobile {
	let event = "WWDC"
}

이 상태에서 iphone에 점을 찍어도 event 프로퍼티를 가져올 수 없다.
type annotation이 Mobile이 아니라면, event를 가져올 수 있다.
Mobile로 지정해두면, Apple의 event를 쓸 수 없다. 이럴 때 쓰는게 다운캐스팅이다.
부모 클래스를 자식 클래스로 내리는거다.

옵셔널 바인딩 구문을 사용해서 다시 확인해보자.

if let apple = iphone as? Apple {
	print(apple.event) // "WWDC"
}

as를 통해 다른 타입의 클래스로 쓴 거고, 여기에서는 자식 클래스로 한 단계 내린거다.
iphone은 Mobile 타입인데, Apple 타입으로 써보겠따! 이게 다운 캐스팅!
옵셔널은 혹시 다운캐스팅 안될까봐 ?를 붙인거고, !는 무조건 된다!이다.
런타임동안에만 저 안에 있는 코드인 프린트가 저렇게 찍히는거지,
iphone자체는 아직도 Mobile타입이다.

정리

DownCasting

  • 부모 클래스 타입을 자식 클래스 타입으로 다운해서 캐스팅하는 것
  • as?는 옵셔널 반환 타입을 반환한다. 다운캐스팅이 성공할 경우 옵셔널이 아닌 인스턴스가 반환이 되고, 만약 실패할 경우 nil을 반환한다,
  • as!는 옵셔널을 반환할 수 없다. 따라서 다운캐스팅이 성공할 경우 옵셔널이 아닌 인스턴스가 반환되고, 만약 실패할 경우 런타임 오류가 발생한다.

UpCasting

  • 컴파일러가 캐스팅에 대한 성공을 확신할 수 있는 경우 as를 사용할 수 있다.
  • 일반적으로 부모 클래스의 타입인 것을 알고 있을 때 as를 사용한다.

Class vs Struct

앱을 켜서 실행한다고 생각해보면,
앱을 실행하는 것은 코드가 움직인다는 것이고,
코드가 움직인다는 것은 메모리에 내용이 올라오는 거다.
유저디폴트처럼 영구 저장은 아니어도, 엑셀이나 파워포인트로 작업하다보면 저장버튼을 누르기 전에도 내가 작성하는 내용이 남아 있다. 강제 종료하면 복구할 수도 있다. 이게 다 메모리랑 관련된건데,
프로그램을 실행하면 메모리에 올라가는데, 메모리는 한정되어 있다.
앱도 마찬가지로 아이폰 용량 중에 일정 부분을 앱이 차지하고 있는거다.
안쓰는 것 같거나, 앱을 종료하면 메모리 공간이 확보된다. 메모리가 얼마나 있냐에 따라 실행되는 거다.

Class

클래스는 항상 초기화 해야된다.

1. 프로퍼티를 선언과 동시에 초기화한다(초기값을 설정한다)

class Monster {
	let name: String = "easy"
    let power: Int = 1
}

2. 옵셔널 타입

class Monster {
	var name: String?
    var power: Int?
}

let monster = Monster()
monster.name // nil
monster.power // nil

3. initializer 이니셜라이저

  • 가장 많이 사용할 방법이다!
class Monster {
	var name: String?
    var power: Int?
    
    init(name: String, power: Int) {
    	self.name = name
        self.power = power
    }
}
let monster = Monster(name: "easy", power: 1)
monster.name // "easy"
monster.power // 1

더 들어가보면

// name을 바꾸고 싶어
monster.name = "hard"
monster.power = 10

monster.name // "hard"
monster.power // 10

Q. let으로 선언했는데 어떻게 값이 바뀔 수가 있냐?!
-> 밑에서 답을 다시 확인해보자!

메모리

코드 ==== 데이터 ==== 힙 ==== 스택

메모리 공간은 역할에 따라 이렇게 4가지로 나뉜다.
앱을 실행하는건 xcode상의 코드들이 실행되고, 메모리에 올라가게 되는거다.
내 코드가 메모리에 올라가면, 메모리 상의 코드 부분에 올라간다.

우리가 다루는 리소스에 따라 사용되는 메모리의 크기가 다른데,
처음 앱을 실행하면 사용되는 메모리의 크기는 화면 띄우고 앱 안에서 기능이 동작하는것보다 작다.

사용자가 화면에 머무르면 그 화면에 있어야 하는 것들만 준비하고 있는거고,
다음 화면으로 넘어가는 액션을 취하면 그 때 다음 화면에서 필요하는 것들을 load한다.

화면도 결국엔 클래스나 구조체로 구성되어 있다. viewController도 클래스다.
이 클래스를 사용할 때 메모리가 올라가고, 필요 없어질 때 메모리가 내려간다.
그러니깐 쓸 때 올라가고, 안 쓸때 내려간다.
여기에서 "쓴다"의 의미는 코드상에서 인스턴스를 생성하는 것을 말한다.
클래스로 만들어진 건 메모리에서 "쓰고 있는" 단계라기 보다는 준비 단계이다.

위에서 Monster 클래스를 만들어놓은 것까지는 메모리를 쓰고 있다고 하지 않고,
인스턴스를 생성해서 변수나 상수에 담는 순간에 메모리에 올라온다.

만약에 인스턴스를 만드는데에 1mb가 든다고 치면, 2개 만드는데에 2mb, ...이런식으로,

인스턴스 생성 == 사용할 수 있게 메모리에 올라온다!

이어지는 내용,,,

let hard = monster
monster.name = "hard"
monster.power = 10

print(monster.name, monster.power) // "hard 10\n"
print(hard.name, hard.power) // "hard 10\n"

악 왜 위에 변수들은 자동으로 안바뀌고 얘(hard에 대한 name, power값)은 알아서 바뀌었을까?

왜..?
메모리 공간이 4개가 있었잖아.
(코드, 데이터, 스택, 힙)
코드 역할에 따라 각 공간에 저장이 된다.

절대적이진 않은데, 인스턴스 생성을 하면 메모리에 올라온다.
생성된 인스턴스는 스택 or 힙에 저장된다.
힙에는 보통 참조 타입이 저장되고,
스택에는 값 타입이 저장된다.

일반적으로 나눌 때, 클래스는 참조 타입, struct는 값 타입니다.

var nickname = "고래밥"
var subname = nickname

이렇게 인스턴스를 만들어서 넣는 순간,
스택에는 nickname 공간 안에 고래밥 값이 들어가게 될거다.
subname 공간 안에 고래밥 값이 또 들어가고.

nickname = "쿠크다스"

얘는 nickname 공간을 찾아서, 이 값을 "쿠크다스"로 바꾸는거야.

그래서 subname은 고래밥이 남아있을 수밖에 없음
각각 다른 공간을 차지하기 때문이고,
값을 대입한거지(복사), 같은 공간이 아님.
구조체는 이런식으로 저장이 된다. 값 타입이라 스택에 저장된다.

반면에 클래스는..

let monster = Monster()
let hard = monster

monster.name = "hard"
monster.power = monster

Monster() 클래스는 힙에 저장된다. 그 안에 있는 name, power도. 그리고 이 공간에는 주소가 있을거다.
그러면 monster는 스택에 저장된다. nickname 처럼????엥 뭔말이지

대신 얘는 Monster의 주소를 힙에 연결해서 갖고 있다.
hard도 같은 주소를 갖는 상태로 스택에 저장된다.

그래서 monster의 name, power를 바꾸면, 주소를 찾아가서 그 주소의 값을 바꾸는거다.
hard도 같은 주소를 바라보고 있기 때문에 영향을 받는거다.

인스턴스를 변경하는 순간 모든 곳에 영향을 받게 된다.
그래서 struct는 영향을 안받고, class는 영향을 받는다.

(공부할때는 외우려고 하겠지만, 나중에 클래스 안에 구조체 있고 복합적으로 만들어지면 이렇게 단순하게 끝나지 않어. 그래서 일반적으로 어떻게 저장되는지 이해하면 돼. 모든 변수 상수를 따지면서 왜 어디에 올라가는지 따질 필요는 없다.)

그래서 다시 위 질문에 대한 답을 해보자

let hard = monster
monster.name = "hard"
monster.power = 10

왜 let으로 선언했는데 값이 바뀌냐?
상수는 맞다. 스택에 있는 monster는 못바꾸는게 맞는데, 얘가 갖고 있는 Monster의 값이 var이기 때문에 바꿀 수 있는거다.

자물쇠가 monster에 잠겨있는거지, Monster 안에 있는 값들은 var.

class Monster {
	let name: String
    let power: Int
}

그래서 요렇게 바꾸면 Cannot assign to property: 'name' is a 'let' constant 이런 에러 뜬다.

또 만약에 Monster가 struct라면?
let monster = Monster() -> monster는 스택에 저장되고,
let hard = monster -> 스택에 저장

monster를 상수로 선언했기 때문에, 안에 내용도 못바꾸는거다. 그래서 struct 안에 있는 프로퍼티도 var로 바꾸면 된다!

그래서 이전에 학습했던 dateFormatter에서도 let인데 왜 값이 바뀌는거지?했던 것도 마찬가지이고,
let cell = tableView.dequeue 어쩌고 이 부분도 마찬가지이다.
클래스다.
힙 - 스택 이렇게 두 가지로 나누어서 저장한다.

안쪽에 루트(힙)가 var이면, 바깥 스택에 루트 주소를 갖고 있는 애들이 let이어도 알맹이를 수정할 수 있는 것이다!

profile
🍎 Apple Developer Academy@POSTECH 2기, 🍀 SeSAC iOS 4기

0개의 댓글