2. A Swift Tour (2)

문인범·2023년 10월 9일

Swift

목록 보기
3/9

스위프트 투어 두번째 시간
가보자고~

Objects and Classes (객체와 클래스)

생성

Swift의 특징 중 하나가 객체지향인 만큼 클래스를 다룬다.
클래스를 정의하여 객체를 생성하고 사용하게 된다.
클래스의 선언방법과 메소드, 프로퍼티 접근 방법은 다른 언어들과 비슷하다.

사용 방법 또한 다른 언어들과 비슷하다.
클래스를 선언하여 인스턴스를 만들고 . 을 이용해 프로퍼티나 메소드에 접근이 가능하다.

클래스의 프로퍼티가 초기화가 되어있지 않는 경우 초기화를 시켜주어야 하는데 클래스 안에 init() 메소드를 구현하면 된다.

이와 마찬가지로 객체를 할당해제하기 이전에 필요한 작업을 시키고 싶다면 deinit() 메소드를 구현하면 된다.


상속

사용법

객체지향 언어의 특징 중 하나인 상속이다.
상속은 단어 뜻 그대로 기존 클래스가 가지고 있는 특성을 물려받겠다는 의미로써 is a의 관계로써 이해하면 된다.
상속을 하는 클래스를 슈퍼클래스 상속을 받은 클래스를 서브클래스라고 하며 서브클래스는 슈퍼클래스의 메소드와 프로퍼티를 그대로 사용할 수 있다.

스위프트에서 상속하는 방법은 : 를 사용한다.

class (클래스 명): (상속할 클래스 명) { ... }

여기서 보면 init() 안에 달라진 것이 하나 있는데 super.init이 보인다.
우리는 studyClass 라는 상위 클래스(super)를 studySuper 라는 하위 클래스(sub)에 상속을 했는데 상위 클래스에 있는 num이라는 프로퍼티를 초기화 하지 않으면 사용을 할 수 없다. (is a의 관계이기 때문에 두 클래스는 동등한 관계라고 볼 수 있다.)
그래서 상위 클래스에 있는 num 프로퍼티 또한 초기화를 시켜주어야 한다.

여기서 super란 키워드를 사용하게 되는데 위에서 본 뜻 대로 상위 클래스를 의미한다.
그러므로 super.init()이란 상위 클래스의 init 메소드를 사용하겠다는 뜻이다.
해당 과정을 통해 상위 클래스의 프로퍼티의 초기화를 진행할 수 있고 프로퍼티를 사용할 수 있다.

override

상위 클래스의 프로퍼티나 메소드를 내 입맛대로 뜯어 고칠 수 있는데 그럴때 override라는 키워드를 사용해 재정의를 할 수 있다.

이런 클래스들이 있다고 할 때
하위 클래스에서 상위 클래스의 메소드를 사용하려고 하면 그대로 사용하면 된다.
하지만 하위 클래스에서 이 상위 클래스의 메소드를 다르게 변경시키고 싶을 수가 있다.
그럴 때에는 하위 클래스에서 재정의 할려는 함수 앞에 override 붙여 사용할 수 있다.

override func (재정의 하려는 함수 명) { ... }

같은 메소드를 실행시켰지만 재정의된 메소드가 실행된 것을 확인할 수 있다.

변수의 경우에도 똑같이 앞에 override를 추가함으로써 재정의를 할 수 있다.

연산 프로퍼티

기본적으로 쓰는 저장 프로퍼티 외에도 연산 프로퍼티 또한 클래스에서 사용할 수 있다.

잠깐! 여기서 연산 프로퍼티란?
일반적으로 쓰이는 저장 프로퍼티와 다르게 말 그대로 연산용으로 쓰이는 프로퍼티이다.
getter 와 setter가 있으며
getter에 경우 read(읽기), setter 는 write(쓰기)로 사용된다.
클래스에서 보호가 필요해 만든 private 변수에 간접적으로 접근을 하기 위해 사용이 가능하다.

