ios 8일차

bin·2026년 1월 8일

회고

문법 강의를 급하게 끝내버릴 생각은 없었다. 오히려, 내가 알던 지식과 비교하며 보다 깊게 이해하기 위해 천천히 공부하고자 계획했었다. 그러나 4~5주차 강의까지 수강을 완료하면서 남은 과제에 시간을 사용해보고자 했다.
현재까지 공부한 내용들을 바탕으로, 도전 문제의 어느 부분에서 막히는지, 어느정도의 생각이 가능한가를 시험해보고 싶었다. 문제1, 2, 3은 오히려 필수 문제보다 편하게 다가왔다. 큰 문제는 마지막 4번이었다. 아무래도, 강의를 통해 듣고 넘어가던 부분들 중 마음에 가장 걸렸던 부분들 중 하나가 클로저 캡쳐였는데, 문제로 마주하니 더 이해하기 힘들었다. 요구 사항을 하나하나 따라가다보니 1번 요구 사항은 문제 없었으나, 클로저 캡쳐와 관련된 요구 사항 2부터 머리가 아파오기 시작했다. 생각대로 구현해본 로직에 큰 문제는 없어보였다. 그것이 가장 큰 문제이지만, 본인이 작성한 코드의 정확한 동작 명세를 숙지하지 못하고있는 점은 개발자의 큰 문제라고 생각했다.
한시간 이상을 쳐다봤다. 답이 안나온다.
다른 사람들이 작성한 많은 코드를 보고 비슷한 사례가 존재하는 지 궁금해서 많이 찾아봤다. 순환 참조, 클로저 캡쳐에 관한 여러 자료들을 아무리 찾아봐도 비슷한 연결고리가 없어보였다. 확실히 연결된 자료가 없네.....라고 생각하다가 정말 문득 연결고리라는 키워드에서 LinkedList가 떠올랐다. 순환 참조는 마치 CircularLinkedList와 같지 않은가? 정확히 그렇다라고 할 수는 없지만, 이를 통해 풀리지 않던 의문점이 개인적으로 해소됐다.(아래 도전 문제 4 풀이 참고.) 물론, 이 해설이 틀렸다고하면 다시 공부하여 이해해야 하겠지만

오늘 공부를 하며 도움을 받은 자료들

도전 문제

도전 문제 1.

/* 도전 문제 1.

  • ‘자동차’ 라는 개념을 가지고 객체 지향 설계를 해봅니다.
    - [ ] Base Class Car 를 설계해주세요.
    - 4가지의 상태를 정의해주세요.
    - 브랜드, 모델, 연식
    - 모두 String 타입입니다.
    - 엔진
    - Engine 이라는 커스텀 타입으로 정의해주세요.
    - 1개의 동작을 정의해주세요.
    - 운전하기
    - 동작 예시) “Car 주행 중…” 출력
    - 추가하고 싶은 상태와 동작은 마음껏 추가해주세요.
    - stop(), charge(), refuel() 등..
    - 정의한 각 상태 및 동작에 적절한 접근 제어자를 명시적으로 지정해주세요.
    - [ ] Car 를 상속한 ElectricCar 를 설계해주세요.
    - ElectricEngine 타입의 Engine 을 사용해야합니다.
    - [ ] Car 를 상속한 HybridCar 를 설계해주세요.
    - 새로운 엔진 타입 HydrogenEngine 을 정의해주세요.
    - HybridCar 에는 기존 Car 에 없던 새로운 동작이 추가됩니다.
    - 엔진을 런타임에 바꿀 수 있는 switchEngine(to:) 입니다.
    - [ ] HybridCar 인스턴스를 생성하고, switchEngine(to:) 를 호출하여 서로 다른 타입의 엔진으로 교체하는 코드를 작성해주세요.
    - [ ] 상속을 사용하여 기능을 추가하는 것과, 프로토콜 채택을 통해서 기능을 추가하는 것의 장단점, 차이를 고민하고 주석으로 서술해주세요.
    */
/* 1. Base Class `Car` 설계 */
class Car{
   private let brand: String
   private let model: String
   private let year: String
   public var engine: Engine
   private var isDrive: Bool = false
   private var battery: Int = 50
   public var fuel: Int = 95
   
