Search

[Swift 공식 문서] 25. ARC

Automatic Reference Counting

Swift 는 Automatic Reference Counting(이하 ARC)를 사용해서 앱의 메모리 사용을 추적하고 관리합니다. Swift 에서는 메모리 관리는 ‘알아서' 해준다는 의미죠. ARC 는 클래스의 인스턴스가 사용중인 메모리를 인스턴스가 더이상 필요없을 때, 자동으로 메모리를 할당해제 합니다.
그러나 가끔 ARC도 여러분이 작성한 코드에 대해 추가적인 정보를 필요로 할때가 있습니다. 이 챕터에서는 ARC 가 앱의 메모리를 관리하는 상황을 설명합니다.
Reference Counting 은 오직 클래스의 인스턴스에만 적용됩니다. 구조체나 열거형은 밸류타입이므로 참조되지 않습니다.

How ARC Works

클래스의 인스턴스가 생성될떄마다, ARC 는 인스턴스에 대한 정보를 저장하기 위해 메모리 공간을 할당합니다. 이 메모리에는 인스턴스의 타입과 더불어 모든 프로퍼티의 정보가 저장됩니다.
인스턴스가 더이상 필요 없다면, ARC 는 인스턴스가 점유하고 있는 메모리를 해제합니다.
그러나 ARC 가 사용중인 인스턴스를 할당해제하는 경우, 인스턴스의 프로퍼티나 메서드에 접근할 수 없어지고, 접근을 시도하면 앱은 크래시가 발생합니다.
이러한 상황이 일어나지 않도록 하기위해 ARC 는 프로퍼티, 상수 변수가 각각의 클래스 인스턴스를 얼마나 참조하고 있는지를 추적합니다. ARC 는 하나의 참조라도 살아 있는한, 인스턴스를 할당해제하지 않습니다.

ARC in Action

아래는 ARC가 동작하는 방식을 보여주기 위한 예제입니다.
class Person { let name: String init(name: String) { self.name = name print("\(name) is being initialized") } deinit { print("\(name) is being deinitialized") } }
Swift
복사
var reference1: Person? var reference2: Person? var reference3: Person?
Swift
복사
하나의 객체를 생성해서 이를 모두 각각의 변수에 저장하면, 총 3번의 Strong reference(이하 강한참조)가 일어납니다.
reference1 = Person(name: "John Appleseed") // Prints "John Appleseed is being initialized" reference2 = reference1 reference3 = reference1
Swift
복사
총 3번의 강한참조가 일어났기 때문에 2개의 참조를 제거해도 객체는 할당해제되지 않습니다.
reference1 = nil reference2 = nil
Swift
복사
마지막 참조가 제거되어서야, ARC 는 Person 인스턴스를 할당해제합니다.
reference3 = nil // Prints "John Appleseed is being deinitialized"
Swift
복사

Strong Reference Cycles Between Class Instances

