먹고 기도하고 코딩하라

Swift의 기본 문법 (5) - 옵셔널 체이닝과 타입 캐스팅 본문

앱/Swift

Swift의 기본 문법 (5) - 옵셔널 체이닝과 타입 캐스팅

2G Dev 2021. 12. 10. 20:10
728x90
728x90

 

(11) 옵셔널 체이닝

옵셔널 체이닝은 nil일 수도 있는 프로퍼티, 메소드, 서브스크립트에 질의하는 과정이다. 이 포스팅에서 다루는 주제는 구조체/클래스/열거형의 연장선상인 셈이다. 여러 질의를 연결할 수도 있는데, 그 질의문 중 어느 하나라도 값이 nil이면 전체 결과는 nil이 된다. 옵셔널이 연속으로 연결되는 경우에 유용하게 쓸 수 있다.

옵셔널 체이닝은 옵셔널값 뒤에 ?를 붙여서 표현할 수 있다. 옵셔널 체이닝 값은 항상 옵셔널값이 된다. 이 값을 보면 옵셔널 체이닝이 잘 됐는지 아니면 nil이 반환됐는지를 알 수 있다. 

이 예제는 스위프트 가이드보다 야곰님 강의가 더 보기 좋을 거 같다. 그 예제를 보자. 옵셔널 체이닝의 설명을 보면 알 수 있다시피 구조체, 클래스 등에서 쓸 수 있는 것이기 때문에 사람과 집 클래스를 만들고 시작해보겠다.

class Person {
  var name: String
  var job: String?
  var home: Apartment?
  
  init(name: String) {
    self.name = name
  }
}

class Apartment {
  var buildingNumber: String
  var roomNumber: String
  var `guard`: Person?	// 이렇게 키워드와 겹치는 경우 백틱 ₩을 이용해서 쓰면 됨
  var owner: Person?
  
  init(dong: String, ho: String) {
    buildingNumber = dong
    roomNumber = ho
  }
}

var kendra: Person? = Person(name: "Kendra")
let apart: Apartment? = Apartment(dong: "104", ho: "205")

사람에게 이름은 필수지만, 직업과 집은 있을 수도 있고 없을 수도 있다. 또 집은 이름과 호수가 필수지만, 경비원과 집주인은 있을 수도 없을 수도 있다. 이제 사람과 집을 하나씩 만들어주는데 주의해서 봐야 할 건 Person?과 Apartment? 로 명시적으로 옵셔널 타입으로 선언했다는 점이다. 이걸 빼면 kendra와 apart는 자동적으로 Person과 Apartment 타입이 된다. (이니셜라이저가 실패할 수 있는 이니셜라이저가 아니기 때문에 그냥 타입 인스턴스가 만들어짐)

뭐... 사실 이렇게 해도 큰 문제 없긴 하다. 솔직히 왜 ? 타입으로 선언했는지 잘 모르겠다. 일관성 있게 다 ? 붙이려고 옵셔널로 만든 건가? 여튼, 옵셔널 타입은 당연히 해당 타입을 받을 수 있다. 그 해당 타입이거나 nil이거나 둘 중 하나니까. 여기선 그냥 ? 붙여서 해보자.

이제 우리는 옵셔널 체이닝을 하는 함수를 만들 것이다. 참고로 함수 아니어도 되는데 그냥 함수로 써보자.

func guardJobWithOptionalChaining(owner: Person?) {
  if let guardJob = owner?.home?.guard?.job {
    print("\(owner)의 집 경비원의 직업은 \(guardJob)입니다.")
  } else {
    print("문제가 있습니다")
  }
}

guardJobWithOptionalChaining(owner: kendra)

if let을 할 때 ?가 되게 많은데, 잘 보면 ?가 붙은 것들은 다 옵셔널 타입들이다. 그러니까, nil일 수도 아닐 수도 있는 값들인 것이다. 이처럼 어떤 프로퍼티나 메소드까지 깊숙이 접근해야 할 때 거쳐가는 것들 중 옵셔널 타입이 하나라도 있어서 저렇게 표현해야 하는 것들이 옵셔널 체이닝이다. 만약 중간에 nil이 나오면 중단되고 else 구문으로 가게 된다. 여기서는 home에 guard가 없으므로 "문제가 있습니다"가 출력된다.

 

 

(12) 타입 캐스팅

어떤 타입 간에 부모-자식 관계가 있는 경우 타입 캐스팅이 가능하다. 타입 캐스팅이란 인스턴스 타입을 확인하거나 인스턴스를 다른 부모/자식 인스턴스로 취급하는 방법이다. 위에서 마침 Person을 선언했으므로, Person을 상속받는 학생과 대학생 클래스를 만들어 보자.

