포스트

구조체(Struct)와 클래스(Class)의 차이점

구조체 vs. 클래스

구조체(Struct)와 클래스(Class)는 서로 연관된 데이터를 묶어준다는 비슷한 특징을 가지고 있지만, 작동 방식에 확연한 차이가 있습니다. 실제로 구조체와 클래스는 겉으로 보이는 차이가 뚜렷하게 보이지 않기 때문에 정의할 커스텀 데이터 타입의 특징을 고려하지 않은 채 의식이 흐르는대로 코딩을 하는 경우도 종종 있습니다.

공통점

구조체와 클래스는 아래 표와 같은 공통점을 지니고 있습니다.

■ 값을 저장하기 위한 프로퍼타(Property)를 정의할 수 있습니다.
■ 기능을 제공하기 위한 메서드(Method)를 정의할 수 있습니다.
■ 값에 접근하기 위한 서브스크립트(Subscript)를 정의할 수 있습니다.
■ 프로퍼티의 초기값을 세팅하기 위해 이니셜라이저(Intializer)를 정의할 수 있습니다.
■ 이미 구현된 기능을 확장하기 위해 확장(Extension)을 정의할 수 있습니다.
■ 보편적인 기능 제공을 위해 프로토콜(Protocol)을 준수할 수 있습니다.

이러한 공통점 덕분에 구조체와 클래스는 아래 코드와 같이 거의 비슷한 코드 작성이 가능합니다. 겉으로 보기에는 StructClass 키워드 차이일 뿐입니다. 동일한 방식으로 인스턴스(Instance)를 생성해 변수에 할당하고 있고, 내부 프로퍼티에 접근해 값을 출력하는 모습을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
struct Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let zizi = Person(name: "Zizi_Kim", age: 26)
print("\(zizi.name) - \(zizi.age)") // Zizi_Kim - 26
1
2
3
4
5
6
7
8
9
10
11
12
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let zizi = Person(name: "Zizi_Kim", age: 26)
print("\(zizi.name) - \(zizi.age)") // Zizi_Kim - 26

차이점

구조체와 클래스는 우리 눈에 보이지 않는 결정적인 차이점이 존재합니다. 구조체는 값(Valeu) 타입이고, 클래스는 참조(Reference) 타입이라는 점입니다. 그리고 클래스는 상속(Inheritance)이 가능합니다. 이 두 가지 차이가 구조체와 클래스의 차이를 줄줄이 소시지마냥 더욱 명확하게 만들어줍니다. 아래 표는 구조체와 비교해 클래스만이 가지는 독특한 특징을 보여줍니다.

■ 상속(Inheritance)이 가능합니다.
■ 런-타임에 인스턴스 타입을 확인하고 변환(Casting)하는 게 가능합니다.
■ 디이니셜라이저(Deinit)로 클래스의 인스턴스가 할당받은 모든 리소스를 해제하는 게 가능합니다.
■ ARC(Automatic Reference Counting)으로 인스턴스가 메모리에서 할당받거나 해제되는 시점을 관리할 수 있습니다.
■ 인스턴스가 힙(Heap) 메모리 영역에 할당됩니다.

구조체의 특징

구조체는 값 타입입니다. 값 타입은 변수나 상수에 값을 할당하거나, 함수에 인자로 전달될 때 복사(Copy)가 일어납니다. 변수 A에 구조체의 인스턴스를 할당하고, 변수 A의 값을 새로 만든 변수 B에 할당하면, 변수 A와 B 서로 다른 인스턴스를 가지게 됩니다. 왜냐하면 변수 A의 값을 변수 B에 할당하면 복사가 일어나니까요. 아래 예제는 구조체의 인스턴스가 복사가 일어나는 상황을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

var p1 = Person(name: "김지지", age: 26)
var p2 = p1 // p1에 할당된 값을 p2에 할당, 값의 복사가 일어남

p2.name = "김문어"
p2.age = 15

print("\(p1.name) - \(p1.age)") // 김지지 - 26
print("\(p2.name) - \(p2.age)") // 김문어 - 15

변수 p1Person구조체의 인스턴스를 할당하고, 변수 p1의 값을 새로운 변수 p2에 할당하고 있습니다. 이때, 복사가 일어납니다. 겉 보기에는 동일한 인스턴스처럼 보여도 서로 다른 두 개의 인스턴스가 변수 p1p2에 할당된 겁니다! 변수 p2에 할당된 인스턴스를 수정하고 콘솔에 출력을 해보면 이 사실을 더욱 명확하게 알 수 있습니다. 변수 p2에 할당된 인스턴스를 수정하더라도 변수 p1에 할당된 인스턴스에 아무런 영향을 끼치지 않습니다.