위 예제처럼, ARC 는 참조의 갯수를 세어서 참조 횟수가 0이 될 때, 할당해제합니다.
하지만 두개의 클래스 인스턴스가 서로를 강한 참조로서 붙잡고 있는 경우, 이 두 인스턴스의 참조 횟수는 결코 0이 되지 못하고 실행 도중 계속해서 살아있게 됩니다. 이를 “strong reference cycle” 이라고 부릅니다.
클래스간의 소유 관계를 weak 또는 unowned 참조로 정의하게 되면 강한 순환 참조 현상을 해결할 수 있습니다. 그러나, 강한 순환 참조를 해결하는 방법을 배워보기 전에 왜 이러한 순환 참조가 야기되는 지를 이해하면 좋습니다.
아래에는 사고에 의해 강한 순환 참조가 일어난 예제 코드가 작성되어 있습니다.
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") } } class Apartment { let unit: String init(unit: String) { self.unit = unit } var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
Swift
복사
Person 은 apartment 라는 프로퍼티에 Apartment 클래스의 인스턴스를 참조할 수 있으며, Apartment 는 tenant 라는 프로퍼티에 Person 클래스의 인스턴스를 참조가능하게 정의되어 있습니다.
두 클래스 모두 deinit 되는지 확인할 수 있도록 print 문이 작성되어 있는 deinitializer 가 정의되어 있습니다.
이제 각각을 담는 변수를 정의하고 이에 인스턴스를 참조해보도록 하겠습니다.
var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A")
Swift
복사
이제 두 인스턴스의 apartment, tenant 프로퍼티에 서로를 할당하여 서로를 연결해줄 수 있습니다.
john!.apartment = unit4A unit4A!.tenant = john
Swift
복사
이렇게 두 인스턴스를 연결하게 되면 강한 순환참조 현상이 발생합니다. Person 인스턴스는 이제 Apartment 인스턴스를 강하게 참조하며, Apartment 인스턴스 또한 Person 인스턴스를 강하게 참조합니다. 따라서, john 또는 unit4A 의 강한 참조를 해제하여도, reference count 는 0 이 되지 않고, 두 인스턴스는 ARC 에 의해 메모리로부터 할당해제되지 않습니다. (메모리 누수)
john = nil unit4A = nil
Swift
복사
위 코드를 실행하여도 어떠한 deinitializer 도 실행되지 않습니다. 두 인스턴스를 담는 변수가 없기 때문에 두 인스턴스를 사용할 방법은 없지만, 두 인스턴스는 메모리에 살아있게 됩니다.

Resolving Strong Reference Cycles Between Class Instances

Swift 에는 강한 순환참조 현상을 해결하기위한 두가지 방법을 제공합니다.
weak
unowned
weak 과 unowned 참조는 인스턴스 끼리 강하게 서로를 잡지 않으면서 순환 참조할 수 있게 해줍니다. 인스턴스는 강한 순환 참조를 일으키지 않으며 서로를 참조할 수 있게 되죠.
다른 하나의 인스턴스가 더 짧은 수명을 가지는 경우 weak 를 사용해서 이를 해결하세요. 위 Apartement 예제에서는, 세입자가 아파트보다 수명이 짧을 것입니다. 아파트가 존재해야 세입자가 존재할 수 있으며, 아파트가 없는 경우 세입자도 없죠. 따라서 이경우 세입자가 아파트보다 수명이 짧을 것이 보장됩니다. 이런 경우 weak 참조를 사용해서 강한 순환 참조를 막을 수 있습니다. 반대로 다른 인스턴스의 수명이 같거나 더 긴 경우에는 unowned 참조를 사용하면 됩니다.

Weak References

weak reference 란 참조하는 인스턴스를 강하게 붙잡지 않는 참조를 의미합니다. 그래서 ARC 가 참조된 인스턴스를 배치하지 못하게 막습니다. 따라서 강한 순환참조의 구성요소가 되지 않게됩니다. 변수 선언시 weak 키워드를 사용해서 약한 참조임을 나타낼 수 있습니다.
약한 참조는 인스턴스를 강하게 붙잡지 않기 때문에, 참조를 하고 있는 경우에도 인스턴스가 할당해제될 수 있습니다. 그리고 인스턴스가 할당해제되면, ARC 는 자동으로 weak 참조를 nil 로 변경하게됩니다. weak 로 선언된 참조는 값이 런타임 도중에 nil로 변경될 수 있기 때문에, 항상 변수, 그리고 optional 타입으로 선언되어야 합니다.
weak 는 항상 변수, optional 타입으로 사용된다. ( nil 로 변경 가능하기 때문 )
약한 참조의 값이 존재하는 것을 다른 optional 값처럼 체크할 수 있습니다.
ARC 가 약한 참조에 nil 을 할당할 때 프로퍼티 옵저버(didSet, willSet)는 호출되지 않습니다.
이번에는 위 Apartment 예제에서 tenant 프로퍼티를 약한 참조를 사용하여 선언해보겠습니다.
class Person { let name: String init(name: String) { self.name = name } var apartment: Apartment? deinit { print("\(name) is being deinitialized") } } class Apartment { let unit: String init(unit: String) { self.unit = unit } weak var tenant: Person? deinit { print("Apartment \(unit) is being deinitialized") } }
Swift
복사
var john: Person? var unit4A: Apartment? john = Person(name: "John Appleseed") unit4A = Apartment(unit: "4A") john!.apartment = unit4A unit4A!.tenant = john
Swift
복사
이번에는 Apartment가 Person을 강하게 참조합니다. 이 경우 Person 인스턴스를 강하게 참조하는건 john 이라는 변수 뿐이므로, john 을 nil 로 설정하게 되면, 더이상 Person 인스턴스에 대한 강한 참조가 존재하지 않게됩니다.
john = nil // Prints "John Appleseed is being deinitialized"
Swift
복사
Person 인스턴스에 대한 강한 참조가 더이상 존재하지 않기 때문에, ARC 는 tenant 프로퍼티를 nil 로 할당합니다.
Person 인스턴스를 가리키는 참조가 더이상 존재하지 않으므로 Person 인스턴스는 deinitialize 됩니다.
unit4A = nil // Prints "Apartment 4A is being deinitialized"
Swift
복사
Apartment 인스턴스를 가리키는 강한 참조 또한 사라졌기에, Apartment 인스턴스도 deinitialize 됩니다.
garbage collection을 사용하는 시스템 내부에서, weak 포인터는 가끔 단순한 캐싱 매커니즘을 위해 사용됩니다. 이는 강한참조가 없는 참조는 오직 메모리 압박이 garbage collection 을 유발할 때에만 할당 해제되기 때문인데요. 하지만, ARC에서 값들은 강한 참조가 제거 되자마자 할당해제시키므로 해당 목적에 weak 참조를 사용하는 것은 올바르지 않습니다.