class Student: Person {
  var age: Int?
  func selfIntro() { print("학생"); }
}

class UnivStudent: Student {
  var major: String?
  override func selfIntro() { print("대학생"); }
}

var miles: Student = Student(name: "miles");
var dean: UnivStudent = UnivStudent(name: "dean");

Student의 부모는 Person 클래스이고, UnivStudent의 부모는 Student이다. 즉, UnivStudent의 조상은 Person이다. is 연산자를 이용하면 어떤 인스턴스가 특정 타입에 속하는지 아닌지 알 수 있다.

print(kendra is Person);	// true
print(miles is UnivStudent);	// false
print(dean is Student);	// true

이런 경우도 있을 수 있다. 변수의 타입을 부모/조상 클래스로 하되, 실제 인스턴스는 자식 클래스로 만드는 것이다.

var gloria: Person = UnivStudent(name: "gloria") as Person;
print(gloria is Student);	// true

이 경우에 실제로 만들어진 인스턴스는 UnivStudent이므로 상관없다. 즉, 부모 클래스를 타입으로 갖는 변수자식 클래스 인스턴스를 넣는 데에는 아무런 문제가 없다는 뜻이다. 이건 업캐스팅이다. 자식 클래스를 부모 클래스로(관계를 보면 아래에 있는 자식이 위로 올라가는 것) 올라가는 것이기 때문이다. 내가 UnivStudent를 만들긴 하지만 Person처럼 동작하면 좋겠어~ 할 때 업캐스팅을 한다.

하지만 반대의 경우, 즉 자식 클래스 타입 변수부모 클래스 인스턴스를 넣으면 문제가 생긴다. 

var shawn: UnivStudent = Person(name: "shawn");
// cannot convert value of type 'Person' to specified type 'UnivStudent'

Person을 UnivStudent로 변경할 수 없다는 에러 메시지가 뜬다. 이 때 쓸 수 있는 방법이 명시적 타입 캐스팅이다. 그 중 다운 캐스팅을 알아볼 건데, 다운 캐스팅은 부모 클래스가 자식 클래스로(관계를 보면 위에 있는 부모가 아래로 내려가는 것) 타입이 변경되는 것을 말한다.

방법은 조건부와 강제 두 가지다. 강제로 바꾸는 거 먼저 살펴보자면, 일단 변수를 생성할 때 타입을 옵셔널로 주지 않는다. 부모 클래스 인스턴스를 새로 생성하되 as! 자식클래스 식으로 뒤에 덧붙여주면 된다. as! 는 특정 타입인 게 확실할 때 사용한다.

var shawn: UnivStudent = Person(name: "shawn") as! UnivStudent;

이렇게 하면 컴파일이 된다. 그런데 사실 이렇게 강제로 다운 캐스팅을 해서 썼을 때 문제가 생기면 프로그램이 죽어 버린다. 안 되는 걸 억지로 하려고 하면 문제가 생기기 마련이다.. 정상적으로 동작했을 때 일반 인스턴스 타입을 반환한다.

조건부 다운 캐스팅을 살펴 보자. 이 경우에, 변수는 옵셔널 타입이며 as? 자식클래스 식으로 덧붙여주면 된다. 어떤 특정 타입인지 아닌지 확실하지 않을 때 사용하면 된다.

var optionalCasted: Student?
optionalCasted = gloria as? UnivStudent;	// Optional(__lldb_expr_26.UnivStudent)
optionalCasted = miles as? UnivStduent;	// nil
optionalCasted = kendra as? Student;	// nil

gloria는 원래 UnivStudent 인스턴스를 생성해 만들었는데, Person으로 업캐스팅되어 사용한 것이므로 잘 동작한다. 결과가 정상인 경우, 옵셔널 타입을 반환한다.

하지만 miles는 원래 Student이다. 이 경우에는 UnivStudent로 다운 캐스팅할 수가 없다. (UnivStudent에는 Student에 없는 프로퍼티가 있기 때문에) 그렇기 때문에 nil을 반환한다. Person인 kendra 역시 마찬가지다. 이처럼 변환하려는 타입에 부합하지 않는다면 조건부 다운 캐스팅에서는 nil을 반환하게 된다. 강제 다운 캐스팅은.. 뭐 얄짤없다. 바로 프로그램이 죽어 버린다. 웬만하면 조건부 다운 캐스팅을 쓰는 게 좋을 것이다.

 

다음은 상속으로 얽힌 여러 인스턴스들을 함수에 넣어서 어떻게 되는지 실험해보는 코드다. 

func doSomething(someone: Person) {
    if let univSt = someone as? UnivStudent {
        univSt.selfIntro()
    } else if let st = someone as? Student {
        st.selfIntro()
    } else if let p = someone as? Person {
        print("person")
    }
}

