[ETC_010] 프로퍼티와 메서드 (1)
👨🏻‍💻iOS 공부/Swift 기본기 다지기

[ETC_010] 프로퍼티와 메서드 (1)

728x90
반응형

프로퍼티와 메서드 (1)

프로퍼티와 메서드는 무엇일까?

                                        ( 집의 정보를 나타내는 프로퍼티 )

 

간단하게 먼저 알아보면 프로퍼티란 구조체(struct), 클래스(class), 열거형(enum) 등에 관련된 값을 말하고, 메서드는 특정 타입에 관련된 함수를 뜻한다.

프로퍼티는 크게 3가지로 나누어 볼 수 있다.

 

  • 저장 프로퍼티 (Stored Property)
  • 연산 프로퍼티 (Computed Property)
  • 타입 프로퍼티 (Type Property)
 
 

저장 프로퍼티

저장 프로퍼티란 인스턴스의 변수(var) 혹은 상수(let)을 의미한다. 이는 클래스 혹은 구조체의 인스턴스와 연관된 값을 저장하는 가장 단순한 개념의 프로퍼티이다. 변수를 사용하면 변수 저장 프로퍼티, 상수를 사용하면 상수 저장 프로퍼티라고 부른다!

저장 프로퍼티를 정의할 때는 “프로퍼티 기본값”과 “초깃값”을 설정해줄 수 있다.
아래의 기본적인 저장 프로퍼티의 선언과 인스턴스 초기화 방법을 보자!

import UIKit

// 이미지 크기
struct imgSize {
    var width: Float // 저장 프로퍼티(변수)
    var height: Float // 저장 프로퍼티(변수)
}

// struct에는 기본적으로 저장 프로퍼티를 매개변수로 갖는 이니셜라이저가 있다.
// 그렇기에 (width: ~, height: ~) 처럼 사용 가능하다.

let myImgSize: imgSize = imgSize(width: 120.3, height: 170.2)


// 이미지 정보
class imgInfo {
    // var(변수)로 저장하여 size 값이 변경될 수 있음을 나타내준다.
    var size: imgSize

    let imgType: String // 저장 프로퍼티(상수)

    // 프로퍼티의 기본값을 저장해주지 않는다면, 이니셜라이저를 따로 정의해주어야 한다.

    init(firstSize: imgSize, imgType: String) {
        self.imgType = imgType
        self.size = firstSize
    }
}



// 초깃값을 할당을 못해 인스턴스 생성을 하지 못하는 상황을 방지하기 위해, 사용자 정의 이니셜라이저를 호출한다.
let pictureSize: imgInfo = imgInfo(firstSize: myImgSize, imgType: "B")

 

struct의 경우 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지만, 클래스의 경우는 그렇지 않다. 하지만 클래스의 경우도 저장 프로퍼티에 초깃값을 지정해주면 따로 사용자 이니셜라이저를 구현해줄 필요가 없다. 아래의 경우를 보자

struct imgSize {
    var width: Float = 0.0 // 저장 프로퍼티(변수)
    var height: Float = 0.0 // 저장 프로퍼티(변수)

}



// 프로퍼티 초깃값을 할당했기에, 굳이 전달인자로 초깃값을 넘기지 않아도 된다.

// 조금 더 깔끔해보이는 것을 확인할 수 있다.

// 물론 초깃값을 할당했더라도, 기존에 초깃값을 할당할 수 있는 이니셜라이저를 사용할 수 있다.
let albumSize: imgSize = imgSize()


class imgInfo {
    var size: imgSize = imgSize() // 저장 프로퍼티
    var imgType: String = "png" // 저장 프로퍼티
}


// 위 처럼 초깃값을 지정해주었다면 사용자 정의 이니셜라이저를 사용하지 않아도 된다.

let picSize: imgInfo = imgInfo()


picSize.size = albumSize

picSize.imgType = "png"

 

초깃값을 지정했더니 더 깔끔하고 간단하게 인스턴스를 만든 모습을 볼 수 있다. 다만 값을 할당하기 위해 imgInfo의 imgType을 var로 정의한 것을 볼 수 있다. 만약 해당 imgType의 값을 변경하지 못하도록 상수로 정의해주고자 할 경우에는 위처럼 구현하면 제 기능을 하지 못한다. 

 