   public func drive() {
       isDrive = true
       fuel -= 10
       print("Car 주행 중...")
   }
   public func stop(){
       isDrive = false
       print("Car 멈춤")
   }
   
   public func isDriving(){
       if isDrive == true{
           print("주행 중입니다.")
       }else{
           print("차가 멈춰있습니다.")
       }
   }
   
   public func charge(){
       if battery == 100{
           print("배터리 충전이 완료된 상태입니다.")
       }else{
           battery += 10
           print("현재 배터리 잔량은 \(battery)입니다.")
       }
   }
   
   private func isFull(){
       if fuel >= 100{
           print("연료가 가득찼습니다.")
       }
   }
   public func refuel(){
       if fuel >= 100{
           isFull()
       }else if (fuel + 20) >= 100{
           fuel = 100
           isFull()
       }else {
           fuel += 20
           print("현재 연료의 양은 \(fuel)입니다.")
       }
   }
   
   init(brand: String, model: String, year: String, engine: Engine) {
       self.brand = brand
       self.model = model
       self.year = year
       self.engine = engine
   }
}

Car 클래스 내부에 추가하고 싶은 상태 및 동작은 수정이 필요한 부분들도 존재하지만, 중요한 점은 그보다 요구사항에 맞게 수렴하는 것이 중요하기에 간단하게만 구현하고 예외사항, 처리들은 크게 구현하지 않았습니다.

/* 2. Custom Engine */
class Engine {
   var engineName: String
   
   init(engineName: String) {
       self.engineName = engineName
   }
}
/* 3. Engine 상속받는 ElectricEngine, HydrogenEngine */
class ElectricEngine: Engine {
   init() {
       super.init(engineName: "Electric Engine")
   }
}
class HydrogenEngine: Engine {
   init() {
       super.init(engineName: "Hydrogen Engine")
   }
}

Custon Engine을 정의하고 이를 상속받는 ElectricEngine과 HydrogenEngine을 정의합니다. 처음 요구사항을 확인했을 때, "enum Engine을 선언하여 case로 ElectricEngine과 HydrogenEngine을 선언하여 사용하는 것은 어떨까?"라고 생각했지만, 엔진마다의 고유 속성을 추가하기에는 class 상속을 이용하는 것이 더 좋다고 생각했다.

/* 4. Car 상속받는 ElectricCar, HybridCar*/
class ElectricCar: Car{
}
class HybridCar: Car{
    /* 엔진을 런타임에 바꿀 수 있는 `switchEngine(to:)` */
    func switchEngine(to newEngine: Engine){
        engine = newEngine
    }
}
/* 5. `HybridCar` 인스턴스를 생성 */
var hybridcar = HybridCar(brand: "현대", model: "제네시스", year: "2025", engine: ElectricEngine())
print(hybridcar.engine.engineName)
/* 6. `switchEngine(to:)` 를 호출 */
hybridcar.switchEngine(to: HydrogenEngine())
print(hybridcar.engine.engineName)

도전 문제 2.

/* 도전 문제 2.

  • SortableBox 라는 이름의 제네릭 구조체를 정의해주세요.
    • 타입 파라미터는 1개이며, T 라는 이름으로 지정합니다.
  • SortableBox 에 인스턴스 프로퍼티 var items: [T] 를 추가해주세요.
  • 타입 T 가 Comparable을 준수할 때에만 sortItems() 메서드를 사용할 수 있도록 구현하세요.
    • sortItems() 메서드는 items 배열을 오름차순으로 정렬합니다.
    • 정렬 결과는 items 프로퍼티에 반영되어야 합니다.
  • T 가 Comparable 을 따르지 않는 타입일 경우, sortItems() 호출 시 컴파일 오류가 발생해야합니다.
    */

순서에 따른 정리

  • 요구사항 1. SortableBox 제네릭 구조체 정의(line 1)
  • 요구사항 2. 프로퍼티 추가(line 2)
  • 요구사항 3. Comparable 준수 시에 sortItems() 메서드 사용 조건 ( 조건에 부합하지 않을 경우 오류 발생)

초기 생각

