swift 프로퍼티에 대해 알아보자
다양하게 공부를 하다보니
프로토콜 사용시 저장 프로퍼티만 가능하다는 말이 나왔다.
잉... 프로퍼티는 클래스나 구조체이서 변수 상수 인데 다른 기능이 있는건가?
궁굼하게 되어서 찾아보게 되었다.
Swift에선 이 프로퍼티가 총 3가지 형태로 존재를 한다
Stored Property : 저장 프로퍼티
Computed Property : 연산 프로퍼티
Type Property : 타입 프로퍼티
저장 프로퍼티(Stored Property)란?
클래스와 구조체에서만 사용할 수 있고, 값을 저장하기 위해 선언되는 상수/변수
ㅇㅖ... 먼저 우리가 지금껏 아무렇지 않게 클래스, 구조체에서
변수와 상수를 담기 위해 사용하는 다음과 같은
class Human {
let name: String = "unknown"
var age: Int = 0
}
struct Person {
let name: String = "unknown"
var age: Int = 0
}
|
Human이란 클래스와 Person이란 구조체에 저장된
name이란 상수, age란 변수 모두 저장 프로퍼티임!!
근데 여기서 저장 프로퍼티의 클래스와 구조체의 차이점?
클래스 인스턴스를 let/var으로 선언한다는 것은
자, 우리가 만약 sodeul이란 옵셔널 "상수"로 Human 클래스 인스턴스를 만들었음
let sodeul: Human? = .init()
|
위와 같이!!!!! 이때 선언 위치는, 지역변수라고 가정 하겠음!!!
그러고 나서 내가 이 sodeul이란 인스턴스를 통해
저장 프로퍼티인 name과 age를 각각 변경 해보겠음!!!
자, 그럼 name이란 값 변경은 에러가 나고, age란 값 변경은 문제가 없음!
엥? name은 상수니 초기화 이후로 변경을 못해서 에러가 나고,
age는 변수니 초기화 이후에 변경이 가능하니 당연한 거 아니예여??
라고 하겠져?
그럼 나는 이렇게 의문을 던질 것임
sodeul이란 인스턴스를 let, 즉 "상수"로 선언 했는데!!
어떻게 age란 저장 프로퍼티가 var라 해도 변경할 수 있을까요!!??
메모리에 대해 좀 아시는 분이라면 왜 저런 질문을 던지나 의아할테고,
모르시는 분이면 혼란이 올 수 있음!!!
일단 sodeul이란 클래스 인스턴스를 할당할 경우
클래스는 참조타입이기 때문에, 메모리에 이렇게 저장됨!!!!
지역 상수 sodeul은 스택에 할당 되고
실제 Human 인스턴스는 힙에 할당 됨
스택에 있는 sodeul은 힙 영역에 있는 인스턴스를 참조하고 있는 형태임
따라서 sodeul 안엔 힙에 할당된 인스턴스의 주소값이 들어가 있음
자 근데, 내가 sodeul이란 클래스 인스턴스를 생성할 때 let으로 한단 것은
실제 힙 영역에 저장된 저장 프로퍼티 name, age와는 상관 없이
스택 영역에 저장된 sodeul 안의 주소값이 상수로 설정되는 것임!!!!
상수니까 값을 바꿀 수 없어서 자물쇠 건 것으로 표현해 봤음!
따라서,
클래스의 경우, 인스턴스 생성 당시 let으로 선언하든 var로 선언하든
클래스의 저장 프로퍼티에 접근하는 것엔 아무 영향을 주지 않음
그럼 무엇에 영향을 줄까?
sodeul이란 스택 상수는 0x11111111이란 인스턴스의 값을 가지고 있음
그럼 이제 이 sodeul이란 상수 안의 값을 변경하는 것에 영향을 줌!
상수니까 값이 변경될 수 없으니
sodeul이란 상수가 옵셔널 타입이지만 nil을 할당할 수도 없고,
sodeul이란 상수에 다른 Human Instance를 대입할 수도 없음
(다른 인스턴스의 주소값을 가질 수 없으니!!)
아, 당연히 다음과 같이 클래스 인스턴스를 생성할 때 var로 한다면
var sodeul: Human? = .init()
|
(옵셔널 타입이면) nil도 할당 가능하고,
다른 Human Instance를 대입할 수도 있음!!!
(name은 상수니 인스턴스 선언과 별개로 변경 불가능함~.~)
이것이 Class에서 인스턴스를 let 혹은 var로 선언하는 것의 차이임!!!
구조체 인스턴스를 let/var으로 선언한다는 것은
자, 우리가 만약 sodeul이란 옵셔널 "상수"로 Person 구조체 인스턴스를 만들었음
(아 Swift에선 클래스, 구조체 모두 인스턴스라 합니다!!:))
let sodeul: Person? = .init()
|
위와 같이!!!!!
그러고 나서 내가 이 sodeul이란 인스턴스를 통해
저장 프로퍼티인 name과 age를 각각 변경 해보겠음!!!
자, 이번엔 name이란 상수도, age란 변수도 모두 값 변경을 하지 못한다며 에러가 발생함
엥? name은 상수니 초기화 이후로 변경을 못해서 에러가 나고,
age는 변수니 초기화 이후에 변경할 수 있는데 왜 에러가 나나염??????
라고 의문을 품을 수 있음!! (안 품는다면 메모리 이해도가 높으시군요!)
일단 sodeul이란 구조체 인스턴스를 할당할 경우
구조체는 값타입이기 때문에, 메모리에 이렇게 저장됨!!!!
구조체의 저장 프로퍼티들도 모두 stack에 같이 올라간다는 것임
(클래스처럼 힙에 할당된 인스턴스를 참조하는 것이 아니기 때문에)
따라서, sodeul이란 구조체 인스턴스를 생성할 때 let으로 한단 것은
name이 상수 저장 프로퍼티고, age가 변수 프로퍼티고 나발이고
구조체 인스턴스를 let으로 선언한 순간 구조체의 모~든 멤버를 변경할 수 없는 것임
당연히 nil을 할당하는 것도 안 되겠지!! :)
다른 구조체 인스턴스를 할당받는 것 또한 안 되고!!!
당연히 구조체 인스턴스 또한 var로 선언하면
var sodeul: Person? = .init()
|
nil도 할당받을 수 있고, 다른 구조체 인스턴스를 할당받을 수 있고
또 var로 선언된 저장 프로퍼티의 값도 변경할 수 있음 :)
(name은 상수니 인스턴스 선언과 별개로 변경 불가능함~.~)
이것이 구조체에서 인스턴스를 let 혹은 var로 선언하는 것의 차이임!!!
이해가 됐기를!!!
지연 저장 프로퍼티의 특징
인스턴스가 초기화와 상관 없이, 처음 사용될 때 개별적으로 초기화 된다
이건 위에서 봤으니 패쓰
따라서 항상 "변수"로 선언 되어야 한다
머선 말이냐면, let으로 선언될 경우
우리가 필요한 시점에 초기화를 진행할 수 없기 때문임
(이미 메모리에 올라는 가 있지만, 필요한 시점에 내가 원하는 값으로 초기화 되어야 하니)
3-2. 지연 저장 프로퍼티는 어떨 때 쓸까?
자, 만약 내가 미쳐갖고 Contacts란 클래스에 100,000개의 Element를 갖는
email이란 저장 프로퍼티를 선언 했음!
class Contacts {
var email: [String] = .init(repeating: "", count: 100000)
var address: String = ""
init() { print("Contacts Init 🐙") }
}
class Human {
let name: String = "unknown"
lazy var contacts: Contacts = .init()
}
|
근데 Contacts에 대한 정보가 있는 유저도 있고, 없는 유저도 있을 거 아님???
만약 5,000명의 유저가 Contacts 정보를 제공하고,
5,000명의 유저가 Contacts 정보를 제공하지 않는다고 생각해보셈
lazy로 선언하지 않는다면, Contacts란 인스턴스가
유저 수만큼 10,000개 생겨 버림
(Human 인스턴스가 초기화 됨과 동시에 생성되니)
근데, lazy로 선언할 경우 Contacts란 인스턴스가
contacts란 프로퍼티에 접근하는 유저 수인 5,000개 만큼만 생기고,
접근하지 않는 유저 수인 5,000개는 초기화 되지 않아 생기지 않을 것임!!
따라서,
이럴 경우 lazy를 사용하면 메모리가 반으로 절약된다는 말!
예제가 좀 띠용스럽긴 한데..;;쨌든 이 lazy를 잘 사용할 경우
성능도 향상되고, 메모리 낭비도 줄일 수 있다
라고 함네다..........:)
열거형에선 못 쓸까?
1. 연산 프로퍼티(Computed Property)란?
클래스, 구조체, 열거형에서 사용된다
저장 프로퍼티와 달리 저장 공간을 갖지 않고,
다른 "저장 프로퍼티"의 값을 읽어 연산을 실행하거나, 프로퍼티로 전달받은 값을 다른 프로퍼티에 저장한다
때문에 항상 var로 선언되어야 한다
저장 프로퍼티는 열거형에서 사용 못했는데, 연산 프로퍼티는 되다
연산 프로퍼티는 직접 값을 "가지지는(저장하진) 않고", "다른 저장 프로퍼티"를 지지고 볶고 하는 건가 보네??
연산 프로퍼티의 생김새는 어떻게 생겼냐면
var name: Type {
get { //getter (다른 저장 연산프로퍼티의 값을 얻거나 연산하여 리턴할 때 사용)
statements
return expr
}
set(name) { //setter (다른 저장프로퍼티에 값을 저장할 때 사용)
statements
}
}
|
자 이렇게 생겼음!!!!!!!!!!1
get을 getter, set을 setter라 부름!!!(포스팅에선 섞어쓰겠음)
먼저,
연산 프로퍼티는 어떠한 값을 저장하는 것이 아니기 때문에,
타입 추론을 통해 형식을 알 수 없어서, 반드시 선언할 때 타입 어노테이션을 통해 자료형을 명시해야 함!!!
그리고 선언된 자료형 뒤에 {} 를 붙이는 것이 연산 프로퍼티의 사용법임
따라서, 내가 어떤 타입의 값을 받아서 다른 저장 프로퍼티에 저장할 것인지,
어떤 타입의 값을 리턴할 것인지를 명시해주어야 함!!!
getter는 말 그대로 "얻는" 것임!
따라서 어떤 저장 프로퍼티의 값을 연산해서 return할 것인지, return 구문이 항상 존재해야 함
setter는 말 그대로 "설정"하는 것임!
따라서 파라미터로 받은 값을 어떤 저장 프로퍼티에 어떻게 설정할 것인지를 구현함!!
흠.... 여기까지도 아리송 할거 같음!!! 더 좋은 이해를 위해 예제를 보자 :)
자, 만약 다음과 같은 Person이란 클래스가 있음
class Person {
var alias: String {
get {
return alias
}
set(name) {
self.alias = name
}
}
}
|
오 alias란 프로퍼티는 신기하게 생겼네??
근데 Type Annotation 뒤에 {}가 시작되고, 그 안에 get과 set이 있네???
아하! alias는 연산 프로퍼티이구나!!
이렇게 생각할 수 있는 것까진 좋음!!
근데 위 코드는 잘못된 코드임!!!!! 왜냐???
연산 프로퍼티는 다른 "저장 프로퍼티"를 지지고 볶고 하는 놈이라 했음!!!
근데 위 코드는 get, set에서 alias란 "연산 프로퍼티"를 지지고 볶고 있음
따라서, 엄청난 오류 메세지가 발생하고,
실제 런타임까지 가면,
getter가 무한으로 호출되는 무한 재귀 함수에 빠져버려용
따라서, 연산 프로퍼티를 사용하려면 무조건
class Person {
var name: String = "Sodeul"
var alias: String {
get {
return name
}
set(name) {
self.name = name
}
}
}
|
name과 같이 읽거나 쓸 수 있는 "저장 프로퍼티"가 먼저 존재해야 하고,
연산 프로퍼티에선 이 다른 "저장 프로퍼티"의 값을 읽거나 쓰는 작업을 해야 함!!!!
물론 위처럼 단순하게
name이란 저장 프로퍼티의 값을 읽어서 return하는 get,
파라미터로 받은 값을 그대로 name이란 저장 프로퍼티에 값을 저장하는 set
처럼 구현해도 되지만, 뭐 원한다면 다음과 같이 다른 연산 작업들을 직접 해줄 수도 있음!!!
class Person {
var name: String = "Sodeul"
var alias: String {
get {
return self.name + " 바보"
}
set(name) {
self.name = name + "은 별명에서 지어진 이름"
}
}
}
|
이런 식으로 원하는 연산을 해서 getter를 작성할 수도,
파라미터로 받은 값을 원하는 연산을 해서 setter를 작성할 수도 있음!!!
아 그럼 연산 프로퍼티인 alias는 어떻게 쓰는 건데염;;;
1-1. 연산 프로퍼티의 사용 방법
ㅇㅖ.. 다를 거 없고 그냥
우리가 아무렇지 않게 사용하던 저장 프로퍼티처럼 사용하면 됨!!!!!!!!1
let sodeul: Person = .init()
// get에 접근
print(sodeul.alias) // Sodeul 바보
// set에 접근
sodeul.alias = "소들"
print(sodeul.name) // 소들은 별명에서 지어진 이름
|
이런 식으로 마치 저장 프로퍼티에 접근하는 마냥
연산 프로퍼티인 alias값을 읽으면, alias의 getter가 실행되어 "Sodeul 바보" 라는 값이 나온 것이고,
연산 프로퍼티인 alias에 값을 쓰면, "소들"이란 값이 setter의 파라미터로 넘어가 set 함수가 실행됨
이제 연산 프로퍼티에 대해 감이 왔기를!!! :)
1-2. newValue : set의 파라미터는 생략이 가능하다!
음 set 생략 전에 set에 대해 좀 더 짚고 넘어가자면,
✔️ set은 파라미터를 받을 때 왜 타입을 명시해주지 않음?
이라는 의문을 품을 수 있음
심지어 아예 자동완성도 안해줌! 이거에 대한 답은
이미 연산 프로퍼티를 선언할 때 alias의 타입을 "반드시" 명시 해줬기 떄문임
따라서 set으로 들어오는 저 name이란 파라미터는 반드시 명시된 String형일테니까,
따로 지정할 수 없는 것임!!!! 지정하면 타입에 민감한 스위프트에선 매우 클나니까..
아, 그리고 또 혹시 몰라 말하지만...
✔️set의 파라미터는 단 하나만 존재하고,
파라미터이름은 name 말고 알아서 지으시면 됨니당..ㅎㅎ;;
set(wow) {
self.name = wow + "은 별명에서 지어진 이름"
}
|
이렇게...! wow로 지을수도 있음....!
자, 이제 본론으로 돌아가서!!!!
여러분 프로그래밍 할 때 제일 힘든 게 먼줄 아셈?
그것은 바로 변 수 명 짓 기........
따라서 Swift는 매우 친절한 언어이기 때문에
✔️ set의 파라미터 이름 짓기 힘드신 분은 걍
get처럼 파라미터 받는 부분을 다 날려버려도 됨!!!!!!
그럼 파라미터를 뭘로 접근하냐?????/
newValue 라는 이름으로 접근하면 됨!!!!!!!!!!
set {
self.name = newValue + "은 별명에서 지어진 이름"
}
|
이렇게!!!ㅎㅎㅎ 아이 깔끔하다~~~~~~
(newvalue안됨, Newvalue안됨, 무조건 이미 정의된 이름인 newValue만 가능함)
1-3. get-only
연산 프로퍼티를 쓸 때 setter가 필요 없다면, getter만 선언해 줄 수 있음!!
이것을 get-only라고 함! 값을 읽기만 하니까
class Person {
var name: String = "Sodeul"
var alias: String {
get {
return self.name + "은 별명에서 지어진 이름"
}
}
}
|
이렇게 get만 써도 되는데,
이런 경우엔 더 간편하게 get 구문 자체를 없애서
class Person {
var name: String = "Sodeul"
var alias: String {
return self.name + "은 별명에서 지어진 이름"
}
}
|
이렇게 더 간단하게 쓸 수 있음!!
당연히 alias는 set이 없기 때문에 다음과 같이 연산 프로퍼티인 alias에 값을 넣으려고 하면,
get-only라서 alias에 값을 할당할 수 없다는 에러 뜸!!!
1-4. set-only
결론부터 말하자면 안 됨
반드시 getter를 지녀야 한다고 에러 뜸!!!
따라서 무조건 get, set을 같이 구현 하거나, get-only로만 구현해야 함!
타입 프로퍼티(Type Property)란?
클래스, 구조체, 열거형에서 사용된다
저장 타입 프로퍼티와 연산 타입 프로퍼티가 존재하며,
저장 타입 프로퍼티의 경우 선언할 당시 원하는 값으로 항상 초기화가 되어 있어야 한다
"static"을 이용해 선언하며, 자동으로 lazy로 작동한다(lazy를 직접 붙일 필요 또한 없다)
흐음.... 정의는 늘 어렵지만 이번엔 더 어려운 것 같네 하핳하
아니 저장 프로퍼티와 연산 프로퍼티는 저번 포스팅에서 공부해서 알겠는데,
저장 "타입" 프로퍼티와 연산 "타입" 프로퍼티는 뭥미!?! 싶겠지만..
쉽게 말하면, 저장&연산 프로퍼티 앞에 "static"이란 키워드만 붙이면
그것은 저장 타입 프로퍼티 & 연산 타입 프로퍼티가 되는 것임
(물론 나중에 연산 타입 프로퍼티에서 사용하는 class 키워드도 공부할 것임)
class Human {
let name: String = "sodeul" // 저장 프로퍼티
var alias: String { // 연산 프로퍼티
return name + "은 바보"
}
}
|
이렇게 평범하게 생긴 저장 프로퍼티와 연산 프로퍼티 앞에다가 static을 뙇 붙이면
class Human {
static let name: String = "sodeul" // 저장 타입 프로퍼티
static var alias: String { // 연산 타입 프로퍼티
return name + "은 바보"
}
}
|
저장 타입 프로퍼티와 연산 타입 프로퍼티가 되는 것임
또한 위에서 정의에서 저장 타입 프로퍼티의 경우, 항상 초기값을 가져야 한다고 했음
위에선 name이란 저장 타입 프로퍼티가 "sodeul"이란 값으로 초기화 되어 이씅니 문제는 없다만,
근ㄷㅔ 만약 선언과 동시에 저장 타입 프로퍼티를 초기화 안 해주면? ㅇㅓ케될까?
static으로 선언할 경우, initializer가 필수거나 getter/setter를 지정해야 한다는 오류가 뜸
이게 먼말이냐??
너 저장 타입 프로퍼티 선언하고 싶으면 초기 값 지정 하든가! 아니면 연산 타입 프로퍼티로 만들든가!
하고 에러 뱉는 것임
그 이유는, static으로 선언되는 저장 타입 프로퍼티의 경우 초기화할 때
값을 할당할 initializer가 없기 때문임
오잉??
name은 타입 프로퍼티로 선언되어 있긴 하지만, Human이란 클래스 안에 있구
Human 클래스의 인스턴스가 생성될 때 initializer에 의해 모든 프로퍼티가 초기화 되지 않나용!?
이란 의문을 가질 수 있는데,
타입 프로퍼티는 인스턴스가 생성될 때마다 "매번 생성"되는 "기존 프로퍼티"와 다름!!!!
인스턴스가 생성 된다고 매번 해당 인스턴스의 멤버로 매번 생성되는 것이 아니라,
언제 한번 누군가 한번 불러서 메모리에 올라가면, 그 뒤로는 생성되지 않으며
언제 어디서든~ 이 타입 프로퍼티에 접근할 수 있는 것임
전역 변수로 생각하면 조금 더 편할 것 같음!!!
당연히, 인스턴스 생성할 때마다 각자 가지는 프로퍼티가 아니기 때문에
sodeul이란 Human 인스턴스를 생성했지만, 그 안엔 저장 타입 프로퍼티인 name이 없음!!
그럼 타입 프로퍼티인 name엔 어떻게 접근 하느냐??
이렇게, 타입 이름을 통해서만 접근 가능함!!!!
자, 이제 왜 저장 타입 프로퍼티가 무조건 초기값을 가져야 하는지에 대해 답이 나왔음!
인스턴스가 생성된다고 타입 프로퍼티가 매번 생성되는 것이 아니라,
타입 프로퍼티는 누군가 나를 불러줬을 때 한번 메모리에 올라가고,
그 뒤로는 어디서든 해당 프로퍼티를 "공유" 하는 형태임
따라서 인스턴스 생성과 전~~혀 상관이 없기 때문에
인스턴스 생성 시 불리는 initializer 또한 상관 없는 내용이고!!
따라서 초기값이 없을 경우, 초기 값을 셋팅할 방법이 없기 때문에 반드시 초기값을 지녀야 한다!!!
그로하다........:D
자, 근데 위에서 자꾸 타입 프로퍼티의 경우
"누군가 나를 불러줬을 때 메모리에 올라간다"
라는 말을 하는데, 이 말은 머 저장 프로퍼티의 속성인 lazy와 동일한 말임!!
정의에서 설명했듯이, 타입 프로퍼티의 경우 기존 속성이 lazy이기 때문에
name이란 프로퍼티를 최초 호출하기 전까진, 초기화되지 않음!!!!
근데, 다음과 같이 name이란 타입 프로퍼티를 최초 호출 하면,
Human.name
|
이때! 메모리에 올라가서 초기화 되는 것임 :)
흠...... 근데 지연 저장 프로퍼티의 경우 항상 var로 선언해야 했잖음??...
근데 static은 기본이 lazy 동작인데,,, let/var 저장 프로퍼티로 선언해도 문제가 없네..?ㅠ;;
--> 먼저, static이 아닌 저장 프로퍼티는 인스턴스 프로퍼티로, init 함수가 불리는 시점에 모든 값이 초기화가 되어야 합니다. (직접 값을 지정하든, 옵셔널 타입으로 설정하여 nil로 초기화 되든!) 근데 lazy의 경우엔 초기화 단계에서 값이 없음으로 설정 되었다가, 실제 해당 프로퍼티가 불릴 때 원하는 값으로 초기화가 됩니다. 따라서 lazy 프로퍼티를 let으로 선언할 경우, 초기화 단계에서 값이 없음으로 설정되어버리면 이후 실제 사용 시 값을 설정할 수 없어서 let으로 선언하지 못한 것입니다.
그럼 static, 즉 타입 프로퍼티의 경우 왜 let이 가능할까요? 타입 프로퍼티는 인스턴스 프로퍼티처럼 초기화 구문의 영향을 받지 않습니다! 인스턴스가 초기화 즉 init 함수가 불리든 말든 타입 프로퍼티와는 상관 없다고 포스팅에서 말씀 드렸죠 :) 따라서 타입 프로퍼티의 경우 초기화 구문에서 값이 없음으로 초기화 되지 않고, 실제 사용할 때 값이 초기화 되기 때문에 let으로 선언해도 문제가 없는 것입니다!
2. 타입 프로퍼티의 접근
위에서 이미 설명 했지만, 타입 프로퍼티의 경우
타입 자체의 이름에 .(dot) 문법을 통해 접근함
class Human {
static var name = "Sodeul"
static let age = 100
}
Human.name // "Sodeul"
Human.name = "Unknown"
Human.name // "Unknown"
Human.age = 200 // error!! 상수 변경 불가
|
이렇게!!!! var로 선언된 타입 프로퍼티의 경우, 당연히 수정도 가능함!!!
3. 연산 타입 프로퍼티의 오버라이딩
타입 프로퍼티 하다가 갑자기 오버라이딩이 나와서 당황 했겠지만
하하,,, 사실 "연산 타입 프로퍼티"는 Subclass에서 오버라이딩이 가능함!
다만, 앞에 class를 붙여주나 static으로 붙여주냐의 차이임!!!!
3-1. class : 오버라이딩이 가능한 "연산 타입 프로퍼티"
class Human {
class var alias: String {
return "Human Type Property"
}
}
class Sodeul: Human {
override class var alias: String {
return "Sodeul Type Property"
}
}
Human.alias // "Human Type Property"
Sodeul.alias // "Sodeul Type Property"
|
이렇게 class로 선언한 연산 프로퍼티의 경우,
static 선언과 마찬가지로 "연산 타입 프로퍼티"임!!!!
다만, Subclass에서 위처럼 연산 타입 프로퍼티를 오버라이딩 해서 쓸 수 있음!!!
3-2. static : 오버라이딩이 "불"가능한 "연산 타입 프로퍼티"
class Human {
static var alias: String {
return "Human Type Property"
}
}
class Sodeul: Human {
override static var alias: String { // error! Cannot override static property
return "Sodeul Type Property"
}
}
|
위에서 공부한 static으로 선언한 경우,
static property는 오버라이딩이 불가능하다며 에러가 남 :)
4. 타입 프로퍼티는 왜 쓸까?
보통 모든 타입이 공통적인 값을 정의하는 데 유용하게 쓰임!!!
가장 대표적인 것(?)이 싱글톤이다