Struct vs. Class에 대한 정리
이번 포스트에서는 Struct와 Class에 대하여 정리해보도록 하겠습니다. 아무 생각 없이 쓰다가 문득 이런 생각이 들 수도 있는데요
둘이 비슷한 것 같은데 왜 굳이 만들어 놓았을까?
과연 정말 비슷하면 만들어 놓았을까요?
Swift에서는 Value-Type(값 타입), Reference-Type(참조 타입)을 제공하고 있습니다. JAVA에서도 이런 비슷한 이야기를 들은 적이 있을텐데 Call-By-Value, Call-By-Reference 가 그것입니다.
단순히 구조적인 차이부터 시작해서 실제 프로그램이 구동할 때 어떤 차이가 보이는지 알아보도록 하겠습니다.
차이점 1. 구조체는 Value-Type, 클래스는 Reference-Type
struct Structure { var structValue = 0 } class Class { var classValue = 0 } let struct1 = Structure() var struct2 = struct1 let class1 = Class() var class2 = class1
위의 간단한 예제를 살펴보도록 하겠습니다.
변수 1개를 갖는 구조체와 클래스입니다. 인스턴스화를 하여 인스턴스 변수 struct1과 class1를 만들었는데요 여기서 struct2와 class2 변수에 각각 struct1과 class1를 대입하였습니다.
만약 인스턴스 변수의 필드 값을 변경하게 된다면 값 타입을 갖는다면 두 인스턴스 변수의 필드 변수 값이 달라야 하며 참조 타입을 갖는다면 두 인스턴스 변수의 필드 변수 값이 같아야 합니다. 아래 코드는 이 작업을 수행하는 코드입니다.
struct2.structValue = 3 class2.classValue = 5 print("struct1 : \(struct1.structValue)") print("struct2 : \(struct2.structValue)") print("class1 : \(class1.classValue)") print("class2 : \(class2.classValue)")
필드 변수의 값을 변경하는 코드입니다.
결과를 한 번 살펴볼까요?
여기서 알 수 있듯이 클래스는 Reference Type이기 때문에 다른 인스턴스 변수에서 값을 변경하면 값이 같이 변한다는 것을 알 수 있습니다. 그럼 여기서 무엇을 알 수 있을까요?
앞에서 ARC(Automatic Reference Counting)에 대해 알아본 적이 있었는데요 ARC가 Compile Time에 레퍼런스 카운트를 계산하여 0이 되는 시점에 Release코드를 삽입하여 메모리 누수를 방지해준다고 한 적이 있습니다.
Struct를 사용하면 카운팅이 되지 않아 너무 많이 사용하면 메모리를 낭비할 수 있고 Class를 사용하면 정상적으로 메모리 해제가 될 수 있다는 것을 알 수 있을 것입니다. (물론 잘 사용하지 못하면 메모리 Leak이 발생할 수 있습니다.)
하지만 마지막으로 하나 더 알 것이 있다면 단순 값 복사 보다는 Copy-on-Write 방식이기 때문에 처음에는 복사가 일어나지 않다가 값이 수정 될 때 복사가 일어난다는 점을 알면 좋을 것 같습니다.
차이점 2. 구조체는 상속이 불가하며 클래스는 상속이 가능하다
두 번째 차이점은 구조체는 상속이 불가능하며 클래스는 상속이 가능하다는 점입니다.
실제로 XCode에 다음과 같이 코드를 입력해보면 에러가 발생할 것입니다.
struct ParentStruct { var structValue = 0 } struct ChildStruct : ParentStruct { }
Inheritance from non-protocol type 'ParentStruct'
구조체는 상속을 지원하지 않는다는 것을 알 수 있습니다. Parent Class 형식으로 한다면 될까요? 역시 안될 것입니다.
그렇다면 어떤 것을 상속할 수있을까요? Protocol은 상속할 수 있습니다.
protocol ParentProtocol { func testA() func testB() } struct ChildStruct : ParentProtocol { func testA() { } func testB() { } }
구조체에서 ':'이 나온다면 그 우측에는 무조건 Protocol이 온다는 것을 알 수 있습니다.
차이점 3. 인스턴스 메소드 내에서 변수 수정 가능 여부
클래스에서는 인스턴스 메소드 내에서 필드 변수를 수정할 수 있지만 Struct는 인스턴스 메소드 내에서 필드 변수를 수정할 수 없습니다.
struct Point { var x = 0.0, y = 0.0 func moveByX(deltaX: Double, y deltaY: Double) { x += deltaX y += deltaY } } var somePoint = Point(x: 1.0, y: 1.0) somePoint.moveByX(deltaX: 2.0, y: 3.0)
간단한 구조체 코드입니다. 구조체 내에 moveByX라는 함수가 있습니다. 이 함수는 Formal Parameter로 부터 받은 값으로 현재 좌표의 값을 변경해주는 함수인데 현재 코드를 입력하면 XCode에서 오류가 발생합니다.
moveByX 함수 내의 x, y 변수가 immutable하기 때문에 값을 변경할 수 없다는 것인데요. immutable이란 “변경할 수 없는” 이라는 뜻입니다. 그렇다면 구조체에서는 이러한 값 변경이 불가능한 걸까요?
mutating이라는 키워드를 추가하면 간단하게 해결할 수 있습니다.
struct Point { var x = 0.0, y = 0.0 mutating func moveByX(deltaX: Double, y deltaY: Double) { x += deltaX y += deltaY } } var somePoint = Point(x: 1.0, y: 1.0) somePoint.moveByX(deltaX: 2.0, y: 3.0)
추가적으로, 구조체는 변경 메소드 안에서 self를 통해 새로운 구조체 값을 설정할 수도 있는데요 다음 코드를 보시면 될 것 같습니다.
struct Point { var x = 0.0, y = 0.0 mutating func moveByX(deltaX: Double, y deltaY: Double) { self = Point(x: x + deltaX, y: y + deltaY) } }
변경 메소드는 암시적인 self 속성에 새로운 인스턴스를 전부 할당할 수 있습니다.
차이점 4. 구조체는 AnyObject로 타입 캐스팅이 불가능합니다.
struct는 참조 타입이 아닌 값 타입이기 때문에 AnyObject와 같이 클래스 인스턴스 변수를 타입 캐스팅 할 수 있도록 하는 것은 사용할 수 없습니다. 대신에 모든 타입을 지원하는 Any를 사용해야 합니다.
차이점 5. Struct는 생성자를 자유롭게 호출할 수 있습니다.
클래스를 사용하면 우리가 별도의 생성자를 직접 구성해주어야 합니다. 아래 코드처럼 말이죠.
class Parent { var a: Int; var b: Int; init() { a = 0 b = 0 } init(aa: Int, bb: Int) { a = aa b = bb } }
위와 같은 코드에서는 우리는 2가지 종류의 생성자만 호출할 수 있습니다. 하지만 a만 받거나 b만 받고 싶은 상황에서는 어떨까요?
구조체에서는 자유롭게 생성자를 호출할 수 있습니다. 이해가 잘 안되면 코드를 통해 바로 알아보도록 하겠습니다.
struct Point { var x = 0.0, y = 0.0 mutating func moveByX(deltaX: Double, y deltaY: Double) { self = Point(x: x + deltaX, y: y + deltaY) } } var test1 = Point(x: 3.0) var test2 = Point(y: 3.0) var test3 = Point(x: 3.0, y: 3.0)
위에서 정의한 구조체를 다시 불러왔습니다.
분명 아무런 생성자를 정의해준 적이 없지만 사용자는 사유롭게 생성자를 호출하여 초기화를 하고 있습니다.
에러가 발생하지 않으며 동작 역시 문제 없이 진행됩니다.
이번 포스트에서는 구조체와 클래스를 비교해보았습니다.
마지막으로 언제 구조체를 사용해야할 지 언제 클래스를 사용해야 하는지 정리했습니다.
클래스를 사용해야 하는 경우
- ===를 사용하여 인스턴스 ID를 비교해야 할 경우
- 공유할 수 있는 변경 가능한 상태가 필요한 경우
- Objective-C 상호 운용성이 필요한 경우
구조체를 사용해야 하는 경우
- ==를 사용하여 인스턴스 데이터를 비교해야 하는 경우
- 독립적인 복제본이 필요한 경우
- 데이터가 여러 스레드에서 사용되는 경우