본문 바로가기
Dev/고민과 삽질의 기록들🤔

[Swift] 객체 구현시에 class와 struct 어떤게 맞을까 ?? 🤔

by Mintta 2023. 10. 24.

제가 class와 struct을 정할 때 생각하는 기준들을 나열해보고자 합니다.

어떤 객체를 만들 때 그 객체를 class로 만들지, struct으로 만들지에 대한 고민이 늘 항상 개발하면서 있었던 것 같아 그 기준을 정리해보고자 합니다.

이는 우선순위대로 나열한 것이 아닌 모든 사항이 고려된 후에 결정되어야 한다고 생각합니다.

첫번째 고려할 것 : 목적에 필요한 기능을 가지고 있는가 ? 

class와 struct의 차이점으로 상속의 가능 여부가 있습니다. class는 상속이 가능하지만, struct은 상속이 불가능합니다. 만약 구현할 때 필요한 것이 상속이라면 class를 사용할 수 밖에 없습니다. 

 

예를 들어 Cocoa Touch Framework에 있는 UIKit class를 상속받을 필요가 있다면, class밖에 선택지가 없을 것 입니다.

하지만 이 때 유의해야할 점은 class를 쓰는 이유가 상속을 통한 다형성만이 목적이라면 조금 더 고민이 필요하다고 생각합니다.

value type인 struct 또한 protocol을 활용한 다형성의 이점을 얻을 수 있기 때문입니다.

 

따라서 상속을 통해서 부모 클래스의 많은 속성과 메서드를 재사용해야하는 상황일 때 class로 객체를 만드는 것이 좋습니다.

특히 is-a 관계가 성립하는 경우에 가장 적절할 것 입니다.

 

번외) 상속 자체의 단점 또한 고려.

객체지향적으로 설계를 할시에 상속이 정말 불가피한 경우가 아니라면 웬만하면 피하고자 합니다.

그 이유는 바로 상속이 곧 강한 결합이기 때문입니다. 객체간의 강한 결합을 경계하는 이유로는 강한 결합이 객체들을 변경에 유연하게 대처할 수 없게 만들기 때문입니다.

한 객체에서 생긴 에러가 해당 객체를 상속받고 있는 자식 객체로 전파되기에 매우 쉽다는 것이 그 증거입니다.

 

두번째 고려할 것: Identity를 관리해줘야할 필요성이 있는가 ?

이는 애플에서 제공하는 Choosing Between Structures and Classes에서도 설명하고 있습니다.

 

여기서 말하는 Identity는, 우리가 사람한테 있어서의 아이덴티티라고 부르는 것과 같은 의미라고 생각합니다.

그 객체가 그 객체로 존재할 수 있게 만드는 유일한 것, 자기 동일성의 개념 이라고 받아들이면 의미가 맞을 것 같습니다.

 

이 identity의 개념을 그대로 가져와 class와 struct에 대입해본다면 value semancticsreference semantics얘기가 나와야할 것 같습니다.

 

class는 reference type으로, 객체를 주고받을 때 객체를 가리키는 포인터, 객체가 저장된 메모리의 주소를 주고받습니다. 따라서 원본은 그대로 있고 참조만을 주고받습니다.

 

반면, struct은 value type으로, 객체를 주고받을 때 원본을 copy한 복사본을 생성해 넘겨줍니다. 그렇기 때문에 원본은 원본대로, 복사본은 복사본대로 독립적인 객체가 되는 것입니다. 추가로 Swift에서는 COW(Copy-on-write)을 활용해서 실제 변경사항이 일어난 경우에 대해서만 복사를 실행하는 것으로 복사 작업에 대한 오버헤드를 줄이고 있습니다.

 

원본은 그대로 두고 참조만을 주고받는 class는 identity는 유일하며 해당 객체를 여러 곳에서 변경한다면 원본에서도 그 변경사항이 그대로 적용이 되어있을 것이며, 이것은 이 객체를 사용하는 모든 곳에 공유가 될 것입니다.

 

반면에, 복사본을 통해서 객체를 주고받는 value type인 struct은 identity라고 할것이 없습니다. 넘겨받은 객체가 원본이 아닌 복사본이기에 변경사항이 원본과는 완전히 별개입니다.

 

Identity를 지켜 무언가 공유되길 원한다면, 우리는 class를. 혹은 identitiy가 중요하지 않고 공유될 필요가 없다면 struct을 사용할 수 있습니다.

 

최근 Combine스터디에서 찾은 것을 예로 들어서 설명해보겠습니다.

Subscriber protocol을 채택한 객체는 모두 class여야만 합니다.

subscriber protocol

위는 subscriber의 protocol입니다. receive메서드를 보면 subscription을 받고 있습니다. 

Subscription protocol

subscription protocol을 보면 위에서 이런 문구를 확인할 수 있습니다.

Subscriptions are class constrained because a `Subscription` has identity

subscriptions은 class constrained여야 합니다. 왜냐하면 Subscription은 identity를 가지고 있기 때문입니다.

 

