[iOS 3주차] Swift: 선형 자료구조 / 타입 정의 / 레퍼런스 타입과 밸류 타입 / 참조 순환 / 일급 객체

DoyleHWorks·2024년 11월 4일
1

오늘 푼 알고리즘 (본문과 관계 없음)


ChatGPT 4o 대화기록: 선형 자료구조

Linear Data Structures

선형 자료구조(Linear Data Structures)는 데이터가 일직선 형태로 배열되며, 요소들이 특정 순서대로 연결된 자료구조입이다. 이러한 자료구조는 요소들이 순서대로 배열되며, 요소 간의 순서가 중요한 특징을 갖는다.

Array, Linked List / Stack, Queue

Array, Linked List

Array
👍빠른 인덱스 접근: O(1)로 임의의 요소에 접근 가능
👍메모리 효율성: 요소가 연속된 메모리 공간에 저장됨
삽입 및 삭제의 비용: 중간에 요소를 삽입/삭제할 때 O(n)의 시간 복잡도 발생
고정된 크기: 크기 변경 시 새로운 배열로 복사 필요

Linked List
👍동적 크기 조정: 요소 추가/삭제 시 새로운 메모리 할당 가능
👍삽입/삭제 효율성: 중간 요소의 삽입/삭제가 O(1)
느린 접근 속도: 특정 요소 접근 시 O(n)의 시간 소요
추가 메모리 사용: 각 노드가 포인터를 포함해 메모리 사용량이 큼

Stack, Queue

Stack
👍간단한 구현: LIFO 원칙으로 쉽게 구현 가능
👍빠른 삽입 및 제거: O(1)로 요소 추가/제거 가능
임의 접근 불가: 중간 요소를 직접 접근할 수 없음
한정된 용도: 특정 문제에만 효율적

Queue
👍FIFO 구조: 순차적인 처리에 유용
👍빠른 삽입 및 제거: O(1)로 요소 추가/제거 가능 (링크드 리스트 기반 구현 시)
임의 접근 불가: 중간 요소를 직접 접근할 수 없음
공간 효율성: 배열 기반 큐는 크기 변경 시 새로운 배열 복사 필요

Array 기반 StackLinked List 기반 Stack
👍요소 추가/제거가 O(1)로 빠름동적 크기 조정 가능, 삽입/제거 O(1)
👍구현이 간단함메모리 제약이 없음
크기 초과 시 배열 복사 필요추가 메모리 사용(포인터 포함)
메모리의 연속성으로 제한이 있을 수 있음캐시 효율성이 낮을 수 있음

Array 기반 QueueLinked List 기반 Queue
👍구현이 간단하고 요소 접근 용이앞과 뒤에서 O(1)로 삽입/제거 가능
👍원형 큐로 개선 가능크기 제한이 없음
앞쪽 요소 제거 시 O(n) 요소 이동 필요추가 메모리 사용(포인터 포함)
크기 조정 시 배열 복사 필요메모리 효율성이 낮을 수 있음

Circular Queue, Deque

Circular Queue

원형 큐(Circular Queue)는 일반적인 큐의 한계(예: 요소 제거 시 공간 낭비)를 극복하기 위해 고안된 자료구조이다. 원형 큐는 배열 기반 큐에서 공간을 효율적으로 사용할 수 있도록 배열의 끝이 처음과 연결된 형태로 구현된다.

특징설명
순환 구조큐의 끝이 배열의 처음과 연결된 구조로, 배열의 끝에 도달하면 처음으로 돌아감
고정된 크기고정된 크기의 배열을 사용하지만, 빈 공간을 최소화하여 활용
공간 낭비 최소화요소 제거 시 배열의 앞쪽에 빈 공간이 생기지 않음
포인터 사용front(첫 번째 요소 인덱스)와 rear(마지막 요소 인덱스)로 관리

