오늘은 개발자라면 기본적으로 알아야 하며, 면접에서 단골 멘트인 Class와 Struct에 대한 것을 알아보도록 하겠습니다.
실제 코드들을 포함하여 여러가지 비교를 할 예정이기 때문에 나눠서 포스팅을 할건데, 첫 번째는 기본적인 개념과 공통점 및 차이점 등입니다.
먼저, Class와 Struct는 모두 사용자 정의 데이터 타입을 정의하는 데 사용되는 구문이며, 상태 및 동작을 설명하는 데에 상요되는 데이터 멤버와 멤버 함수를 포함합니다.
개발자로써 Class와 Struct의 정확한 차이를 이해하고 두 가지 구문을 적절하게 활용하여 코드를 최적화 하는 것이 중요하기 때문에, 이에 대해 자세히 알아 볼 필요가 있습니다.
컴퓨터공학부를 전공하면서 이 두 구문의 차이를 생각했을 때, 단순하게 Class는 참조 타입, Struct는 값 타입
이다. 라며 C++ 시간에 배웠던 것만 기억이 났습니다.
iOS를 공부하며 구글링을 통해 공부해 본 결과, Class는 ARC를 통해 메모리를 관리하고 메모리의 힙 영역에 저장된다
라는 것과 Struct는 메모리의 스택 영역에 저장된다
와 같은 새로운 사실들도 알게되었죠.
저는 보통 이런 것을 보면 어떤 식으로 이렇게 이루어지는 거지? 하며 원리와 활용 예시들을 보며 이해하는 편이기 때문에, 조금 더 자세하게 찾아보았습니다만 아직도 정확하게 모든 것을 이해하진 못해서 아쉬움이 남습니다.
하지만, 지금의 레벨에서는 모두 완벽하게 아는 것은 어렵다고 생각하기 때문에, 천천히 이론을 이해해 나가고 공부하며 성장해 나가려고 합니다.
그럼 먼저 Class와 Struct의 특징부터 알아보겠습니다.
Class
참조 타입(Reference Type)
- 인스턴스는 메모리에서 하나의 객체를 참조하며, 여러 변수나 상수가 같은 객체를 참조할 수 있습니다.
- 인스턴스가 여러 참조에 의해 공유되므로, 한 참조에서 객체의 상태를 변경하면 다른 모든 참조에서도 그 변경이 반영됩니다.
상속(Inheritance)
- 상속을 통해 다른 클래스로부터 상속받아 기능을 확장할 수 있습니다.
타입 캐스팅(Type Casting)
- 인스턴스의 타입을 런타임에 캐스팅할 수 있습니다.
- 클래스 인스턴스의 타입을 확인하거나 부모 클래스의 인스턴스를 자식 클래스의 타입으로 캐스팅할 수 있습니다. (is, as)
소멸자(deinit)
- deinit을 통해 클래스 인스턴스가 할당한 리소스를 해제할 수 있습니다.
- 해당 작업을 통해 메모리 할당 해제 시점을 확인할 수 있습니다.
메모리 영역(Memory Area)
- 클래스 인스턴스는 일반적으로 힙 메모리에 저장됩니다.
- 인스턴스를 가리키는 참조는 스택 영역에 저장됩니다.
- 쉽게 보자면 다음과 같이 저장되는 것이죠.
class AClass { }
class BClass { }
var a1 = AClass()
var a2 = a1
var b1 = BClass()
메모리 관리(Memory Management)
- ARC(Automatic Reference Counting)를 통해 메모리를 관리합니다. (매우 중요)
- 참조 카운트가 0이 되면 메모리에서 객체가 해제됩니다.
Struct
값 타입(Value Type)
- 인스턴스는 값을 직접 저장합니다.
- 구조체를 다른 변수에 할당하거나 함수를 전달할 때, 값이 복사됩니다.
- 복사된 구조체의 변경은 원본 구조체에 영향을 미치지 않습니다.
메모리 영역(Memory Area)
- 구조체 인스턴스는 일반적으로 스택 메모리에 저장됩니다.
- 클래스의 메모리 영역에 있는 그림과 비교하자면, 힙 영역이 아닌 스택 영역에 있는 변수마다 각각 구조체가 들어있다고 생각하면 됩니다.
Class와 Struct의 대표적인 특징들에 대해 알아봤습니다. 딱 봐도 Class가 Struct에 비해 뭐가 많죠??
그만큼 Class에 대해 설명할 것이 아주 많습니다. 그 것들을 여러 포스팅에 나눠서 설명할 예정이구요.
두 타입의 특징을 나열한 것이 곧 차이점과 같은데요, 그럼 공통점을 알아보겠습니다.
Class와 Struct의 공통점
속성(Properties)
- 속성을 가질 수 있습니다.
- 이 속성들을 통해 데이터를 저장하고 나타냅니다.
메서드(Methods)
- 메서드를 정의할 수 있습니다.
- 이 메서드들을 통해 데이터 수정 등을 위한 기능을 제공합니다.
생성자(Initializers)
- 생성자를 정의하여 초기 상태를 설정합니다.
- 생성자를 통해 인스턴스를 생성할 수 있습니다.
- 두 타입 모두 생성자를 정의하여 초기 설정을 할 수 있지만, 속성의 값이 옵셔널이 아니면서 특정한 값으로 정의되어있지 않은 경우, 클래스에선 생성자를 명시적으로 정의해주어야 합니다.
이유는 이렇습니다.
구조체는 기본 생성자를 제공합니다.
값 타입이고 인스턴스가 메모리의 스택 영역에 저장됩니다.
간단한 데이터 컨테이너로 동작하기 때문에, 초기화 과정이 간단하게 설계되어 있습니다.
클래스는 기본 생성자를 제공하지 않습니다.
참조 타입이고 인스턴스가 메모리의 힙 영역에 저장되고 상속을 지원합니다.
초기화 과정에서 상속받은 클래스의 초기화 과정도 고려해야 합니다.
따라서, 상속 관계에서 부모 클래스의 초기화를 보장해야 하므로 자동 생성자를 제공하기 어렵습니다.
- 프로토콜을 채택하고 준수할 수 있습니다.
- 준수한 프로토콜을 통해 특정 기능을 설정할 수 있습니다.
확장(Extension)
- 확장을 통해 기능을 확장하여 정의할 수 있습니다.
서브스크립트(Subscripts)
- 서브스크립트를 정의하여 인스턴스의 특정 값에 접근할 수 있습니다.
- subscript(index: Int) -> Any 이런 식으로 클래스나 구조체 안에 있는 배열에 접근하거나 특정 값을 반환하는 등의 작업을 할 수 있습니다.
하지만, Class와 Struct에 대한 공부와는 조금 거리가 있다고 판단하여 자세히 설명하지는 않겠습니다.
링크만 여기 걸어놓겠습니다.
Class와 Sturct의 사용
두 가지 타입의 특정을 비교하며 느낀 점은 이렇습니다. 그래서 언제 무엇을 사용해야 하는가?
실제로 네이버 부캠 베이직 과정 중, 같은 기능을 구현하는 데에도 불구하고 팀원 한 분은 구조체를 사용하고 저는 클래스를 사용하여 구현하자는 얘기가 나왔었습니다.
저의 경우는 작은 수의 속성을 가진 데이터 모델은 보통 구조체로 구현하고 그 모델을 활용한 특정 기능을 구현하는 객체는 클래스로 구현해왔었고, 팀원 분은 모두 구조체로 구현해왔던 것 같았습니다.
둘 다 현재 상황에선 뭐가 이렇기 때문에 더 좋다 라는 명확한 설명을 할 수 없었던 터라 저는 구조체로 구현하는 것이 궁금하여 구조체로 구현하자고 했던 경험이 있네요.
공식 문서에서는 다음과 같이 설명하고 있습니다.
기본적으로 구조체를 선택하세요.
- Swift에서는 구조체를 사용하여 일반적인 종류의 데이터를 표현하는 것이 좋습니다.
- 구조체는 저장 및 연산 프로퍼티, 메서드를 포함할 수 있으며, 프로토콜을 채택할 수 있습니다.
- 표준 라이브러리와 Foundation에서도 숫자, 문자열, 배열, 딕셔너리 등의 타입은 구조체를 사용합니다.
- 구조체는 값 타입이므로, 앱의 전체 상태를 고려하지 않아도 됩니다.
Objective-C와 상호 운용성이 필요한 경우 클래스를 사용하세요.
- Objective-C API를 사용해 데이터를 처리하거나, Objective-C 프레임워크에서 정의된 기존 클래스 계층에 데이터 모델을 맞춰야 한다면 클래스와 상속을 통해 데이터 모델링을 해야 할 수도 있습니다.
식별 제어가 필요한 경우 클래스를 사용하세요.
뭐가 길게 써있지만, 제대로 이해를 하기 어려워 GPT 교수님을 이용해 쉽게 이해할 수 있도록 계속 질문하며 이해하고 정리하였습니다.
- 클래스 인스턴스는 여러 변수에서 참조할 수 있기 때문에, 객체의 상태가 변경되면 해당 객체를 참조하는 모든 코드에서 변경된 상태를 확인할 수 있습니다.
- 식별 연산자를 통해 현재 접근하려는 객체가 완전히 동일한지 판단할 수 있고, 이를 통해 여러 군데에서 공유되는 인스턴스의 변경 사항이 예상치 못한 결과를 초래하는 것을 방지할 수 있습니다.
구조체와 프로토콜을 사용하여 상속과 동작 공유를 모델링하세요.
- 구조체는 클래스와 달리 상속 기능을 제공하지 않지만, 구조체와 프로토콜 상속을 이용하여 상속을 기능을 구현할 수 있습니다.
- 클래스 상속을 통해 구축할 수 있는 상속 계층을 프로토콜 상속과 구조체를 사용해 모델링할 수 있습니다.
- 상속 관계를 처음부터 구축하는 경우, 프로토콜 상속을 우선적으로 사용하세요.
클래스 상속은 클래스 간에만 호환되지만, 프로토콜은 클래스, 구조체, 열거형 모두 상속이 가능하기 때문입니다.
마치며
포스팅과 공부를 병행하며 여러 가지 자료를 찾아보고 있는데, 아직까지 실제 개발에서 언제 구조체를 사용하고 언제 클래스를 사용해야 하는 지에 대한 명확한 답을 내기가 어려운 것 같습니다.
현업에 종사하고 계시는 iOS 개발자 분들도 이 상황에 대한 의견이 많이 갈리는 것 같더라구요.
코드에 따라 구조체 안에 클래스가 들어가는 경우, 클래스 안에 구조체가 들어가는 경우 등 많은 복잡한 상황들이 존재하고 이를 memory leak을 최소화 하기 위해 어떻게 변경해야 하는 지 등등..
언젠간 저도 더 많은 것을 깨닫고 이를 실제 현업 종사자분들과 토론하며 새로운 지식을 쌓아가는 날이 있길 희망합니다.
Class와 Struct에 관한 내용 2편은 코드를 포함한 더 자세한 내용들을 다루려고 합니다.
Class에서 ARC가 참조 카운트를 어떻게 하는지, 두 타입이 어떤 상황에서 더 빠른지 등등이요.
참조
공식 문서 1
공식 문서 2
핑구님 블로그