현재까지는 프로퍼티가 옵셔널이 아닌 값으로 선언되어 있어, 인스턴스 생성시 이니셜라이저를 통해 초깃값을 보내주고 있다. 하지만 저장 프로퍼티의 값이 필요 없을 경우가 있는데, 이 때는 굳이 초깃값을 넣어주지 않아도 된다. 

아래의 경우가 옵셔널과 사용자 정의 이니셜라이저를 잘 섞어서 의도에 맞게 구조체와 클래스를 생성한 경우이다. 

struct imgSize {
    var width: Float // 저장 프로퍼티(변수)
    var height: Float // 저장 프로퍼티(변수)
}



class imgInfo {
    var size: imgSize?
    let imgType: String

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



// 이미지 타입은 정의가 필요하지만, size는 당장 모를 수 있다.
let dogImgSize: imgInfo = imgInfo(imgType: "jpg")



// 만약 size를 알게되면 그 때 값을 할당해줘도 된다.
dogImgSize.size = imgSize(width: 100.2, height: 110.2)

 

이전의 방법보다 약간의 가이드라인(?)이 있는 방식으로 작성된 것을 볼 수 있다. 

size는 값을 모르니 입력을 스킵해도 되고, imgType은 초깃값을 지정해두도록 하는 방식으로 유도할 수 있다!

 

지연 저장 프로퍼티

앞서 본 것 처럼 인스턴스 생성 시, 프로퍼티에 값이 필요 없다면 프로퍼티를 옵셔널로 선언해줄 수 있다. 하지만 이와는 다르게 다른 용도로써 필요할 때 값이 할당되는 “지연 저장 프로퍼티”가 있다. 기본적으로 지연 저장 프로퍼티는 호출이 있어야 값을 초기화하며, 이 때 lazy 키워드를 사용한다.

상수(let)은 값의 변경이 불가능하기에 인스턴스가 생성되기 전에 초기화해야 하기 때문에 지연 저장 프로퍼티와는 거리가 멀다. 이에 따라 지연 저장 프로퍼티는 var 키워드를 사용하여 변수로 정의한다.

보통 지연 프로퍼티는 복잡한 클래스나 구조체를 구현할 때 많이 사용된다. 위의 경우 처럼 클래스의 인스턴스 내에 프로퍼티로 다른 클래스 인스턴스나 구조체 인스턴스를 할당해야 하는 경우가 있다. 또한 이 때에 인스턴스를 초기화 함과 동시에 저장 프로퍼티로 사용되는 인스턴스들이 생성되어야 한다면? 이럴 경우에 사용하는 것이 지연 저장 프로퍼티이다.

코드를 보면서 이해해보자. 

"인스턴스를 초기화 함과 동시에 저장 프로퍼티로 사용되는 인스턴스들이 생성되어야 한다" 라는 말만 보면 어...? 뭔가 싶다. 

let dogImgSize: imgInfo = imgInfo(imgType: "jpg")

위 처럼 인스턴스(dogImgSize)를 초기화하는데, 동시에 저장 프로퍼티(imgSize)를 생성하기를 원하는 것이다. 

지연 저장 프로퍼티는 잘 사용하면 성능저하나 공간낭비를 줄일 수 있으니 잘 살펴보자. 

struct imgSize {
    var width: Float = 0.0
    var height: Float = 0.0
}



class imgInfo {
    lazy var size: imgSize = imgSize()
    let imgType: String

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



let dogImgSize: imgInfo = imgInfo(imgType: "jpg")

// 이 코드를 토앻 imgInfo의 size 프로퍼티에 처음 접근할 때

// 자동적으로 size 프로퍼티의 imgSize가 생성된다!
print(dogImgSize.size) // imgSize(width: 0.0, height: 0.0)

 

인스턴스를 초기화하고, 프로퍼티에 접근하고자 할 때 imgSize가 뒤늦게 생성되는 것이다!

 

연산 프로퍼티

연산 프로퍼티는 실제 값을 저장하는 프로퍼티가 아니라, 특정 상태에 따른 값을 연산하는 프로퍼티이다.
인스턴스 내/외부의 값을 연산하여 값을 돌려주는 접근자(getter)의 역할이나, 은닉화된 내부의 프로퍼티 값을 간접적으로 설정하는 설정자(setter)의 역할을 할 수도 있다. 연산 프로퍼티는 클래스,구조체,열거형(값 타입이기에)에 정의될 수 있다.

생각해보면 메서드의 역할과 매우 유사한데 왜 연산 프로퍼티를 쓰는 것일까라는 의문이 들 것 이다. 

결론 먼저 말하자면, 연산 프로퍼티를 사용하는 것이 메서드 형식보다 더 훨씬 간편하고 직관적이기 때문이다. 

코드로 알아보기 전에, 접근자는 무엇이고.... 설정자는 또 무엇인가... Python class 생성시에 사용했었는데 Swift에서도 동일할까..?

 

당연히 동일하다! 

 

getter(접근자)의경우 실제적으로 연산을 진행하여 값을 반환해주는 부분이고, setter(설정자)의 경우 값을 설정하는 역할을 해준다. 

말로 이해하는 것 보다는 코드를 보는게 나을 것 같으니 아래 코드를 봐보자!

 

일단 메서드로 구현된 접근자와 설정자를 보자. 

struct imgSize {

