[Swift] 클래스

Ujeon 🍵
10 min readJul 15, 2021

--

Swift의 클래스는 구조체와 매우 비슷한 형태를 가지고 있습니다. 그렇지만 차이점도 존재하는데요, 클래스는 상속이 가능한 반면 구조체는 상속이 불가능합니다. 또한 클래스는 참조 타입인 반면 구조체는 값 타입이죠.

구조체와 비슷하지만 다른, 클래스에 대해서 살펴보겠습니다.

클래스에는 멤버별 생성자가 없습니다.

멤버별 생성자(memberwise initializer)는 대상이 구조체인 경우, 컴파일러가 자동으로 생성해주는 생성자를 의미합니다.

하지만 클래스에는 멤버별 생성자가 없기 때문에 구조체처럼 인스턴스를 생성할 때 속성 값을 전달만 해주어서는 인스턴스를 생성할 수 없습니다 :

구조체에는 멤버별 생성자가 존재하여 속성값을 초기화 할 수 있습니다.

따라서 속성을 초기화하는 생성자를 작성해주어야 하죠 :

옵셔널 활용

만약 속성을 초기화 할 필요가 없다면 옵셔널을 사용하면 됩니다 :

그리고 하나의 클래스 안에 여러 개의 init 함수를 사용할 수도 있습니다. 예를 들어, UserClass 에서 name속성은 옵셔널 이지만, 다른 속성은 초기화가 필요한 경우가 있을 수 있죠 :

init 메서드 안에 내용이 중복된다면 다음과 같이 줄일 수 있습니다 :

암시적 추출 옵셔널

값이 꼭 필요하기는 하지만, 초기값을 할당하지 않고 사용할 필요가 있을 때는 암시적 추출 옵셔널을 사용합니다 :

암시적 추출 옵셔널은 !를 사용합니다.

하지만 값을 할당하지는 않았으니 값을 사용하려고 하면 오류가 발생합니다 :

값을 할당해주고 다시 사용하면 오류가 발생하지 않습니다 :

실패가능한 초기화 생성자

인스턴스를 생성할 때, 전달되는 값이 예상과 다른 경우 인스턴스를 생성하지 않고 nil 을 반환하도록 만들 수 있습니다.

매개변수 gender 로 전달되는 값이 “man” 혹은 “woman” 이 아닌 경우, nil 을 반환합니다 :

그런데 왜 클래스에는 멤버별 생성자가 없을까요?!

가장 큰 이유는 “상속” 때문입니다. 상속과 함께 멤버별 생성자를 사용하기가 어렵기 때문이죠.

예를 들어, 제가 만든 클래스를 여러분이 상속 받아서 사용하고 있다고 가정해보겠습니다. 나중에 제가 만든 클래스에 속성을 추가하게 되면 여러분의 클래스는 망가지게 될 것입니다. 제 멤버별 생성자(클래스에서 사용이 가능하다면)에 의존하던 부분이 속성을 추가함으로써 더이상 제대로 동작하지 않을 것이기 때문이죠!

클래스와 구조체의 차이점

클래스와 구조체는 다음과 같은 차이점이 있습니다 :

  1. 클래스는 멤버별 생성자가 존재하지 않습니다.
  2. 클래스는 다른 클래스를 상속받아 속성과 메서드를 사용할 수 있습니다.
  3. 구조체의 복사본은 언제나 유일하지만, 클래스의 복사본은 공유된 데이터를 바라봅니다.
  4. 클래스에는 인스턴스가 파괴될 때 실행되는 초기화 해제 메서드가 존재하지만, 구조체에는 존재하지 않습니다.
  5. 상수 클래스의 변수 속성은 자유롭게 변경이 가능하지만, 상수 구조체의 변수 속성은 변경이 불가능 합니다.

클래스 상속

클래스는 다른 클래스를 기반으로 생성할 수 있습니다. 이를 “상속”이라고 합니다.

Hero 클래스를 생성하고, 이를 상속받는 IronMan 클래스를 만들어 보겠습니다 :

클래스가 다른 클래스를 상속 받을 때에는 class 자식클래스이름: 부모클래스이름의 형태로 상속받습니다.

또 자식클래스는 자신의 생성자 메서드에서 부모의 생성자를 호출하여 초기화 하는데, 이때 super.init() 을 사용합니다. 부모 클래스가 생성될 때 중요한 작업을 할 수 있기 때문에 안정상의 이유로 super.init() 을 자식클래스의 init 에서 호출합니다.

메서드 오버라이딩

자식 클래스는 상속받은 부모 클래스의 메서드를 덮어쓸 수 있습니다. 이를 ‘오버라이딩’ 이라고 합니다.

Hero 클래스에 히어로의 이름을 출력하는 printHeroName메서드를 만듭니다 :

Hero 클래스를 상속받는 IronMan 클래스에서 위에서 만든 printHeroName 메서드를 오버라이딩 합니다 :

IronMan 의 인스턴스를 생성하고 printHeroName 을 확인해보면, 메서드가 출력하는 내용이 바뀌어 있음을 확인할 수 있습니다 :

그렇다면, 오버라이딩이 왜 필요할까요?!

누군가 만든 기능을 여러분이 사용하고 있다고 생각해봅시다. 그런데 (항상 그런건 아니지만) 어떤 기능을 조금 수정할 수 있다면 좋겠다는 생각이 듭니다.

‘이 부분만 조금 수정하면 더 괜찮을 거 같은데…’

이런 부분을 해결해줄 수 있는 방법이 오버라이딩 입니다. 코드 전체를 다시 만들지 않고도 필요한 부분만 변경할 수 있게끔 해주는 것이죠.