사용 방법은 변수 뒤에 클로져를 만들어 get, set 키워드를 사용해 getter, setter를 가질 수 있다.

var (변수 명): (타입 명) { get { ... } set {...} }

get 안에 리턴 값을 넣어 해당 프로퍼티를 읽었을 때 나올 값을 만들 수 있으며, 해당 프로퍼티를 수정 함으로써 set 명령을 실행할 수 있다.

numPlusTow를 읽었을 때 3+2 인 5가 나오고, numPlusTwo에 5를 대입할 경우 studyClass 의 num 변수가 5+2 = 7로 수정이 된 것을 확인할 수 있다.


프로퍼티 옵져버(Property Observer)

저장 프로퍼티에 사용이 되며 willSet, didSet 키워드가 사용된다.
이것 또한 변수의 값이 변할 때 실행이 되며 willSet은 값이 변하기 직전 실행이 되며 새로운 값이 파라미터(newValue)로 주어진다.
didSet은 값이 변한 후 실행이 되며 바뀌기 전 값이 파라미터(oldValue)로 주어진다.




Enumerations and Structures

열거형(enum)

열거형이란 무엇인가???

동일한 주제로 모인 데이터들을 정의하고 사용하는 것이 가능한 타입

이라고 한다.....
뭔 소린지 하나도 모르겠다!

쉽게 말하자면
만약에 카드게임과 관련된 프로젝트를 진행하고 있는데, 카드별로 숫자와 모양을 정리해야 하는 부분이 있다고 보자

평소에 하던대로 String을 이용해 선언할 수 있다.
하지만 이렇게 직접 입력을 하다보면 오타나 날 가능성이 있고 상당히 귀찮다!

이것을 해결해주는 친구들이 바로 열거형이라고 할 수 있다.
열거형을 이용하여 동일한 주제를 가진 친구들을 모아 미리 작성해 놓고 필요할때 마다 꺼내 쓰는 것이다.

그럼 어떻게 사용을 해야 하는지 알아보자

1. 원시값이 없는 열거형

말 그대로 원시 값이 없는 열거형이다.
바로 위에서 한 대로 case 뒤에 값이 없는 경우가 이에 해당한다.

enum (열거형 명) { case (케이스 명) ... }

이렇게 만들어진 열거형은 . 을 이용해 접근이 가능하다!
이렇게 보다 더 간단하게 입력이 가능하다.

2. 원시값이 있는 열거형

이와 반대로 원시값이 있는 열거형이다.

원시값은 총 3가지 타입으로 정의가 가능하다.

  • Number Type
  • Character Type
  • String Type

이 원시값을 사용하기 위해서는 클래스 상속할 때와 비슷하게 열거형 이름 뒤에 : 를 붙여야 사용이 가능하다.

  • Number Type
    이렇게 Int 선언하면 사용이 가능하며, 초기값을 따로 지정해 주지 않은 경우 첫번째 케이스에 0이 들어가며 +1씩 증가하게 된다.
    초기값을 정해주는 경우에는 초기값을 기준으로 +1씩 증가하게 된다.

    이와 비슷하게 Double, Float도 선언이 가능한데, 이 경우에는 모든 케이스에 있어 값을 지정해 주어야 한다.
    왜냐하면 케이스에 값이 없을 경우 전 케이스의 값에 +1 한 정수값을 반환하기 때문에 에러가 일어난다.
    이렇게!

  • Character Type
    Character 타입을 원시값으로 가지는 열거형을 만들 수 있다.

    더블 타입과 비슷하게 모든 케이스에 대해 값을 넣어주어야 한다.
    여기에서는 H 라는 캐릭터에서 +1이 불가능하여 에러가 나기 때문이다.

  • String Type
    String 타입에 경우엔 모든 케이스에 대해 값을 넣어주지 않아도 된다.
    비어있는 케이스는 케이스명 그대로 값이 들어간다.

3. 연관 값을 가진 열거형

연관 값(Associated Value)을 가진 열거형이다.
이게 무슨 말이냐 하면.....

