Swift 구조체와 클래스

꼼꼼한 재은씨의 Swift3 기본편을 보며 Swift 문법 내용을 정리한 내용입니다.

클래스와 구조체

구조체와 클래스는 하나의 큰 코드 블록이다. 안에 변수 또는 상수를 넣어 값을 정의하거나, 함수를 넣어서 기능을 정의할 수도 있다. 스위프트는 객체지향 언어이기 때문에 구조체와 클래스는 문법 상 중요한 요소 중 하나이다.

  • 프로퍼티(Properties) : 구조체와 클래스 내부의 변수와 상수
  • 메서드(Method) : 구조체와 클래스 내부에서 정의된 함수
  • 멤버(Member) : 프로퍼티와 메소드를 합하여 구조체나 클래스의 멤버라고 한다.

구조체 vs 클래스

  • 공통점
    • 프로퍼티
    • 메소드
    • 서브스크립트 : 속성값에 접근할 수 있는 방법을 제공
    • 초기화 블록 : 객체를 원하는 초기 상태로 설정
    • 확장 : 객체에 함수적 기능을 추가하는 확장(extends) 구문 사용
    • 프로토콜 : 특정 형식의 함수적 표준을 제공
  • 클래스만 할 수 있는 기능
    • 상속 : 클래스의 특성을 다른 클래스에게 물려줄 수 있다.
    • 타입 캐스팅 : 실행 시 컴파일러가 클래스 인스턴스의 타입을 미리 파악하고 검사할 수 있다.
    • 소멸화 구문 : 인스턴스가 소멸되기 직전에 처리해야 할 구문을 미리 등록할 수 있다.
    • 참조에 의한 전달 : 클래스 인스턴스가 전달될 때에는 참조 형식으로 제공되며, 이 때 참조가 가능한 개수는 제약이 없다.

기본 개념

정의

구조체와 클래스의 이름은 대문자로 시작한다. 명명 규칙에 따르도록 하자.

1
2
3
4
5
6
7
8
9
// 구조체
struct 구조체 이름 {
// 정의 내용
}

// 클래스
class 클래스 이름 {
// 정의 내용
}

메소드와 프로퍼티

선언된 값에 따라 타입 선언이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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?
}

인스턴스

클래스와 구조체를 생성한 후 여러 개를 찍어내듯이 만들고 할당할 수 있다. 이 찍어낸 복사본과 같은 개념을 인스턴스라고 한다. 같은 건물의 설계도를 가지고 여러 개의 건물을 세우는 것과 유사한 방식이라고 보면 되겠다.

1
2
3
let g = guJoChae()

let c = makeClass()

. (dot)을 이용하여클래스와 구조체 내의 프로퍼티에 접근할 수 있다. 예를 들어 g.name, c.width와 같이 말이다.

초기화

구조체나 클래스 뒤에 빈괄호를 붙이면 기본적인 인스턴스가 만들어진다. 때로는 파라미터를 괄호 속에 넣어주기도 하는데, 이를 초기화(Initialize)하기 위해 반드시 필요한 값들입니다.

구조체나 클래스를 초기화할 때는 매개변수의 값 역시 초기화가 필요하다.

1
let initTest = testMethod(a: 1, b: 2)

구조체의 값 전달 방식: 복사에 의한 전달

구조체와 클래스의 차이는 값 전달 방식이다. 구조체는 인스턴스를 생성한 후 이를 변수나 상수에 할당하거나 함수의 인자값으로 전달할 때 값을 복사하여 전달하는 방식이다. 이를 값 타입(Value Type) , 또는 복사에 의한 전달 이라고 한다. 앞서 자료형은 모두 복사를 통해 값이 전달된다고 언급한 적이 있는데, 이는 자료형 자체가 실제로는 구조체로 구현되었기 때문이다.

구조체 인스턴스는 변수에 대입 시, 복사한 값으로 대입되는 것이다. 그러므로 기존 인스턴스와 대입된 인스턴스는 독립적이다. 한 쪽에서 변경이 되어도 다른 쪽에 영향을 주지 않는다.

클래스의 값 전달 방식 : 참조에 의한 전달

메모리 주소를 사용하여 객체 자체를 전달한다. 따로 주소값을 전달할 필요 없이, 객체 주소값만 넘긴다면, 자유롭게 사용이 가능하다. 클래스 초기화와 동시에 프로퍼티를 사용하는 것이 가능하다.

인스턴스 비교 연산자

1
2
// === : 동일 인스턴스인지 비교
// !== : 다른 인스턴스인지 비교