동작설명
삽입 (enqueue)rear 포인터가 다음 위치로 이동해 요소 삽입. 끝을 넘으면 처음으로 돌아감
제거 (dequeue)front 포인터가 다음 위치로 이동해 요소 제거. 끝을 넘으면 처음으로 돌아감
공백 상태count가 0이거나 frontrear가 특정 조건일 때 큐가 비어 있는 상태로 간주
가득 찬 상태countcapacity와 같거나, rearfront 바로 앞에 위치할 때 큐가 가득 찬 것으로 간주

Deque

덱(Deque)은 양방향으로 삽입과 삭제가 가능한 큐이다. Deque는 Double-Ended Queue의 줄임말로, 일반적인 큐나 스택과 달리 양쪽 끝에서 요소를 삽입하거나 제거할 수 있는 유연한 자료구조이다.

특징설명
양방향 삽입/삭제양쪽 끝에서 삽입과 삭제가 가능한 자료구조
유연한 구조FIFO와 LIFO 둘 다 지원 가능
동적 크기 조정필요에 따라 동적으로 크기를 조정할 수 있음
빠른 삽입/제거양쪽 끝에서 O(1) 시간 복잡도로 요소 삽입/제거 가능
다목적 사용큐와 스택의 특성을 모두 가질 수 있어 다양한 알고리즘에 활용 가능
양방향 탐색특정 알고리즘에서 양쪽 끝을 사용해 효율적인 탐색 및 수정 가능

동작설명
addFirst / pushFront앞쪽에 요소 추가
addLast / pushBack뒤쪽에 요소 추가
removeFirst / popFront앞쪽에서 요소 제거 및 반환
removeLast / popBack뒤쪽에서 요소 제거 및 반환
peekFront앞쪽 요소를 조회 (제거하지 않음)
peekBack뒤쪽 요소를 조회 (제거하지 않음)

ChatGPT 대화 기록: Swift 타입 / 메모리 구조 / 순환 참조

Type Definition

타입 정의 (Type Definition) : 프로그래밍에서 새로운 데이터 타입을 만들기 위한 선언.

타입 정의설명예시 코드
Class참조 타입, 객체 지향 프로그래밍에 사용class MyClass { var value: Int }
Struct값 타입, 간단한 데이터 구조에 사용struct MyStruct { var value: Int }
Enum여러 연관된 값을 하나의 타입으로 정의enum MyEnum { case one, two }
Protocol타입이 준수해야 하는 속성과 메서드의 청사진protocol MyProtocol { func doSomething() }
Type Alias기존 타입에 새로운 이름을 붙임typealias Name = String
Generic Type다양한 타입을 지원하기 위한 일반화된 타입struct Box<T> { var item: T }
Function Type함수의 입력과 출력을 정의하는 타입(Int, Int) -> Int
Extension기존 타입에 새로운 기능을 추가extension Int { func squared() -> Int }

이 외에도 클로저(Closure) 와 옵셔널(Optional) 같은 타입도 있지만, 이는 특별한 형태의 구조이거나 다른 타입의 확장으로 취급된다.

Data Types by Memory Behavior

분류데이터 타입 예시저장 방식
Value TypeInt, Double, Bool, Struct, Enum, Array, String주로 스택에 저장, 복사 시 새로운 인스턴스 생성
값을 전달할 때 참조가 아닌 복사된 값이 전달됨
멀티스레드 환경에서 안전하게 사용할 수 있음
Reference TypeClass, Function, Closure힙에 저장, 참조가 복사되어 동일한 인스턴스를 가리킴
ARC에 의해 메모리 관리가 자동으로 이루어짐
참조카운팅을 통해 매모리 해제 시점이 결정됨

Process Memory Layout

메모리 영역설명
Code 영역프로그램 명령어가 저장되는 영역
Data 영역전역 및 정적 데이터가 저장되는 영역
Heap 영역동적 데이터가 저장되는 영역
Stack 영역함수 호출 및 지역 변수가 저장되는 영역

