[Swift] 왕초보를 위한 Class 와 Struct

술술·2024년 3월 22일

TIL

목록 보기
11/21

오늘은 저만 모르는 것 같은 Class와 Struct에 대해 알아보려고 합니다.
여러 블로그를 봤지만 무슨 소리인지 모르겠는 말이 너무 많아서, 다른 용어 검색했다가 꼬리에 꼬리를 물고 모르는 것 검색 무한 반복을 하고 있더라고요. 그러다가 다른 길로 새서 결국 원래 찾아보고자 했던 건 잊어버리는..😅
그래서 한꺼번에 정리를 해보려고 해요.
포스팅이 상당히 길어질 것이라는 말ㅎ
이것만 읽고 Class와 Struct를 이해했다! 가 목표

Class의 특징

Reference Type (참조 타입)
같은 클래스 인스턴스를 만들고 값을 변경하면 모든 변수에 영향을준다.

  • 인스턴스란?
  class Person {
      var name: String = "gosim"
  }
  
  // 클래스 / 구조체 / 열거형에서 생성된 객체
  // 이게 인스턴스
  var instance = Person() 
  
  • 상속이 가능하다.
  • 타입 캐스팅을 통해 런타임에서 클래스 인스턴스의 타입을 확인할 수 있다.

    타입 캐스팅 (Type casting) 은 인스턴스의 타입을 확인하거나 해당 인스턴스를 자체 클래스 계층 구조의 다른 곳에서 다른 상위 클래스 또는 하위 클래스로써 취급하는 방법

  • 인스턴스가 소멸될때 deinit 메서드가 호출된다.
    • 클래스는 참조타입이기 때문에, 더 이상 참조할 필요가 없을때 메모리에서 소멸된다. (class 인스턴스에 nil을 넣었을때)
    • 이때 호출되는 메서드가 deinit 메서드이다. 따라서 인스턴스가 소멸되기 직전에 처리할 코드를 넣어주게된다.
    • 그래서 몇군데서 참조하고 있는지 알 수 있게 자동으로 참조 카운트(automatically ref counted) 를 세고, 얘를 가리키는 애가 더이상 존재하지 않으면 힙에서 사라짐.
class Person{
  	var name = "kim"
  	var age = "20"
  	deinit {
    	print("Person 클래스 인스턴스가 소멸됩니다.")
  	}
}

let kim = Person()
kim = nil // >>> Person 클래스 인스턴스가 소멸됩니다.
  • 객체 지향 프로그래밍(Object-Oriented Programming, OOP) 지원

    객체지향 프로그래밍(Object-Oriented Programming, OOP)은 프로그램을 명령어의 집합으로 보는 것을 넘어, 여러 객체들의 모임으로 보는 것이다.

  • 언제나 변경 가능함(mutable). pointer 갖고 있기 때매 해당 Heap 에 가서 변경하면 되는 거
    • Heap에 대한 이야기는 아래에서!
  • 자동 참조 카운팅 (Automatic Reference Counting, ARC)로 메모리 관리를 한다.


Struct의 특징

Value Type
여러 인스턴스를 만들고 값을 변경해도 , 각 인스턴스의 값은 다르다.

  • 구조체는 상속할 수 없다.
  • 타입캐스팅은 클래스의 인스턴스에만 허용된다.
  • deinit은 클래스의 인스턴스에서만 호출된다. deinit은 사용 불가능하다.
  • 참조 횟수 계산은 클래스의 인스턴스에만 적용된다.
  • 구조체는 생성자를 구현하지 않아도 default initalizer 를 사용할 수 있다.
  • 구조체 내부에 구조체, 클래스 내부에 클래스 등 중첩타입의 정의 및 선언이 가능하다.
  • Swift 표준 라이브러리의 기본 타입은 모두 구조체이다.(String, Bool, Int, Array, Dictionary, Set .....)
  • 함수형 프로그래밍(Functional Programming, FP) 지원

    함수형 프로그래밍(Functional Programming, FP)이란 "순수함수"를 이용해서 프로그래밍을 하는 것


Class, Struct 공통점

  • 값을 저장할 프로퍼티를 선언할 수 있다.
  • 메서드를 선언할 수 있다.
  • 내부 프로퍼티에 .를 사용하여 접근할 수 있다.
  • 생성자(init)를 사용해 초기 상태를 설정할 수 있다.
  • extension을 사용하여 기능을 확장할 수 있다.
  • Protocol을 채택하여 기능을 설정할 수 있다.

    프로토콜 (protocol) 은 메서드, 프로퍼티, 그리고 특정 작업이나 기능의 부분이 적합한 다른 요구사항의 청사진을 정의합니다.

    확장 (Extensions) 은 기존의 클래스, 구조체, 열거형, 또는 프로토콜 타입에 새로운 기능을 추가합니다.


Class, Struct 차이점

  • 가장 큰 차이점은 구조체는 값타입, 클래스는 참조타입이라는 점이다.
    이 때문에 파생되는 차이점들이 존재한다.
class SimpleClass {
    var count: Int = 0
    
    deinit {
        print("할당 해제")
    }
}
struct SimpleStruct {
    var count: Int = 0
}

var class1 = SimpleClass()
var class2 = class1
var class3 = class1

class3.count = 3

print(class1.count) // 3
// class3의 값을 변경했지만 참조타입이므로 class1도 변경 되는 것을 볼 수 있습니다.

var struct1 = SimpleStruct()
var struct2 = struct1
var struct3 = struct1

struct2.count = 2
struct3.count = 3