Unowned References

weak reference 와 마찬가지로 unowned 도 인스턴스를 강하게 붙잡지 않습니다. 하지만 weak 과는 달리, unowned 는 다른 인스턴스가 동일한 수명 또는 더 긴 수명을 가질 때에만 사용합니다. unowned 키워드를 프로퍼티 변수 선언 앞에 작성해서 unowned 참조를 사용할 수 있습니다.
약한 참조와 달리 unowned 참조는 항상 값을 가지고 있는 것으로 기대됩니다. 따라서 optional 타입이 아니고 ARC는 참조 값에 nil 을 할당하지 않습니다.
인스턴스가 사용 도중 할당해제되지 않음이 보장될때에만 사용하자
unowned 참조의 대상 인스턴스가 할당해제된 후 이에 접근하고자 한다면 런타임에러가 발생!
CustomerCreditCard 클래스의 예제를 보며 설명하겠습니다.
이 두 클래스는 각각 서로를 프로퍼티로서 저장합니다. 이 관계는 앞서 이야기한 바와 같이 강한 순환 참조 문제를 발생시킬 가능성이 존재합니다.
CustomerCreditCardApartmentPerson 의 관계와는 사뭇 다릅니다.이 데이터 모델에서 customer 는 신용카드를 가지고 있을 수도, 그러지 않을 수도 있습니다만, 신용카드 데이터는 반드시 고객데이터를 갖고 있어야 합니다. CreditCard 클래스는 optional 한 card 프로퍼티를 가지고 있습니다만, CreditCard 클래스는 optional 하지 않은 customer 프로퍼티를 가지고 있습니다.
신용카드는 항상 고객을 가지고 있어야하므로, CreditCard 의 생성자에서는 Customer 객체를 파라미터로 받아 할당하게됩니다.
이렇게 하면, CreditCard 인스턴스는 항상 Customer 인스턴스를 보유함을 보장받습니다.
항상 Customer 인스턴스를 보유하므로, customer 프로퍼티를 unowned 로 선언하면, 강한순환참조를 에방할 수 있습니다.
class Customer { let name: String var card: CreditCard? init(name: String) { self.name = name } deinit { print("\(name) is being deinitialized") } } class CreditCard { let number: UInt64 unowned let customer: Customer init(number: UInt64, customer: Customer) { self.number = number self.customer = customer }
Swift
복사
var john: Customer?
Swift
복사
john = Customer(name: "John Appleseed") john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
Swift
복사
johnnil 을 할당해 Customer 인스턴스의 강한 참조를 제거하면, Customer 인스턴스에 강한 참조가 존재하지 않게됩니다.
따라서, Customer의 인스턴스가 먼저 할당해제 됩니다.
그 후 CredictCard 에도 강한 참조가 남아있지 않게되어, CreditCard 의 인스턴스가 할당해제됩니다.
john = nil // Prints "John Appleseed is being deinitialized" // Prints "Card #1234567890123456 is being deinitialized"
Swift
복사
위 예제는 안전한 unowned 참조법의 예시입니다. Swift 는 안전하지 않은 unowned 참조 기능도 제공하는데요. 이는 런타임 safety 체크 기능을 사용하지 않아도 될 때 사용하면 됩니다. 예를 들면 퍼포먼스적인 이슈에 의해서요. 개발자가 코드의 안정성에 대해 점검할 필요가 있게됩니다. unowned(unsafe) 키워드를 사용해서 safety 체크 기능을 끌 수 있습니다. 이를 끄게 되면 참조 하는 대상이 할당해제된 후에 이 인스턴스에 접근하고자 하면, 프로그램은 인스턴스가 있었던 공간에 접근하게 됩니다. 이는 안전하지 않죠.

Unowned Optional References

optional 참조를 사용해 클래스를 사용되지 않은 것으로 나타낼 수 있습니다. ARC 소유 모델의 관점에서, unowned optional reference 와 약한 참조는 모두 같은 맥락에서 사용됩니다. unowned optional reference 를 사용할 때의 차이는 , 참조하는 대상이 유효한 객체를 참조하는 지, nil 값인지를 직접 체크할 필요성이 있다는 것이지요.
아래는 학교의 특정학과와 그 학과의 수업에 대한 예시입니다.
class Department { var name: String var courses: [Course] init(name: String) { self.name = name self.courses = [] } } class Course { var name: String unowned var department: Department unowned var nextCourse: Course? init(name: String, in department: Department) { self.name = name self.department = department self.nextCourse = nil } }
Swift
복사
Department 는 강한 참조를 사용해서 각각의 수업 내용을 저장합니다. ARC 소유 모델의 관점에서 department 는 수업을 소유하죠. Course 는 두개의 unowned 참조를 가지고 있습니다. 하나는 학과, 다른 하나는 학생들이 들어야하는 다음 수업에 대한 정보입니다. 모든 수업들은 학과의 일부분이므로 department 프로퍼티는 optional 하지 않습니다. 그러나, 몇몇 수업들은순서대로 듣지 않아도 되어서, nextCourse 프로퍼티는 optional 하지요.
아래는 몇개의 수업에 대한 예시입니다.
let department = Department(name: "Horticulture") let intro = Course(name: "Survey of Plants", in: department) let intermediate = Course(name: "Growing Common Herbs", in: department) let advanced = Course(name: "Caring for Tropical Plants", in: department) intro.nextCourse = intermediate intermediate.nextCourse = advanced department.courses = [intro, intermediate, advanced]
Swift
복사
unowned optional reference 는 해당 인스턴스를 감싸고 있는 클래스를 강하게 붙잡지 않습니다. 그래서 ARC 가 인스턴스를 할당해제하지 못하게 막지 않습니다. unowned reference 와 똑같이 동작하지만, optional 하기 때문에 referernce 가 nil 일 수 있습니다.
non optional unonwned reference 와 같이, nextCourse 가 항상 할당해제 되지 않은 Course 만을 참조하는 것은 보장됩니다. 다만 그 참조 자체가 존재하지 않을 수 있다는 것만 다르죠. 또한 언제든지 그 참조를 제거할 수 있게 됩니다.
정리
1.
unowned 는 할당해제된 클래스를 참조하지 않도록 보장하는 기능을 가짐.
2.
unowned optional 은 참조를 해제하거나, 참조가 존재하지 않을 수 있음.
둘 다 ARC 소유 모델에서 자신을 소유하고 있는 클래스가 할당 해제되면 자신들도 할당 해제됨

Unowned References and Implicitly Unwrapped Optional Properties

앞서 보았던 weak, unowned 참조에 대한 예시는 대부분의 흔한 강한 순환 참조 사이클 시나리오에 대한 예시입니다.
PersonApartment 예제에서 두 프로퍼티는 nil 이 될 수 있고, 잠재적으로 강한 순환 참조를 야기시킵니다. 이러한 시나리오의 최고의 해결책은 weak 참조이죠.
CustomerCreditCard 예제에서 하나의 프로퍼티가 nil 이 될 수 있고 다른 하나의 프로퍼티는 nil 일 수 없는 상황입니다. 이런 경우 unowned 참조를 사용해서 강한 순환 참조 사이클 문제를 해결하는 것이 바람직합니다.
그러나 그 이외의 시나리오도 존재합니다. 두 프로퍼티 모두 값이 항상 존재해야하고, 두 프로퍼티 모두 nil 이어서는 안되는 시나리오말이죠. 이러한 경우, unowned 프로퍼티와 implicitly unwrapped 한 optional 프로퍼티를 함께 사용하는 것이 바람직합니다.
initialization 이 종료된 이후, 두 프로퍼티는 직접 접근이 항상 가능해야합니다. 강한 순환 참조를 피하면서요. 이 섹션에서는 어떻게 이러한 관계를 정의하는 지 보여드리겠습니다.
class Country { let name: String var capitalCity: City! init(name: String, capitalName: String) { self.name = name self.capitalCity = City(name: capitalName, country: self) } } class City { let name: String unowned let country: Country init(name: String, country: Country) { self.name = name self.country = country } }
Swift
복사
두 클래스 사이에 상호의존적인 관계를 설정하기 위해서, City 의 initializer 는 Country 인스턴스를 필요로 하고, 이를 country 프로퍼티에 저장합니다.
Country 의 생성자 내부에서 City 를 위한 생성자가 호출됩니다. 하지만, Country 를 위한 생성자는 City 생성자에게 self 를 건네줄 수 없죠. 이는 새 Country 인스턴스가 완전히 생성되기 이전이기 때문이에요
이러한 요구사항을 충족시키기 위해서는, capitalCity 프로퍼티를 암묵적으로 unwrap 된 옵셔널 프로퍼티로 정의해야합니다. capitalCity 프로퍼티가 초기값으로 nil 을 갖지만, unwrap 할 필요없이 프로퍼티에 접근할 수 있음을 의미합니다.
capitalCity 는 초기값이 nil 이기 때문에, 새 Country 인스턴스는 name 에 값이 할당 되자마자, 완전히 initialize 된 것으로 고려됩니다. 완전히 initialize 되었기 때문에, self 에 접근이 가능해지고 City 생성자가 정상적으로 실행될 수 있습니다.
이렇게 강환순환참조 없이 서로가 서로를 반드시 가지고 있어야하는 관계를 생성할 수 있습니다.
var country = Country(name: "Canada", capitalName: "Ottawa") print("\(country.name)'s capital city is called \(country.capitalCity.name)") // Prints "Canada's capital city is called Ottawa"
Swift
복사
위 예제에서, 암묵적으로 unwrap 된 옵셔널은 두단계의 class initializer 요구사항을 충족합니다. capitalCity 프로퍼티는 한번 intiialize 가 되면, optional 하지 않은 값처러점 접근 가능합니다.

Strong Reference Cycles for Closures

강한순환참조는 클래스의 인스턴스의 프로퍼티에 클로져를 할당하고자 할 때도 발생할수 있습니다. 이러한 capture 는 클로져의 바디가 인스턴스의 프로퍼타에 접근하기 때문에 발생합니다. 또는 클로져가 인스턴스의 메서드를 호출하기 때문이기도 하죠. 따라서 클로져 내부에서 self 에 접근해서 self 를 캡쳐하게 된다면 강한 순환참조가 일어납니다.
클로져로 인해 발생한 강한 순환참조는 클로져가 클래스처럼 참조타입이기 때문에 발생합니다. 클로져를 프로퍼티에 할당하고자 할때, 클로져 자체가아닌 클로져의 참조가 저장됩니다. 따라서 사실 앞선 상황(클래스가 서로를 참조) 하는 상황과 유사해지는 것이죠. 그러나 이 경우에서는, 클래스의 인스턴스와 클로져가 서로를 참조해서 서로가 할당해제되지 못하는 경우입니다.
Swift 에서는 이런 문제를 위해 ‘closure capture list’ 라는 우아한 해결책을 제공합니다. 클로져 캡쳐 리스트로 강한 순환 참조를 해결하는 법을 배우기전에, 강한 순환참조가 어떻게 일어나는지 먼저 이해하면 좋습니다.
아래의 예시는 클로져내부에서 self 를 참조하는 경우, 어떻게 강한 순환참조가 발생할 수 있는지를 보여주는 예시입니다.
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { print("\(name) is being deinitialized") } } var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) // Prints "<p>hello, world</p>"
Swift
복사
HTMLElement 클래스는 name 이라는 프로퍼티를 정의합니다. 여기에는 element 의 이름에 대해서 지시되어 있습니다. h1 과 p 같은 패러그래프 요소들이죠.
위 경우, 클래스의 인스턴스와 클로져가 서로를 강하게 참조하고 있기 때문에, 어떠한 경우에도, 메모리가 할당해제되지 않는 강한순환참조가 발생합니다.
paragraph = nil
Swift
복사
위 코드를 실행해보아도 deinitilizer 가 실행되었음을 알리는 print 문은 출력되지 않죠.

