2015년 1월 8일 목요일

Swift Memory Management #3 구조체(struct)와 클래스(class)

지금까지 레퍼런스 카운트와 ARC에 관해 설명해 오다가 마치 삼천포에 빠진 것 처럼 구조체와 클래스에 대한 이야기가 나오게 되었다. 그런데 이 구조체와 클래스의 차이점으로 메모리 관리 상의 차이도 존재하기 때문에 짚고 넘어가야 할 것 같다. 다만, 이 둘의 차이에 대해 이미 이해하고 있다면 무의미한 글이 될 수도 있으니 그냥 넘어가자.

struct는 strong을 남발해도 괜찮다?

상황에 따라 틀릴 수도 있지만, 어느 정도는 사실이다. 왜 그런지는 struct와 class의 차이를 잘 이해하고 있다면 아마도 바로 납득할 것이다.

하나씩 짚어보자.

값과 레퍼런스

메모리 관리 시점에서 구조체와 클래스의 차이점을 살펴보자면 이 두 단어로 정리 할 수 있다.
  • 값(Value): struct 타입의 인스턴스는 Value 라고 표기된다.
  • 인스턴스 레퍼런스(Instance Reference): 모든 class 타입의 인스턴스는 레퍼런스로 참조가 된다.

일단 간단한 예제를 보자.
let a = 10
let b = "TEST"
let c = SomeClass()
세 줄의 라인은 모두 인스턴스를 생성한다. 이 중 a와 b에 대입되는 데이터는 값(Value)이라고 생각하자. 왜냐하면 생성되는 인스턴스가 struct로 구성된 Int와 String 타입이기 때문이다.

그런데 마지막 c는 인스턴스(오브젝트)가 생성되지만 이건 값이라 부르지 않고 레퍼런스라고 부른다.

포인터와도 비슷한 레퍼런스 개념

아래 클래스 예제를 보자.
class SomeClass {
    var name: String = ""
}

var a = SomeClass()
a.name = "A"

var b = a
println(b.name)   // “A”가 출력된다
b.name = "B"
println(a.name)   // “B"가 출력된다
b = a 라는 구문이 실행되면 a와 b는 마치 한 몸 처럼 동작한다.

레퍼런스(Reference) 라는건 오브젝트 인스턴스의 메모리 주소와 비슷하다. a = SomeClass() 라는 구문은 SomeClass()로 인스턴스 메모리가 할당되고 a에는 이 인스턴스 메모리의 레퍼런스가 들어가게 된다. 즉, a는 SomeClass()에 의해 생성된 레퍼런스를 가리킨다.

그리고 b = a 라는 구문은 b에도 a가 가지고 있던 메모리 레퍼런스를 대입해 주는 구문이다. 이렇게 되면 a와 b는 둘 다 앞서 생성한 SomeClass() 인스턴스 메모리 가리키게 된다.

결국 a와 b는 동일한 메모리를 가리키게 되므로 이 메모리가 해제되면 a와 b는 사용 할 수 없게 된다.

참고로 Swift에는 레퍼런스 비교를 위한 ‘===‘ 연산자가 제공된다.
let test = a === b
이 경우 test는 true가 된다.

구조체의 경우

이제 비슷하지만 다른 예제를 보자.
struct SomeStruct {
    var name: String = ""
}

var a = SomeStruct()
a.name = "A"

var b = a
println(b.name)    // “A”가 출력된다
b.name = "B"
println(a.name)    // “A”가 출력된다
위의 class를 이용했던 예와는 다른 양상으로 동작한다.

구조체는 대입 단계에서 class와는 다른 방식으로 동작한다. 바로 값 복사가 일어난다는 점이다. b = a 라는 구문이 동작할 때 b에는 a의 레퍼런스가 아니라 동일한 구조체 타입의 메모리를 생성해서 a가 가리키는 인스턴스의 내용을 그대로 복사해 넣는다.

그래서 a와 b는 별개로 동작하게 된다.

당연하겠지만 구조체끼리는 레퍼런스 비교 연산자 ‘===‘ 는 사용 할 수 없다.

ARC 입장에서 안전한 건 구조체(struct) 타입

제일 처음에 했던 이야기를 다시 해 보자. struct는 strong을 남발해도 어느 정도는 괜찮다.

