아요 개발 일기

[Swift] Property 본문

Swift/Grammar

[Swift] Property

소진이 2023. 1. 27. 18:39

안녕하세요! 오늘은 Property에 대해 공부해보겠습니다!!

이 글은 꼼꼼한 재은씨 Swift: 문법편을 참고하여 작성하였습니다.

 


Property

값을 저장하기 위한 목적으로 클래스와 구조체 내에서 정의된 변수나 상수를 말합니다.

간단히 말하자면 값을 제공하는 역할을 하며, 프로퍼티 중 일부는 값을 저장하지는 않지만 값을 제공하는 특성도 가지고 있습니다.

 

어떤 프로퍼티들이 있는지 먼저 간단히 볼까요??!


종류

저장 프로퍼티

  • 입력된 값을 저장하거나 저장된 값을 제공하는 역할
  • 상수 및 변수를 사용해서 정의 가능
  • 클래스와 구조체에서는 사용이 가능하지만, 열거형에서는 사용할 수 없음

 

연산 프로퍼티

  • 특정 연산을 통해 값을 만들어 제공하는 역할
  • 변수만 사용해서 정의 가능
  • 클래스, 구조체, 열거형 모두에서 사용 가능

 

인스턴스 프로퍼티

  •  인스턴스에 소속되는 프로퍼티

타입 프로퍼티

  • 클래스와 구조체 자체에 소속되어 값을 가짐

 

 

역할에 따른 분류

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

소속에 따른 분류

  • 인스턴스 프로퍼티 (Instance Property)
  • 타입 프로퍼티 (Type Property)

정의 위치

클래스 정의 구문 내부에 작성해야하며, 메소드 내부에 작성되면 안 됩니다.

—> 메소드 내에서도 변수나 상수를 사용하여 값을 저장할 수 있는데, 이것은 프로퍼티가 아니라 지역 변수에 불과

즉, 클래스의 내부에, 메소드의 외부에 정의해야함.

 

 


 

프로퍼티 옵저버 (Property Observer)

: 프로퍼티 값을 모니터링하기 위해 사용합니다.

  • 사용자가 정의한 특정 액션과 반응하도록 처리할 수 있습니다.
  • 우리가 직접 정의한 저장 프로퍼티에 추가할 수 있으며, 슈퍼 클래스로부터 상속 받은 서브 클래스에서도 추가할 수 있습니다.

 


 

저장 프로퍼티 (Stored Property)

  • 속성 변수
  • 속성 상수

 

초기값 할당

반드시 선언하는 시점에서 초기값을 할당해야 하는 것은 아닙니다. (초기화 구문에서 초기 값 설정 O)

 

클래스에서 프로퍼티 할당 

 

방법 1.  초기화 구문을 작성하고, 그 안에서 초기 값을 할당해 줍니다.

class User {
	var name: String

	init() {
		self.name = ""
	}
}

 

init 메소드는 일반적인 메소드와 달리 직접 호출되기보다는 인스턴스가 생성될 때 간접적으로 호출되는 경우가 대부분입니다.

init 메소드 내부에 작성된 구문은 인스턴스가 생성될 때 실행됩니다.

 

 

방법 2. 프로퍼티를 옵셔널 타입으로 바꿔 줍니다.

class User {
	var name: String?
}
(또는)
class User {
	var name: String!
}

 

옵셔널 타입으로 선언할 경우, 초기화 하지 않았더라도 시스템이 자동으로 초기화해주므로 문제가 생기지 않습니다.

 

 

방법 3. 프로퍼티에 초기 값을 할당해 줍니다.

class User { 
	var name: String = "" 
}

빈 초기 값을 입력해주면 많은 문제로부터 벗어날 수 있습니다.

 


 

분류

  • var 키워드로 정의되는 변수형 저장 프로퍼티 = 멤버 변수
    • 수정 가능
  • let 키워드로 정의되는 상수형 저장 프로퍼티 = 멤버 상수
    • 수정 불가능

 

// 고정 길이 범위 구조체
struct FixedLengthRange {
    var startValue: Int // 시작 값
    let length: Int // 값의 범위
}


var rangeOfFiexedIntegers = FixedLengthRange(startValue: 0, length: 5) // 0,1,2,3,4
rangeOfFiexedIntegers.startValue = 3 // 3,4,5,6,7


// 가변 길이 범위 구조체
struct FiexibleLengthRange {
    let startVlaue: Int
    var length: Int
}