Code

  • 프로그램의 실행 코드(명령어)가 저장되는 영역이다. 컴파일된 기계어 코드(0과 1)가 위치하며, 읽기 전용이라 실행중에는 변경이 불가능하다.
  • 프로그램 실행과 동시에 메모리에 할당되어, 프로그램이 종료되면 메모리에서 해제된다.

Data

  • 전역 변수(Global Variable) 및 정적 변수(Static Variable)가 저장되는 영역이다. 프로그램이 시작될 때 할당되며, 프로그램이 종료될 때 해제된다.
  • 초기화된 데이터와 초기화되지 않은 데이터(.bss)로 나뉜다. 프로그램 실행 중 변수 값이 변경될 수 있기에 읽기-쓰기 가능이다.

Heap

  • 동적 메모리 할당에 사용되는 영역이다. 런타임 시 크기가 결정되는 데이터가 저장된다.
  • 개발자가 직접 메모리 할당(malloc 등) 및 해제를 관리해야 하며 <하지만 Swift는 ARC가 해결하니 안심하라구>, 메모리 누수 가능성이 있다. 객체, 동적으로 할당된 배열 등이 여기에 저장된다. (Reference Type)

Stack

  • 함수 호출과 관련된 지역 변수, 매개변수, 반환 주소 등이 저장되는 영역이다. 함수가 호출될 때마다 스택 프레임이 생성되고, 함수가 종료되면 해당 프레임이 제거(= 메모리 해제)된다.

  • Heap 영역보다 빠르게 할당 및 해제되며, 메모리 사용량은 고정된 크기의 프레임으로 제한된다. 이를 초과하면 스택 오버플로(Stack Overflow) 오류가 발생해 프로그램이 종료될 수 있다.

    스택 메모리는 메모리 관리의 한 부분이며, 함수 호출 시 데이터가 저장되는 공간이다. 반면, 스택 자료구조는 데이터를 LIFO 방식으로 저장하는 추상 자료구조로, 프로그램의 메모리 관리와는 다른 개념이다.

Reference Type Declaration

  • 레페런스 타입을 '선언(declaration)'하면, 클래스나 함수로부터 '설계도'를 가져와 인스턴스를 생성한다. 그 인스턴스를 여러 식별자(변수나 상수)가 참조할 수 있다.

  • 레퍼런스 타입은 저장하고 있는 인스턴스의 주소값(참조)을 전달한다. 즉, 레퍼런스 타입을 참조하는 여러 식별자(참조자)가 동일한 인스턴스를 가리키고 있을 때, 그 인스턴스의 값을 하나의 참조자가 변경하면 모든 참조자가 변경된 값을 보게 된다.

    // MyClass란 레퍼런스 타입을 정의(Defitnition)함 -> Code 영역에 저장
    class MyClass { 
        var value: Int
        init(value: Int) {
            self.value = value
        }
    }
     
     // a를 레퍼런스 타입으로 선언(Declaration)함 -> Heap에 객체 생성, Stack에는 참조 저장
    let a = MyClass(value: 10)
    let b = a  // `a`와 `b`는 같은 인스턴스를 참조 (Stack에 또 다른 참조 저장: 같은 Heap 객체)
    
    b.value = 20  // `b`를 통해 값을 변경
    
    print(a.value)  // 출력: 20, `a`도 동일한 인스턴스를 참조하므로 값이 변경됨

Copy On Write

Copy on Write (COW)는 Swift에서 컬렉션 타입값 타입의 메모리 사용을 최적화하는 기법이다. 이 기법은 복사 시점에서 실제 메모리 복제를 지연하여 성능과 메모리 효율성을 높인다. Swift의 Array, Dictionary, Set, String과 같은 표준 컬렉션 타입은 COW를 자동으로 사용하여 메모리 관리를 최적화한다(데이터가 변경되지 않는 한, 원본과 복사본은 같은 메모리 공간을 공유하여 메모리 사용량을 줄임).