print(struct1.count) // 0
print(struct2.count) // 2 <- 구조체는 값 타입이므로 항상 새로운 메모리가 할당됩니다.
print(struct3.count) // 3
  • 구조체는 언제 생기고, 사라질지 컴파일 단계에서 알 수 있기 때문에 메모리의
    Stack 공간에 할당되고

  • 클래스는 참조가 어디서 어떻게 될지 모르기 때문에 Heap이라는 공간에 할당

  • 일단 참조타입은 스택에 저장되긴 하는데, 값이 저장되는 것이 아니라 주소가 저장.
    값은 힙에 저장

  • 즉, 참조타입은 힙 영역에 값을 저장하고 이를 가리키는 주소를 스택 영역에 저장

Stack 할당

  • 스택 자체의 크기는 컴파일 시점에 결정
  • Stack은 LIFO(Last In First Out) 형태의 자료구조로 가장 마지막에 들어간 객체가 가장 먼저 나오게 되는 자료구조인데요, 자료구조 특성상 하나의 명령어로 메모리를 할당, 해제할 수 있습니다. 또한 컴파일 단계에서 언제 생성되고 해제되는지 알 수 있는 구조체와 같은 값들이 스택에 저장되게 됩니다.
  • 스레드들은 각각 독립적인 Stack 공간을 가지고 있기 때문에 상호 배제를 위한 자원이 필요하지 않습니다. 즉 스레드로부터 안전하다는 말이 됩니다. 이러한 특징 때문에 Stack의 값을 사용하는 것이 Heap의 값을 사용하는 것보다 빠르다고 할 수 있습니다.

    스레드(thread)는 어떠한 프로그램 내에서, 특히 프로세스 내에서 실행되는 흐름의 단위를 말한다. 일반적으로 한 프로그램은 하나의 스레드를 가지고 있지만, 프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행할 수 있다. 이러한 실행 방식을 멀티스레드(multithread)라고 한다.


Heap 할당

  • Heap에는 컴파일 단계에서 생성과 해제를 알 수 없는 참조 타입의 객체가 할당됩니다.
  • 즉 Swift에서는 클래스 객체가 힙에 할당되게 됩니다.
  • Heap은 Stack보다 관리하기가 어려운데요, 이는 메모리 할당과 해제가 하나의 명령어로 처리되지 않기 때문입니다. 아까 Stack에서는 pop, push라는 하나의 명령어로 할당, 해제가 이루어졌지만 Heap은 참조 계산도 해줘야 하므로 Stack보다 복잡합니다.
  • 또한 Heap은 스레드가 공유하는 메모리 공간이기 때문에 스레드로부터 안전하지 않습니다. 즉 이를 관리해주기 위한 lock과 같은 자원도 필요하게 되고 이는 곧 오버 헤드로 이어지게 됩니다.

    오버헤드(overhead)는 어떤 처리를 하기 위해 들어가는 간접적인 처리 시간 · 메모리 등을 말한다.
    이번에 말하는 오버헤드는 실제 실행되는 메서드를 코드를 훑으며 찾는 과정이다.
    즉, 오버헤드가 발생하면 비용이 발생한다.



구조체와 클래스가 중첩된 경우는 힙 영역에 저장될까? 스택 영역에 저장될까?

1. 값 타입을 포함하는 참조타입

  • 클래스 내부에 구조체 프로퍼티가 있는 경우를 말한다. 이러한 경우에는 참조타입이 소멸되기 전에 값 타입도 힙에 저장된다.
  • 클로저 내부에 사용하는 값 타입도 이러한 경우에 포함된다.

2. 참조 타입을 포함하는 값 타입

  • 구조체 내부에 클래스 프로퍼티가 있는 경우를 말한다. 내부에 참조타입이 있기 때문에 참조 카운팅을 처리해주어야 한다.

값 타입의 Copy-on-assignment, Copy-on-write
값 타입을 다른 변수에 할당하면 복사를 하게 됩니다. 즉 새로운 메모리 공간에 같은 값을 복사하게 되는데요, 이를 Copy-on-assignment라고 합니다.

이와 다르게 Copy-on-write는 다른 변수에 할당하면 일단은 메모리를 할당하지 않고 같은 곳을 봅니다. 그러다 해당 값을 변경할 때 실제로 메모리에 값을 복사하고 값을 변경하게 됩니다. 이는 메모리를 최적화해주기 위함이며 Swift에서는 Int, Double, String, Array, Set, Dictionary에서만 사용하고 있습니다.

참조 타입을 포함하고 있는 값 타입은 이러한 메모리 최적화를 할 수 없습니다. 물론 억지로 만들 순 있지만 이는 많은 오버헤드를 발생시킵니다.


어떤 상황에 클래스를 쓰고, 구조체를 쓸까 ?

구조체를 사용하는게 좋은 경우

  • 연관된 간단한 값의 집합을 캡슐화하는 것만이 목적일 경우
  • 캡슐화한 값을 참조하는 것보다 복사하는 것이 합당할때
  • 구조체에 저장된 프로퍼티가 값 타입이며, 참조하는 것보다 복사하는것이 합당할때
  • 다른 타입으로부터 상속받거나, 자신을 상속할 필요가 없을때
  • 추가적으로 Objective-C와 상호 운용성이 필요할 때는 클래스를 사용해야 한다.
  • 단순한 데이터 값을 보유하기 위해서는 구조체가 낫습니다.
  • 메모리의 스택은 크기가 크지 않기 때문에 작은 값을 갖는 데이터를 처리할 때 구조체를 사용합니다.

출처/참고


https://infinitt.tistory.com/392
https://hasensprung.tistory.com/181
https://velog.io/@scutiuy/Swift-성능-최적화

profile
Hello

0개의 댓글