여기서 subscription은 구독권, 즉 data stream입니다. Publisher가 이 subscription을 만들어서 subscriber의 receive(subsription:) 메서드의 인자로 subscription을 넘겨주면서 Publisher와 Subscriber간의 data stream이 형성이 되는 것입니다.

 

여기서 이 data stream, subscription은 identity를 보장받아야합니다.

 

왜냐하면 Publisher는 자신의 구독자들, subscriber들에게 값이 생성되었을 때 해당 값을 data stream을 통해서 모든 구독자들에게 전달해주어야합니다. 그런데 이때 data stream에 해당하는 subscription이 struct으로 구현되어  identity를 유지하지 못하고 복사본으로 관리된다면 각각 subscriber는 서로 다른 data stream을 가지게 되어 Publisher가 값을 보낸 특정 subscription을 가진 subscriber만 그 값을 관찰 할 수 있고 나머지는 영문도 모른채 값이 오기를 기다리고 있을 것 입니다.

 

이와 마찬가지로 subscription을 cancel하고자 할 때에도 한 곳에서 cancel한 작업이 여전히 다른 쪽에서는 작업이 이어지고 있다면 문제가 생기게 될 것 입니다.

 

 

즉, 이런 identity(정확하게 동일해야하는)를 보장해야할 필요가 있다면 ! class를 써야하지만 이런 identity를 관리해줄 필요가 없다면 struct을 쓰는게 적절합니다.

그런데 struct이 uuid을 가지면 identity을 보장받을 수 있는 거 아닌가 ? 🤔🤔🤔

이런식으로 말이죠. a객체와 b객체는 value semanctics에 의해서 다른 주소값을 가지지만 id를 통해서 저 둘은 같은 객체라고 보장해주는 것 입니다. 여기까지 생각이 들었을 때는 identity가 struct 대신 class를 쓰는 이유가 되기에는 부족하지 않을까 하고 생각했습니다. 

 

하지만 조금 더 생각해보면 이것은 struct만을 이용해서 구현한 것은 아니죠. class는 자체적으로 identity의 특성을 가지기 때문에 여기에서 오는 관리의 유연함과 편리함 또한 있었을 것 입니다. struct같은 value type에서의 identity를 보장해줘야 하는 상황이 생길 수 있기 때문에 UUID가 나온 것이라고 생각하고, 다른 관리하는데 있어서의 편리함을 보아서는 여전히 identity를 위해서는 class를 써야한다는 것이 조금 더 적절할 수 있겠다고 생각합니다.

 

이 부분은 다른 분들의 생각도 정말 들어보고 싶네요..!

 

세번째 고려할 것 : 성능적인 이슈는 없는가 ?

성능적인 측면을 위해서 Swift는 struct을 선호합니다. 

 

왜 그럴까요?? 🤔

 

1. 우선 heap allocation이 존재하지 않습니다. heap allocation에서 cost가 가장 많이 드는 작업이 Reference Count를 atomic하게 증감소 시키는 것인데, 이런 작업이 필요가 없습니다.

2. static dispatch. 기본적으로 static dispatch를 통해서 메서드를 dispatch하기 때문에 inling과 같은 컴파일 타임에서 최적화의 이점을 누릴 수 있습니다.

 

하지만 만약 복잡한 struct가 내부에 Reference type의 변수들을 많이 들고 있으면 들고 있을수록 문제가 됩니다.

이럴 때는 원복의 복사본 생성과 더불어 N개의 Reference type 변수에 대한 참조 또한 N개 만큼 늘어나기 때문입니다.

참조가 늘어난다는 것은 위에서 언급한 heap allocation의 메인 cost인 reference count를 atomic하게 증가 시켜줘야하는 일이 늘어난 것이라고 할 수 있습니다.

 

따라서 이런 경우에는 빠른 성능을 위해 struct을 사용했는데, 오히려 더 느려지는 상황이 될 수 있습니다 🙁

 

따라서 해당 객체를 여러곳에서 주고받을 때 생길 수 있는 오버헤드를 충분히 고려할 필요가 있습니다.

 

WWDC 영상 중 정말 인상깊게 봤던 영상인 Understanding Swift Performance라는 영상이 있습니다.

해당 영상에서 성능적 이점에 대해서 정말 정말 자세히 설명해주시기 때문에 안보신 분들은 꼭 보셔도 좋을 것 같습니다 !!

( 영상에 대한 정리는 아래 링크를 통해 남겨놓겠습니다! 혹시 궁금하신 분들은 보셔도 좋을 것 같습니다 )

1부: https://codingmon.tistory.com/50 

2부: https://codingmon.tistory.com/31


마무리

객체를 생성할 때 class여야할지, struct이여야할지에 대한 고민을 매 객체를 생성할 때마다 머릿속으로 하게 되는데, 기준을 한번 정리하고 넘어가면 좋을 것 같아서 정리해보았습니다 ! 생각했던 것보다 글이 조금 길어졌는데, 아마 틀린 이야기들도 많을 거라 생각합니다..!

틀린 부분이나 궁금한 점이 있다면 알려주시면 같이 얘기해볼 수 있으면 좋겠네요! 감사합니다

댓글