doSomething(someone: dean as Student)	// 대학생
doSomething(someone: dean)	// 대학생
doSomething(someone: gloria)	// 대학생
doSomething(someone: miles)	// 학생
doSomething(someone: kendra)	// person

dean을 Student로 업캐스팅해서 변수로 줬음에도 UnivStudent로 받아들인 것에 주목하자. 실제로 함수 매개변수로 넘어가는 변수가 어떤 인스턴스인지를 조건부 다운 캐스팅으로 판단하는 것이고, 상속 관계에서 가장 아래에 있는 자손 클래스 인스턴스인지를 먼저 검사하기 때문에 dean as Student는 UnivStudent에 걸리게 되는 것이다.

 

 

(13) assert, guard

assert와 guard는 앱 동작 중에 생성되는 결과값들을 확인하고 안전히 처리할 수 있도록 도와주는 구문들이다.

assert부터 살펴보면, 이건 디버깅 중에 조건 검증을 위해 주로 사용되는 구문이다. 디버깅 모드에서만 동작하며 실제 앱으로 올라갈 때는 동작하지 않는다.

assert(검사할 조건, 조건이 맞지 않을 때 출력할 메시지) 이렇게 사용한다.

var someInt: Int = 2
assert(someInt == 0, "someInt != 0")

이렇게 사용할 수 있다. someInt라는 게 0이어야만 다음 문장으로 넘어갈 수 있다. 그렇지 않고 지금처럼 0이 아닌 경우에는 런타임 에러가 나면서 프로그램이 뻗는다. 두 번째 매개변수로 주어진 것은 assert를 통과하지 못하고 프로그램이 중단됐을 때 출력될 에러 메시지이다. 

이걸 어떻게 유용하게 사용할 수 있을까? 위에 쓴 것처럼 어떤 값이 유효한지 검증하고 싶을 때 사용하면 좋다.

다음은 예시다. assert 문의 첫 번째 매개변수는 검사 조건문이 된다. 이 조건문이 true가 되어야만 통과할 수 있다.

func functionWithAssert(age: Int?) {
  assert(age != nil, "nil age");
  assert((age! >= 0) && (age! <= 130), "잘못된 입력");
}

functionWithAssert(age: 30);	// 통과
functionWithAssert(age: -1);	// 잘못된 입력. 프로그램 중단

functionWithAssert 함수를 무사히 통과하면, 매개변수로 주어진 age는 nil이 아니고 0 이상 130 이하의 값임을 보장할 수 있게 된다.

 

다음은 guard이다. guard도 assert와 비슷하게 결과값을 확인하고 처리할 수 있도록 하는 구문이지만, assert와 달리 디버깅 모드가 아닐 때도 사용할 수 있다. assert 문을 통과하지 못하면 프로그램이 중단되는 것과 달리, 잘못된 값으로 인해 guard 문을 통과하지 못하면 그 실행구문을 빠르게 종료(Early exit)한다. guard의 else 블록 내부에 반드시 return이나 break 등 종료 키워드가 있어야 한다.

예시를 보자. 위에서 만든 functionWithAssert를 guard를 써서 만들어볼 것이다. guard 다음 조건문을 쓰고, 조건문의 결과가 false가 나오면 guard에 묶인 else 블록이 실행된다. 이 else 블록에서는 반드시 탈출을 해줘야 한다. 조건문의 결과가 true라면 else 블록을 실행하지 않고 그냥 지나간다.

func functionWithGuard(_ age: Int?) {
  guard let uage = age, uage < 130, uage >= 0 else {
    print("잘못된 입력");
    return
  }
  print("당신의 나이는 \(uage)세입니다");
}

functionWithGuard(30);	// 당신의 나이는 30세입니다
functionWithGuard(-12);	// 잘못된 입력
functionWithGuard(153);	// 잘못된 입력

var cnt = 1
while true {
  guard cnt < 3 else { break }
  print(cnt);
  cnt += 1
}

// 1 2

assert의 경우, assert에 건 조건을 통과하지 못하면 프로그램이 중단된다. 하지만 guard는 조건문을 통과하지 못하더라도 진행은 된다. 다만 유효값일 때 실행될 문장들이 실행되지 못하고 그냥 해당 함수 혹은 블록이 종료된다는 특징이 있다. 

 

글이 꽤 길어진다. 자바스크립트와 비슷한 점도 있고 아닌 것도 있다. 특히 assert나 guard, 옵셔널 체이닝 같은 건 좀 낯설다.

다음에는 프로토콜, 익스텐션, 에러 처리 정도만 다루고 이제 본격적으로 앱 프로그래밍을 다뤄보겠다. 두근두근ㅋㅋㅋ

 

 

 

References

 

 

728x90
반응형
0 Comments
댓글쓰기 폼