var rangeOfFlexibleLengthRanges = FiexibleLengthRange(startVlaue: 0, length: 4) // 0,1,2,3
rangeOfFlexibleLengthRanges.length = 6 //0,1,2,3,4,5


// 변수 할당
var variablesOfInstance = FixedLengthRange(startValue: 3, length: 4)
variablesOfInstance.startValue = 0 // (o)

// 상수 할당
let constantsOfInstance = FixedLengthRange(startValue: 3, length: 4)
constantsOfInstance.startValue = 0 // (x)

 

반면 클래스는 인스턴스를 상수에 할당하더라도 클래스 내에서 변수로 선언한 저장 프로퍼티는 얼마든지 값을 수정할 수 있다

 

이유는?

구조체는 값에 의한 전달 방식

클래스는 참조에 의한 전달 방식 이기때문입니다.

따라서 구조체는 저장 프로퍼티의 값이 바뀌면 상수에 할당된 인스턴스 전체가 변경되고,

클래스는 저장 프로퍼티의 값이 바뀌더라도 상수에 할당된 인스턴스의 레퍼런스는 변경되지 않습니다.

 

struct ExmStruct {
    var startnum: Int
    let endnum: Int
}

let constantsOfStruct = ExmStruct(startnum: 2, endnum: 24)
constantsOfStruct.num = 4 
//Error! 구조체 인스턴스를 상수에 할당하면 내부에 저장 프로퍼티가 변수로 선언되어있어도 변경 불가능

class ExmClass {
    var startnum = 0
    let endnum = 0
}
let constantsOfclass = ExmClass()
constantsOfclass.startnum = 5 
// 클래스 인스턴스를 상수에 할당해도 내부에 저장 프로퍼티가 변수로 선언되어있으면 변경 가능

 

 


 

지연 저장 프로퍼티 (lazy)

일반적으로 저장 프로퍼티는 클래스 인스턴스가 처음 생성될 때 함께 초기화되지만, 저장 프로퍼티 정의 앞에 lazy라는 키워드가 붙으면 예외입니다.

즉, 저장 프로퍼티의 초기화를 지연시킵니다.

클래스 인스턴스가 생성되어 모든 저장 프로퍼티가 만들어지더라도 lazy 키워드가 붙은 프로퍼티는 선언만 될 뿐 초기화되지 않고 계속 대기하고 있다가 프로퍼티가 호출되는 순간에 초기화됩니다.

이처럼 호출되기 전에는 선언만 된 상태로 대기하다가 실제로 호출되는 시점에서 초기화가 이루어지는 저장 프로퍼티를 지연 저장 프로퍼티라고 합니다.

 

class OnCreate {
    init() {
        print("OnCreate!!")
    }
}

class LazyTest {
    var base = 0
    lazy var late = OnCreate()
    
    init() {
        print("Lazy Test")
    }
}

let lz = LazyTest() //LazyTest
lz.late // OnCreate

지연 저장 프로퍼티에 대입된 인스턴스는 프로퍼티가 처음 호출되는 시점에서 생성됩니다.

만약, 호출되지 않는다면 끝까지 인스턴스를 만들지 않습니다.

지연 프로퍼티는 처음으로 호출이 발생할 때 값을 평가하여 초기화되며, 이후 두 번째 호출부터는 처음 초기화된 값을 그대로 사용할 뿐 다시 초기화하지 않습니다.


 

클로저를 이용한 저장 프로퍼티 초기화

저장 프로퍼티 중의 일부는 연산이나 로직 처리를 통해 얻어진 값을 이용하여 초기화해야 하는 경우가 있습니다.

스위프트에서는 이와 같은 경우 클로저를 사용하여 필요한 로직을 실행한 다음, 반환되는 값을 이용하여 저장 프로퍼티를 초기화할 수 있도록 지원합니다.

이렇게 정의된 프로퍼티는 로직을 통해 값을 구한다는 점에서 나중에 배울 연산 프로퍼티와 유사하지만 참조될 때마다 매번 값이 재 평가되는 연산 프로퍼티와 달리 최초 한 번만 값이 평가된다는 차이를 가집니다.

 

 

구문형식

let/var 프로퍼티명: 타입 = {
	코드 내용
	return 반환값
}()

 

클로저 구문은 클래스나 구조체의 인스턴스가 생성될 때 함께 실행되어 초기 값을 반환하고, 이후로는 해당 인스턴스 내에서 재실행되지 안습니다.

 

