티스토리 뷰

안녕하세요 Diana 입니다.

 

iOS 개발자로써 면접에 가면 필수로 듣는, 그리고 제가 면접을 봐도 필수로 질문하게 되는 질문 중 하나가 바로 Class 와 Struct의 차이를 묻는 질문 같습니다.

이제는 거의 필수 질문으로 자리잡아 대답하지 못하는 분들은 없는 것 같은데 문득 개발을 하다보니 그래서 이걸 개발에 어떻게 적용해야 성능 향상에 도움을 줄 수 있을까 생각하게 되었습니다.

 

따라서 오늘은 좀더 이 두 타입의 차이에 대해 깊게 파보려고 합니다.

 

시작하겠습니다.

 


 

 Class와 Struct 비교

우선 Class와 Struct에 대해 간단히 이야기 해보겠습니다.

 

모두 알고 있다시피 Class는 참조 타입이며 Stack에 인스턴스를 가리키는 포인터를 저장하고 Heap에 데이터를 저장합니다.

그리고 Struct는 값 타입으로 Stack에 데이터를 저장하며 인스턴스 생성 시 매번 값이 복사됩니다.

 

하지만 Struct라고 하여 매번 Stack에 저장되는 것은 아닙니다.

Struct는 Class 내부에서 프로퍼티로 사용되는 경우 클래스의 인스턴스와 함께 Heap에 올라가게 되며 클로저에 캡쳐되는 경우(Boxing)에도 마찬가지 입니다.


여기서 다들 Heap보다 Stack에 올리는 것이 효율적이라는 이야기는 들어보셨을 것 입니다.
왜인지 알아보기 위해선 우리는 메모리를 할당/해제하는 비용을 생각해볼 필요가 있습니다.

 

Heap의 경우 Stack과 달리 메모리에 할당이 되는 경우, 먼저 비어있는 적절한 크기의 공간을 탐색해야 합니다.

이 과정에서 Free List를 순회하며 적합한 블록을 찾아야 하고, 경우에 따라 메모리 단편화(Fragmentation)가 발생하면 이를 관리하기 위한 추가 비용도 발생합니다.

 

또한 Heap은 여러 스레드가 공유하는 메모리 영역이기 때문에, 동시에 여러 스레드가 할당을 요청할 경우 데이터 무결성을 보장하기 위한 Lock(동기화) 비용이 추가됩니다. 해제 시에도 마찬가지로 해당 블록을 다시 Free List에 반환하는 작업이 필요합니다.

 

반면 Stack은 단순히 Stack Pointer를 이동시키는 것만으로 할당과 해제가 끝납니다.

함수가 호출되면 필요한 만큼 포인터를 내리고, 함수가 종료되면 포인터를 원위치 시키는 것이 전부이기 때문에 O(1)의 매우 빠른 속도를 보장합니다. 스레드마다 독립적인 Stack을 갖기 때문에 동기화 비용도 존재하지 않습니다.

 

여기에 더해, Swift의 Class는 ARC(Automatic Reference Counting) 로 관리되기 때문에 참조가 추가되거나 해제될 때마다 retain / release 연산이 발생합니다. 이 연산들은 멀티스레드 환경에서의 안전성을 보장하기 위해 atomic 연산으로 수행되는데, atomic 연산은 일반적인 연산보다 비용이 크기 때문에 빈번하게 참조가 오갈수록 성능에 영향을 주게 됩니다.

(Swift는 싱글스레드에서조차 Class의 Atomic 연산을 강제 함)

 

마지막으로 캐시 지역성(Cache Locality) 관점에서도 Stack이 유리합니다. Stack에 순차적으로 쌓인 데이터는 메모리상 연속된 위치에 존재하므로 CPU 캐시에 적재되기 좋은 반면, Heap에 흩어져 저장된 데이터는 캐시 미스(Cache Miss)를 유발할 가능성이 높습니다.

+ 캐시미스: CPU에서 사용하고자 하는 데이터가 메모리에 적재되지 않아 RAM에 내려갔다와야(?)하는 상태

정리하면, Struct가 Stack에 올라갈 수 있는 조건이라면 할당/해제 비용, 동기화 비용, ARC 비용, 캐시 효율 모든 측면에서 Class보다 이점을 갖는 것이죠.

 

CoW(Copy on Write)

여기서 의문이 생깁니다.

 