일반적으로 다음 조건에 하나 이상 해당한다면 구조체 사용을 권장한다.

  1. 서로 연관된 몇 개의 기본 데이터 타입들을 캡슐화하여 묶는 것이 목적일 때
  2. 캡슐화된 데이터에 상속이 필요하지 않을 때
  3. 캡슐화된 데이터를 전달하거나 할당하는 과정에서 참조 방식보다는 값이 복사되는 것이 합리적일 때
  4. 캡슐화된 원본 데이터를 보존해야 할 때

프로퍼티(Property)

클래스와 구조체를 구성하는 중요 요소이다. 프로퍼티는 값을 저장하기 위한 목적으로 클래스와 구조체 내에서 정의된 변수나 상수를 의미한다. 정확히는 값을 제공 한다.

프로퍼티에는 저장 프로퍼티와 연산 프로퍼티가 있다.

  • 저장 프로퍼티

    • 입력된 값을 저장, 저장된 값을 제공함
    • 상수 및 변수를 사용하여 정의가 가능함
    • 클래스와 구조체에서 사용이 가능하지만, 열거형에서는 사용할 수 없음
  • 연산 프로퍼티

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

저장 프로퍼티

클래스 내에서 선언된 변수나 상수를 부르는 이름이다. 변수는 속성 변수이고, 상수는 속성 상수라고 한다. 저장 프로퍼티는 일반적인 변수, 상수를 초기화하듯이 초기화 값을 지정할 수 있다. 단, 필수는 아니다.

초기값을 할당하지 않으면 유의해야 할 점들이 있다. 초기값이 할당되지 않은 저장 프로퍼티는 반드시 옵셔널 타입으로 선언해야 한다. 클래스의 프로퍼티에 값이 비어있다면 인스턴스를 생성할 때 무조건 nil로 초기화한다. 물론 처음부터 값 할당을 하면 옵셔널로 할 필요가 없다.

초기값을 주지 않으면서 옵셔널 타입으로 선언하지 않을 수 있는 방법도 존재한다. 초기화구문에서 프로퍼티의 값을 초기화하는 것이다. 클래스 프로퍼티는 인스턴스를 생성할 때 초기화하기 때문에 프로퍼티 초기값은 인스턴스를 생성하기 전까지만 할당하면 된다. 초기화 구문에서 프로퍼티 값 할당이 가능하다면 옵셔널로 선언하지 않아도 된다.

1
2
3
4
class User {
var name : String
// 클래스 프로퍼티의 초기값 설정이 안되어서 Error
}
1
2
3
4
5
6
7
8
/* 올바른 예시 */
class User {
var name : String

init() {
self.name = ""
}
}
1
2
3
4
5
class User {
// 옵셔널의 예시
var name : String?
// var name : String! 도 가능.
}
1
2
3
4
class User {
// 아예 초기화하는 방법
var name : String = ""
}

저장 프로퍼티의 분류

  • var 키워드로 정의되느 변수형 저장 프로퍼티(멤버 변수)
  • let 키워드로 정의되는 상수형 저장 프로퍼티(멤버 상수)
1
2
3
4
5
6
7
8
9
10
11
// 구조체의 저장 프로퍼티

struct guJoChae1 {
var intValue1 : Int
let intValue2 : Int
}

struct guJoChae2 {
let intValue3 : Int
var intValue4 : Int
}

지연 저장 프로퍼티

저장 프로퍼티는 클래스 인스턴스가 생성될 때 함께 초기화되지만, lazy키워드가 있다면 예외가 된다. 키워드 이름처럼 초기화를 지연시키는 역할을 한다. 프로퍼티가 호출되는 순간에 초기화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class OnCreate {
init() {
print("OnCreate")
}
}

class LazyTest {
var base = 0
lazy var late = OnCreate()

init() {
print("Lazy Last")
}
}

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

1
2
3
4
5
// 프로퍼티 참조 시에 최초 한 번만 실행한다.
let/var 프로퍼티명: 타입 = {
정의 내용
return 리턴값
}()

연산 프로퍼티

저장 프로퍼티와는 다르게 다른 프로퍼티의 값을 연산 처리하여 간접적으로 값을 제공한다. 프로퍼티의 값을 참조하기 위해 구문이 get구문이다.

set 구문을 추가할 수 있다. 값을 할당하거나 변경할 때 실행된다. set 구문은 필요에 따라 생략이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
class/struct/enum 객체명 {
var 프로퍼티명 : 타입 {
get {
//......
return 리턴값
}

set ( 매개변수 ) {
//......
}
}
}

다음은 진짜 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Foundation

// 나이 계산
struct UserInfo {
var birth : Int!

var thisYear : Int {
get {
let df = DateFormatter()
df.dateFormat = "yyyy"
return Int(df.string(from: Date()))!
}
}

var age : Int {
get {
return (self.thisYear - self.birth) + 1
}
}
}

let info = UserInfo(birth: 1990)
print(info.age)

// 28

프로퍼티 옵저버