    // 저장 프로퍼티
    var width: Float
    var height: Float

    // 이미지를 두 배로 크게 해주는 메서드를 구현해보자(접근자)
    // Self는 자기 자신을 의미한다.
    func twiceImgSize() -> Self {
        return imgSize(width: width * 2, height: height * 2)
    }


    // 늘리고자 하는 이미지의 크기를 설정한다(설정자)
    // ** muatating : 자신의 프로퍼티 값을 수정할 때 클래스의 인스턴스 메서드는 참조 타입이기에 신경쓰지 않아도 되나
    // 구조체나 열겨형의 경우는 값 타입이기에 앞에 mutating 키워드를 붙여 인스턴스 내부의 값을 변경한다는 것을 명시해야 한다!!
    // 한 마디로 값을 바꾸겠다는 것을 의미!
    mutating func setTwiceImgSize(_ twice: imgSize) {
        width = twice.width
        height = twice.height
    }
}

var dogImgSize: imgSize = imgSize(width: 100.5, height: 110.5)

// 현재 크기
print(dogImgSize) // imgSize(width: 100.5, height: 110.5)

// 두 배 크기
print(dogImgSize.twiceImgSize()) // imgSize(width: 201.0, height: 221.0)


// 현재 크기를 (200.5, 300.5)로 설정해보자.
dogImgSize.setTwiceImgSize(imgSize(width: 200.5, height: 300.5))


// 실행
print(dogImgSize.twiceImgSize()) // imgSize(width: 401.0, height: 601.0)

 

처음 인스턴스 생성시에 초기화를 해주고 접근자를 이용하여 두 배로 사이즈를 키웠다. 여기서 또 다시 크기를 재 설정하기 위해 설정자를 이용하였고 다시 접근자를 통해 두 배로 키운 것을 볼 수 있다!

twiceImgSize()를 통해 두 배로 키울 수 있으며, setTwiceImgSize( _ : ) 메서드로 값을 다시 설정해 줄 수 있었다. 보다시피 접근자와 설정자의 이름이 다르고, 해당 사이즈에 접근할 때와 설정할 때 사용되는 코드가 개별적이어서 읽기에 조금 불편함이 있다. 

이제 연산 프로퍼티를 활용해서 두 메서드를 간결하고 직관적으로 바꾸어 표현해보자. 

struct imgSize {
    var width: Float
    var height: Float

    // 두 배
    // ( : imgSize대신 Self 사용 가능)
    var twiceImgSize: imgSize { // 연산 프로퍼티!
        // 접근자
        get {
            return imgSize(width: width * 2 , height: height * 2)
        }
        
        // 설정자
        // newValue 자리에는 아무 값이나 입력해도 된다 ex) cha, min, food 등등
        // 다만 set(~)가 아닌 set으로만 정의하고자 할 경우 newValue만 인식하기에 newValue로만 작성해야한다!!
        set(anyName) {
            width = anyName.width * 2
            height = anyName.height * 2
        }
    }
}

var dogImgSize: imgSize = imgSize(width: 100.5, height: 110.5)

// 현 크기
print(dogImgSize) // imgSize(width: 100.5, height: 110.5)

// 두 배 크기
print(dogImgSize.twiceImgSize) // imgSize(width: 201.0, height: 221.0)

// 새로 값을 변경해주면?
dogImgSize.twiceImgSize = imgSize(width: 200.5, height: 210.5)


// 두 배 크기
print(dogImgSize) // imgSize(width: 401.0, height: 421.0)

이런 식으로 연산 프로퍼티를 사용하면 하나의 프로퍼티에 접근자와 설정자가 모여있게 되고, 각 프로퍼티가 어떤 역할을 하는지 좀 더 명확하게 표현이 가능하다!

또한 인스턴스를 사용하는 입장에서도 저장 프로퍼티를 쓰는 것 마냥 편하게 사용할 수 있다는 장점이 있다.

 

위에 말한 것 처럼 set 메서드 내부의 전달인자를 따로 사용하지 않을 수 있다. 다만 앞서 말한 것 처럼 newValue만이 매개변수 이름을 대신할 수 있다는 점! 기억하고 사용해야 한다.

set {
    width = newValue.width * 2
    height = newValue.height * 2
}

추가적으로 굳이 설정자를 통해 값을 설정할 필요가 없다면 get만 사용하여 읽기 전용으로 연산 프로퍼티를 사용할 수도 있다. 

이를 읽기 전용 연산 프로퍼티(Read-Only Computed Property)라고 명명한다. 

struct imgSize {
    var width: Float
    var height: Float