Swift 표준 라이브러리의 주요 컬렉션 타입인 Array, Dictionary, Set, String 등은 크기가 커지면 무한으로 값을 복사하는 걸까요?
100개의 값을 가지고 있는 Array가 매번 모든 값을 복사한다면 아마 비용이 상당할 것입니다.

이를 방지하기 위해서는 Swift에서는 CoW(Copy on Write)라는 기능을 지원합니다.

 

CoW의 핵심 아이디어는 아래와 같습니다.

  • 값을 복사하는 시점에는 실제 데이터를 복사하지 않으며 내부적으로 Heap에 있는 실제 저장소(Buffer)에 대한 참조만 공유합니다. 즉, 이 순간에는 ARC의 참조 카운트만 증가합니다.
  • 쓰기(mutation)가 발생하는 시점에, 참조가 공유되고 있다면 그제서야 실제로 버퍼를 복사합니다. 참조가 하나뿐이라면 복사 없이 그대로 수정합니다.

코드로 확인해보겠습니다

 

final class Storage {
    var data: [Int]
    init(_ data: [Int]) { self.data = data }
    func copy() -> Storage { Storage(data) }
}

struct MyBuffer {
    private var storage: Storage
    
    init(_ data: [Int]) {
        self.storage = Storage(data)
    }
    
    var data: [Int] { storage.data }
    
    mutating func append(_ value: Int) {
        // 참조가 공유 중이면 복사 후 수정
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
        storage.data.append(value)
    }
}

 

MyBuffer은 Struct 타입으로 값 복사가 일어납니다.
하지만 Storage의 경우 참조 타입인 Class의 힘을 빌려 값의 변경이 일어나지 않는 이상 Class의 같이 보는(?) 특성을 빌립니다.

 

 Boxing 이란

앞서 Struct가 클로저에 캡쳐되는 경우 Boxing이 발생한다고 언급했습니다.

그럼 이 Boxing이 정확히 무엇일까요?

Boxing은 값 타입(Struct, Enum 등)을 힙(Heap)에 할당된 참조 타입 컨테이너로 감싸는 것 입니다
말 그대로 값을 "상자(Box)에 넣어" 참조로 다룰수 있게 만드는 것이죠.

 

그럼 이 Boxing이 왜 필요할까요?

값 타입은 기본적으로 스택(Stack)에 올라가고 복사(Copy)되며 전달됩니다. 그런데 아래와 같은 상황에서는 이 모델이 깨집니다.

  1. 여러 곳에서 같은 값을 공유해야 할 때
  2. 값의 수명이 스택 프레임을 넘어서 살아남아야 할때
  3. 값이 언제 해제되는지 알 수 없을 때

클로저가 Struct를 캡쳐하는 상황이 바로 2번과 3번에 해당합니다.
클로저는 언제 실행될 지 모르며 비동기적으로 오래 살아남기도 합니다. 하지만 이때 캡쳐된 변수가 Stack에 있다면 이미 사라진 메모리를 참조하게 됩니다.

따라서 Swift에서는 해당 값을 Heap에 올리고 참조로 바꿉니다.

이것을 우리는 Boxing이라고 부릅니다.

즉, 스택에 있어야 할 값을 힙으로 옮기는 작업을 말합니다.

 

물론 Boxing은 공짜가 아닙니다.

  1. 힙 할당(malloc) 비용 - 스택 대비 수십~수백 배 느림
  2. ARC 트래픽 - retain/release 카운팅
  3. 캐시 미스
  4. 간접 참조

자주 사용되는 루프나 값 타입 기반 자료구조에서 Boxing이 마구 일어나면 성능에 직격타가 됩니다. 따라서 Swift 컴파일러는 가능한 Boxing을 피하려하며 개발할 때도 이 부분을 고려하는 것이 성능 향상에 좋습니다.

 

Boxing을 줄이는 방법은 아래와 같습니다.

  1. 가능하면 제네릭(some) 쓰기
  2. 클로저 캡쳐 최소화
  3. 참조가 필요하다면 class로 구현

오늘은 이렇게 Struct와 Class의 차이를 알아보며 조금 더 깊이 파보았습니다.

감사합니다.

'Swift' 카테고리의 다른 글

Swift - Swift Concurrency  (0) 2026.06.23
Swift - GCD vs Swift Concurrency  (0) 2026.06.08
Swift - @frozen과 @unknown default  (0) 2026.04.22
Swift - RunLoop와 Timer 원리 이해하기  (0) 2026.04.15
Swift - Value Semantic  (0) 2026.01.19
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/08   »
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 26 27 28 29
30 31
글 보관함