부트캠프에서 클래스를 공부하면서 '프로퍼티'라는 개념을 알게 되었다.
대충 어떤 느낌인지는 알겠으나, 더욱 깊이 공부할 가치가 있다고 느껴져서 이렇게 포스팅을 하게 되었다 💻
머릿속에 중구난방으로 떠도는 개념들을 이번 기회에 잘 정리하여, 추후 개념이 헷갈리거나 기억이 안날때 참고할 수 있는 자료를 만든다고 생각하고 잘 정리해보자!
특정 클래스, 구조체, 열거형 등에 관련된 '값'
프로퍼티는 크게 3가지로 나눌 수 있다.
본 포스팅에서는 위 3가지 프로퍼티들을 하나하나 살펴볼 예정이다.
특정 클래스, 구조체 인스턴스의 변수 또는 상수에 담기는 '값'
쉽게 설명하자면, 클래스나 구조체 내부에 선언된 변수/상수라고 생각하면 된다.
한가지 특징이 있다면, 열거형은 저장 프로퍼티를 가지지 않는다.
다음은 저장 프로퍼티를 선언한 예시 코드이다.
class BankAccount {
var accountBalance: Float
var accountNumber: Int
let fees: Float = 25.00 //프로퍼티에 기본값 할당
init(accountBalance: Float, accountNumber: Int) {
self.accountBalance = accountBalance
self.accountNumber = accountNumber
}
//이니셜라이저를 통한 초깃값 할당
struct CoordinatePoint {
var x: Int
var y: Int
}
let yagomPoint: CoordinatePoint = CoordinatePoint(x: 10, y: 5)
예시코드에서는 클래스와 구조체의 경우를 나누어서 저장 프로퍼티를 선언하고 초기값을 부여하는 작업을 하였다.
코드 상으로는 비슷해보이지만, 클래스와 구조체는 결정적인 한 가지 차이가 존재한다.
그 차이점은 바로 '이니셜라이저의 자동제공 여부'이다.
클래스의 경우에는 저장 프로퍼티에 맞는 이니셜라이저를 자동으로 제공하지 않는다.
따라서, 저장 프로퍼티에 기본값을 부여하지 않으면 별도의 이니셜라이저 정의를 통해 초깃값을 제공하여야 한다.
왜냐하면 클래스에서는 저장 프로퍼티가 옵셔널이 아닌 이상 이니셜라이저를 통해 초기화를 시켜주거나 기본값을 지정해주어야하기 때문이다.
반면, 구조체의 경우에는 저장 프로퍼티에 맞는 이니셜라이저를 자동으로 제공한다.
따라서, 저장 프로퍼티에 기본값을 부여하지 않아도 별도의 이니셜라이저 정의없이 초깃값 설정이 가능하다.
처음 접근될 때까지 초기값이 계산되지 않는 프로퍼티를 말한다.
타입의 인스턴스 초기화가 완료된 후에도 초기값이 없을 수 있으므로 지연 저장 프로퍼티는 var
키워드를 사용하여 변수로 선언하여야 한다는 특징이 있다.
왜냐하면 상수는 인스턴스 초기화가 완료되기 전에 항상 값을 가지고 있어야 하기 때문이다.
지연 저장 프로퍼티는 다음과 같은 상황에서 주로 쓰인다고 한다.
- 복잡한 클래스에 불필요한 초기화를 피하기 위해
- 인스턴스의 초기화가 완료될 때까지 값을 알 수 없는 외부 요인에 인해 초기값이 달라질 때
지연 저장 프로퍼티는 코드에서 어떻게 사용되는지 아래 예제 코드를 통해 쉽게 이해해보자, 지연 저장 프로퍼티는 lazy
라는 키워드로 선언할 수 있다.
class DataImporter {
var filename = "data.txt"
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
print(manager.importer.filename)
DataManager 클래스의 importer 라는 프로퍼티는 DataImporter 클래스 인스턴스를 값으로 가진다.
하지만, importer는 lazy
키워드로 선언된 지연 저장 프로퍼티이다.
따라서, DataManager 클래스 내부에서 importer는 최초 접근이 이뤄지기 전까지 DataImporter 클래스의 인스턴스를 할당 받을 수 없다.
importer 프로퍼티의 최초 접근은 코드의 마지막 줄 manager.importer.filename
를 통해 이뤄진다.
이렇게 최초 접근이 이뤄진다면, DataImporter 클래스의 인스턴스가 생성되고 importer 프로퍼티로의 할당이 이루어진다.
만일, importer가 지연 저장 프로퍼티로 선언되지 않았다면 어떻게 되었을까?
그런 경우에는, 만일 코드 상에서 importer 프로퍼티가 사용되지 않았을 때를 생각해보면 된다.
importer 프로퍼티를 지연 저장 프로퍼티로 선언하지 않는다면, 사용되지도 않는 프로퍼티에 굳이 DataImporter 인스턴스를 할당하게 됨으로써 쓸 때 없는 메모리 소모가 발생할 것이다.
이를 통해 우리는 지연 저장 프로퍼티가 불필요한 공간 낭비를 줄일 수 있다는 것을 알 수 있다.
클래스, 구조체, 열거형 타입에서 '특정 상태에 따른 값을 연산'하는 프로퍼티
프로퍼티에 대해 공부하면서 가장 이해가 힘들었던 부분이 바로 '연산 프로퍼티' 였던 것 같다.
정의에 따르면 타입에 관련된 '값'이 프로퍼티라고 앞서 언급한 바 있다.
그런데 연산 프로퍼티는 '실제 값을 저장하는 프로퍼티가 아니다'라는 점에서 프로퍼티의 본래 정의와 모순된다고 느껴졌기 때문이다.
실제로 앱 스쿨 강사님께서도 연산 프로퍼티는 메서드의 색채가 짙다고 하셨다.
그래서 나도 그냥 연산 프로퍼티는 '값'을 다루는 것일 뿐, 사실상 메서드라고 보는 것이 맞다고 받아들이기로 했다,,,😕
실제로 연산 프로퍼티의 기능을 메소드로 구현할 수 있지만, 가독성과 역할의 명확한 표현 등을 이유로 이렇게 프로퍼티 형식으로 사용하고 있다고 한다.
연산 프로퍼티는 아래의 2가지 요소로 구성이 된다고 한다.
또한, 저장 프로퍼티와는 다르게 열거형도 가질 수 있다는 특징이 있었다.
인스턴스 내/외부의 값을 연산하여 적절한 값을 돌려줌
저장 프로퍼티를 특정 목적에 맞게 연산하여 return 해주는 역할을 한다.
(값을 돌려주는 역할인만큼 return 구문을 필수로 사용해야 함)
전달받은 인자를 목적에 맞게 연산한다
파라미터로 전달받은 인자를 특정 목적에 맞게끔 연산하고 저장해주는 역할을 한다.
아래의 예시 코드를 살펴보면서 연산 프로퍼티를 어떻게 선언하는지 알아보자.
struct CoordinatePoint {
var x: Int
var y: Int
//저장 프로퍼티 선언 (기본값 설정 x인 상태)
var oppositePoint: CoordinatePoint {
//접근자 (getter)
get {
return CoordinatePoint(x:-x, y:-y)
}
//설정자 (setter)
set(opposite) {
x = -opposite.x
y = -opposite.y
//전달 받은 인자를 바탕으로 연산 수행
}
}
}
var yagomPosition: CoordinatePoint = CoordinatePoint(x: 10, y: 20)
//구조체 인스턴스 -> 이니셜라이저 선언 없이 매개변수로 바로 인스턴스 생성 가능
print(yagomPosition)
//10, 20
print(yagomPosition.oppositePoint)
//get의 반환 값에 따라 -10, -20 출력
yagomPosition.oppositePoint = CoordinatePoint(x:15, y:10)
//x, y값을 인자로 넣어주었으므로 인자 값을 바탕으로 set의 연산과정 수행
print(yagomPosition)
//get의 반환 값에 따라 -15, -10 출력
var oppositePoint: CoordinatePoint
와 같은 형식으로 연산 프로퍼티를 선언하였다.
연산 프로퍼티는 값을 저장하는 것이 아닌 계산을 하는 것이기 때문에 타입 추론이 적용되지 않는다.
따라서, 꼭 연산 프로퍼티를 선언할 때 타입을 반드시 명시해야한다.
또한, 연산 프로퍼티를 사용하기 위해서는 반드시 읽고 쓸 수 있는 저장 프로티가 존재해야 하며, 계산을 통해 반환되는 값이 고정적이지 않기 때문에 무조건 연산 프로퍼티는 var
키워드를 통해 변수로 선언해주어야 한다.
연산 프로퍼티에서 접근자만 존재하고, 설정자가 존재하지 않을 경우
연산 프로퍼티에서 접근자(getter)는 필수이지만, 설정자(setter)는 선택적으로 사용한다.
따라서, 필요에 따라 별도의 값 설정이 필요가 없는 경우에는 설정자를 생략할 수 있다.
struct CoordinatePoint {
var x: Int
var y: Int
//저장 프로퍼티 선언 (기본값 설정 x인 상태)
var oppositePoint: CoordinatePoint {
//접근자 (getter) 만 사용
get {
return CoordinatePoint(x:-x, y:-y)
}
}
}
set(opposite) {
x = -opposite.x
y = -opposite.y
}
위의 코드는 opposite라는 매개변수를 통해 저장 프로퍼티 x, y 값을 새롭게 설정해주는 설정자를 선언한 것이다.
Swift의 연산 프로퍼티에서는 매개변수 없이 설정자를 작성할 수도 있다.
매개변수 없이 작성한 설정자는 아래와 같다.
set {
x = -newValue.x
}
이 때, 여기서 newValue는 매개변수(opposite)을 의미한다.
set 옆에 소괄호로 매개변수를 작성하지 않고, newValue를 매개변수라고 생각하며 코드를 적어준다고 이해하면 편하다.
Swift의 이러한 기능은 사용자로 하여금 코드를 좀 더 편하게 작성할 수 있게끔 도와준다.
자기 자신을 가리키는 프로퍼티
모든 인스턴스는 암시적으로 생성된 self 프로퍼티를 갖는다.
self 프로퍼티는 주로 인스턴스를 더 명확히 지칭하고 싶을 때 사용하는데, 대표적인 예로 메서드의 매개변수 이름이 인스턴스 프로퍼티의 이름과 같은 경우가 있다.
class LevelClass {
var level: Int = 0
func jumpLevel(to level: Int) {
print("Jump to \(level)")
self.level = level
}
}
위 코드를 보면 jumpLevel 메서드의 매개변수 이름과 Levelclass 클래스의 인스턴스 프로퍼티의 이름이 모두 level로 같은 것을 확인할 수 있다.
이렇게 되면 level 이라는 프로퍼티에 매개변수 level의 인자 값을 넣어줄 때 서로의 정체가 헷갈려지는 상황이 벌어질 수도 있다.
level = level
물론 좌측의 level이 누가 봐도 인스턴스 프로퍼티임을 알 수 있지만, 이러한 코드 자체가 혼란을 야기하는 것은 사실이다.
따라서, self 프로퍼티를 앞에 붙여서 자신이 인스턴스 프로퍼티라는 사실을 확실하게 명시해주는 것이 가독성 측면에서 좋다.
추가로 self 프로퍼티는 값 타입 인스턴스 자체의 값을 치환할 수 있다는 특징도 가진다.
(클래스는 참조 타입이므로 불가능, 구조체와 열거형은 값 타입이므로 가능)
class LevelClass {
var level: Int = 0
func reset() {
self = LevelClass()
}
}
클래스에 관한 다양한 개념들이 있지만, 양이 너무 방대해 이번 포스팅에서는 그 중 일부만 정리하게 되었다.
추가적인 내용들은 포스팅 2편에서 이어서 정리해 볼 생각이다.
위에서 나열한 오늘의 학습 내용들을 머릿속으로 잘 정리해야겠다 👀