var name = "". Swift에서 가장 많이 쓰는 한 줄이지만, 이 자리에 올 수 있는 선택지가 생각보다 많습니다. 저장 프로퍼티, 연산 프로퍼티, lazy, willSet과 didSet, 타입 프로퍼티까지. 문법은 다 배웠는데 막상 “이 값은 lazy로 해야 하나, 연산으로 해야 하나”를 묻는 순간 기준이 흐릿해지는 경우가 많아요.
이 글은 Swift 기초 시리즈 3편입니다. 프로퍼티 다섯 종류를 문법 나열이 아니라 선택 기준 중심으로 정리합니다. 핵심 질문은 하나예요. 이 값은 저장되는가, 계산되는가, 그리고 언제 만들어지는가.
저장 vs 연산 — 메모리에 있는 값과 그때그때 만드는 값
프로퍼티의 첫 갈림길은 저장이냐 연산이냐입니다.
저장 프로퍼티(stored property)는 인스턴스 안에 실제로 자리를 차지하는 값입니다. var name = "Kim"이라고 쓰면 인스턴스 메모리에 name의 공간이 잡혀요. struct의 크기는 저장 프로퍼티들의 합으로 결정됩니다.
연산 프로퍼티(computed property)는 자리가 없습니다. 접근할 때마다 코드를 실행해 값을 만들어내는, 사실상 함수예요.
struct Rectangle {
var width: Double // 저장
var height: Double // 저장
var area: Double { // 연산 — 저장 공간 없음
width * height
}
}
area를 저장 프로퍼티로 만들 수도 있었을 겁니다. 하지만 그러면 width가 바뀔 때마다 area도 갱신해줘야 하고 둘이 어긋나는 순간 버그가 됩니다. 여기서 첫 번째 선택 기준이 나옵니다. 다른 값에서 유도되는 값은 저장하지 말고 연산하라. 진실의 원천(source of truth)을 하나로 유지하면 동기화 버그가 원천적으로 사라집니다.
연산 프로퍼티에 set을 붙이면 쓰기도 가능합니다. celsius를 저장하고 fahrenheit를 연산으로 두되, fahrenheit에 대입하면 celsius를 역산해 갱신하는 식이죠. 이때도 저장되는 진실은 하나고, 나머지는 그 진실을 보는 창입니다.
함수 대신 연산 프로퍼티를 쓰는 기준도 짚고 갈게요. 애플 API 디자인 가이드라인의 관례는 이렇습니다. 계산이 싸고(대체로 O(1)에 가깝고), 부수효과가 없고, 개념적으로 “그 객체의 속성”이면 프로퍼티. 계산이 비싸거나 부수효과가 있으면 메서드. array.count는 프로퍼티지만 array.sorted()가 메서드인 이유입니다.
lazy — 처음 쓸 때 만들어지는 저장 프로퍼티
lazy는 세 번째 선택지입니다. 저장 프로퍼티인데, 인스턴스 생성 시점이 아니라 처음 접근하는 순간 초기화됩니다.
class ImageProcessor {
lazy var filters: [Filter] = loadExpensiveFilters()
}
쓰는 자리는 두 가지입니다. 첫째, 초기화 비용이 큰데 안 쓸 수도 있는 값. 무거운 리소스를 인스턴스 생성마다 준비하는 건 낭비니까요. 둘째, self를 참조해야 하는 프로퍼티. 일반 저장 프로퍼티의 기본값은 init이 끝나기 전에 계산되므로 self를 쓸 수 없지만 lazy는 초기화가 첫 접근 시점으로 미뤄지니 self 참조가 허용됩니다. 클로저 편에서 본 즉시 실행 클로저(= { ... }())와 lazy가 자주 짝으로 다니는 이유예요.
주의점도 두 가지입니다. lazy는 반드시 var여야 합니다. 값이 “나중에 변한다”(nil 비슷한 미초기화 상태에서 실제 값으로)는 점 때문에 let이 될 수 없어요. 그리고 스레드 안전하지 않습니다. 여러 스레드가 동시에 첫 접근을 하면 초기화가 두 번 실행될 수 있습니다. 멀티스레드 환경에서 한 번만 만들어져야 하는 값이라면 다른 장치가 필요합니다.
연산 프로퍼티와 어떻게 다른지도 분명합니다. lazy는 한 번 계산해서 저장하고 이후엔 그 값을 돌려줍니다. 연산은 매번 다시 계산합니다. “비싸지만 변하지 않는 값”은 lazy, “쌀지만 항상 최신이어야 하는 값”은 연산입니다.
willSet과 didSet — 값 변경에 반응하기
저장 프로퍼티에는 변경 관찰자를 붙일 수 있습니다. 값이 바뀌기 직전(willSet)과 직후(didSet)에 실행되는 코드예요.
class ProgressBar {
var progress: Double = 0 {
didSet {
// oldValue가 자동 제공됨
guard progress != oldValue else { return }
updateUI()
}
}
}
전형적인 용도는 값이 바뀌면 따라와야 하는 일을 자동으로 처리하는 겁니다. UI 갱신, 로깅, 값 검증(범위를 벗어나면 되돌리기) 같은 것들이죠. 세터를 직접 만들 필요 없이 대입 문법을 유지하면서 부수 동작을 심을 수 있습니다.
동작 규칙 중 실무에서 헷갈리는 두 가지만 짚습니다. 첫째, init 안에서 값을 대입할 때는 옵저버가 호출되지 않습니다. 초기화는 “설정”이지 “변경”이 아니라는 설계예요. 그래서 didSet에 건 UI 갱신이 초기값에는 안 먹습니다. 둘째, 값 타입의 프로퍼티를 바꾸면 그걸 담은 프로퍼티의 didSet도 호출됩니다. person.name = "Lee"처럼 struct의 내부만 바꿔도 person 자체의 didSet이 불립니다. 값 타입 편에서 본 “수정은 곧 새 값으로의 교체”라는 의미론이 여기서도 관철되는 거죠.
한 가지 경계할 것은 didSet 남용입니다. didSet에 로직이 쌓이면 대입 한 줄이 무슨 일을 벌이는지 추적하기 어려워집니다. 값 하나 바꿨는데 네트워크 요청이 나간다면 최소 놀람의 원칙 위반이에요. didSet에는 가벼운 동기화만 두고 무거운 로직은 명시적 메서드로 빼는 게 안전합니다.
타입 프로퍼티 — 인스턴스가 아니라 타입에 붙는 값
static을 붙이면 프로퍼티가 인스턴스가 아니라 타입 자체에 붙습니다.
struct APIConfig {
static let baseURL = URL(string: "https://api.example.com")!
static var requestCount = 0
}
인스턴스를 몇 개 만들든 타입 프로퍼티는 하나입니다. 상수 모음(설정값, 공유 포매터)에 잘 맞고, Int.max나 Double.pi처럼 표준 라이브러리도 애용하는 자리예요.
알아두면 좋은 사실 하나. static let은 스레드 안전한 지연 초기화가 보장됩니다. 첫 접근 때 단 한 번 초기화되고 동시 접근에도 안전해요. lazy var에는 없는 보장이 static에는 있는 겁니다. 싱글톤의 static let shared가 별도 잠금 장치 없이 성립하는 근거이기도 합니다. 다만 static var 가변 상태는 사실상 전역 변수라서 테스트 격리를 해치고 데이터 레이스 후보가 됩니다. 싱글톤이 왜 안티패턴 소리를 듣는지는 별도 글에서 다뤘으니 여기서는 “static var는 마지막 수단”이라는 기준만 남겨둘게요.
선택 플로차트 — 네 가지 질문으로 정리
다섯 종류를 한 줄 질문으로 압축하면 이렇게 됩니다.
- 다른 값에서 유도되는가? → 연산 프로퍼티. 진실은 하나만 저장합니다.
- 초기화가 비싸거나 self가 필요한가? → lazy var. 단 멀티스레드 첫 접근은 조심.
- 값 변경에 따라오는 동작이 있는가? → 저장 프로퍼티 + didSet. 단 가벼운 일만.
- 인스턴스와 무관하게 하나면 되는가? → static. let이면 안전한 지연 초기화 덤.
- 전부 아니라면 → 평범한 저장 프로퍼티. 대부분의 경우 이게 정답입니다.
참고로 SwiftUI의 @State, @Published 같은 속성 래퍼(property wrapper)도 결국 이 프로퍼티 시스템 위에 지어진 문법입니다. 저장·연산·옵저버의 원리를 알고 있으면 래퍼가 “저장 프로퍼티를 감싸 didSet류의 동작을 자동화한 것”이라는 게 보여요. 속성 래퍼 직접 만들기는 중급 시리즈에서 다룰 예정입니다.
정리
- 프로퍼티의 첫 기준은 저장이냐 연산이냐입니다. 다른 값에서 유도되는 값은 연산으로 만들어 진실의 원천을 하나로 유지합니다.
- lazy는 첫 접근 때 초기화되는 저장 프로퍼티로, 비싼 초기화와 self 참조 문제를 풉니다. var 강제, 스레드 비안전이 대가입니다.
- willSet/didSet은 값 변경에 반응하는 장치지만 init에서는 안 불리고, 무거운 로직을 심으면 추적이 어려워집니다.
- static let은 스레드 안전한 지연 초기화가 보장되는 타입 프로퍼티, static var는 전역 상태라 마지막 수단입니다.
다음 편은 기초 4편, guard입니다. 옵셔널 편에서 잠깐 본 조기 탈출을 제대로 파서, 왜 Swift 커뮤니티가 들여쓰기와 전쟁을 벌이는지 정리합니다.
