안녕하세요! 보노입니다.
오늘은 Tuist를 사용하며, 자동으로 생성해주는 Resource 파일 코드를 보다 느낀 의문과 이해 과정에 대해 기록해 보려 합니다.
기록에 의미를 두고 편한 말투를 사용하나 모르는 것이 많습니다.
꼭꼭 질문 사항이나 피드백 사항이 있다면 댓글 부탁드립니다!
최근 Xcode Instruments에 관심이 많아서 이것저것 눌러보고 있다.
가장 관심있게 보고 있는 것은 leak 이다.
최근 진행 중인 프로젝트에 Instruments leak을 돌리자 특정 class 인스턴스가 여럿 생성되는 걸 발견했다.
WineyLit.WineyKitColors
WineyKit은 내가 진행 중인 프로젝트의 디자인모듈(프레임워크) 명이다. 이 곳에 내가 넣어둔 Font와 Color가 있다.
그리고 이 모듈에서 11번 이나 생성되고 단 한 번도 메모리 해제되지 않은 class, WineyKitColors
가 있었다
코드를 찾아보자.
Tuist가 자동 생성해 준 Asset 파일들
위의 코드는 TuistAssets+WineyKit의 일부로, 내가 넣어둔 Color에 대해 Tuist가 자동 생성해 준 코드이다.
Tuist에서 Resource를 넣으면 자동으로 관리 파일을 생성해 주기에 이제껏 별 생각 없이 사용하고 있었다.
(우선 나는 SwiftGen 사용 경험이 없다)
그런데 지금 보니, WineyKitColors에 해당하는 코드가 class로 생성되어 있음을 알게 되었다.
그러니 색상에 접근할 때 인스턴스가 생성되어 leak 상에 찍혔던 것이다. (누수가 발생한 것은 아니다)
인스턴스 내부에 여러 색상이 전부 들어 있는 게 더 좋지 않나/ 더 일반적이지 않나?
왜 색상을 struct가 아닌 class로 만들어 타입변수로 두는 거지?
우선 첫 번째 의문은 추가하는 Assets(Color)을 정해진 양식으로 추가해 주기 위해 객체화 한 것으로 판단하고 해결하였다.
하지만 class 사용 판단은 여전히 의문이다.
생각해 보면 나는 class와 sturct를 자의로 판단해 사용해 본 적이 없었다.
그저 커다란 묶음이면 class (UIKit의 ViewController나 ViewModel)
작은 조각이면 struct를 사용해 왔다.
통상 대부분의 코드가 그렇게 구성되어 있길래, 체득한 것에 가깝다. 그러니 이론적으로 배운 지식과 실제 개발의 불일치 영역이기도 했다.
class의 상속을 프로젝트에 적용해 본 건 반복적인 작업을 구조화한 BaseViewController 뿐이다. (그 마저도 ViewController를 위한 class)
또한 만일 기능 정의가 필요하다면 대게 protocol과 extension을 사용하였다.
무의식 중에 더 작은 단위가 컨트롤하기 편하다는 생각이 있었기 때문이다.
다시 본론으로 돌아와서, 내게 위 코드가 어색하게 느껴지는 이유는 이런 식으로 많은 인스턴스 생성을 필요로 하는 class를 사용해 본 적이 없기 때문이다.
만약 WineyKitColors가 class라는 것을 몰랐더라면, 나는 위의 코드를 보고 WineyKitColors가 당연히 struct 일 것이라 예상하였을 것이다.
내가 사용한 class는 대게 누군가에게 특정 부분 자유도(확장)을 제공하기 위해 제작된 framework나 library 속 기능, 특정 주제에 대한 코드 묶음 (ViewModel) 이었다.
확장도, 상속도 일어날 일 없는 기능을 왜 class로 만들었을까.
여기서 확장과 상속을 고려하지 않았으리라 추측하는 건 해당 class의 init이 fileprivate이기 때문이다.
위 클래스는 TuistAssets+WineyKit.swift
내부에서 외부에 보일 아래 코드의 color들을 규격화된 양식으로 찍어내기 위한 틀의 역할을 수행한다.
그래서 왜 class로 만들었을까.
나름의 답을 내리기 위해 WineyKitColors의 코드를 다시 살펴보자.
사실 잘 모르겠다.
그냥 struct로 바꾸어보자.
struct는 상속을 수행하지 않으므로 final
을 지워주고 기존 get
함수가 내부 변수를 변경하므로 mutating
키워드를 추가한다.
아무런 이상 없는데? 라고 생각하는 순간 에러가 하나 발생했다.
코드 사용 단에서 발생한 문제였다.
에러 내용은 아래와 같다.
Cannot use mutating getter on immutable value: 'gray950' is a 'let' constant
아.
static let으로 선언되어 있던 내부 변수를 static var로 변경해 주니 문제가 사라졌다.
왜냐하면
현재 Color 값에 접근시 그때야 _swiftUIColor에 값을 할당하기 때문이다.
let 으로 선언된 struct는 속성 변경을 허용하지 않는다.
let 선언 시 인스턴스의 주소값이 고정되고 속성 변경은 허용되는 class와는 개념이 다르다.
이후 build 하면 에러가 사라진다.
또한 정적 변수로 생성된 struct는 메모리에 존재하므로 메모리 leak (누수)를 검사해도 찍히지 않는다.
그래도 실제 앱 사용공간을 차지하기는 마찬가지다.
무엇이 더 실용적인가?
코드를 이리저리 바꿔보며 class 사용이 더 이로울 것 같다는 깨달음을 얻었다.
이유는 아래와 같다.
class가
lazy
사용에 더 적합하다.
위 코드에서 사용된 지연 속성은 static과 lazy이다.
둘 다 접근 시에 활성화되도록 동작한다.
(차이가 있다면 static이 더 thread-safe 하다고 한다)
class 내부 lazy는 접근 시에 값을 활성화 한다는 의미가 있다.
물론 struct도 mutating
과 함께 lazy
를 사용할 수 있지만, '값이 없다'는 상황 자체가 struct 인스턴스의 특징이 되므로 사용상 어색하다.
또한 lazy
상황을 적용하기 위해 static let
이 아닌 static var
라고 표현하는 것도 nameSpace
용도로 사용되는 enum 내의 타입 변수로 사용하기엔 어색한 느낌이다.
public enum WineyKitAsset {
public static let background1 = WineyKitColors(name: "background_1")
}
static let
이 WineyKitAsset의 역할을 더 잘 설명한다.
또한 위 코드를 뜯어 보며 유익한 코드 습관을 두 가지 습득하였는데
첫 번째는 바로 enum
과 static let
의 조합이다.
익숙한 형태의 사용이긴 한데, 이제야 왜 이렇게 자주 쓰는 지 알 것 같다.
enum은 생성자가 없다.
즉, enum + static 조합은 선택지가 여러 개인 싱글톤처럼 보인다.
두 번째 유익한 깨달음은 public으로 노출할 코드와, public 내부에서 사용되나 노출하고 싶지 않은 코드를 분리하기 위해 init을 fileprivate로 설정하는 것이다.
아마 공유 라이브러리를 만든 경험이 있었다면 일찍이 체득했겠지만, 이제야 알았다.
아마 이걸 몰랐다면 숨기고픈 코드도 public으로 열어두고 피눈물만 흘리지 않았을까 싶다.
왜 이걸 class로 쓸까에 대해 공부하다가 이것저것 많은 생각을 해 보았습니다.
공부가 다 이런거겠죠? 유익했습니다!
혹시 class 사용 이유에 대해 조언해주실 것이 있다면 꼭 댓글 부탁드립니다.
다들 즐거운 개발 되세요!