swift 에서 struct 와 class 는 소스로 보면 형태와 쓰임 면에서 크게 다르지 않다. 그렇다면 무엇이 다르고 왜 필요하게 되었는지 생각해 보자.
Immutable 세상
메모리/데이터를 여러 프로그램이 동시에 접근하여 읽기/쓰기를 수행한다면 무결성을 어떻게 유지할 것인가?
이 문제를 해결하기 위한 방법으로 데이터를 immutable 하게 하여 읽기만 수행하고 쓰기를 금지시켰다. 변형된 데이터가 필요하다면 function 내에서 새로 생성된 데이터를 반환하면 된다. 이 방법은 아주 간단하면서도 문제의 근원을 해결해 주었다.
예를 들어 배열에 1부터 10까지의 정수가 들어있는데, 홀수만 필터링된 배열이 필요하다면 새로운 배열을 만들고 기존 데이터에서 홀수만 넣고 새로운 배열을 생성하여 반환하면 된다. 이 데이터는 기존의 데이터와 다른 새로운 데이터이다. 따라서 새로운 데이터에 변형이 생겨도 원본 데이터에 영향을 주지 않는다.
하지만 메모리 효율성 측면에서 본다면 동일 데이터가 중복해서 생성되기 때문에 낭비가 발생한다. 방금 예를 들은 배열문제의 경우 다음과 같이 해결한다.
위 그림에서 보면 1에서 10까지의 원본 배열이 있고, 메모리에 생성되어 있다. 필터링된 새로운 배열이 만들어질 때 원본 데이터의 메모리 참조를 그대로 활용하여 새로 만들 수 있다. 메모리 측면에서는 손해보는 것 없고, 원본 데이터의 변형없이 새로운 배열이 만들어 졌다. 이 새로운 배열에 변형이 이뤄진다. 역시 변형된 새로운 배열이 만들어 지게 되고, 변형된 데이터만 메모리에 새로 추가되고 이를 참조한다. 새로운 배열이 총 3개가 만들어 졌지만 각각의 원본 배열에는 영향을 주지 않았고, 메모리도 효율적으로 관리되었다. 여기서 짚고 넘어갈 부분이 있는데, 나중에 생성된 2개의 배열은 원본배열을 참조하며 재배치된 참조배열이지 독립된 배열데이터는 아니다. 따라서 엄밀히 말해 이 2개의 배열은 새로운 데이터가 아니다. 새로운 데이터라면 별도의 메모리 영역을 차지하고 있어야 하는 것이다.
Swift 에서 이와 같이 동작되는 것을 엿볼 수 있는데, 간단하게 Substring의 예 하나만 살펴보자. 그림에서 정수배열을 글자가 하나씩 담긴 배열로 만들어진 String 이라고 생각해 본다면 필터링된 배열은 Substring 으로 볼 수 있다. 이것은 원본 데이터가 조합된 레퍼런스 데이터지 별도의 독립 데이터가 아니다. 따라서 완전한 String이라고 할 수 없다. 그렇다면 독립된 String 과는 구분을 할 필요가 있으니 새로운 데이터 형으로 구분을 해 줄 필요가 있다.
문자열의 첫 일부를 잘라내는 prefix 메소드를 보면 리턴타입이 Substring 으로 String 과 구분되어 있는 것을 볼 수 있고, Swift 에서도 이처럼 관리하고 있다는 것을 알 수 있다. Substring 으로 독립된 String 을 생성하려면 String("1234567890".prefix(3))
과 같은 명령으로 새로운 String 인스턴스를 만들어 줘야 한다.
struct 의 필요성
immutable 데이터 세상에서 변형된 데이터를 얻기 위해서 기존 데이터를 복사할 필요가 있고, 이 과정에서 메모리 효율성을 얻는 방법을 살펴보았다. 만약 개발하는 모델 객체를 NSObject 를 상속받은 클래스로 만들었고, 이 모델 객체를 immutable 하게 처리한다면 데이터 변형을 위해 복사본을 만들 필요가 있다. 그리고 그것이 쉽지만은 않다. (자세한 설명은 생략한다. 직접 해보면 금방 알 수 있다.) 이러니 모델처럼 여러 데이터를 담고 있는 객체를 Int 나 Float 처럼 Call By Value 로 처리하는 방법이 필요하게 되었고 그래서 struct 가 필요한 것이다.
struct 가 구조 데이터의 call by value 취급을 위해 필요하게 되었으니 당연히 struct 는 변수에 대입하면 값이 복사된다. 반대로 class 는 복사되지 않는다. (레퍼런스만 복사된다.)
1 | class ReferenceObject {} |
이 코드에서 ReferenceObject 인스턴스는 1개 만들어 졌고, classA 와 classB 는 같은 인스턴스를 가리킨다. 레퍼런스만 복사되었으니까. 그래서 classB 의 값을 변경하면 classA에서 조회하는 값도 달라진다. call by reference 라고 한다.
1 | struct ValueObject {} |
이 코드에서 ValueObject 인스턴스는 2개 만들어 졌고, valueA 와 valueB 는 복사본으로 내용은 같지만 다른 인스턴스다. 그래서 valueB 의 값을 변경해도 valueA 의 값이 영향받지 않는다. call by value 라고 한다.
(급) 마무리
immutable 관점에서 보면 struct 의 필요성은 당연하다. 그러한 이유로 만들어 졌으니 그러한 용도로 사용하면 된다. 차이점과 존재 이유를 알았으니 ‘어떨 때 뭘 써야하나요?’ 같은 문의도 당연히 해결될 것이다.