class PropertyInit {
    
    // 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
    var value01: String! = {
        print("value01 execute")
        return "value01"
    }()
    
    // 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
    let value02: String! = {
        print("value02 execute")
        return "value02"
    }()
}

let cs = PropertyInit()
// value01 execute
// value02 execute

// 실행 결과 없음
cs.value01
cs.value02

 

만약, 클로저 구문을 실행하여 결과 값을 저장 프로퍼티에 대입하고 싶지만,

처음부터 클로저를 실행하는 저장 프로퍼티의 특성이 부담스러울 경우에는 어떻게하면 좋을까요?

 

이때는 lazy 구문을 사용하면 됩니다.

lazy 구문은 기본적으로 저장 프로퍼티에 사용되는 구문입니다. 하지만 값이 처음부터 초기화되는 다른 저장 프로퍼티와는 달리 실제로 참조되는 시점에서 초기화된다는 차이점을 가지고 있죠.

즉, 클로저를 통해 초기화하도록 구성하면 클래스 인스턴스가 생성될 때 무조건 실행되는 것이 아니라 실제로 값을 참조하는 시점에 실행되고, 처음 한 번만 실행된 후에는 다시 값을 평가하지 않는 특성을 지닌 저장 프로퍼티를 정의할 수 있습니다.

위의 코드에서 value03 클로저를 추가해보겠습니다.

 

	lazy var value03: String! = {
			print("value03 execute")
			return"value03"
	}()
}

let cs1 = PropertyInit()
// value01 execute
// value02 execute

s1.value03
// value03 execute

 

이처럼, 인스턴스 생성과 동시에 실행되어 로그 메시지가 출력되는 value01,value02의 클로저와는 달리 value03의 클로저는 참조를 해야 메시지가 출력됩니다.

lazy 키워드를 붙여서 정의한 저장 프로퍼티를 클로저 구문으로 초기화하면 최초 한 번만 로직이 실행되는데다 실제로 참조되는 시점에 맞추어 초기화되기 때문에 메모리 낭비를 줄일 수 있어 여러 용도로 활용됩니다.

특히, 네트워크 소켓 관련 개발을 할 때에 서버와의 소켓 통신 채널을 최초 한 번만 연결해 둔 다음 이를 재사용하여 통신하는 경우가 대부분이기때문에 lazy 프로퍼티를 클로저로 초기화하여 연결 객체를 저장하는 이 같은 방식이 매우 효율적입니다.

 


 

연산 프로퍼티(computed property)

실제 값을 저장했다가 반환하지는 않고 대신 다른 프로퍼티의 값을 연산 처리하여 간접적으로 값을 제공합니다.

연산 프로퍼티는 항상 클래스나 구조체, 또는 열거형 내부에서만 사용할 수 있습니다.

get

  • 프로퍼티의 값을 참조하기 위해 내부적으로 사용하는 구문
  • return 키워드를 사용하여 값을 반환하는데, 여기서 반환되는 값이 프로퍼티가 제공하는 값이 됩니다.
  • 생략 불가능

set

  • 연산 프로퍼티에 값을 할당하거나 변경하고자 할 때 실행되는 구문
  • 생략 가능하지만 생략하게 되면
    1. 외부에서 연산 프로퍼티에 값을 할당할 수 없음
    2. 내부적인 연산 처리를 통해 값을 제공받는 읽기 전용 프로퍼티가 만들어짐

 

 

연산 프로퍼티는 언제 사용할까?

  1. 다른 프로퍼티에 의존적일 때
  2. 특정 연산을 통해 얻을 수 있는 값을 정의할 때

 

struct Rect {
    var originX: Double = 0.0, originY: Double = 0.0
    var sizeWidth: Double = 0.0, sizeHeight: Double = 0.0
    
    var centerX: Double {
        get {
            return self.originX + (sizeWidth / 2)
        }
        set(newCenterX) {
            originX = newCenterX - (sizeWidth / 2)
        }
    }
    
    var centerY: Double {
        get {
            return self.originY + (self.sizeHeight / 2)
        }
        set(newCenterY) {
            self.originY = newCenterY - (self.sizeHeight / 2)
        }
    }
}

var square = Rect(originX: 0.0, originY: 0.0, sizeWidth: 10.0, sizeHeight: 10.0)
print("square.centerX = \(square.centerX), square.centerY = \(square.centerY)")
//square.centerX = 5.0, square.centerY = 5.0

 

