프로퍼티✍🏻

DEVJUN·2022년 6월 1일
0

Swift 문법

목록 보기
9/14
post-thumbnail

  프로퍼티는 앞서 구조체와 클래스 챕터에서 클래스나 구조체 내에서 저장된 변수나 상수라고 설명했다.

  프로퍼티는 값에 대한 저장 여부를 기준으로 저장 프로퍼티연산 프로퍼티로 나누어진다.

  저장 프로퍼티

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

  연산 프로퍼티

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

  저장 프로퍼티와 연산 프로퍼티는 클래스나 구조체를 바탕으로 만들어진 개별 인스턴스에 소속되어 값을 저장하거나 연산 처리하는 역할을 한다. 따라서 프로퍼티를 사용하려면 인스턴스가 필요하다. 이렇게 인스턴스에 소속되는 프로퍼티를 인스턴스 프로퍼티라고 한다.

  그러나 예외적으로 일부 프로퍼티는 클래스와 구조체 자체에 소속되어 값을 가지기도 한다. 이런 프로퍼티들을 타입 프로퍼티라고 한다. 타입 프로퍼티는 인스턴스를 생성하지 않아도 사용될 수 있다.


1. 저장 프로퍼티

  저장 프로퍼티(Stored Property)는 클래스 내에서 선언된 변수나 상수를 부르는 이름이다.

	class User {
    	var name: String
    }
    
    // 컴파일 오류

  위 코드에서 name 프로퍼티를 일반 문자열 타입으로 선언했다. 이때 컴파일러는 프로퍼티에 값이 할당되어 있지 않으므로 초기화 구문을 정의하고 저장 프로퍼티를 초기화하라는 컴파일 오류를 뱉는다.

  이를 해결하기 위해서는 세가지 해결책이 있다.

	// 1. 초기화 구문을 작성하고, 그 안에 초기값을 할당
	class User {
    	var name: String
       
        init() { // 초기화 메소드 init()
            self.name = ""  // 클래스에서 선언된 프로퍼티나 메소드는 self 키워드로 구분!!
        }
    }
  
  
  	// 2. 프로퍼티를 옵셔널 타입으로 변경
    class User {
    	var name: String?
    }
    (또는)
    class User {
    	var name: String!
    }
    
    
    // 3. 프로퍼티에 초기값을 할당
    class User {
    	var name: String = ""
    }

1 - 1 지연 저장 프로퍼티

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

  lazy 키워드가 붙은 프로퍼티는 선언만 될 뿐 초기화되지 않고 계속 대기하고 있다가 프로퍼티가 호출되는 순간에 초기화된다.

  만약 이 프로퍼티에 클래스나 구조체 인스턴스가 대입된다면, 프로퍼티가 호출되기 전까지는 해당 인스턴스는 초기화되지 않는다. 이처럼 호출되기 전에는 선언만 된 상태로 있다가 실제로 호출되는 시점에서 초기화가 이루어지는 저장 프로퍼티는 지연 저장 프로퍼티라고 한다.


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

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


let lz = LazyTest() // "Lazy Test"

lz.late // "OnCreate!!"

  위 예제에서 lazy라는 키워드가 없었다면 let lz = LazyTest() 이 구문이 실행되는 시점에 OnCreate가 출력됬을 것이다. 그러나 lazy 키워드로 인해 late 프로퍼티를 호출해서야 OnCreate!!구문이 출력된 것이다.

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

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

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

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

	class PropertyInit {
    	
        // 상수, 변수 모두 사용 가능
        // 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
        var value01: String! = {
        	print("value01 execute")
            return "value01"
        }()
        
        // 저장 프로퍼티 - 인스턴스 생성 시 최초 한 번만 실행
        let value02: String! = {
        	print("value02 execute")
            return "value02"
        }()
    }
    
    let s = PropertyInit()
    // value01 execute 출력
    // value02 execute 출력
    
    s.value01 // 결과 없음
    s.value02 // 결과 없음

  위 코드에서 단순히 PropertyInit()으로 인스턴스를 생성했을 뿐인데 실행 결과에 두 개의 메시지가 출력된 것을 볼 수 있다.

  그러나 밑에서 프로퍼티를 참조했을 때에는 아무런 결과도 찍히가 않는다. 이는 저장 프로퍼티에 정의된 클로저 구문이 더 이상 재실행되지 않기 때문이다.

  만약 클로저 구문을 실행하여 결과값을 저장 프로퍼티에 대입하고 싶지만, 위의 예제 코드와 같이 처음부터 클로저를 실행하는 저장 프로퍼티의 특성이 부담스러운 경우 lazy 키워드를 사용하면 된다.

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

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

	//실행 결과 
    // value01 execute
    // value02 execute
    
    
    // lazy 키워드 때매 참조해야 출력
    s1.value03 // value03 execute
    
    
    // 재참조시 결과 없음
    s1.value03 // ..

  lazy키워드가 붙은 value03 클로저는 인스턴스 생성을 하고 그 인스턴스를 통해 참조해서야 결과값이 출력되는 것을 볼 수 있다.

  또한 재참조할 경우 아무런 결과값이 없는데 이는 위에서 이야기 했듯이 저장 프로퍼티의 특성상 최초에 값이 평가되고나면 이후로 재평가되지 않기에 클로저가 실행되지 않아 발생하는 현상이다.

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