final 로 클래스 선언하기

Swift 에서 상속은 자주 사용되고, 권장하는 방법이지만 때로는 자식 클래스가 부모 클래스를 덮어 쓰는 것을 허용해서는 안되는 상황이 있을 수 있습니다. — 예를 들면 수정해서는 절대절대 안되는 비즈니스 로직 같은 것들이죠.

클래스를 final 로 선언하면 다른 클래스는 이 final 로 선언된 클래스를 상속 받을 수 없습니다.

IronMan 클래스를 final 로 정의합니다. 이제 다른 클래스들은 IronMan 클래스를 상속받을 수 없습니다 :

이처럼 final 키워드는 누군가가 실수로 상속받아 내용을 변경하는 것을 막고자 할 때 유용한 기능입니다.

클래스는 인스턴스를 복사하여 수정하면 원본도 함께 변경됩니다.

구조체의 인스턴스를 생성한 후, 인스턴스를 다른 변수에 복사하여 해당 변수에서 구조체의 속성 값을 변경해도 원본 인스턴스의 값은 변경되지 않습니다.

반면 클래스의 인스턴스를 생성한 후, 인스턴스를 다른 변수에 복사하여 해당 변수에서 클래스의 속성 값을 변경하면 원본 인스턴스의 값도 함께 변경됩니다.

Thor 구조체와 클래스를 생성하여 차이점을 확인해보겠습니다 :

Thor 구조체의 속성 ability를 Storm breaker 로 변경하여도 여전히 Mjölnir 가 출력됩니다.
Thor 클래스의 속성 ability를 Storm breaker로 변경하면 원본의 속성 값도 변경됩니다.

이처럼 두 경우에서 차이가 발생하는 이유는, 구조체는 값 타입인 반면 클래스는 참조 타입이기 때문입니다.

클래스에는 해체 메서드가 존재합니다.

클래스에는 해체 메서드(deinitializers)가 존재합니다. 이 메서드는 클래스의 인스턴스가 파괴될 때 호출됩니다.

반복문을 사용하여 Thor클래스의 인스턴스를 생성하고, 다음 인스턴스가 생성되기까지의 과정을 로그에 출력해보겠습니다 :

먼저 Thor 클래스의 인스턴스가 생성될 때 init 메서드가 실행되고, 그다음 내부의 printHeroName 이 실행되며, 다음 인스턴스가 생성되기 전 deinit 이 호출되는 것을 확인할 수 있습니다.

클래스에 deinit 이 필요한 이유는 무엇일까요?!

바로 클래스의 복사 방식 때문입니다. 클래스는 프로그램 여러 부분에 걸쳐서 복사본이 존재할 수 있습니다. 또 참조 타입이기 때문에 모든 복사본은 같은 데이터를 바라보고 있죠. 이 때문에 실제 클래스의 인스턴스가 언제 파괴되었는지 알리기(클래스를 가리키고 있는 마지막 변수가 사라졌을 때)가 곤란하죠.

Swift는 이를 관리하기 위해 ARC (Automatic Reference Counting)을 사용합니다. 클래스 인스턴스의 복사본이 생길 때 마다 1씩 추가되고, 복사본이 사라지면 1을 차감하는 방식으로 동작합니다. 이 ARC가 0이 되었을 때 Swift는 해당 클래스의 해체 메서드를 호출하고 객체를 파괴하죠.

반면 구조체는 deinit 이 존재하지 않는데, 구조체는 deinit 이 존재할 필요가 없기 때문이죠. 바로 구조체는 값 타입이기 때문에 복사될 때 마다 자신의 데이터를 새로이 가지게 됩니다. 이 때문에 복잡하게 관리할 필요가 없는 것이죠.

상수로 정의된 클래스 속성의 값을 변경할 수 있습니다.

구조체와는 다르게 상수로 정의된 클래스여도 클래스의 속성 값을 변경할 수 있습니다.

Thor 클래스에 name 속성을 정의하고, Thor 클래스의 인스턴스를 상수로 정의한 뒤, 속성의 값을 변경해보겠습니다 :

name의 값이 변경되었습니다.
구조체인 경우, 값을 변경할 수 없습니다.

클래스에서 값이 변경되는 것을 막기 위해서는 속성을 상수로 정의해야 합니다 :

속성을 let을 사용하여 상수로 정의하면 변경이 불가능합니다.

구조체에서 값을 변경하는 것의 의미는 구조체를 파괴하고 다시 생성하는 것을 의미합니다. 따라서 상수로 정의된 구조체의 값을 변경한다는 것은, 변하지 않을 값을 파괴하고 다시 생성한다는 의미이므로 불가능합니다.

이와 다르게 클래스에서는 클래스의 파괴와 재생성 없이 속성 값 변경이 가능하므로 상수로 정의된 클래스에서도 속성 값을 변경할 수 있습니다.

지금까지 Swift의 클래스에 대해서 알아보았습니다!

참고 자료

100 Days Of Swift : Day10 classes and inheritance

야곰의 스위프트 기본 문법 강좌 : 클래스

야곰의 스위프트 기본 문법 강좌 : 상속

야곰의 스위프트 기본 문법 강좌 : 인스턴스의 생성과 소멸

야곰의 스위프트 기본 문법 강좌 : 값 타입과 참조타입

--

--

Ujeon 🍵
Ujeon 🍵

Written by Ujeon 🍵

Hi there, this is Ujeon. I want to be a developer who passes on value through development :)

No responses yet