Resolving Strong Reference Cycles for Closures

클로져의 정의부에서 캡쳐 리스트를 정의함으로써 강한순환참조를 해결하세요. 캡쳐리스트에는, 하나 이상의 참조 타입을 캡쳐할 때 사용할 규칙들을 정의하게 됩니다. 각각의 캡쳐된 참조들을 weak 로 참조할 지, unowned 로 참조할 지 선언하세요. weak 이 적절할지 unowned 이 적절할 지는 코드의 관계에 따라 달라집낟.
Swift 는 self 를 캡쳐할 때 항상 self 를 명시하도록 요구합니다. 이를 통해 self 가 캡쳐될 가능성이 있음을 리마인드시켜주죠.

Defining a Capture List

캡쳐 리스트 내의 각각의 항목들은 참조하는 대상의 클래스 인스턴스 또는 특정 변수와 weak, unowned 키워드의 쌍으로 이루어져 있습니다. 이러한 쌍들은 대괄호 안에 콤마로 구분되어 작성합니다.
lazy var someClosure = { [unowned self, weak delegate = self.delegate] (index: Int, stringToProcess: String) -> String in // closure body goes here }
Swift
복사
클로져가 파라미터 리스트 또는 리턴 타입을 가지지 않는다면, in 키워드 앞에 이를 작성하면 됩니다.
lazy var someClosure = { [unowned self, weak delegate = self.delegate] in // closure body goes here }
Swift
복사

Weak and Unowned References

항상 서로를 참조하고, 항상 같은 순간에 할당해제 되어야 한다면, 캡쳐를 unowned 로 정의하세요.
반대로 참조가 미래에 nil 일 가능성이 존재한다면, weak 을 사용해서 캡쳐하세요. weak 참조하는 항상 optional 타입이고, 자동으로 인스턴스가 할당해제 되는 경우 nil 이 할당됩니다. 따라서 클로져의 내부에서 이 값의 존재유무를 체크해야하죠.
캡쳐하고자 하는 참조가 nil 이 될 가능성이 없다면, unowned 를 사용하세요.
위 HTMLElement 예제에서는 unowned 를 사용하는 것이 적합합니다.
class HTMLElement { let name: String let text: String? lazy var asHTML: () -> String = { [unowned self] in if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { print("\(name) is being deinitialized") } }
Swift
복사
self 가 nil 일 가능성이 없고, self 가 할당해제 되는 경우, asHTML 또한 할당해제 되어야 하니, unowned 가 적합합니다.
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world") print(paragraph!.asHTML()) // Prints "<p>hello, world</p>"
Swift
복사
이 경우 클래스의 인스턴스를 강하게 참조하는 변수는 paragraph 뿐입니다. 따라서 해당 강한 참조를 없애면 클로져와 인스턴스 모두 할당해제됩니다.
paragraph = nil // Prints "p is being deinitialized"
Swift
복사