2. 연산 프로퍼티

  연산 프로퍼티(computed propery)는 필요한 값을 제공한다는 점에서 저장 프로퍼티와 같지만, 실제 값을 저장했다가 반환하지는 않고 대신 다른 프로퍼티의 값을 연산 처리하여 간접적으로 값을 제공한다.

  프로퍼티의 값을 참조하기 위해 내부적으로 사용하는 구문이 get 구문(필수 요소)이다. 또한 연산 프로퍼티는 set 구문(선택요소)을 사용하여 연산 프로퍼티의 값을 할당하거나 변경할 수 있다.

	struct UserInfo {
    	//저장 프로퍼티: 태어난 연도
        var birth: Int!
        
        //연산 프로퍼티: 올해가 몇년도인지 계산
        var thisYear: Int! {
        	get {
            	let df = DateFormatter()
                df.dateFormat = "yyyy"
                return Int(df.string(from: Date()))
            }
            
            //연산 프로퍼티: 올해 - 태어난 연도 + 1
            var age: Int {
            	get {
                	return (self.thisYear - self.birth) + 1
                }          
            }
        }
    }
    
    let info = UserInfo(birth: 1980)
    print(info.age)

// 실행 결과 
// 37

  위 예제와 같이 저장 프로퍼티와 연산 프로퍼티의 정의 형식은 많이 다르다. 연산 프로퍼티는 항상 클래스나 구조체, 또는 열거형 내부에서만 사용할 수 있다. 좀 더 복잡한 예제를 본다면 ~

	// x축 y축 좌표
	struct Position {
    	var x: Double = 0.0
        var y: Double = 0.0
    }
    
    // 크기 구조체
    struct Size {
    	var width: Double = 0.0
    	var height: Double = 0.0
    }

	
    struct Rect {
    	// 사각형이 위치한 기준 좦
        var origin = Position()
        
        // 가로, 세로 길이
        var size = Size()
        
        // 사각형의 X좌표 중심
        var center: Position {
        	get {
            	let centerX = self.origin.x + (self.size.width / 2)
                let centerY = self.origin.y + (self.size.height / 2)
                return Position(x: centerX, y: centerY)        
            }
            set(newCenter) {
            	self.origin.x = newCenter.x - (size.width / 2)
                self.origin.y = newCenter.y - (size.width / 2)
            }
        }
    }
    
    let p = Position(x: 0.0, y:0.0)
    let s = Size(width: 10.0, height: 10.0)
    
    var square = Rect(origin: p, size: s)
    print("square.centerX =\(square.center.x), square.centerY = \(square.center.y)")
    
	
    // 실행결과
    // squre.centerX = 5.0, square.centerY = 5.0

  set 구문은 활용하기에 따라 다른 저장 프로퍼티의 값을 변경하는 데도 사용할 수 있다.

	// set구문 호출
	square.center = Position(x: 20.0, y: 20.0)
    print("square.x = \(square.origin.x), square.y = \(square.origin.y)")
    
    // 실행결과
    // squre.x = 15.0, square.y = 15.0

  만약 set 구문이 정의되어 있지 않으면 프로퍼티를 통해 값을 읽기만 할 뿐 할당을 할 수 없다. 이를 읽기 전용 프로퍼티(read-only프로퍼티)라고 한다.