구조체를 객체 지향 구조로 만들기

 

struct Position {
    var x: Double = 0.0
    var y: Double = 0.0
}

struct Size {
    var width: Double = 0.0
    var heigh: Double = 0.0
}

struct Rect {
    var origin = Position()
    var size = Size()
    
    var center: Position {
        get {
            let centerX = self.origin.x + (self.size.width / 2)
            let centerY = self.origin.y + (self.size.heigh / 2)
            return Position(x: centerX, y: centerY)
        }
        
        set(newCenter) {
            self.origin.x = newCenter.x - (size.width / 2)
            self.origin.y = newCenter.y - (size.heigh / 2)
        }
    }
}


let p = Position(x: 0.0, y: 0.0)
let s = Size(width: 10.0, heigh: 10.0)

var square1 = Rect(origin: p, size: s)
print("square.center = \(square1.center.x), square.centerY = \(square1.center.y)")
//square.centerX = 5.0, square.centerY = 5.0

let s2 = Size(width: 12, heigh: 12)

let p2 = Position(x: 2.5, y: 2.5)
var square2 = Rect(origin: p2, size: s)
print("square.center = \(square2.center.x), square.centerY = \(square2.center.y)")
//square.centerX = 7.5, square.centerY = 7.5

let p3 = Position(x: 5, y: 5)
var square3 = Rect(origin: p3, size: s)
print("square.center = \(square3.center.x), square.centerY = \(square3.center.y)")
//square.center = 10.0, square.centerY = 10.0

 

 

set 구문을 제거

read-only 프로퍼티, get-only 프로퍼티, 읽기 전용 프로퍼티라고 합니다.

var center: Position {
    get {
        let centerX = self.origin.x + (self.size.width / 2)
        let centerY = self.origin.y + (self.size.width / 2)
        return Position(x: centerX, y: centerY)
    }
}

// 읽기 전용이면 get 생략 가능
var center: Position {
    let centerX = self.origin.x + (self.size.width / 2)
    let centerY = self.origin.y + (self.size.width / 2)
    return Position(x: centerX, y: centerY)

}

 

mutating 키워드

일반적으로 구조체는 메소드 내에서 프로퍼티를 수정할 수 없는 제약이 있는데,

이 제약을 풀고 메소드 내에서 멤버 변수를 수정할 수 있도록 합니다.

 


 

프로퍼티 옵저버(Property Observer)

특정 프로퍼티를 계속 관찰하고 있다가 프로퍼티의 값이 변경되면 이를 알아차리고 반응합니다.

프로퍼티 옵저버는 프로퍼티의 값을 직접 변경하거나 시스템에 의해 자동으로 변경하는 경우에 상관없이 일단 프로퍼티의 값이 설정되면 무조건 호출됩니다. 심지어 프로퍼티에 현재와 동일한 값이 재할당되더라도 어김없이 호출됩니다.

 

호출되는 경우

  1. 저장 프로퍼티에 값을 대입하는 구문이 수행
  2. 연상 프로퍼티에서 set 구문이 실행되는 모든 경우

 

종류

  • willSet : 프로퍼티의 값이 변경되기 직전에 호출되는 옵저버
  • didSet: 프로퍼티의 값이 변경된 직후에 호출되는 옵저버

WillSet

  • willSet 옵저버를 구현해 둔 프로퍼티에 값을 대입하면 그 값이 프로퍼티에 대입되기 직전에 wilSet 옵저버가 실행됩니다.
  • 프로퍼티 값 = 옵저버의 실행 블록에 매개 상수
  • 매개 상수 이름을 생략 가능하며, 그때는 기본 상수명인 newValue라는 이름으로 전달됩니다.
  • 전달된 값은 참조 O But 수정 X —> 상수 형태로 전달하는 값이기 때문에

 

정의 구문

[ ] 대괄호에 둘러싸여서 표시되는 부분은 생략 가능

var <프로퍼티명> : <타입> [ = <초기값>] {
	wilSet [ (<인자명>) ] {
		<프로퍼티 값이 변경되기 전에 실행할 내용>
	}
}

 

didSet

값이 할당된 직후에 호출되는데, 새로 할당된 값이 아닌 기존에 저장되어 있던 값이 매개 변수 형태로 전달합니다.

이 값의 이름을 부여할 수 있지만, 생략하더라고 oldValue라는 이름으로 자동 전달되기 때문에 값을 사용하는 데에 아무런 문제가 없습니다.

 