사실 class이든 struct이든 뭐든 간에, Swift에서 모든 변수는 특정 인스턴스의 레퍼런스를 가지게 된다. 다만, 이 값들의 대입(assignment)이 발생 할 때 struct는 동일한 타입의 인스턴스를 새로 생성해서 여기에 참조하려는 인스턴스의 데이터를 몽땅 복사해 넣은 다음 이 인스턴스의 레퍼런스를 대입한다. 하지만 class의 경우는 이런 새 인스턴스 생성이나 복사 과정이 없이 원본 레퍼런스 자체가 그대로 넘겨지게 된다.

결과적으로 강한 참조(strong) 프로퍼티라고 해도 만약 이 프로퍼티의 타입이 struct로 만들어져 있다면 '서로를 묶어버리는 리테인순환(Retain Cycles) 문제'가 발생할 여지가 없어진다. 왜냐하면 해당 프로퍼티는 동일한 레퍼런스를 가질 수가 없으니까.

단순히 생각해보자. strong property에 값을 대입하면 무조건 리테인을 건다. 하지만 struct 타입이라면 값을 복사한 새로운 인스턴스를 받아서 여기다 리테인을 건다. 따라서 대입되는 원본(?) 인스턴스에는 아무런 영향이 가지 않는다. 둘은 별개의 데이터가 된다는 말이다.

물론 struct 타입이라도 내부에서 class 타입의 프로퍼티를 가지게 된다면 이건 차원이 다른 이야기다. 이 경우 대입에 의한 복사가 일어나더라도 class 타입 프로퍼티는 레퍼런스만 복사하는 선이 되기 때문에 순수한 struct 타입과는 상황이 다르다. ARC의 혼란을 극복해야 한다.

물론 레퍼런스도 장점이 있다

레퍼런스 참조라는 형태는 현대 거의 모든 고급 언어에서 기본 모토로 삼고 있다. 값 대입 시 struct가 데이터를 복사하는 형태로 대입이 된다는 건 반대로 '레퍼런스만 넘겨주는 형태' 에 비해 퍼포먼스가 떨어진다. 여러 가지 상황이 있겠지만, 대입 명령이 루프로 매우 많이 실행되는 코드라면 struct 타입은 지양하는 편이 좋을 것이다.

또 다른 레퍼런스의 장점은 ‘공유(sharing)’가 가능해진다. 하나의 오브젝트 인스턴스를 만들어 놓고 이 인스턴스의 레퍼런스를 어디서든 가져다 쓸 수 있다. 이 말은 다르게 표현하면 싱글턴(singleton) 패턴과 비슷하다. 반대로 Swift에서 기본적으로 제공되는 struct 타입 자료구조에는 싱글턴 팩토리가 없다는 것을 생각해보자.

잡설

struct와 class의 차이는 이 밖에도 몇 가지 더 있지만 (예를 들어 immutable을 선언하는 let 등등) ARC 입장에서 본다면 값과 레퍼런스라는 개념 차이가 중요하니 이 정도로 마무리한다.

처음 Swift를 접했을 때 왜 String이나 Int 같은 기본 자료형은 모두 struct인가를 심하게 고민했었는데, 요즘은 여러가지 면에서 이유가 있었다고 느끼고 있다. 아마도 구조체(struct)는 Swift의 특징 중 하나로 꼽을 수 있을 것 같다.

[관련글] 좀 더 단순한 싱글턴 패턴(Singleton Pattern)
[관련글] Swift - 언제 class 대신 struct 를 사용하는가

[이전글] Swift Memory Management #2 ARC 기초
[다음글] Swift MemoryM anagement #4 클로져(Closure)의 경우

댓글 4개 :

aqua :

처음 Swift를 접했을 때 왜 String이나 Int 같은 기본 자료형은 모두 struct인가를 심하게 고민했었다고 하셨는데, 여러 가지 측면에서 이유가 있다는 부분은 어떤 부분인지 여쭤 보아도 될까요??

그 이유가 저도 궁금하긴 했었는데 String과 int가 Struct 인 경우 얻게 되는 이득이?? 어떤게 있을까 싶어 질문 드려요.. ㅎ

aqua :
작성자가 댓글을 삭제했습니다.
Renn Seo :

struct는 대입 시 복제라는 큰 특징이 있고 이로 인해 불변성이 가능해지고 멀티스레드 안정성과 메모리 관리 안정성이라는 잇점을 얻게 됩니다.

결국 이로 얻을 수 있는건 신뢰성이지요. 특히 기본 타입의 경우 여기 저기서 많이 쓰일테니 안정성/신뢰성이 큰 이슈가 됩니다. 그렇다면 답이 나오는 것이지요.

aqua :

앗 감사합니다. :)