2

이는 Swift의 기본 자료형을 떠올려보면 쉽게 알 수 있습니다. Swift에서 Int, Double, String과 같은 기본 자료형은 모두 구조체라는 사실을 알고 계신가요? 그렇기에 우리가 정수나 문자열을 서로 다른 변수에 할당하고 지지고 볶더라도 원본에는 아무런 영향을 끼치지 않습니다. 우리는 이를 당연하게 생각하고 써왔지만, 사실 이런 비밀이 숨어 있었던 겁니다.

Copy-On-Assignment(COA)와 Copy-On-Write(COW)
Swift는 성능을 향상시키기 위해 처음부터 무작정 복사를 하지 않습니다. Int, Double과 같은 기본 자료형과 배열(Array), 딕셔너리(Dictionary)와 집합(Set)와 같은 컬렉션은 복사가 일어나는 대신, 원본 인스턴스가 저장된 메모리 공간을 가리킵니다. 복사된 인스턴스에 수정을 시도한다면, 이때 실질적인 복사가 일어납니다. 코드에서는 바로 인스턴스의 복사가 일어나는 것처럼 보이지만, 실제로는 정말로 필요할 때만 복사가 일어납니다. 이를 Copy-On-Write(COW)라고 합니다.

반면에, 우리가 일반적으로 만드는 커스텀 데이터 타입은 별도 코드를 작성해주지 않는다면 COW를 지원하지 않습니다. 한 인스턴스를 다른 변수에 할당하면 인스턴스의 모든 내용이 풀-카피(Full-Copy)됩니다. 이를 Copy-On-Assignment(COA)라고 합니다.

클래스의 특징

클래스는 구조체와 전혀 다른 특징을 가집니다. 클래스는 참조 타입입니다. 참조 타입은 변수나 상수에 값을 할당하거나, 함수에 인자로 전달할 때 참조가 전달됩니다. 즉, 변수 A에 클래스의 인스턴스를 할당하고, 변수 A의 값을 새로 만든 변수 B에 할당하면, 변수 A와 B 동일한 인스턴스를 가리키게 됩니다. 변수 A에 할당된 인스턴스가 수정되면, 변수 B에 할당된 인스턴스에도 영향을 미칩니다. 아래 예제는 클래스의 인스턴스가 참조를 전달하는 상황을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

var p1 = Person(name: "김지지", age: 26)
var p2 = p1 // p1에 할당된 값을 p2에 할당, 참조가 전달됨

p2.name = "김문어"
p2.age = 15

print("\(p1.name) - \(p1.age)") // 김문어 - 15
print("\(p2.name) - \(p2.age)") // 김문어 - 15

변수 p1Person 클래스의 인스턴스를 할당하고, 변수 p1의 값을 새로운 변수 p2에 할당하고 있습니다. 이때, 참조가 전달됩니다. 변수 p1p2는 동일한 클래스의 인스턴스를 가리키게 되어 어느 변수에 할당된 인스턴스를 수정하더라도, 동일한 인스턴스가 할당된 다른 변수에 영향을 끼칩니다! 위 예제의 출력에서 보시다시피, 분명 변수 p2에 할당된 인스턴스 정보를 수정했지만, 변수 p1에 할당된 인스턴스도 함께 변경된 걸 확인할 수 있습니다.

3

마무리

구조체는 다른 프로그래밍 언어에서는 볼 수 없는, 메소드를 가지거나 프로토콜을 준수하는 등 클래스에서 볼 법한 기능을 포함하고 있습니다. 그래서 둘 중 어느걸 선택해야 할 지 고민할 때가 많습니다. 애플 공식 문서에서는 다음의 조건 중 하나라도 만족한다면 구조체를 사용하는 게 바람직하다고 정의하고 있습니다.

■ 관계된 간단한 값을 캡슐화(Encapsulate)하기를 원할 때
■ 인스턴스가 참조되기보다 복사되기를 원할 때
■ 프로퍼티가 참조되기보다 복사되기를 원할 때
■ 프로퍼티나 메서드가 상속되기를 원하지 않을 때

구조체는 새로운 변수에 할당하거나 함수에 전달될 때 복사가 이루어지므로, 특정 구조체를 변경해도 앱 전체에 영향을 끼치지 않습니다. 로직을 단순하게 만들고, 코드를 덜 복잡하게 만들어주죠. 게다가 ARC를 통해 메모리 관리를 하는 클래스의 특성 상, 너무 복잡하고 무거워 구조체를 권장하는 점도 없잖아 있으리라 생각됩니다.

참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.