🚀 유연성 : 코드를 떼어서 다른 곳으로 옮기거나 새로운 코드를 추가하기가 쉽다는 뜻으로 의존성의 반대 개념
🚀 구조체 VS 클래스
[ 두 객체의 공통점 ]
- 프로퍼티 : 변수나 상수를 사용하여 값을 저장하는 프로퍼티를 정의할 수 있다.
- 메소드 : 함수를 사용하여 기능을 제공하는 메소드를 정의할 수 있다.
- 서브 스크립트 : 속성값에 접근할 수 있는 방법을 제공하는 서브스크립트를 정의할 수 있다.
- 초기화 블록 : 객체를 원하는 초기 상태로 설정해주는 초기화 블록을 정의할 수 있다.
- 확장 : 객체에 함수적 기능을 추가하는 확장 구문을 사용할 수 있다.
- 프로토콜 : 특정 형식의 함수적 표준을 제공하기 위한 프로토콜을 구현할 수 있다.
하지만, 두 객체가 서로 완전히 같다면 굳이 구조체와 클래스를 구분할 필요가 없을 것이다. 클래스와 구조체는 몇 가지 면에서 결정적인 차이가 있는데, 클래스의 기능 범위가 구조체보다 더 크다. 구조체는 할 수 없지만, 클래스는 할 수 있는 기능에는 다음과 같은 것들이 있다.
- 상속 : 클래스의 특성을 다른 클래스에게 물려줄 수 있다.
- 타입 캐스팅 : 실행시 컴파일러가 크래스 인스턴스의 타입을 미리 파악하고 검사할 수 있다.
- 소멸화 구문 : 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록해 놓을 수 있다.
- 참조에 의한 전달 : 클래스의 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 이때 참조가 가능한 개수는 제약이 없다.
구조체와 클래스 기본 개념
- 구조체 정의 형식
struct 구조체_이름 {
// 구조체 정의 내용이 들어갈 부분
}
- 클래스의 정의 형식
class 클래스_이름 {
// 클래스 정의 내용이 들어갈 부분
}
- 각 객체의 이름을 작성할 때에는 표준 스위프트 객체 코딩 형식에 따라 구조체 이름과 클래스 이름의 첫 글자는 대문자로, 나머지 글자는 소문자로 작성하는 것이 원칙이다. (Camel 표기법)
메소드와 프로퍼티
🚀 프로퍼티 (혹은 속성) : 구조체와 클래스 내부에서 정의된 변수나 상수
🚀 메소드 : 함수를 정의하여 특정 기능을 정의하는 것
// 프로퍼티와 메소드가 정의된 구조체와 클래스 예시
struct Resolution {
var width = 0
var height = 0
func desc() -> String {
return "Resolution 구조체"
}
}
class VideoMode {
var interlaced = false
var frameRate = 0.0
var name: String?
func desc() -> String {
return "VideoMode 클래스"
}
}
인스턴스
인스턴스란 타입의 설계도를 사용하여 메모리 공간을 할당받은 것. 우리가 실질적인 값을 담을 수 있는 것은 바로 인스턴스이다.
🚀 구조체와 클래스의 인스턴스를 생성하는 방식은 거의 같다. 다음은 앞에서 정의한 구조체와 클래스를 이용하여 인스턴스를 만드는 과정이다.
// Resolution 구조체에 대한 인스턴스를 생성하고 상수 insRes에 할당
let insRes = Resolution()
// VideoMode 클래스에 대한 인스턴스를 생성하고 상수 insVMode에 할당
let insVMode = VideoMode()
🚀 위 예제에서는 구조체와 클래스 각각의 인스턴스를 생성하고 있다. 객체가 초기화 되면서 인스턴스가 생성되고, 이 값을 변수나 상수에 할당하면 이제 원하는 곳에서 사용할 수 있게 된다. 이 구문을 "인스턴스 생성 구문" 이라고 한다.
🚀 위의 예제에서 선언된 프로퍼티는 오직 인스턴스를 통해서만 접근할 수 있다. 인스턴스가 생성되지 않은 상태에서는 프로퍼티도 존재하지 않는 것이나 마찬가지이다. 이 때문에 구조체나 클래스의 이름을 통해서는 프로퍼티에 접근할 수 없다. 프로퍼티에 접근하려면 반드시 인스턴스를 먼저 생성해야 한다. 프로퍼티에 접근할 때는 점 문법을 이용하여 인스턴스의 하위 객체에 접근할 수 있다. 점을 이용하여 프로퍼티에 접근할 때는 인스턴스 이름 바로 다음에 점으로 구분하여 프로퍼티의 이름을 작성한다. 이 때 점 앞이나 뒤는 공백이 없어야 한다.
<인스턴스이름>.<프로퍼티이름>
let width = insRes.width
print("insRes 인스턴스의 width 값은 \(width)입니다.")
실행 결과 >>
insRes 인스턴스의 width 값은 0입니다.
🚀 만약 객체에 정의된 프로퍼티가 서브 프로퍼티를 가지고 있는 객체라면 다음과 같이 계속 점 구문을 이용하여 단계적 접근이 가능하다.
<인스턴스 이름>.<프로퍼티 이름>.<프로퍼티의 서브 프로퍼티 이름>
초기화
스위프트에서 옵셔널 타입으로 선언되지 않은 모든 프로퍼티는 명시적으로 초기화해 주어야 한다. 초기화되지 않은 프로퍼티가 있을 경우 컴파일러는 이를 컴파일 오류로 처리한다. 명시적 초기화란 아래 두 가지 경우 중 하나이다.
- 프로퍼티를 선언하면서 동시에 초기값을 지정하는 경우
- 초기화 메소드 내에서 프로퍼티의 초기값을 지정하는 경우
이것의 의미하는 바는 클래스나 구조체의 모든 프로퍼티는 적어도 인스턴스가 생성되는 시점까지는 반드시 초기화되어야 한다는 것이다. 구조체는 모든 프로퍼티의 값을 인자값으로 입력받아 초기화하는 기본 초기화 구문을 자동으로 제공한다. 프로퍼티를 보통 멤버 변수라고 하기 때문에 이 초기화 구문을 멤버와이즈 초기화 구문이라고 부르기도 한다.
// width와 height를 매개변수로 하여 Resolution 인스턴스를 생성
let defaultRes = Resolution(width: 1024, height: 768)
- 클래스의 프로퍼티와 초기화 구문에 대해 가급적 자음의 두 가지 원칙을 지키는 것이 좋다.
- 모든 프로퍼티는 정의할 때 초기값을 주던가, 아니면 옵셔널 타입으로 선언한다.
- 인스턴스를 생성할 때에는 클래스명 뒤에 ( )를 붙여준다.
구조체의 값 전달 방식 : 복사에 의한 전달
구조체는 인스턴스를 생성한 후 이를 변수나 상수에 할당하거나 함수의 인자값으로 전달할 때 값을 복사하여 전달하는 방식을 사용한다. 이를 값 타입 또는 복사에 의한 전달이라고 한다.
구조체 인스턴스를 변수에 대입하면 기존의 인스턴스가 그대로 대입되는 것이 아니라 이를 복사한 새로운 값이 대입된다. 따라서 변수에 대입된 인스턴스와 기존의 인스턴스는 서로 독립적이다. 인스턴스 할당 후에 기존 인스턴스나 할당된 쪽의 인스턴스에 변경이 발생하더라도 서로 전혀 영향을 끼치지 않는다. 양쪽은 값의 복사가 끝난 순간부터 아무 연관도 없는 독립된 인스턴스이기 때문이다.
- 복사된 전체 값이 할당되는 구조체의 특성은 인스턴스를 상수에 할당할 것인지 변수에 할당할 것인지에도 영향을 미친다. 구조체 인스턴스가 상수에 할당되면 프로퍼티 값을 변경할 수 없다. 값을 변경할 수 있으려면 변수에 할당해야 한다.
클래스의 값 전달 방식 : 참조에 의한 전달
클래스는 메모리 주소 참조에 의한 전달 방식을 사용한다. 이를 참조 타입이라고 한다. 참조 타입은 변수나 상수에 할당될 때 혹은 함수의 인자값으로 전달될 때 값의 복사가 이루어지지 않는다. 대신, 현재 존재하는 인스턴스에 대한 참조가 전달된다.
let video = VideoMode()
video.name = "Original Video Iinstance"
print("video 인스턴스의 name 값은 \(video.name!) 입니다.")
VideoMode 클래스를 초기화하여 인스턴스를 생성하고 video 상수에 할당하였다.
그런 다음 인스턴스의 name 프로퍼티에 값을 입력해준 것이다.
인스턴스의 비교 연산자
동일 인스턴스인지 비교할 때 : ===
동일 인스턴스가 아닌지 비교할 때 : !==
🚀 클래스와 구조체는 주로 프로그램을 작성하는 과정에서 원하는 대로 데이터 형식을 정의하기 위해 사용한다. 단순히 배열이나 딕셔너리, 집합 등의 데이터 형식만으로는 원하는 타입을 만들기 어려울 때 클래스나 구조체의 형식을 이용하여 원하는 타입으로 작성하게 된다. 그런데 지금까지 알아본 것처럼 구조체는 값 자체가 복사 전달되는 타입이고, 클래스는 참조 정보가 전달되는 타입이다. 어떤 경우에 구조체를 사용하고, 어떤 경우에 클래스를 사용해야 할까?
일반적으로 다음 조건에 하나 이상에 해댱한다면 구조체를 사용하는 것이 좋다.
- 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때
- 캡슐화된 데이터에 상속이 필요하지 않을 때
- 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다 값이 복사되는 것이 합리적일 때
- 캡슐화된 원본 데이터를 보존해야 할 때
프로퍼티
저장 프로퍼티
- 입력된 값을 저장하거나 저장된 값을 제공하는 역할
- 상수 및 변수를 사용해서 정의 가능
- 클래스와 구조체에서는 사용이 가능하지만 열거형에서는 사용할 수 없음
- 클래스 내에서 선언된 변수나 상수를 부르는 이름
- 프로퍼티 선언 시 초기값이 할당되지 않은 저장 프로퍼티는 반드시 옵셔널 타입으로 선언해 주어야 한다.
- 물론 초기값을 처음부터 할당해준다면 옵셔널 타입으로 선언할 필요가 없다.
- 구조체는 초기값으로부터 자유롭기 때문에 초기값 할당하지 않고 선언만 하더라도 프로퍼티의 타입을 옵셔널로 저장해주지 않아도 된다. 멤버와이즈 초기화 구문이 제공되기 때문이다.
// 초기화 구문 작성하고 그 안에서 초기값 할당
class User {
var name: String
init() {
self.name = ""
}
}
// 프로퍼티를 옵셔널 타입으로
class User {
var name: String?
}
(또는)
class User {
var name: String!
}
// 프로퍼티에 초기값을 할당
class User {
var name: String = ""
}
- 지연 저장 프로퍼티 - lazy
일반적으로 저장 프로퍼티는 클래스 인스턴스가 처음 생성될 때 함께 초기화되지만, 저장 프로퍼티 정의 앞에 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!"
- 클로저를 이용한 저장 프로퍼티 초기화
let/var 프로퍼티명: 타입 = {
정의 내용
return 반환값
}
이렇게 정의된 클로저 구문은 클래스나 구조체의 인스턴스가 생성될 때 함께 실행되어 초기값을 반환하고 이후로는 해당 인스턴스 내에서 재실행되지 않는다.
연산 프로퍼티
- 특정 연산을 통해 값을 만들어 제공하는 역할
- 변수만 사용해서 정의 가능
- 클래스, 구조체, 열거형 모두에서 사용 가능
🚀 저장 프로퍼티와 연산 프로퍼티는 대체로 클래스나 구조체를 바탕으로 만들어진 개별 인스턴스에 소속되어 값을 저장하거나 연산 처리하는 역할을 한다. 따라서 프로퍼티를 사용하려면 인스턴스가 필요하다. 인스턴스를 생성한 다음 이 인스턴스를 통해 프로퍼티를 참조하거나 값을 할당해야 한다. 이러게 인스턴스에 소속되는 프로퍼티를 인스턴스 프로퍼티 라고 한다.
🚀 예외적으로 일부 프로퍼티는 클래스와 구조체 자체에 소속되어 값을 가지기도 한다. 이런 프로퍼티들을 타입 프로퍼티라고 한다. 타입 프로퍼티는 인스턴스를 생성하지 않아도 사용할 수 있다.
프로퍼티 옵저버
특정 프로퍼티를 계속 관찰하고 있다가 프로퍼티의 값이 변경되면 이를 알아차리고 반응하는 것이다. 프로퍼티 옵저버는 우리가 프로퍼티 값을 직접 변경하거나 시스템에 의해 자동으로 변경하는 경우에 상관없이 일단 프로퍼티의 값이 설정되면 무조건 호출된다. 심지어 프로퍼티에 현재와 동일한 값이 재할당되더라도 어김없이 호출된다.
🚀 프로퍼티의 옵저버에는 두가지 종류가 있다.
- willSet : 프로퍼티 값이 변경되기 직전에 호출되는 옵저버
예전 값 : 프로퍼티 참조 / 새로운 값 : new Value 참조
var <프로퍼티명> : <타입> [ = <초기값> ] { willSet [ (<인자명>) ] { <프로퍼티 값이 변경되기 전에 실행할 내용> } } // [] 안의 내용은 생략 가능
- didSet : 프로퍼티의 값이 변경된 직후에 호출되는 옵저버
예전 값 : old Value 참조 / 새로운 값 : 프로퍼티 참조
var <프로퍼티명> : <타입> [ = <초기값> ] { didSet [ (<인자명>) ] { <프로퍼티 값이 변경된 후에 실행할 내용> } } // [] 안의 내용은 생략 가능
- 항상 willSet 옵저버와 didSet 옵저버를 함께 구현해야 하는 것은 아니다. 어느 한쪽 옵저버만 필요한 경우에는 나머지 하나는 구현할 필요 없이, 사용해야 할 옵저버만 선택적으로 구현하면 된다.
타입 프로퍼티
인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하는 것이다. 타입 프로퍼티는 클래스나 구조체의 인스턴스에 속하는 값이 아니라 클래스나 구조체 자체에 속하는 값이므로 인스턴스를 생성하지 않고 클래스나 구조체 자체에 저장하게 되며, 저장된 값은 모든 인스턴스가 공통으로 사용할 수 있다.
static let/var 프로퍼티명 = 초기값
혹은
class let/var 프로퍼티명 : 타입 {
get {
return 반환값
}
set {
}
}
- 타입 프로퍼티는 인스턴스에 속하지 않는 값이므로 만약 인스턴스를 생성한 다음 점 구문을 이용하여 타입 프로퍼티를 읽으려고 하면 선언되지 않은 프로퍼티라는 오류가 발생한다. 타입 프로퍼티는 반드시 클래스나 구조체 혹은 열거형 자체와 함께 사용해야 한다.
메소드
메소드는 일종의 함수로서, 클래스나 구조체, 열거형과 같은 객체 내에서 함수가 선언될 경우 이를 메소드라고 통칭한다. 즉, 메소드는 특정 타입의 객체 내부에서 사용하는 함수라고 할 수 있다.
인스턴스 메소드
클래스, 구조체 혹은 열거형과 같은 객체 타입이 만들어내는 인스턴스에 소속된 함수이다. 객체의 인스턴스에 대한 기능적 측면을 제공한다. 인스턴스 메소드는 객체 타입 내부에 선언된다는 점을 제외하고는 일반 함수와 선언하는 형식이 완전히 동일하다.
인스턴스 메소드는 같은 객체 내에서 정의된 다른 인스턴스 메소드나 인스턴스 프로퍼티에 접근 할 수 있도록 권한이 부여되며, 해당 메소드가 속한 인스턴스를 통해서만 호출될 수 있다. 인스턴스 없이 독립적으로 호출될 수 없다는 뜻이다. 따라서 인스턴스 메소드는 구조체나 클래스, 열거형 등의 객체 타입을 인스턴스화 한 후, 이 인스턴스를 통해 호출하게 된다.
struct Resolution {
var width = 0
var height = 0
// 구조체의 요약된 설명 리턴해주는 인스턴스 메소드
func desc() -> String {
let desc = "이 해상도는 가로 \(self.width) X \(self.height) 로 구성")
return desc
}
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
// 클래스의 요약된 설명을 리턴해주는 인스턴스 메소드
func desc() -> String {
if self.name != nil {
let desc = "이 \(self.name!) 비디오 모드는 \(self.frameRate)의 프레임 비율로 표시된다."
return desc
} else {
let desc = "이 비디오 모드는 \(self.frameRate)의 프레임 비율로 표시된다.")
return desc
}
}
}
- 인스턴스 메소드와 일반 함수의 차이는 아래와 같다.
- 구조체와 클래스의 인스턴스에 소속된다.
- 메소드 내에서 정의된 변수와 상수뿐만 아니라 클래스 범위에서 정의된 프로퍼티도 모두 참조할 수 있다
- self 키워드를 사용할 수 있다.
self.프로퍼티명
// 인스턴스 메소드 내에서 프로퍼티를 읽어올 경우 위처럼 참조한다.
- 프로퍼티와 일반 변수의 이름이 충돌할 경우에는 프로퍼티 앞에 반드시 self 키워드를 붙여주어야 한다.
- 구조체나 열거형의 인스턴스 메소드 내부에서 프로퍼티의 값을 수정할 때는 반드시 메소드의 앞에 'mutating'이라는 키워드를 붙여주어야 한다. 그리고 구조체나 열거형 인스턴스를 상수로 할당받으면 mutating 메소드를 호출할 수 없다.
struct Point {
var x = 0.0, y = 0.0
mutating func moveByX(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}
var point = Point(x: 10.5, y:12.0)
point.moveByX(x:3.0, y: 4.5)
print("이제 새로운 좌표는 (\(point.x), \(point.y)) 입니다.")
실행 결과 >>
이제 새로운 좌표는 (13.5, 16.5) 입니다.
// 클래스의 인스턴스 메소드에서는 프로퍼티 수정할 때 별도의 키워드를 필요로 하지 않는다.
class Location {
var x = 0.0, y = 0.0
func moveByX(x deltaX: Double, y deltaY: Double) {
self.x += deltaX
self.y += deltaY
}
}
var loc = Location()
loc.x = 10.5
loc.y = 12.0
loc.moveByX(x: 3.0, y:4.5)
print("이제 새로운 좌표는 (\(loc.x), \(loc.y)) 입니다.")
실행 결과 >>
이제 새로운 좌표는 (13.5, 16.5) 입니다.
타입 메소드
- 인스턴스를 생성하지 않고 클래스나 구조체 자체에서 호출할 수 있는 메소드
- 구조체나 열거형, 클래스 모두 타입메소드를 선언할 때에는 static 키워드를 사용한다. 반면 하위 클래스에서 재정의 가능한 타입 메소드를 선언할 때는 class 키워들르 사용한다. 물론 이 키워드는 클래스 타입에서만 사용할 수 있다.
- 타입 메소드는 객체 타입 전체에 영향을 미친다. 즉, 타입 메소드를 사용하여 객체의 값을 변경하면 해당 객체 타입을 사용하는 모든 곳에서 변경된 값이 적용된다는 뜻이다. 타입 메소드를 선언하고 사용할 때에는 반드시 이런 영향 범위를 고려하여 사용해야 한다.