struct SortableBox<T: Comparable>{
}
  • 위 코드는 Comparble을 준수하지 않을 경우 인스턴스 생성 자체가 불가능하기에 요구사항 3에 적합하지 않다고 생각하여 아래처럼 수정하여 구현함.
  • 초기 구현
struct SortableBox<T>{
    var items: [T]
    mutating func sortItems() where T: Comparable{
        self.items.sort()
    }
}
  • 생각해본 추가 방법 (Extension 사용)
struct SortableBox<T>{
    var items: [T]
}

extension SortableBox where T: Comparable{
    mutating func sortItems(){
        items.sort()
    }
}

여러가지 생각을 가지고 문제에 접근을 해 보았는데 요구사항3에 수렴하도록 구현하기 위해서는 위 두가지 방법이 옳다고 판단했다.

도전 문제 3.

/* 도전 문제 3.
필수문제 4 구현에서 연속된 문제입니다.

  • Introducible 프로토콜을 채택하는 타입들에게 기본 introduce() 동작을 제공하세요.
    • 각 타입들이 introduce() 를 구현하지 않고도 introduce() 를 호출할 수 있어야합니다.
  • Robot, Cat, Dog 타입을 정의하고 Introducible 프로토콜을 채택해주세요.
    - 이 때, Robot 타입은 기본 introduce() 동작 이 아닌 커스텀 동작을 하도록 구현해주세요.
    */
/* 1. Introducible 프로토콜 정의 */
protocol Introducible{
    var name: String {get set}
    func introduce()->String
}
/* 2. protocol extension 정의 */
extension Introducible{
    func introduce() -> String {
        return "안녕하세요. 저는 \(name)입니다."
    }
}
/* 3. Robot 정의 */
class Robot: Introducible{
    var name: String
    func introduce() -> String {
        return "I am a robot. My name is \(name)."
    }
    init(name: String){
        self.name = name
    }
}
/* 4. Cat 정의 */
class Cat: Introducible{
    var name: String
    init(name: String) {
        self.name = name
    }
}
/* 5. Dog 정의 */
class Dog: Introducible{
    var name: String
    init(name: String) {
        self.name = name
    }
}

/* 테스트용 */
var robot = Robot(name: "피규어")
var cat = Cat(name: "고양이")
var dog = Dog(name: "강아지")
print(robot.introduce())
print(cat.introduce())
print(dog.introduce())

도전 문제 4.

/* 도전 문제 4.

  • 클래스 A, B 사이에 순환참조가 발생하도록 구현해주세요.
    • 각 클래스에 deinit 을 정의하여, 메모리 해제 여부를 확인할 수 있도록 해주세요.
  • 또한 클래스 B 에는 closure: (() -> Void)? 프로퍼티를 만들고, 클로저 내부에서 A의 인스턴스를 참조하게 하여 클로저 기반의 순환 참조도 발생시켜보세요.
  • 순환 참조를 해결할 수 있도록 weak, unowned 키워드를 클로저 캡처 리스트를 적절히 사용하여 순환 참조를 해결해주세요.
    */
class A{
   let name: String
   init(name: String){
       self.name = name
   }
   weak var B: B?
   deinit{ print("\(name) is deinitialized")}
}

class B{
   let age: Int
   init(age: Int) {
       self.age = age
   }
   var A: A?
   deinit{ print("\(age) is deinitialized")}
   
   var closure: (()->Void)?
}

var a: A? = A(name: "A") //A RC: 1, B RC: 0
var b: B? = B(age: 26) //A RC: 1, B RC: 1

a?.B = b //A RC: 1, B RC: 1
b?.A = a //A RC: 2, B RC: 1

a = nil //A RC: 1, B RC: 1
b = nil //A RC: 1, B RC: 0 -> A RC:0, B RC: 0 -> B.deinit 출력 이후 A.deinit 출력
class A{
    let name: String
    init(name: String){
        self.name = name
    }
    var B: B?
    deinit{ print("\(name) is deinitialized")}
}

class B{
    let age: Int
    init(age: Int) {
        self.age = age
    }
    weak var A: A?
    deinit{ print("\(age) is deinitialized")}
    
    var closure: (()->Void)?
}