기존 원시값을 가진 열거형의 경우에는 특정 타입 하나만 케이스에 저장할 수 있으므로 다양한 정보를 담지 못한다. 또한 케이스 마다 다른 정보를 담고 싶은 경우에도 각 케이스마다 원시값이 하나로 정해지는 만큼 다양하게 쓰이지 못한다.
이렇게 카드의 숫자가 얼마인지, 언제 뽑았는지를 저장하고 싶은데 원시값이 고정되어 있어 사용하기 힘들다.

이럴때 활용할 수 있는 것이 연관 값 이다.

선언 방법

이런 식으로 케이스 뒤에 튜플형식으로 타입을 지정하여 사용이 가능하다.

라벨을 붙여 사용도 가능하다.

사용 방법

이런 식으로 직접 원하는 값을 넣어 선언하면 된다.

이런식으로 만들어진 열거형들은 switch 구문을 사용해 활용이 가능하다!
다양한 조건을 주어 사용하면 된다.

또한 프로토콜 채택이 가능하다.
이런 식으로 CaseIterable 같은 다양한 프로토콜을 채택하고 사용 또한 가능하다!


구조체(Structures)

구조체는 클래스와 거의 비슷한 역할을 하며 쓰이는 방법도 거의 똑같다.

인스턴스 생성 방법 또한 똑같이 가능하다!

일단 눈썰미가 좋은 분들이라면 한 가지 차이점을 찾았을 것이다.
"저 위에 구조체는 init()함수가 없는데요???"

구조체 에서는 초기화 함수를 따로 정의할 필요가 없이 자동으로 만들어준다.
그래서 생략이 가능하며, 혹시 직접 초기화 함수를 정의하고 싶다면 클래스와 똑같이 init() 함수를 직접 정의해주면 된다!(구조체에선 deinit() 함수는 지원하지 않는다고 합니다.)

그렇다면 이걸 쓰는 이유가 무엇인지 모르겠는 사람들이 있을것이다. 나도 모르겠더라. ㅎㅎ;;

일단 제일 큰 차이점은 클래스는 참조 타입(Reference Type) 으로 작동하지만 구조체는 값 타입(Value Type)으로 작동한다.
예시를 보며 이해해보자.
이런 클래스가 있다고 할 때
클래스는 참조 타입이라고 했다.
그래서 ab는 서로 참조하는 관계가 되어 있기 때문에 anum 프로퍼티를 10으로 수정하면 a를 참조하고 있는 bnum 프로퍼티 또한 10으로 변할 것이다. a만 바꿨는데 b또한 같이 바뀐 것을 알 수 있다.

이와 반대로 구조체의 경우에는
값 타입 이기 때문에 a의 값이 b로 복사 되었고 a의 값만 바뀐 것을 확인할 수 있다.

또한 클래스와 다르게 상속이 불가능하다는 점이 있다.(프로토콜은 똑같이 채택이 가능함)

정리하자면 다음과 같은 차이점이 있다.

  • 초기화 함수가 자동으로 제공된다.(deinit()은 사용 불가)
  • 클래스와 다르게 값 타입으로 작동된다.
  • 상속이 불가능하다.(프로토콜은 채택 가능)

그렇다면 어떨 때 구조체를 사용해야 할까?

애플에서는 이렇게 알려주고 있다.

기본적으로는 구조체를 사용하되 Objective-C와의 상호운영이 필요할 때, 모델링 한 데이터의 동일성의 통제가 필요할 때는 클래스 사용을 추천한다고 한다.
또한 클래스를 통한 상속을 통한 계층 형성 보다는 구조체와 프로토콜 채택을 통한 계층 형성을 우선시 하는 것이 좋다고 나와있다.

자세한 내용은 Choosing Between Structures and Classes를 참고하기 바란다.

다음 글에는 Concurrency(동시성), Protocols(프로토콜)에 대해 알아보도록 하자😙

profile
월클 개발자를 향한 도전일지

0개의 댓글