var <프로퍼티명> : <타입> [ = <초기값>] {
	didSet [ (<인자명>) ] {
		<프로퍼티 값이 변경되기 전에 실행할 내용>
	}
}

 

didSet 구문에서 새로 할당된 값이 필요할 때에는 어떻게 하나요?

새로 할당된 값이 필요할 경우에는 프로퍼티 자체를 그냥 참조하면 됩니다.

새로운 값은 이미 프로퍼티에 저장되어 있는 상태이기 때문입니다.

 

struct Job {
    var income: Int = 0 {
        willSet(newIncome) {
            print("이번 달 월급은 \(newIncome)원 입니다.")
        }
        didSet {
            if income > oldValue {
                print(" 월급이 \(income - oldValue)원 증가하셨네요. 소득세가 상항조정될 예정입니다.")
            } else {
                print("저런, 월급이 삭감되었군요. 그래도 소득세는 깍아드릴 수 없어요ㅜㅜ")
            }
        }
    }
}

var job = Job(income: 1000000)
job.income = 2000000
// 이번 달 월급은 2000000원 입니다.
//월급이 1000000원 증가하셨네요. 소득세가 상항조정될 예정입니다.

job.income = 1500000
//이번 달 월급은 1500000원 입니다.
//저런, 월급이 삭감되었군요. 그래도 소득세는 깍아드릴 수 없어요ㅜㅜ

 

프로퍼티 옵저버를 언제 사용할까요?

  1. 값의 변화를 주시하고 있어야 할 때,
  2. 값의 변화에 따른 처리가 필요할 때

 


 

타입 프로퍼티 (Type Property)

  • 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하는 프로퍼티를 말합니다.
  • 클래스나 구조체의 인스턴스에 속하는 값이 아니라 클래스나 구조체 자체에 속하는 값이므로 인스턴스를 생성하지 않고 클래스나 구조체 자체에 저장하게 되며, 저장된 값은 모든 인스턴스가 공통으로 사용합니다.
  • 인스턴스가 아무리 많더라도 모든 인스턴스가 하나의 값을 공용으로 사용하기때문에, 하나의 인스턴스에서 타입 프로퍼티의 값을 변경하면 나머지 인스턴스들이 일괄적으로 변경된 값을 적용받습니다.

 

언제 사용할까요?

  1. 특정 클래스나 구조체, 그리고 연거형에서 모든 인스턴스들이 공유해야 하는 값을 정의할 때 유용

 

접근 범위는?

선언된 객체 내에서만 접근 가능한 범위를 가짐

 

선언

static 키워드 추가

이때!!! class에서 타입 프로퍼티를 선언하면 상속받은 하위 클래스에서 재정의(Override)할 수 있는 타입 프로퍼티가 됩니다.

 

static let/var 프로퍼티명 = 초기값

// 하위 클래스에서 재정의 할 수 있는 타입프로퍼티 정의 방법 
class 클래스 명 {
	class let/var 프로퍼티명 : 타입 {
		실행 코드
	}
}

 

 

struct Foo {
    // 타입 저장 프로퍼티
    static var sFoo = "구조체 타입 프로퍼티 값"
    
    // 타입 연산 프로퍼티
    static var cFoo: Int {
        return 1
    }
}

class Boo {
    // 타입 저장 프로퍼티
    static var sFoo = "클래스 타입 프로퍼티 값"
    
    //타입 연산 프로퍼티
    static var cFoo: Int {
        return 10
    }
    
    // 재정의가 가능한 타입 연산 프로퍼티
    class var oFoo: Int {
        return 100
    }
}

print(Foo.sFoo)
//"구조체 타입 프로퍼티값"

Foo.sFoo = "새로운 값"
print(Foo.sFoo)
// 새로운 값

print(Boo.sFoo)
// "클래스 타입 프로퍼티 값"

print(Boo.cFoo)
//10

 

주의할 점

  • 반드시 클래스나 구조체, 또는 열거형 자체와 함께 사용해야 함

'Swift > Grammar' 카테고리의 다른 글

[Swift] Protocol  (0) 2023.01.27
[Swift] Type Casting - 타입 캐스팅  (0) 2023.01.27
[Swift] Function (함수)  (0) 2023.01.16
[Swift] Static(정적) & Dynamic(동적)  (0) 2023.01.16
[Swift] Closures (클로저)  (0) 2022.12.30