Car 도전 문제

hyun·2025년 6월 9일
1

iOS

목록 보기
12/54

도전 문제를 다 풀어보자..!

 도전 문제 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:) 를 호출하여 서로 다른 타입의 엔진으로 교체하는 코드를 작성해주세요.
    • 상속을 사용하여 기능을 추가하는 것과, 프로토콜 채택을 통해서 기능을 추가하는 것의 장단점, 차이를 고민하고 주석으로 서술해주세요.

요구사항 요약

문제는 Car라는 개념을 코드로 설계하는 것이고

요구사항은 크게 3가지로 나타낼 수 있음

  • Car라는 기본 클래스 설계하기 (상태 4개, 동작 1개)

  • Car를 상속받은 ElectricCar와 HybridCar 만들기

  • HybridCar는 엔진을 바꿀 수 있는 기능 추가하기


1. Car라는 기본 클래스 만들기

open class Car {
    public let brand: String // 브랜드 이름
    public let model: String // 모델 이름
    public let year: String // 출시 연도
    public private(set) var engine: any Engine

    // 자동차를 만들 때 필요한 정보
    public init(brand: String, model: String, year: String, engine: any Engine) {
        self.brand = brand
        self.model = model
        self.year  = year
        self.engine = engine
    }

    open func drive() {
        engine.start()
        print("\(brand) \(model) 주행 중...")
    }

    open func stop() {
        print("\(brand) \(model) 정지")
    }

    open func refuelOrCharge() {
        engine.recharge()
    }
}

👉 모든 자동차의 기본 설계도

브랜드, 모델, 연식, 엔진 같은 정보를 가지고 있고,
운전하다, 멈추다 같은 기능을 가지고 있음

근데 코드를 이렇게 짜니까

Cannot assign to property: 'engine' setter is inaccessible

이라는 에러 메시지가 뜸.
swift에서 engine 프로퍼티의 setter 접근 수준이 외부에서 접근 불가능하다는 뜻인 거 같은데
Car 클래스에서 engine 프로퍼티를

public private(set) var engine: Engine

이렇게 하면 읽기는 외부에서 가능하지만 쓰기(=set)는 내부에서만 가능해짐
HybridCar는 Car를 상속했지만 Car 외부로 간주되니까 engine으로 바꿀 수 없어서 이런 에러가 뜬 것 같았음
그래서

open var engine: Engine // 변경 가능하도록

이렇게 수정하였음

internal(set) public var engine: Engine

이렇게 하면 더 제한적으로 내부 모듈 내에서만 변경 가능하도록 할 수 있긴 함
engine을 누구나 바꾸게 하고 싶지 않을 때 setter에 제한을 주고 메서드를 통해 변경할 수 있을텐데

일단 지금은 open으로 해보기로 함..ㅎㅎㅎㅎ
var engine 같은 변경 가능한 프로퍼티는 외부에서 마음대로 바꾸면 클래스 상태가 변할 수 있기 때문에
프로퍼티에 open을 주는 건 신중해야 할 거 같긴 했는데 일단 지금은 테스트..ㅎㅎㅎㅎㅎㅎㅎ

2. Engine 이라는 설계도 만들기

public protocol Engine {
    var description: String { get }
    func start()
    func recharge()
}

👉 엔진을 만들기 위한 규칙
이걸 따르면 여러 종류의 엔진을 만들 수 있음

3. 전기 엔진, 수소 엔진, 연료 엔진 만들기

public struct CombustionEngine: Engine {
    public func start() { print("CombustionEngine") }
    public func recharge() { }
}

public struct ElectricEngine: Engine {
    public func start() { print("ElectricEngine") }
    public func recharge() { print("배터리 충전 중...") }
}

public struct HydrogenEngine: Engine {
    public func start() { print("HydrogenEngine") }
    public func recharge() { print("수소 충전 중...") }
}

4. 전기차 (ElectricCar) 만들기

public final class ElectricCar: Car {
    public init(brand: String, model: String, year: String) {
        super.init(brand: brand, model: model, year: year, engine: ElectricEngine())
    }
}