COW의 작동 원리

  • 초기 복사: 값 타입(예: 배열)이 다른 변수에 복사될 때, 메모리 공간을 바로 복사하지 않고, 원본 데이터의 참조만 공유한다. 복사된 변수들은 같은 메모리 주소를 가리키게 된다.
  • 변경 시점: 복사본 중 하나가 변경되려고 할 때, 그 시점에 원본 데이터의 복사본이 생성되고, 수정된 복사본이 새로운 메모리 공간에 저장된다. 이를 통해 원본과 복사본이 독립적으로 동작한다.
  • 메모리 관리: 이 과정은 Swift의 ARC(Automatic Reference Counting)에 의해 관리된다. ARC는 힙 메모리에서 객체의 참조 횟수를 추적하고, 필요에 따라 메모리를 해제한다.
// Array 예시
var originalArray = [1, 2, 3]  // 힙 메모리에 저장됨
var copiedArray = originalArray  // 스택에서 참조 공유, 힙 메모리의 주소를 참조

copiedArray.append(4)  // 새로운 힙 메모리 공간이 할당되어 copiedArray가 독립적으로 변경됨

print(originalArray)  // 출력: [1, 2, 3]
print(copiedArray)    // 출력: [1, 2, 3, 4]

// String 예시
var originalString = "Hello"  // 힙 메모리에 저장됨
var copiedString = originalString  // 스택에서 참조 공유, 힙 메모리의 주소를 참조

copiedString.append(", World!")  // 새로운 힙 메모리 공간이 할당되어 copiedString이 독립적으로 변경됨

print(originalString)  // 출력: "Hello"
print(copiedString)    // 출력: "Hello, World!"

ARC & Reference Count

ARC(Automatic Reference Counting)는 Swift에서 메모리 관리를 자동으로 수행하는 메커니즘이다. ARC는 힙 메모리에 할당된 객체의 참조 카운트(Reference Count)를 추적하여, 더 이상 객체를 참조하는 변수가 없을 때 해당 객체를 메모리에서 해제한다. 이를 통해 메모리 누수를 방지하고 메모리를 효율적으로 사용할 수 있다.

Reference Count

  • 참조 카운트(Reference Count)는 객체가 몇 개의 변수나 상수에 의해 참조되고 있는지를 나타낸다.
  • 객체가 참조될 때마다 참조 카운트가 증가하고, 참조가 해제될 때마다 감소한다.
  • 참조 카운트가 0이 되면 ARC는 객체를 메모리에서 자동으로 해제한다.

Circular Reference

  • 순환 참조(Circular Reference)는 두 객체가 서로를 strong 참조할 때 발생한다. 이로 인해 두 객체는 참조 카운트가 0이 되지 않아 메모리에서 해제되지 않는 메모리 누수가 발생할 수 있다.
  • 이를 방지하기 위해, weakunowned 참조를 사용하여 순환 참조를 해결한다.

참조 방식 (Reference Ownership)

참조 타입참조 카운트 증가특징객체 해제 시 동작
strongDefault 참조 방식, 객체의 생명 주기 보장참조 카운트가 0이 될 때 해제
weak아니요참조 카운트 증가 없음, 옵셔널이어야 함객체 해제 시 nil로 설정
unowned아니요참조 카운트 증가 없음, 비옵셔널객체 해제 시 참조 시도 시 런타임 에러

unowned 참조 사용 시 주의사항

  • unowned 참조는 참조 대상이 해제된 후에 접근하려고 하면 런타임 에러가 발생하므로, 참조 대상의 생명 주기가 명확히 보장될 수 있는 경우에만 사용해야 한다.

ChatGPT 4o 대화기록: weak 참조와 unowned 참조

참조 순환 해결 - weak

  • 한쪽만 weak이면 충분: 객체 간의 참조 관계에서 한쪽이라도 weak 참조를 가지고 있으면 그 weak 참조는 참조 카운트를 증가시키지 않는다.
class A {
    var b: B? // 강한 참조
    deinit {
        print("A is being deinitialized")
    }
}

