본 포스팅은 애플 디벨로퍼 아카데미 @POSTECH 테크 포럼 이벤트, '기술 글자랑 대회'의 게시글을 가져와 작성되었습니다.
최근 SwiftUI를 공부하며 구조체(struct)를 사용할 일이 많아졌다. 더불어 데이터 모델링 과정에서 Entity를 생성하다 문득 머릿속에 의문이 들었다.
struct로 데이터를 모델링할 때 저장 속성을 변수로 만들까 상수로 만들까?
내 나름대로의 기준이 있었지만,
(안바뀔거 같은거는 상수로 바뀔거 같은거는 변수로...)
그 기준이 어떤 근거로 왔는지 명확히 설명할 수는 없었다.
또한 변수로 선언하든 상수로 선언하든 사용에 있어 큰 차이를 경험해본적도 없었기에, 더더욱 추측 밖에는 할 수 없었다.
..라는 글을 작성하고 싶었는데 이것저것 파다보니 결국 왜 struct을 데이터 모델링에 사용해야 하는가? 라는 답이 선행되어야 할 것 같아 제목을 변경했다.
..라는 글로 적으려 했는데 너무 길어지는 것 같아 struct와 class의 차이점을 다시 한번 명확히 짚고가자 라는 주제를 선정하게 되었다. (나중에 꼭 적고 말거다)
😼 나 자신을 포함해 두 개념의 차이를 다시 한번 쉽게 이해하고 전달해보고자 하는 취지로 작성해보려 한다!
모델이라 하면 무엇이 떠오르는가? 보통은 요런 모양을 떠올린다.
/// 🐶 강아쥐
struct Dog {
let name: String
var age: Int
}
나만의 귀여운 강아지를 만들려고 한다면(표현하려 한다면) 이런 데이터 '모델'을 정의할 수 있을 것이다. 그렇다! 데이터 모델이라 하면 struct(구조체)로 만들어진 것을 떠올린다.
생각해보면 나는 iOS 개발을 처음 접했을 때부터 지금까지, 다양한 경험에 빗대어 관성적으로 struct를 사용하고 있었다.
왜 struct를 사용하고 있었을까 조금 더 깊이 고민해보자.
struct를 사용하는 맥락에 대해 이해하려면 자연스럽게 class와의 차이점에 대해 고민하게 된다.
귀여운 강아지와 고양이 예제를 이용해 빠르게 훑어보자!
/// 🐶 강아쥐
struct Dog {
let name: String
var age: Int
}
/// 😺 고앵이
class Cat {
let name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
Swift를 공부하면서 가장 많이 듣는 말 중 하나는
"struct는 값 타입, class는 참조타입이다." 라는 것이다.
하지만 예전이나 지금이나 이 문장은 한번에 와닿기에는 무리가 있는 것 같다. 단어 그 자체로서는 인지하고 있지만, 실제 의미에 대해서는 파악하기가 어려운,,, 느낌이다.
어찌되었든, 이렇게나 중요한 차이점을 표현해보고자 예제 코드를 아래 작성해두었고, 흐름은 다음과 같다.
var mintol = Dog(name: "민톨", age: 1)
var mintolCopy = mintol
mintol.age += 1
print("[Struct] 민톨 복제본 나이: \(mintolCopy.age)")
var nabi = Cat(name: "나비", age: 1)
var nabiCopy = nabi
nabi.age += 1
print("[Class] 나비 복제본 나이: \(nabiCopy.age)")
결과는 어땠을까?
아니 분명! 같은 동작을 해줬는데 값이 다르다,,, 왜 이런 말도 안되는 것 같은 일이 벌어졌을까? 그림으로 간단하게 표현해봤다.
1번 동작: struct/class로 객체(인스턴스) 생성하기
struct은 필요할 때만 스택에 올라가고, 필요 없어지면 스택에서 자동으로 해제 된다. 그렇기에 class 처럼 힙에 메모리 주소를 저장할 필요가 없다. 즉, struct의 인스턴스는 값 그 자체를 의미한다.
반면, class는 힙에 메모리 주소를 저장해놓고 사용한다. 즉, class의 객체는 메모리 주소를 가리키고 있음을 의미한다.
⭐️2번 동작: 새로운 변수 만들고 할당하기⭐️
struct : 기존 민톨(값)을 복사해 새로운 변수에 담는다.
class : 기존 나비의 주소(참조)를 새로운 변수에 담는다.
결국 struct의 인스턴스는 값 그 자체로서 복사되어 새로운 변수에 저장되고, class의 객체는 동일한 메모리 주소가 새로운 변수에 저장되는 것이다!
struct과 class 예제가 동일해 보이지만 내부에서는 이러한 차이가 있던 것이다!
3번 동작: 기존 객체(인스턴스)의 저장 속성 업데이트
예상했던 것처럼, struct의 경우 두개의 값 중 원래 값만 바뀌었으니 복제본의 나이는 업데이트가 되지 않았다.
class의 경우 메모리 주소는 동일하게 하나를 가리키고 있으니 같은 값이 출력되고 있는 것이다.
위 예시를 통해 struct과 class의 가장 중요한 특징을 알아봤다. 이것저것 많은 특징 중에 내가 가장 강조하고 싶은 문장은 다음과 같다!
struct는 값타입, 값 그 자체를 의미한다.
class는 참조타입, 메모리 주소를 의미한다.
이제야 값타입, 참조타입이라는 모호했던 개념이 정리가 되는 느낌이다.
그리고 이를 더 명확하게 이해할 수 있는 예제를 가져와봤다.
struct Dog {
let name: String
var age: Int
}
class Cat {
let name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
var mintol = Dog(name: "민톨", age: 3)
let hantol = Dog(name: "한톨", age: 26)
mintol.name = "밍톨"
mintol.age = 4
hantol.name = "두톨"
hantol.age = 27
이 코드의 컴파일 결과는 어떻게 될까? 값 그 자체 라는 맥락에서 struct의 인스턴스를 바라보고, 메모리 주소라는 맥락에서 class의 객체를 바라보자.
여기서 주목해야할 점은, 32번째 줄의 에러이다.
hantol의 age
는 변수라 언뜻 보기엔 값을 변경할 수 있어 보이지만,
인스턴스(struct) 자체가 let으로 선언되어 있어 컴파일 에러가 난다.
당연하게도 값 그 자체인 인스턴스가 불변하기 때문이다.
그리고 자연스레 41번째 줄은 객체(class) 자체가 let임에도 불구하고 age
속성을 변경할 수 있는 이유는,
메모리 주소는 그대로 두고 안의 age 값만 변경하기 때문이다.
개발 공부를 하며 가장 크게 느낀 부분은, 개념적으로 이해하고 사용했을 때의 차이는 생각 외로 정말 크다는 것이다.
오늘도 다시 한번 딥다이브와 기본기의 중요성을 느낀다! 👍