5. 하이브리드 자동차 (HybridCar)

public final class HybridCar: Car {
    public func switchEngine(to newEngine: any Engine) {
        print("엔진 교체 : \(engine.description)\(newEngine.description)")
        self.engine = newEngine
    }
}

👉 엔진을 바꾸기 가능
전기 ↔ 수소 ↔ 휘발류

6. 적용

let miraiHybrid = HybridCar(
    brand: "기아",
    model: "K8-Hybrid",
    year: "2025",
    engine: HydrogenEngine()
)

miraiHybrid.drive()
miraiHybrid.switchEngine(to: ElectricEngine()) // 엔진 바꾸기
miraiHybrid.drive()

상속 vs 프로토콜 차이 쉽게 설명

구분상속 (class)프로토콜 (protocol)
부모 자식 관계처럼 이어받음이런 기능 꼭 만들라고 약속하는 규칙
재사용기능을 통째로 물려받음필요한 기능만 골라서 구현
유연성한 부모만 가질 수 있음여러 프로토콜 동시에 쓸 수 있음
예시ElectricCar는 Car를 상속Engine은 프로토콜로 여러 엔진에 사용

 도전문제 2

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

요구사항 요약

SortableBox라는 구조체를 만들고, 정렬 가능한 타입만 정렬 메서드를 사용할 수 있게 해보장.

  • 제네릭 구조체 SortableBox 만들기

  • 여러 자료형을 담을 수 있도록 T 라는 타입 파라미터 사용

  • items 라는 배열 속성 만들기
    안에 T 타입의 값

  • 정렬 기능 sortItems() 만들기
    T가 Comparable 을 따를 때만 사용 가능

Comparable : 크기 비교가 가능한 자료형
👉 T가 Comparable이 아닐 경우 정렬 시 에러가 나야 함


## 코드 구조 요약

1. 구조체 정의

struct SortableBox<T> {
    var items: [T]
}

SortableBox 는 여러 자료형을 담을 수 있는 제네릭 박스
items는 T타입 배열, 어떤 자료형이든 배열로 저장 가능

2. 정렬 기능 (조건부 제공)

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

extension을 써서 T가 Comparable을 따를 경우에만 sortItems() 사용할 수 있게,
정렬이 가능한 타입만 정렬 메서드를 쓸 수 있음

ex) Int, String은 정렬 가능 → 사용 가능

ex) Person은 정렬 불가 → 컴파일 오류 발생

예시 코드 요약

정렬 가능한 경우

var intBox = SortableBox(items: [1, 10, 2, 9, 5])
intBox.sortItems()
print(intBox.items) // ➜ [1, 2, 5, 9, 10]

Int는 Comparable을 따르기 때문에 정렬 잘 됨.

정렬 불가능한 경우

struct Person {
    let name: String
}

let personBox = SortableBox(items: [Person(name: "?"), Person(name: "??")])
// personBox.sortItems() // 오류 발생

Person은 Comparable을 따르지 않아서 정렬 불가
이 타입은 크기 비교 못 해!!!!!!!! 라는 뜻

문제 접근 방법

제네릭을 사용하면 다양한 타입을 다룰 수 있음

근데 타입이 정렬 가능한 건 아니기 때문에 Comparable을 따를 때만 정렬 기능을 제공하는 게 좋음

제네릭 + 조건부 확장 으로 쉽게 구현 가능


 도전문제 3

필수문제 4 구현에서 연속된 문제입니다.

  • Introducible 프로토콜을 채택하는 타입들에게 기본 introduce() 동작을 제공하세요.
    • 각 타입들이 introduce() 를 구현하지 않고도 introduce() 를 호출할 수 있어야합니다.
  • Robot, Cat, Dog 타입을 정의하고 Introducible 프로토콜을 채택해주세요.
    • 이 때, Robot 타입은 기본 introduce() 동작 이 아닌 커스텀 동작을 하도록 구현해주세요.

Introducible이라는 프로토콜을 중심으로 cat, dog, robot이 자기소개할 수 있도록

