도전 문제를 다 풀어보자..!
Car 를 설계해주세요.Car 를 상속한 ElectricCar 를 설계해주세요.Car 를 상속한 HybridCar 를 설계해주세요.HydrogenEngine 을 정의해주세요.switchEngine(to:) 입니다.HybridCar 인스턴스를 생성하고, switchEngine(to:) 를 호출하여 서로 다른 타입의 엔진으로 교체하는 코드를 작성해주세요.문제는 Car라는 개념을 코드로 설계하는 것이고
요구사항은 크게 3가지로 나타낼 수 있음
Car라는 기본 클래스 설계하기 (상태 4개, 동작 1개)
Car를 상속받은 ElectricCar와 HybridCar 만들기
HybridCar는 엔진을 바꿀 수 있는 기능 추가하기
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을 주는 건 신중해야 할 거 같긴 했는데 일단 지금은 테스트..ㅎㅎㅎㅎㅎㅎㅎ
public protocol Engine {
var description: String { get }
func start()
func recharge()
}
👉 엔진을 만들기 위한 규칙
이걸 따르면 여러 종류의 엔진을 만들 수 있음
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("수소 충전 중...") }
}
public final class ElectricCar: Car {
public init(brand: String, model: String, year: String) {
super.init(brand: brand, model: model, year: year, engine: ElectricEngine())
}
}
public final class HybridCar: Car {
public func switchEngine(to newEngine: any Engine) {
print("엔진 교체 : \(engine.description) → \(newEngine.description)")
self.engine = newEngine
}
}
👉 엔진을 바꾸기 가능
전기 ↔ 수소 ↔ 휘발류
let miraiHybrid = HybridCar(
brand: "기아",
model: "K8-Hybrid",
year: "2025",
engine: HydrogenEngine()
)
miraiHybrid.drive()
miraiHybrid.switchEngine(to: ElectricEngine()) // 엔진 바꾸기
miraiHybrid.drive()
| 구분 | 상속 (class) | 프로토콜 (protocol) |
|---|---|---|
| 뜻 | 부모 자식 관계처럼 이어받음 | 이런 기능 꼭 만들라고 약속하는 규칙 |
| 재사용 | 기능을 통째로 물려받음 | 필요한 기능만 골라서 구현 |
| 유연성 | 한 부모만 가질 수 있음 | 여러 프로토콜 동시에 쓸 수 있음 |
| 예시 | ElectricCar는 Car를 상속 | Engine은 프로토콜로 여러 엔진에 사용 |
var items: [T] 를 추가해주세요.SortableBox라는 구조체를 만들고, 정렬 가능한 타입만 정렬 메서드를 사용할 수 있게 해보장.
제네릭 구조체 SortableBox 만들기
여러 자료형을 담을 수 있도록 T 라는 타입 파라미터 사용
items 라는 배열 속성 만들기
안에 T 타입의 값
정렬 기능 sortItems() 만들기
T가 Comparable 을 따를 때만 사용 가능
Comparable : 크기 비교가 가능한 자료형
👉 T가 Comparable이 아닐 경우 정렬 시 에러가 나야 함
struct SortableBox<T> {
var items: [T]
}
SortableBox 는 여러 자료형을 담을 수 있는 제네릭 박스
items는 T타입 배열, 어떤 자료형이든 배열로 저장 가능
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을 따를 때만 정렬 기능을 제공하는 게 좋음
제네릭 + 조건부 확장 으로 쉽게 구현 가능
필수문제 4 구현에서 연속된 문제입니다.
Robot, Cat, Dog 타입을 만들고 Introducible 채택
Cat, Dog는 기본 introduce() 사용
Robot은 직접 다른 동작을 구현해서 오버라이드
protocol Introducible {
var name: String { get set }
func introduce() -> String
}
extension Introducible {
func introduce() -> String {
return "안녕하세요, 저는 \(name)입니다."
}
}
introduce()는 기본적으로 "안녕하세요, 저는 (name)입니다." 를 출력
Cat, Dog는 이 기본값을 따르는데
Robot은 직접 introduce()를 구현해서 다른 동작을 함
struct Dog: Introducible {
var name: String
func bark() {
print("\(name) : 멍멍")
}
}
introduce() 구현 안 함 → 기본 동작 사용
bark()로 멍멍 출력
struct Cat: Introducible {
var name: String
func meow() {
print("\(name) : 야옹")
}
}
introduce() 구현 안 함 → 기본 동작 사용
meow()로 야옹 출력
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는 기본값 자동 사용
필요한 경우에만 직접 커스텀
deinit 을 정의하여, 메모리 해제 여부를 확인할 수 있도록 해주세요.closure: (() -> Void)? 프로퍼티를 만들고, 클로저 내부에서 A의 인스턴스를 참조하게 하여 클로저 기반의 순환 참조도 발생시켜보세요.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("[순환 참조 끝]")
}
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 끝]")
}
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일 수 없다는 걸 전제로 함
→ 참조 대상이 없으면 앱이 크래시 날 수 있음
그래서 진짜 무조건 살아있다고 확신할 때만 쓰는 것
| 순서 | 상황 | 결과 |
|---|---|---|
| 1 | A → B, B → A로 강한 참조 | deinit 호출 안됨 (순환 참조 발생) |
| 2 | B → A를 weak로 바꿈 | 순환 참조 해결 |
| 3 | 클로저가 A를 강하게 참조 | 또 순환 참조 생김 |
| 4 | 클로저에서 [weak a], [unowned a] 사용 | 순환 참조 해결 |