본문 바로가기

iOS 앱 개발자 프로젝트/Swift 문법 정복하기

[Swift] Property Observer, Type Casting, Access Modifier

2024년 3월 13일 수요일

 

  • observing하여 특정 preperty의 값 변경 전후에 추가 동작을 수행할 수 있는 Property Observer와,
  • 변수나 객체의 타입을 다른 타입으로 변환하는 프로세스인 Type Casting,
  • 코드 요소에 대한 외부 접근을 제한하여 모듈 간의 접근성과 보안을 조절하는 Access Modifier 이해하기

#1. Property Observer (프로퍼티 옵저버)

#2. Type Casting (타입 캐스팅)

#3. Access Modifier (접근 제한자)


 

#1. Property Observer

 

  • 변수에 프로퍼티 옵저버를 정의하여 프로퍼티 값의 변경 사항을 모니터링하고, 미리 구현한 코드로 이에 대응
  • 다시 말해, 해당 프로퍼티를 관찰(observe)하면서 변경 사항이 발생할 때 실행
  • willSet과 didSet을 둘 다 작성했을 경우 willSet이 먼저 실행
  • 추가할 수있는 경우
    • 저장 프로퍼티(stored property)
    • 연산 프로퍼티(computed property) 

 

○  didSet 과 willSet

  • ☑️  didSet
    • didSet은 새 값이 저장된 직후에 호출
    • 이전 프로퍼티의 값이 oldValue 로 제공
    ☑️  willSet
    • willSet은 값이 저장되기 직전에 호출
    • 새로운 프로퍼티의 값이 newValue 로 제공됩니다.
var myProperty: Int = 20{
   didSet(oldVal){
      //myProperty의 값이 변경된 직후에 호출, oldVal은 변경 전 myProperty의 값
   }
   willSet(newVal){
      //myProperty의 값이 변경되기 직전에 호출, newVal은 변경 될 새로운 값
   }
}

var name: String = "Unknown" {
    willSet {
        print("현재 이름 = \(name), 바뀔 이름 = \(newValue)")
    }
    didSet {
        print("현재 이름 = \(name), 바뀌기 전 이름 = \(oldValue)")
    }
}
 
name = "Peter"
// willSet이 먼저 실행됨
// 현재 이름 = Unknown, 바뀔 이름 = Peter
// 현재 이름 = Peter, 바뀌기 전 이름 = Unknown
class UserAccount {
    var username: String
    var password: String
    var loginAttempts: Int = 0 {
        didSet {
            if loginAttempts >= 3 {
                print("로그인 시도가 3회 이상 실패하였습니다. 계정이 잠겼습니다.")
                lockAccount()
            }
        }
    }
    
    var isLocked: Bool = false {
        didSet {
            if isLocked {
                print("계정이 잠겼습니다.")
            } else {
                print("계정이 잠금 해제되었습니다.")
            }
        }
    }
    
    init(username: String, password: String) {
        self.username = username
        self.password = password
    }
    
    func login(with enteredPassword: String) {
        if enteredPassword == password {
            print("로그인 성공!")
            loginAttempts = 0 // 로그인 성공 시 로그인 시도 횟수 초기화
        } else {
            print("잘못된 비밀번호입니다.")
            loginAttempts += 1 // 로그인 실패 시 로그인 시도 횟수 증가
        }
    }
    
    private func lockAccount() {
        isLocked = true
    }
    
    func unlockAccount() {
        isLocked = false
    }
}

// 사용자 계정 생성
let user = UserAccount(username: "user123", password: "password123")

// 로그인 시도
user.login(with: "wrongpassword") 
// 출력:
// 잘못된 비밀번호입니다.

user.login(with: "wrongpassword") 
// 출력:
// 잘못된 비밀번호입니다.

user.login(with: "wrongpassword") 
// 출력:
// 잘못된 비밀번호입니다.
// 로그인 시도가 3회 이상 실패하였습니다. 계정이 잠겼습니다.
// 계정이 잠겼습니다.

// 계정 잠금 해제
user.unlockAccount() // 계정이 잠금 해제되었습니다.

 

#2. Type Casting

 

  • 변수나 객체의 타입을 다른 타입으로 변환하는 프로세스
  • ☑️   is
    • is 연산자는 타입을 체크하는 연산자로, 비교 결과를 bool 타입을 반환(타입 체킹)
let char: Character = "A"
 
print(char is Character)
// 출력값: true
print(char is String)   
// 출력값: false
 
let bool: Bool = true

