선형 자료구조(Linear Data Structures)는 데이터가 일직선 형태로 배열되며, 요소들이 특정 순서대로 연결된 자료구조입이다. 이러한 자료구조는 요소들이 순서대로 배열되며, 요소 간의 순서가 중요한 특징을 갖는다.
Array | |
---|---|
👍 | 빠른 인덱스 접근: O(1)로 임의의 요소에 접근 가능 |
👍 | 메모리 효율성: 요소가 연속된 메모리 공간에 저장됨 |
❌ | 삽입 및 삭제의 비용: 중간에 요소를 삽입/삭제할 때 O(n)의 시간 복잡도 발생 |
❌ | 고정된 크기: 크기 변경 시 새로운 배열로 복사 필요 |
Linked List | |
---|---|
👍 | 동적 크기 조정: 요소 추가/삭제 시 새로운 메모리 할당 가능 |
👍 | 삽입/삭제 효율성: 중간 요소의 삽입/삭제가 O(1) |
❌ | 느린 접근 속도: 특정 요소 접근 시 O(n)의 시간 소요 |
❌ | 추가 메모리 사용: 각 노드가 포인터를 포함해 메모리 사용량이 큼 |
Stack | |
---|---|
👍 | 간단한 구현: LIFO 원칙으로 쉽게 구현 가능 |
👍 | 빠른 삽입 및 제거: O(1)로 요소 추가/제거 가능 |
❌ | 임의 접근 불가: 중간 요소를 직접 접근할 수 없음 |
❌ | 한정된 용도: 특정 문제에만 효율적 |
Queue | |
---|---|
👍 | FIFO 구조: 순차적인 처리에 유용 |
👍 | 빠른 삽입 및 제거: O(1)로 요소 추가/제거 가능 (링크드 리스트 기반 구현 시) |
❌ | 임의 접근 불가: 중간 요소를 직접 접근할 수 없음 |
❌ | 공간 효율성: 배열 기반 큐는 크기 변경 시 새로운 배열 복사 필요 |
Array 기반 Stack | Linked List 기반 Stack | |
---|---|---|
👍 | 요소 추가/제거가 O(1)로 빠름 | 동적 크기 조정 가능, 삽입/제거 O(1) |
👍 | 구현이 간단함 | 메모리 제약이 없음 |
❌ | 크기 초과 시 배열 복사 필요 | 추가 메모리 사용(포인터 포함) |
❌ | 메모리의 연속성으로 제한이 있을 수 있음 | 캐시 효율성이 낮을 수 있음 |
Array 기반 Queue | Linked List 기반 Queue | |
---|---|---|
👍 | 구현이 간단하고 요소 접근 용이 | 앞과 뒤에서 O(1)로 삽입/제거 가능 |
👍 | 원형 큐로 개선 가능 | 크기 제한이 없음 |
❌ | 앞쪽 요소 제거 시 O(n) 요소 이동 필요 | 추가 메모리 사용(포인터 포함) |
❌ | 크기 조정 시 배열 복사 필요 | 메모리 효율성이 낮을 수 있음 |
원형 큐(Circular Queue)는 일반적인 큐의 한계(예: 요소 제거 시 공간 낭비)를 극복하기 위해 고안된 자료구조이다. 원형 큐는 배열 기반 큐에서 공간을 효율적으로 사용할 수 있도록 배열의 끝이 처음과 연결된 형태로 구현된다.
특징 | 설명 |
---|---|
순환 구조 | 큐의 끝이 배열의 처음과 연결된 구조로, 배열의 끝에 도달하면 처음으로 돌아감 |
고정된 크기 | 고정된 크기의 배열을 사용하지만, 빈 공간을 최소화하여 활용 |
공간 낭비 최소화 | 요소 제거 시 배열의 앞쪽에 빈 공간이 생기지 않음 |
포인터 사용 | front (첫 번째 요소 인덱스)와 rear (마지막 요소 인덱스)로 관리 |
동작 | 설명 |
---|---|
삽입 (enqueue) | rear 포인터가 다음 위치로 이동해 요소 삽입. 끝을 넘으면 처음으로 돌아감 |
제거 (dequeue) | front 포인터가 다음 위치로 이동해 요소 제거. 끝을 넘으면 처음으로 돌아감 |
공백 상태 | count 가 0이거나 front 와 rear 가 특정 조건일 때 큐가 비어 있는 상태로 간주 |
가득 찬 상태 | count 가 capacity 와 같거나, rear 가 front 바로 앞에 위치할 때 큐가 가득 찬 것으로 간주 |
덱(Deque)은 양방향으로 삽입과 삭제가 가능한 큐이다. Deque는 Double-Ended Queue의 줄임말로, 일반적인 큐나 스택과 달리 양쪽 끝에서 요소를 삽입하거나 제거할 수 있는 유연한 자료구조이다.
특징 | 설명 |
---|---|
양방향 삽입/삭제 | 양쪽 끝에서 삽입과 삭제가 가능한 자료구조 |
유연한 구조 | FIFO와 LIFO 둘 다 지원 가능 |
동적 크기 조정 | 필요에 따라 동적으로 크기를 조정할 수 있음 |
빠른 삽입/제거 | 양쪽 끝에서 O(1) 시간 복잡도로 요소 삽입/제거 가능 |
다목적 사용 | 큐와 스택의 특성을 모두 가질 수 있어 다양한 알고리즘에 활용 가능 |
양방향 탐색 | 특정 알고리즘에서 양쪽 끝을 사용해 효율적인 탐색 및 수정 가능 |
동작 | 설명 |
---|---|
addFirst / pushFront | 앞쪽에 요소 추가 |
addLast / pushBack | 뒤쪽에 요소 추가 |
removeFirst / popFront | 앞쪽에서 요소 제거 및 반환 |
removeLast / popBack | 뒤쪽에서 요소 제거 및 반환 |
peekFront | 앞쪽 요소를 조회 (제거하지 않음) |
peekBack | 뒤쪽 요소를 조회 (제거하지 않음) |
타입 정의 (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) 같은 타입도 있지만, 이는 특별한 형태의 구조이거나 다른 타입의 확장으로 취급된다.
분류 | 데이터 타입 예시 | 저장 방식 |
---|---|---|
Value Type | Int , Double , Bool , Struct , Enum , Array , String | 주로 스택에 저장, 복사 시 새로운 인스턴스 생성 값을 전달할 때 참조가 아닌 복사된 값이 전달됨 멀티스레드 환경에서 안전하게 사용할 수 있음 |
Reference Type | Class , Function , Closure | 힙에 저장, 참조가 복사되어 동일한 인스턴스를 가리킴 ARC에 의해 메모리 관리가 자동으로 이루어짐 참조카운팅을 통해 매모리 해제 시점이 결정됨 |
메모리 영역 | 설명 |
---|---|
Code 영역 | 프로그램 명령어가 저장되는 영역 |
Data 영역 | 전역 및 정적 데이터가 저장되는 영역 |
Heap 영역 | 동적 데이터가 저장되는 영역 |
Stack 영역 | 함수 호출 및 지역 변수가 저장되는 영역 |
.bss
)로 나뉜다. 프로그램 실행 중 변수 값이 변경될 수 있기에 읽기-쓰기 가능이다.malloc
등) 및 해제를 관리해야 하며 <하지만 Swift는 ARC가 해결하니 안심하라구>, 메모리 누수 가능성이 있다. 객체, 동적으로 할당된 배열 등이 여기에 저장된다. (Reference Type)함수 호출과 관련된 지역 변수, 매개변수, 반환 주소 등이 저장되는 영역이다. 함수가 호출될 때마다 스택 프레임이 생성되고, 함수가 종료되면 해당 프레임이 제거(= 메모리 해제)된다.
Heap 영역보다 빠르게 할당 및 해제되며, 메모리 사용량은 고정된 크기의 프레임으로 제한된다. 이를 초과하면 스택 오버플로(Stack Overflow) 오류가 발생해 프로그램이 종료될 수 있다.
※ 스택 메모리는 메모리 관리의 한 부분이며, 함수 호출 시 데이터가 저장되는 공간이다. 반면, 스택 자료구조는 데이터를 LIFO 방식으로 저장하는 추상 자료구조로, 프로그램의 메모리 관리와는 다른 개념이다.
레페런스 타입을 '선언(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 (COW)는 Swift에서 컬렉션 타입 및 값 타입의 메모리 사용을 최적화하는 기법이다. 이 기법은 복사 시점에서 실제 메모리 복제를 지연하여 성능과 메모리 효율성을 높인다. Swift의 Array, Dictionary, Set, String과 같은 표준 컬렉션 타입은 COW를 자동으로 사용하여 메모리 관리를 최적화한다(데이터가 변경되지 않는 한, 원본과 복사본은 같은 메모리 공간을 공유하여 메모리 사용량을 줄임).
// 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(Automatic Reference Counting)는 Swift에서 메모리 관리를 자동으로 수행하는 메커니즘이다. ARC는 힙 메모리에 할당된 객체의 참조 카운트(Reference Count)를 추적하여, 더 이상 객체를 참조하는 변수가 없을 때 해당 객체를 메모리에서 해제한다. 이를 통해 메모리 누수를 방지하고 메모리를 효율적으로 사용할 수 있다.
strong
참조할 때 발생한다. 이로 인해 두 객체는 참조 카운트가 0이 되지 않아 메모리에서 해제되지 않는 메모리 누수가 발생할 수 있다.weak
나 unowned
참조를 사용하여 순환 참조를 해결한다.참조 타입 | 참조 카운트 증가 | 특징 | 객체 해제 시 동작 |
---|---|---|---|
strong | 예 | Default 참조 방식, 객체의 생명 주기 보장 | 참조 카운트가 0이 될 때 해제 |
weak | 아니요 | 참조 카운트 증가 없음, 옵셔널이어야 함 | 객체 해제 시 nil 로 설정 |
unowned | 아니요 | 참조 카운트 증가 없음, 비옵셔널 | 객체 해제 시 참조 시도 시 런타임 에러 |
unowned
참조는 참조 대상이 해제된 후에 접근하려고 하면 런타임 에러가 발생하므로, 참조 대상의 생명 주기가 명확히 보장될 수 있는 경우에만 사용해야 한다.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
참조를 통한 참조 순환은 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
클래스는 owner
를 unowned
참조로 가지므로 참조 카운트를 증가시키지 않는다.unowned
참조는 nil
이 될 수 없기 때문에, owner
객체가 존재할 것으로 가정하고 사용된다.john
객체가 메모리에서 해제될 때, CreditCard
객체도 참조가 끊어지면서 메모리에서 해제된다.일급 객체 (First-Class Citizen)란 프로그래밍 언어에서 특정 요소가 다음의 조건을 충족할 때를 말한다:
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에서 일급 객체로 취급된다.