요구사항 요약

  • Introducible 프로토콜을 만들고,
    → 기본 introduce() 동작을 extension으로 제공
    → 기본값을 주는 것. 안 만들면 기본 걸로 실행됨

Robot, Cat, Dog 타입을 만들고 Introducible 채택

Cat, Dog는 기본 introduce() 사용

Robot은 직접 다른 동작을 구현해서 오버라이드

코드 분석

1. Introducible 프로토콜과 기본 구현

protocol Introducible {
    var name: String { get set }
    func introduce() -> String
}

extension Introducible {
    func introduce() -> String {
        return "안녕하세요, 저는 \(name)입니다."
    }
}

introduce()는 기본적으로 "안녕하세요, 저는 (name)입니다." 를 출력

Cat, Dog는 이 기본값을 따르는데

Robot은 직접 introduce()를 구현해서 다른 동작을 함

2. Dog 타입

struct Dog: Introducible {
    var name: String

    func bark() {
        print("\(name) : 멍멍")
    }
}

introduce() 구현 안 함 → 기본 동작 사용

bark()로 멍멍 출력

3. Cat 타입

struct Cat: Introducible {
    var name: String

    func meow() {
        print("\(name) : 야옹")
    }
}

introduce() 구현 안 함 → 기본 동작 사용

meow()로 야옹 출력

4. Robot 클래스

class Robot: Introducible {
    var name: String {
        didSet {
            if oldValue != name {
                print("name 변경 알림")
                print("변경 이전 값: \(oldValue)")
                print("변경 이후 값: \(name)")
            }
        }
    }

    init(name: String) {
        self.name = name
    }

    func introduce() -> String {
        return "로봇 \(name)이 작동을 시작합니다."
    }

    func batteryCharge() {
        print("\(name): 배터리 충전 중")
    }
}

introduce()를 직접 구현해서 커스텀 동작 제공
→ 작동을 시작합니다.

name 바뀔 때 알림도 출력

batteryCharge() 메서드로 충전하는 기능

이전 코드와의 차이

원래는?

Cat, Dog, Robot 전부 introduce()를 따로 구현해야 했음

지금은?

protocol extension 덕분에 Cat, Dog는 기본값 자동 사용

필요한 경우에만 직접 커스텀


 도전문제 4

  • 클래스 A, B 사이에 순환참조가 발생하도록 구현해주세요.
    • 각 클래스에 deinit 을 정의하여, 메모리 해제 여부를 확인할 수 있도록 해주세요.
  • 또한 클래스 B 에는 closure: (() -> Void)? 프로퍼티를 만들고, 클로저 내부에서 A의 인스턴스를 참조하게 하여 클로저 기반의 순환 참조도 발생시켜보세요.
  • 순환 참조를 해결할 수 있
    좋아! 이 문제는 Swift에서 자주 겪는 순환 참조(Reference Cycle)를 실험해보고,
    그걸 어떻게 weak, unowned 키워드로 해결할 수 있는지 배우는 거야.

요구사항 요약

  • 클래스 A와 B가 서로를 강한 참조해서 메모리 누수가 나는 상황 제작
  • B 안의 클로저가 A를 캡처해서 클로저 기반 순환 참조 제작
  • weak / unowned 키워드로 문제 해결

코드 분석

A랑 B가 서로를 너무 꽉 붙잡고 있어서,
프로그램이 끝나도 둘 다 메모리에 남아있음
이걸 순환 참조라고 하는데

순환 참조가 왜 생기는지 보여주고
어떻게 해결하는지도 같이 보여주면 됨

순환 참조 발생 코드

class A {
    var b: B?
    let name: String

    init(name: String) {
        self.name = name
        print("A init")
    }

    deinit {
        print("A deinit")
    }
}

class B {
    weak var a: A?

    var closure: (() -> Void)?

    init() {
        print("B init")
    }

    deinit {
        print("B deinit")
    }
}

func createStrongReferenceCycle() {
    let a = A(name: "a-instance")
    let b = B()

    a.b = b
    b.a = a

    b.closure = {
        print("클로저 내부에서 A 접근: \(a.name)")
    }
}