print(bool is Bool)     
// 출력값: true
print(bool is Character)
// 출력값: false

    

 

      ☑️  as, as!, as?  ( type에 대해 복습하기

  • as
    • as 연산자는 컴파일 단계에서 캐스팅이 실행 → 타입 캐스팅이 성공할 경우에만 사용 가능
    • 캐스팅에 실패할 경우 에러가 발생
    • 캐스팅하려는 타입이 같은 타입 이거나 수퍼클래스 타입이라는 것을 알 때 as 연사자를 사용
  • as?
    • as? 연산자는 런타임에 캐스팅이 실행
    • 성공하면 옵셔널 타입의 인스턴스를 반환하고 실패하면 nil 을 반환
    • 실패할 가능성이 있으면 as?를 사용하는 것이 좋다.
  • as!
    • as! 연산자는 런타임에 특정 타입으로 강제 캐스팅
    • 강제 타입 캐스팅에 실패할 경우 런타임 에러가 발생
    • 캐스팅에 성공한 경우 인스턴스를 반환(옵셔널 x)
class Person {
    var id = 0
    var name = "name"
    var email = "hgk@gmail.com"
}

class Worker: Person {
    // id
    // name
    // email
    var salary = 300
}

class Programmer: Worker {
    // id
    // name
    // email
    // salary
    var lang = "Swift"
}


// 업캐스팅 - as
let person1 = Person()
let worker1 = Worker()
let programmer1 = Programmer()

let personList = [person1, worker1, programmer1] // 타입을 선언하지 않았지만 Person 타입으로 인식 -> 즉 업캐스팅이 되었음
personList[1].name
//personList[1].salary // Person 타입으로 보고 있기 때문에 salary에 접근하지 못함

let worker2 = Worker()
worker2.salary

let workerPerson = worker2 as Person
//workerPerson.salary // Person 타입으로 보고 있기 때문에 salary에 접근하지 못함


// 다운캐스팅 - as? / as!
// as?
let pro = programmer1 as? Programmer // 타입 변환이 될 수도 있고 안될 수도 있기 때문에 옵셔널을 리턴

if let person2 = programmer1 as? Programmer {
    person2.lang
}

if let person3 = worker1 as? Programmer {
    person3.lang
}

// as!
let pro2 = worker2 as! Programmer // Error : 타입 변환 실패시 오류

 


#3. AccessModifier

 

  • 다른 소스 파일이나 모듈의 코드에서 코드 일부에 대한 접근을 제한
  • [제약 적음open < public < internal < fileprivate < private  [제약 많음]
    • open : 모든 소스 파일에서 해당 level 접근 가능 + 모든 곳에서 서브클래싱 가능
    • public : 모든 소스 파일에서 해당 level 접근 가능 + 같은 모듈 내에서만 서브클래싱 가능
    • internal : 같은 모듈 내에서만 접근 가능
    • fileprivate : 같은 소스파일 내에서만 접근 가능
    • private : 클래스 내부에서만 접근 가능
  • 접근 제한자를 작성하지 않으면 internal로 판단 
  • 상위 요소보다 하위 요소가 더 높은 접근 수준을 가질 수 없다.
private struct Car {
	  public var model: String // 🚨 에러
}

 

  • 모듈과 소스파일
    • 모듈(module)
      • 배포할 코드의 묶음 단위
      • 하나의 프레임워크/ 라이브러리/ 어플리케이션이 모듈 단위가 될 수 있다.
      • import 키워드를 통해 불러올 수 있다.
    • 소스파일
      • 하나의 swift 소스 코드 파일을 의미
  • public, open
    • 둘 다 모듈 외부까지 접근할 수 있다.
    • open은 클래스와 클래스 맴버에서만 사용할 수 있고 다른 모듈에서 서브클래싱이 가능하지만 public은 그렇지 않다.
    • open으로 클래스를 개방 접근 수준으로 명시하는 것은 그 클래스를 다른 모듈에서도 수퍼클래스로 사용하겠다는 의미→ 해당 클래스를 설계하고 만들었다는 것을 의미(다른 모듈에서 상속을 허용함)
    • public은 주로 프레임워크에서 외부와 연결될 인터페이스를 구현하는데 많이 사용된다.
  • internal
    • 모든 요소에 암묵적으로 지정하는 디폴트 접근 제어자
    • 소스 파일이 속해있는 모듈 어디에든 접근할 수 있지만 외부 모듈에서는 접근할 수 없다.
  • fileprivate
    • 소스 파일 내부에서만 접근할 수 있다.
    • 서로 다른 클래스가 같은 하나의 소스 파일에 정의되어있고 fileprivate로 선언되어 있다면 두 클래스는 서로 접근할 수 있다.
  • private
    • 가장 제한적인 접근 제어자입니다.
    • fileprivate과 달리 같은 파일 안에 있어도 서로 다른 클래스이고 private로 선언되었다면 두 요소는 서로 접근할 수 없다.
  •  
// open 
open class Vehicle {
    open func startEngine() {
        print("Engine started")
    }
}

open class Car: Vehicle {
    open var carType: String = "Sedan"
}

// public 
public struct Point {
    public var x: Int
    public var y: Int

    public init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }

    public mutating func moveByX(_ deltaX: Int, y deltaY: Int) {
        self.x += deltaX
        self.y += deltaY
    }
}

// internal
internal class InternalClass {
    internal var internalProperty: Int = 10

    internal func doSomethingInternally() {
        print("Internal operation performed")
    }
}

internal let internalConstant = 20

// fileprivate
class OuterClass {
    fileprivate var outerVariable = 30

    fileprivate func outerFunction() {
        print("Outer function called")
    }

    fileprivate class InnerClass {
        fileprivate func innerFunction() {
            print("Inner function called")
        }
    }
}

// private
class MyClass {
    private var privateVariable = 40

    private func privateFunction() {
        print("Private function called")
    }
}