var a: A? = A(name: "A") //A RC: 1, B RC: 0
var b: B? = B(age: 26) //A RC: 1, B RC: 1

a?.B = b //A RC: 1, B RC: 2
b?.A = a //A RC: 1, B RC: 2


a = nil //A RC: 0, B RC: 1 -> A.deinit 출력
b = nil //A RC: 0, B RC: 0 -> B.deinit 출력

중요한 문제

class A{
    let name: String
    init(name: String){
        self.name = name
    }
    var B: B?
    deinit{ print("\(name) is deinitialized")}
}

class B{
    let age: Int
    init(age: Int) {
        self.age = age
    }
    var A: A?
    deinit{ print("\(age) is deinitialized")}
    
    var closure: (()->Void)?
}

var a: A? = A(name: "A") // A RC: 1, B RC: 0
var b: B? = B(age: 26) // A RC: 1, B RC: 1

a?.B = b // A RC: 1, B RC: 2
b?.closure = {
        print("참조된 A의 이름은 \(a?.name ?? "없음")")
} // A RC: 2, B RC: 2

a = nil 
b = nil 

위 코드는 deinit이 출력될까?

정답 : 출력된다.
위 상황을 이해하기 위해 코드만 1시간 이상을 쳐다본 것 같다. 분명 RC의 증가와 감소가 맞는 것 같은데 왜 순환 참조가 발생하지 않고 출력이 되는것인가?

  • 연결 고리를 생각해보자
  • a(변수) -> A(인스턴스) -> B(인스턴스) -> closure -> a(변수) -> A(인스턴스)
  • closure는 인스턴스 A와 직접 연결되어있지 않다.
  • a = nil이 되면, 인스턴스 A에 연결되어있는 것이 없기에 A가 해제되고 이로인해 b = nil이 되면 B도 해제된다.
class A{
    let name: String
    init(name: String){
        self.name = name
    }
    var B: B?
    deinit{ print("\(name) is deinitialized")}
}

class B{
    let age: Int
    init(age: Int) {
        self.age = age
    }
    var A: A?
    deinit{ print("\(age) is deinitialized")}
    
    var closure: (()->Void)?
}

var a: A? = A(name: "A") // A RC: 1, B RC: 0
var b: B? = B(age: 26) // A RC: 1, B RC: 1

a?.B = b // A RC: 1, B RC: 2
b?.A = a // A RC: 2, B RC: 2
b?.closure = {
        print("참조된 A의 이름은 \(a?.name ?? "없음")")
} // A RC: 3, B RC: 2

a = nil 
b = nil 
  • 연결고리를 생각해보자.
  • A(인스턴스) -> B(인스턴스)
  • B(인스턴스) -> A(인스턴스)
  • B(인스턴스) -> closure -> a -> A(인스턴스)
  • 위와 마찬가지로 a라는 연결고리 후 A에 연결이 되지만, a = nil이 되었을 때, B -> A 로 연결되는 연결은 사라지지 않는다.
  • 고로 a와 b에 nil이 되어도 인스턴스가 해제되지 않는다.

그렇다면 이 고리를 어떻게 끊어내야 할까 ?

  • 현재 고리는 총 2개로 볼 수 있다.
    1. A <-> B 간의 고리
    1. B -> closure -> A

본인의 해설

class A{
    let name: String
    init(name: String){
        self.name = name
    }
    var B: B?
    deinit{ print("\(name) is deinitialized")}
}

class B{
    let age: Int
    init(age: Int) {
        self.age = age
    }
    weak var A: A?
    deinit{ print("\(age) is deinitialized")}
    
    var closure: (()->Void)?
}

var a: A? = A(name: "A") // A RC: 1, B RC: 0
var b: B? = B(age: 26) // A RC: 1, B RC: 1

a?.B = b // A RC: 1, B RC: 2
b?.A = a // A RC: 1, B RC: 2
b?.closure = { [weak a] in
        print("\(a?.name ?? "없음")")
} // A RC: 1, B RC: 2

a = nil // A RC: 0, B RC: 1 -> A.deinit 출력
b = nil // A RC: 0, B RC: 0 -> B.deinit 출력

0개의 댓글