func resolveReferenceCycleWithWeak() {
    var a: A? = A(name: "a-instance")
    var b: B? = B()

    a?.b = b
    b?.a = a

    b?.closure = { [weak a] in
        print("클로저 내부에서 A 접근: \(a?.name ?? "nil")")
    }

    a = nil
    b = nil
}

func resolveReferenceCycleWithUnowned() {
    var a: A? = A(name: "a-instance")
    var b: B? = B()

    a?.b = b
    b?.a = a

    b?.closure = { [unowned a!] in
        print("클로저 내부에서 A 접근: \(a!.name)")
    }

    a = nil
    b = nil
}

print("----- 순환 참조 -----")
createStrongReferenceCycle()

print("\n----- weak -----")
resolveReferenceCycleWithWeak()

print("\n----- unowned -----")
resolveReferenceCycleWithUnowned()

원래 이렇게 짰었는뎈..

Fields may only be captured by assigning to a specific name

이 에러가 뜸 진짜 솔직히 말하면 뭐지 싶었는데
클로저 캡처 리스트 사용할 때 a! 처럼 강제 언래팅된 표현식을 직접 캡처하려고 하면 에러가 뜨는 듯 함

b?.closure = { [unowned a!] in
  print("클로저 내부에서 A 접근: \(a!.name)")
}

여기서 [unowned a!]는 컴파일러가 허용하지 않는 표현인데
캡처 리스트에서는 변수명만 올 수 있고 a!처럼 표현식이 올 수 없다는 걸 알게 됨..

if let unwrappendA = a {
  b?.closure = { [unowned unwrappendA] in
  	print("클로저 내부에서 A 접근: \(unwrappendA.name)")
}

이렇게 하면
unwrappendA는 a를 강제로 언래핑한 것과 동일하게 사용할 수 있고
unowned 키워드를 사용할 수도 있음


func createStrongReferenceCycle() {
    print("[순환 참조 시작]")

    let a = A(name: "a-instance")
    let b = B()

    a.b = b // A가 B를 강하게 참조
    b.a = a // B도 A를 강하게 참조 => 순환 참조

    b.closure = {
        print("클로저 내부에서 A 접근: \(a.name)") // 클로저가 A를 강하게 참조
    }

    print("[순환 참조 끝]")
}

메모리에서 A, B가 안 지워짐 (deinit 호출 안됨)

weak 사용

class B {
    weak var a: A?  // 👉 weak로 바꾸면 B가 A를 살짝만 잡음 (순환 참조 X)
    var closure: (() -> Void)?
}
func resolveReferenceCycleWithWeak() {
    print("[weak 시작]")

    var a: A? = A(name: "a-instance")
    var b: B? = B()

    a?.b = b
    b?.a = a

    b?.closure = { [weak a] in
        print("A 접근 : \(a?.name ?? "nil")")
    }

    a = nil
    b = nil  // 둘 다 nil 됐을 때 deinit 호출됨!

    print("[weak 끝]")
}

unowned 사용

func resolveReferenceCycleWithUnowned() {
    print("[unowned 시작]")

    var a: A? = A(name: "a-instance")
    var b: B? = B()

    a?.b = b
    b?.a = a

    if let unwrappedA = a {
        b?.closure = { [unowned unwrappedA] in
            print("A 접근 : \(unwrappedA.name)")
        }
    }

    a = nil
    b = nil  // 둘 다 deinit 호출

    print("[unowned 끝]")
}

unowned는 weak처럼 약한 참조지만 nil일 수 없다는 걸 전제로 함
→ 참조 대상이 없으면 앱이 크래시 날 수 있음
그래서 진짜 무조건 살아있다고 확신할 때만 쓰는 것

흐름 요약

순서상황결과
1A → B, B → A로 강한 참조deinit 호출 안됨 (순환 참조 발생)
2B → A를 weak로 바꿈순환 참조 해결
3클로저가 A를 강하게 참조또 순환 참조 생김
4클로저에서 [weak a], [unowned a] 사용순환 참조 해결

0개의 댓글