3. 프로퍼티 옵저버(Propery Observer)

  프로퍼티 옵저버는 특정 프로퍼티를 계속 관찰하고 있다가 프로퍼티의 값이 변경되면 이를 알아차리고 반응한다. 우리가 프로퍼티의 값을 직접 변경하거나 시스템에 의해 자동으로 변경하는 경우에는 당연히 호출되지만, 프로퍼티에 현재와 동일한 값이 할당되더라도 호출된다.

  프로퍼티 옵저버에는 두 가지 종류가 있다. willSetdidSet이 있다. willSet은 프로퍼티의 값이 변경되기 직전에 호출되는 옵저버이고 didSet은 프로퍼티의 값이 변경된 직후에 호출되는 옵저버이다.

	struct Jop {
    	var income: Int= 0
        	willSet(newIncome) {
            	print("이번 달 월급은 \(newIncome)원입니다.")
            }
    		didSet {
            	if income > oldValue {
                	print("월급이 \(income - oldValue)원 증가함. 소득세 상향조정")
                } else {
                	print("소득세 그대로")
                }
            }
    
    }

  willSet은 프로퍼티에 할당될 새로운 값을 매개상수인 newIncome으로 전달받는다. 이는 willSet 구현 블록 내부에서 새로 할당되는 값을 사용할 때 newIncome이라는 이름으로 사용할 수 있게 하고자 우리가 직접 정의한 매개상수이다.

  이와 달리 didSet 구문에서는 매개상수 선언을 생략했는데 이 경우 oldValue라는 기본 이름을 사용하면 바뀌기 전의 income 프로퍼티의 값을 읽어올 수 있다. 이미 값의 할당이 끝난 직후에 호출되는 구문이므로 현재의 income 프로퍼티에는 새로운 값이 할당되어 있다.즉 income에는 새로운 값이, oldValue에는 바뀌기 전 값이 저장되는 것이다.

	var jop = Job(income: 1000000)
    job.income = 2000000
    
    //실행 결과
    // 이번달 월급은 2000000원입니다.
    // 월급이 1000000원 증가함. 소득세 상향조정

  옵저버의 실행 과정은 다음과 같다.

  월급을 삭감해보면 다음과 같은 결과이다.

	job.income = 1500000
    
    //실행 결과
    // 이번달 월급은 1500000원입니다.
    // 소득세 그대로

  이처럼 프로퍼티 옵저버는 프로퍼티에 구현해 두면 그 뒤로는 신경 쓰지 않아서 알아서 동작하므로 값의 변화를 주시하고 있어야 할 때, 혹은 값의 변화에 따른 처리가 필요할 때 요긴하게 사용되는 기능이다.


4. 타입 프로퍼티

  앞서 본 저장 프로퍼티나 연산 프로퍼티는 클래스 또는 구조체인스턴스를 생성한 후 그 인스턴스를 통해서만 참조할 수 있는 프로퍼티였다. 이를 인스턴스 프로퍼티라고 한다.

  하지만 경우에 따라서는 인스턴스에 관련된 값이 아니라 클래스나 구조체, 또는 열거형과 같은 객체 자체에 관련된 값을 다루어야 할 때도 있는데, 이때는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하게 되며 이를 타입 프로퍼티(Type Property)라고 부른다.

  타입 프로퍼티는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 저장하게 되며, 저장된 값은 모든 인스턴스가 공통으로 사용할 수 있다.

  타입 프로퍼티를 선언할 때에는 프로퍼티 앞에 static 키워드만 추가해주면 된다. static 키워드는 구조체나 클래스에 관계없이 저장 프로퍼티와 연산 프로퍼티에 모두 사용할 수 있다.

  타입 프로퍼티를 정의하는 또 다른 키워드인 class는 클래스에서 연산 프로퍼티에만 붙일 수 있는 키워드이다. 구조체이거나 저장 프로퍼티일 경우에느 사용할 수 없다. 또한 이 키워드를 통해 타입 프로퍼티를 선언하면 상속받은 하위 클래스에서 재정의(Override) 할 수 있는 프로퍼티가 된다.

	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

  위 예제에서 클래스나 구조체 자체에 점 구문을 이용하여 타입 프로퍼티를 참조하고 있다.

  class 키워드를 사용하여 정의한 oFoo는 Boo클래스를 상속받는 하위 클래스에서 재정의할 수 있는 타입 프로퍼티라는 점이 cFoo와 다른 점이다.

profile
🧑🏻‍💻iOS

0개의 댓글