class B {
    weak var a: A? // 약한 참조 (참조 카운트 증가 없음)
    deinit {
        print("B is being deinitialized")
    }
}

// 예제 실행
var objectA: A? = A() // objectA의 RC: 1
var objectB: B? = B() // objectB의 RC: 1

objectA?.b = objectB // objectB의 RC: 1 (강한 참조 추가, RC 변화 없음)
objectB?.a = objectA // objectA의 RC: 1 (약한 참조이므로 RC 변화 없음)

// 객체 해제
objectA = nil // objectA의 RC: 0 -> deinit 호출, 메모리 해제
              // objectB?.a는 weak이므로 nil로 설정됨
objectB = nil // objectB의 RC: 0 -> deinit 호출, 메모리 해제

// 결과: 두 객체가 정상적으로 해제되고, 메모리 누수 없음

참조 순환 해결 - unowned

unowned 참조를 통한 참조 순환은 weak과 비슷하게 참조 카운트를 증가시키지 않으면서도, nil을 허용하지 않는 상황에서 사용된다. unowned는 참조 대상이 메모리에서 해제될 때 자동으로 nil이 되지 않기 때문에, 사용 중에 해제된 객체에 접근하려고 하면 런타임 에러가 발생할 수 있다.

class Person {
    let name: String
    var creditCard: CreditCard? // 강한 참조

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deinitialized")
    }
}

class CreditCard {
    let number: String
    unowned let owner: Person // 비소유 참조 (강한 참조 X, nil 허용 X)

    init(number: String, owner: Person) {
        self.number = number
        self.owner = owner
    }

    deinit {
        print("CreditCard \(number) is being deinitialized")
    }
}

// 예제 실행
var john: Person? = Person(name: "John") // john의 RC: 1
john?.creditCard = CreditCard(number: "1234-5678-9012-3456", owner: john!) // john의 RC: 1 (unowned이므로 증가 X)

// 객체 해제
john = nil // john의 RC: 0 -> deinit 호출, 메모리 해제
           // CreditCard의 owner가 unowned 참조이므로 메모리 해제 시 문제가 없음

// 결과: "John is being deinitialized"와 "CreditCard 1234-5678-9012-3456 is being deinitialized" 출력

설명

  • CreditCard 클래스는 ownerunowned 참조로 가지므로 참조 카운트를 증가시키지 않는다.
  • unowned 참조는 nil이 될 수 없기 때문에, owner 객체가 존재할 것으로 가정하고 사용된다.
  • john 객체가 메모리에서 해제될 때, CreditCard 객체도 참조가 끊어지면서 메모리에서 해제된다.

First-Class Citizen

일급 객체 (First-Class Citizen)란 프로그래밍 언어에서 특정 요소가 다음의 조건을 충족할 때를 말한다:

  1. 변수에 할당할 수 있다.
  2. 함수의 인자로 전달할 수 있다.
  3. 함수의 반환값으로 사용할 수 있다.

Swift에서 함수나 클로저는 이러한 조건을 충족하기 때문에 일급 객체로 간주된다. 일급 객체로 취급된다는 것은 해당 요소가 다른 데이터 타입들과 동일한 수준의 유연성과 사용성을 가진다는 의미이다. 예를 들어, 변수처럼 함수나 클로저를 자유롭게 조작하고 다룰 수 있다.

예시

// 함수의 인자로 전달
func performOperation(_ operation: (Int, Int) -> Int, _ a: Int, _ b: Int) -> Int {
    return operation(a, b)
}

// 클로저를 사용하여 전달
let addition = { (x: Int, y: Int) -> Int in
    return x + y
}

let result = performOperation(addition, 5, 3)
print(result)  // 출력: 8

이 예시에서 addition 클로저는 변수에 할당되고 함수의 인자로 전달되며, performOperation 함수에서 실행된다. 이러한 특성 때문에 함수와 클로저는 Swift에서 일급 객체로 취급된다.

profile
Reciprocity lies in knowing enough

0개의 댓글