특정 프로퍼티를 관찰하다가 값 등이 변경되면 이에 반응한다. 직접 변경, 자동 변경 모두 반응하여 호출된다. 동일한 값이 재할당되어도 마찬가지이다.

  • willSet : 프로퍼티의 값이 변경되기 직전 호출되는 옵저버
  • didSet : 프퍼티의 값이 변경된 직후 호출되는 옵저버
1
2
3
4
5
6
7
// willSet 옵저버

var <프로퍼티명> : <타입> [ = <초기값> ] {
willSet [ (<인자명>) ] {
<프로퍼티 값이 변경되기 전에 실행할 내용>
}
}
1
2
3
4
5
6
7
// didSet

var <프로퍼티명> : <타입> [ = <초기값> ] {
didSet [ (<인자명>) ] {
<프로퍼티 값이 변경된 후에 실행할 내용>
}
}

타입 프로퍼티

인스턴스에 관련된 값이 아니라 클래스나 구조체, 또는 열거형과 같은 객체 자체에 관련된 값을 다루는 때도 있다. 이 때는 인스턴스를 생성하지 않고 클래스나 구조체 자체에 값을 저장하게 되며 이를 타입 프로퍼티라고 한다.

저장된 값은 모든 인스턴스가 공통적으로 사용할 수 있다. 타입 프로퍼티는 인스턴스가 아무리 많아도 모든 인스턴스가 하나의 값을 공용으로 사용한다.

static키워드가 핵심이다. 다른 언어에서와 같이 어느 곳에서나 접근이 가능하다.

1
static let/var 프로퍼티명 = 초기값
1
2
3
4
5
6
7
8
class let/var 프로퍼티명 : 타입 {
get {
return 반환값
}
set {
// set 내용
}
}

메소드

함수의 일종이다. 구조체, 클래스 등에서 함수가 선언되면 메소드라고 지칭한다. 허나 함수와는 차이가 있다. 함수는 독립적으로 기능을 구현하지만, 메소드는 객체에서 정의된 다른 메소드와 함께 함수적인 기능을 수행한다.

메소드는 인스턴스 메소드타입 메소드 로 구분된다.

  • 인스턴스 메소드 : 객체의 인스턴스를 생성해야 사용할 수 있는 메소드.
  • 타입 메소드 : 객체의 인스턴스를 생성하지 않아도 사용 가능한 메소드.

인자값이 있는 메소드를 호출할 때에는 함수와 동일하다. 호출 시 인자값 앞에 인자 레이블을 붙여준다.

1
2
let temp = tempMethod()
temp.isParameter(a : 5)