    // 두 배
    // ( : imgSize대신 Self 사용 가능)
    var twiceImgSize: imgSize { // 연산 프로퍼티!
        // 접근자
        get {
            return imgSize(width: width * 2 , height: height * 2)
        }        
    }
}

사용 방법은 위와 동일하다. 

다만 설정자를 구현하지 않았으므로 

dogImgSize.twiceImgSize = imgSize(width: 200.5, height: 210.5)

이처럼 값을 변경해줄 수는 없다는 점!! 기억하길 바란다. 

 

프로퍼티 감시자

프로퍼티 감시자(Property Observer)를 사용하면 프로퍼티 값이 변경됨에 따라 적절한 작업을 취할 수 있다. 즉 프로퍼티 값이 새로 할당될 때 마다 프로퍼티 감시자가 호출되는 것이다. 변경되는 값이 기존의 값과 동일하더라고 호출된다!

다만 지연 저장 프로퍼티에는 사용할 수 없으며, 일반적인 저장 프로퍼티에만 사용될 수 있다.

프로퍼티 감시자에는 프로퍼티의 값이 변경되기 직전에 호출하는 willSet 메서드와 프로퍼티의 값이 변경된 직후에 호출하는 didSet 메서드가 있다.

이 메서드들에는 매개변수가 하나씩 있는데, willSet 메서드에 전달되는 전달인자는 프로퍼티가 “변경될 값”이고 didSet 메서드에 전달되는 전달인자는 프로퍼티가 변경되기 이전의 값이다. 그래서 따로 매개변수의 이름을 정하지 않으면 willSet 메서드에는 newValue가, didSet 메서드에는oldValue 라는 매개변수가 자동 지정된다.

class originScore {

var score: Int = 0 {
    willSet {
        print("점수가 \(score)에서 \(newValue)으로 변경될 예정입니다.")
    }

    didSet {
        print("점수가 \(oldValue)에서 \(score)으로 변경되었습니다.")
    }
}


let mathScore: movingScore = movingScore()
// 점수가 0에서 20로 변경될 예정입니다.

mathScore.score = 20
// 점수가 0에서 20로 변경되었습니다.

이렇게 값의 변동을 변경전/후로 tracking 할 수 있다. 

조금 더 복잡한 경우인, 클래스를 상속받아 기존의 연산 프로퍼티를 재정의하여 프로퍼티 감시자를 구현한 사례를 봐보자. 

class originScore {

    var score: Int = 0 {
        willSet {
            print("점수가 \(score)에서 \(newValue)으로 변경될 예정입니다.")
        }

        didSet {
            print("점수가 \(oldValue)에서 \(score)으로 변경되었습니다.")
        }
    }