구조체나 열거형의 인스턴스 메소드 내부에서 프로퍼티의 값을 수정할 때는 반드시 앞에 mutating 키워드를 추가해야 한다. 이 키워드를 통해 내부 프로퍼티 값을 수정하는 메소드라는 것을 컴파일러에게 지시한다. 당연한 말이지만, 인스턴스가 상수라면 mutating 키워드는 필요가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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("New Point (\(point.x), \(point.y)))

// 클래스 인스턴스

상속 (Inheritance)

클래스와 구조체가 구분되는 특성이다. 하나의 클래스가 다른 클래스에게 특성을 물려줄 수가 있다. 자식 클래스가 부모 클래스의 속성과 메소드를 물려받게 되고, 사용할 수 있다.

  • 프로퍼티와 메소드를 물려준 클래스는 부모 클래스, 상위 클래스, 슈퍼 클래스, 기본 클래스
  • 프로퍼티와 메소드를 물려준 클래스는 자식 클래스, 하위 클래스, 슈퍼 클래스, 파생 클래스

서브클래싱

기존 클래스를 기반으로 새로운 클래스를 작성하는 것을 의미한다.

1
2
3
class 클래스명 : 부모클래스 {
// 추가 구현 내용
}

뒤에 나오는 코코아 터치 프레임워크의 클래스 정의 구문에서 콜론 : 다음에 여러 개의 클래스가 나열되는데, 가장 첫 번째는 상속, 나머지는 자바의 implements 키워드를 의미한다.

오버라이딩

재정의 라는 의미로, 부모 클래스에서 상속받은 내용을 덮어쓴다. 재정의한 내용은 서브클래싱한 하위 클래스에만 적용된다. 부모 클래스까지의 영향이 없다는 의미이다. 스위프트에서는 오버라이딩하는 메소드나 프로퍼티 앞에 override 키워드를 붙인다. 없다면 오류로 간주한다.

만약 오버라이딩 할 수 있는 메소드나 프로퍼티가 아닌 경우는 컴파일러가 override 키워드를 오류로 간주한다. 상위 클래스에서 선언된 것만 오버라이딩이 가능하다.

프로퍼티를 오버라이딩 할 때 허용되는 것과 허용되지 않는 사항이 존재한다.

  • 허용되는 것

    1. 저장 프로퍼티를 get, set 구문이 모두 있는 연산 프로퍼티로 오버라이딩 하는 것
    2. get, set 구문이 모두 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩 하는 것.
    3. get 구문만 제공되는 연산 프로퍼티를 get, set 구문이 모두 제공되는 연산 프로퍼티로 오버라이딩하는 것
    4. get 구문만 제공되는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩 하는 것
  • 허용되지 않는 것

    1. 저장 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
    2. get, set 구문과 관계없이 연산 프로퍼티를 저장 프로퍼티로 오버라이딩하는 것
    3. 저장 프로퍼티를 get 구문만 제공되는 연산 프로퍼티(=readonly)로 오버라이딩하는 것
    4. get, set 구문을 모두 제공하는 연산 프로퍼티를 get 구문만 제공되는 연산 프로퍼티로 오버라이딩하는 것

복잡하지만 단순하다. 상위 클래스의 기능을 하위 클래스가 확장하거나 변경은 할 수 있지만 제한하는 방식이 되어서는 안된다는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LongBoard : SkateBoard {
var deck = 0
var wheel = 0

override var currentSpeed : Double {
get {
return Double(self.wheel * 50)
}
set {
// nothing
}
}

override var description : String {
get {
return "LongBoard : wheel\(self.wheel), so currentSpeed=\(self.currentSpeed)"
}
set {
print("New Value is \(newValue)")
}
}
}

오버로딩 : 같은 메소드이름이지만 매개변수 변화로 새로운 메소드를 만들어 적재할 수 있도록 한다.

오버로딩은 다음과 같다.

1
2
3
4
5
6
func currentSpeed()
func currentSpeed(param : Int)
func currentSpeed(param : String)
func currentSpeed(param : Double) -> String
func currentSpeed(param : Double, second : String)
func currentSpeed(param : Double, secondParam : String)

하위 클래스에서 오버라이딩을 차단하기 위해서는 final 키워드를 사용한다.

초기화 구문

구조체나 클래스는 항상 인스턴스를 생성해서 할당 후 사용해야 하는 데, 이를 초기화라 한다.

이 과정에서 저장 프로퍼티는 생성 과정에서 초기화되어야 한다. 그 후에 구조체나 클래스의 초기화가 가능하다.

Resolution(), Video(), Car() 등은 초기화 구문이다.

저장 프로퍼티를 초기화하는 구조체/클래스의 초기화다.

Board(id: 1, category: 3)

초기화를 할 때 주의할 점은 미리 정의된 형태로 해야 한다는 것이다.

init 메소드 (초기화)

1
2
3
4
init(매개변수 : 타입 ... ) {
// 매개변수 초기화
// 인스턴스 생성 후 처리할 로직
}

일반적인 메소드와 비슷한 모양이지만 init 메소드만의 특징이 있다.

  1. 초기화 메소드의 이름은 init으로 통일한다.
  2. 매개변수의 개수, 이름, 타입은 임의로 정의할 수 있다.
  3. 매개변수의 이름과 개수, 타입이 서로 다른 여러 개의 초기화 메소드를 정의할 수 있다.
  4. 정의된 초기화 메소드는 직접 호출되지만, 대부분 인스턴스 생성 시 간접 호출된다.
  • 여러 모양의 초기화 모양을 두고, 가까운 모양으로 호출된다.

초기화 구문의 오버라이딩

초기화 구문 역시 자식 클래스에서 오버라이딩이 가능하다. 동일하게 override 키워드를 붙여야 한다. 상위 클래스에서 선언된 적이 없는 초기화 형식이라면 붙이면 안 된다.

1
2
3
4
5
6
7
8
9
class Base {

}

class ExBase : Base {
override init() {
// some codes
}
}

메소드와는 달리 초기화 구문에서 오버라이딩은 더 이상 부모 클래스에서 정의한 초기화 구문이 실행되지 않는 오류를 일으킬 수 있다. 부모 클래스의 기본 초기화 구문에서 프로퍼티를 초기화했다면, 자식 클래스에서 기본 초기화 구문을 오버라이딩함으로써 부모클래스 프로퍼티의 초기화가 누락된다. 이는 곧 오류를 발생시키므로, 초기화 구문을 오버라이딩할 경우 부모 클래스에서 정의된 초기화 구문을 내부적으로 호출해야 하는데, 오버라이딩된 초기화 구문 내부에 super.init 구문을 작성한다.

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
var baseValue : Double
init(inputValue : Double) {
self.baseValue = inputValue
}
}

class ExBase : Base {
override init(inputValue: Double) {
super.init(inputValue: 3.14)
}
}
Author

hwiVeloper

Posted on

2017-05-31

Updated on

2022-12-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

댓글

You forgot to set the shortname for Disqus. Please set it in _config.yml.