    // 시험점수 평균이 너무 낮아 전부 보너스 점수를 준다고 생각해보자.
    var upScore: Int { // 연산 프로퍼티
        get {
            return score + 10
        }

        set {
            score = newValue + 10
            print("기존 점수를 보정하여 \(newValue)으로 상향중입니다.")
        }
    }
}



class movingScore: originScore {
    override var upScore: Int {
        willSet {
            print("점수가 \(upScore)에서 \(newValue)으로 상향될 예정입니다.")
        }

        didSet {
            print("점수가 \(oldValue)에서 \(upScore)으로 상향되었습니다.")
        }
    }
}

let engScore: movingScore = movingScore()

// 점수가 0에서 20로 변경될 예정입니다.
engScore.score = 20

// 점수가 0에서 20로 변경되었습니다.


//점수가 30에서 50으로 상향될 예정입니다. (upScore : 30, newValue:50)

//점수가 20에서 60으로 변경될 예정입니다. (oldValue: 20, upScore:60) (=> upScore = newValue + 10)

//점수가 20에서 60으로 변경되었습니다. (oldValue: 20, score: 60)



engScore.upScore = 50 // 기존 점수를 보정하여 50으로 상향중입니다. (newVlaue: 50)

// 점수가 30에서 70으로 상향되었습니다. (oldValue: 30, upScore: 70)

// => oldValue = oldValue + 10

// => upScore = upScore + 10

 

조~금 복잡해보이긴 하나, 단계별로 점수를 추가하다보면 값이 어떻게 변하고 있는지, 변할 예정인지 파악할 수 있다.

상속되다보니 점수를 20점이나 올려주게되었다... 

 

타입 프로퍼티

마지막으로 타입 프로퍼티이다….! 이제껏 본 저장 프로퍼티, 지연 저장 프로퍼티, 연산 프로퍼티들은 모두 타입을 정의하고 해당 타입의 인스턴스가 생성되었을 때 사용할 수 있는 인스턴스 프로퍼티이다. 이에 반해, 각각의 인스턴스가 아닌 타입 자체에 속하는 프로퍼티를 타입 프로퍼티라고 부른다. 말 그대로 타입 자체에 영향을 미치는 프로퍼티이다. 인스턴스의 생성 여부와 상관없이 타입 프로퍼티의 값은 “하나”이며 그 타입의 모든 인스턴스가 공통으로 사용하는 값, 모든 인스턴스에서 공용으로 접근하고 값을 변경할 수 있는 변수 등을 정의할 때 유용하다.

타입 프로퍼티는 두 가지로 분류된다.

 

  • 저장 타입 프로퍼티 : 변수 or 상수로 선언 가능 / 반드시 초깃값 설정 필요! 지연 연산됨(lazy 사용X)
  • 연산 타임 프로퍼티 : 변수로만 선언 가능

 

이 또한 코드를 통해 이해해보자~!

class AClass {
    
    // 저장 타입 프로퍼티
    static var typeProperty: Int = 0

    // 저장 인스턴스 프로퍼티
    var instanceProperty: Int = 0 {
        didSet {
            // Self.typeProperty == AClass.typeProperty
            Self.typeProperty = instanceProperty * 100
        }
    }


    // 연산 타입 프로퍼티
    static var typeComputedProperty: Int {
        get {
            return typeProperty
        }
        set {
            typeProperty = newValue
        }
    }
}


AClass.typeProperty = 176


let classInstance: AClass = AClass()
classInstance.instanceProperty = 100



print(AClass.typeProperty) // 10000
print(AClass.typeComputedProperty) // 10000

자 마지막이다....! 

 

위 코드를 보면 타입 프로퍼티는 인스턴스를 생성하지 않고도 사용할 수 있으며 이는 타입에 해당하는 값이다. 이에 따라 인스턴스에 접근할 필요가 없이 타입 이름 만으로도 프로퍼티를 사용할 수 있는 것이다. 앞서 배웠던 것 처럼 class 내 프로퍼티 값을 classInstance.instanceProperty = 100 으로 선언하여 내부 프로퍼티 값이 변경하였으니 결과값 또한 이를 반영하여 도출된 것을 볼 수 있다. 

 

----------------------------------------

이로써 프로퍼티에 대해 간략하게 알아보았다.

간단하게 정리하고 마무리 해보자..!

 

  • 저장 프로퍼티 (Stored Property)
  : var & let 사용 가능
  • 지연 저장 프로퍼티 (Lazy Stored Property)
  : 값의 변동이 있기에 let이 아닌 var로 선언!
  • 연산 프로퍼티 (Computed Property)
  : 실제 값을 "저장"하는 프로퍼티가 아닌 특정한 연산 결과를 반환해준다!
    값 타입인 클래스,구조체,열거형에 사용 가능하다. -> 값 저장이 안되기에 저장 프로퍼티가 필요!
    var로 선언 해야한다!
    getter와 setter를 통해 값을 돌려주거나, 설정할 수 있다. (get만 구현 가능/set 전달인자 생략시 newValue 사용!)
  • 타입 프로퍼티 (Type Property)
  : 인스턴스가 아닌 각각의 타입에 속하는 프로퍼티!
 
 
 
끄읕.
728x90
반응형