필수 4 ~ 5 introduce

hyun·2025년 6월 5일
1

iOS

목록 보기
11/54

 필수 문제 4

우리는 여러 타입에 “자기소개” 기능을 부여하고자 합니다.

  • Introducible 프로토콜을 정의하세요.
    • name: String 프로퍼티를 요구사항으로 포함합니다.
    • introduce() -> String 메서드를 요구사항으로 포함합니다.
      • 동작 예시: print("안녕하세요, 저는 (name)입니다.")
  • Robot, Cat, Dog 타입을 정의하고 Introducible 프로토콜을 채택해주세요.
  • Robot 타입의 경우, name 값이 변경될 때마다 변경 이전값과 이후 값을 출력하도록 구현해주세요.
    • 만약 변경 이전값과 이후값이 같다면 출력하지 않아야합니다.
    • 출력 예시
      name 변경 알림
      변경 이전 값: 피규어
      변경 이후 값: 옵티머스
  • Introducible 프로토콜 에 정의되지 않은 각 타입 고유의 메서드들도 하나씩 추가 정의해주세요.
    • 예를 들어, Robot 은 충전하기(batteryCharge) 라는 메서드를 추가 할 수 있습니다.
  • [Introducible] 타입 배열을 정의하고, Robot, Cat, Dog 인스턴스 1개씩을 append 해주세요.
    • 배열을 순회하며 각 타입 고유의 메서드들을 호출하는 코드를 작성해주세요.



일단 저 문제를 보고

1. Introducible 프로토콜 정의하기를 먼저 해야겠다고 생각함

모든 타입이 공통으로 갖는 자기소개 기능을 강제하도록 프로토콜을 만들어봄

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

읽기만 하니까 let name으로 해도 되겠지 라고 생각했지만
Robot은 name을 바꾸기 때문에 get set이 필요해서 프로토콜에서도 var name: String { get set }으로 명시해야 함
-> 모든 타입에 공통된 name 속성과 introduce() 메서드를 강제하기 위해 정의
-> Robot은 name을 변경하므로 { get set }으로 명시.

2. Robot, Cat, Dog 정의하고 프로토콜 채택

Robot : didSet 사용하고, 참조타입이라 변경 감지에 유리할 거 같아서 class 사용함
Cat, Dog : struct 사용

struct는 값 타입이라 복사가 일어나는데 배열에 들어간 후에 name을 바꿔도 실제 배열 안 값엔 반영 안 됨
didSet은 구조체의 복사본엔 의미 없을 수 있음
속성 변경 감지를 확실히 하고 싶어서 class로 선언함

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)이(가) 충전 중입니다.")
    }
}

name 변경 감지를 위해 didSet 사용
값 타입은 변경 감지에 불리하니까 참조 타입 선택
변경 전후 비교해서 동일 값 변경은 무시

struct Cat: Introducible {
    var name: String

    func introduce() -> String {
        return "안녕하세요, 고양이 \(name)입니다."
    }

    func meow() {
        print("\(name): 야옹~")
    }
}
struct Dog: Introducible {
    var name: String

    func introduce() -> String {
        return "안녕하세요, 강아지 \(name)입니다."
    }

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

상태 변경 감지가 필요 없고 값 복사가 허용되어 struct로 선언
Introducible 채택 후에 introduce 구현
각 타입 고유의 메서드도 정의

3. Introducible 배열 정의, 순회

배열 타입이 Introducible로 제한되면 공통 기능(name, introduce())만 접근 가능
각 타입 고유 기능은 다운캐스팅 필요 (as?)

var beings: [Introducible] = []

let robot = Robot(name: "피규어")
let cat = Cat(name: "나비")
let dog = Dog(name: "초코")

beings.append(robot)
beings.append(cat)
beings.append(dog)

공통된 기능을 가진 객체들을 한 배열에 담기 위해 Introducible 사용
Robot, Cat, Dog 인스턴스 추가
배열에 담긴 후에도 정보 유지를 통해 다운캐스팅 가능

4. 배열 순회, 기능 호출

for being in beings {
    print(being.introduce())

    if let robot = being as? Robot {
        robot.batteryCharge()
    } else if let cat = being as? Cat {
        cat.meow()
    } else if let dog = being as? Dog {
        dog.bark()
    }
}

Introducible의 공통 기능은 바로 호출 가능
각 타입 고유 기능은 확인 후 다운캐스팅하여 호출

robot.name = "옵티머스" // 변경 알림 출력됨
robot.name = "옵티머스" // 출력 안됨 (같은 값)

Robot은 class라서 동일 인스턴스 상태 변경 가능
name 변경 시 didSet 내 비교 조건에 따라 로그 출력 여부 결정
같은 값으로 다시 설정해도 로그는 출력되지 않음!


 필수 문제 5

우리는 간단한 택배 도착 예측 시스템을 만들고 있다고 가정합니다.

사용자에게 예상 도착일을 알려주려 하지만, 다음과 같은 여러 상황에서 문제가 발생할 수 있습니다:

  • 주소가 잘못된 경우
  • 배송이 아직 시작되지 않은 경우
  • 시스템 서버 에러로 예측이 불가능한 경우
  • 배송 상태를 표현하는 DeliveryStatus 열거형을 구현하고, 아래 3가지 상태를 포함하도록 합니다.
    • notStarted
    • inTransit(daysRemaining: Int)
    • error
  • 사용자 정의 에러 타입 DeliveryError를 Error 프로토콜을 따르도록 정의합니다.
    • invalidAddress
    • notStarted
    • systemError(reason: String)
  • 아래 시그니처를 가진 throwing function 을 구현해봅니다.
    func predictDeliveryDay(for address: String, status: DeliveryStatus) throws -> String
    • 주소가 빈 문자열이면 DeliveryError.invalidAddress를 던져야 합니다.
    • 배송이 아직 시작되지 않은 경우 DeliveryError.notStarted를 던져야 합니다.
    • 시스템 에러 상태면 DeliveryError.systemError(reason:)을 던져야 합니다.
    • 나머지 경우에는 "배송까지 X일 남았습니다." 형태의 문자열을 반환합니다.
  • 위 함수를 do-catch 로 호출하고, 각 에러 상황에 따라 사용자에게 다른 메시지를 출력하세요.

1. DeliveryStatus 열거형 정의

enum DeliveryStatus {
    case notStarted
    case inTransit(daysRemaining: Int)
    case error
}

택배의 현재 상태를 세 가지 경우로

  • notStarted : 아직 배송이 시작되지 않은 상태
  • inTransit: 배송 중, 남은 일수 포함
  • error : 예측이 불가능한 상태

→ 상태마다 다르게 처리할 수 있게 설계
→ 특히 inTransit는 연관 값을 사용해서 남은 일수 정보를 담음

2. DeliveryError 사용자 정의 에러 타입 정의

enum DeliveryError: Error {
    case invalidAddress
    case notStarted
    case systemError(reason: String)
}

예측 도중 발생할 수 있는 문제 상황 표현

  • invalidAddress : 주소가 잘못되었거나 비어 있
  • notStarted : 배송이 시작되지 않아 예측이 불가능함
  • systemError : 시스템적인 문제로 예측 불가
    → Error 프로토콜을 따르므로 throw할 수 있는 에러

3. predictDeliveryDay(for:status:) 함수 구현

func predictDeliveryDay(for address: String, status: DeliveryStatus) throws -> String

사용자에게 예상 도착일을 알려주는 함수
throws로 상황에 따라 에러를 던질 수 있음

내부 동작

guard !address.isEmpty else {
    throw DeliveryError.invalidAddress
}

주소가 빈 문자열이면 invalidAddress 에러를 발생

switch status {
case .notStarted:
    throw DeliveryError.notStarted
case .error:
    throw DeliveryError.systemError(reason: "시스템 서버 에러 예측이 불가능합니다.")
case .inTransit(let daysRemaining):
    return "배송까지 \(daysRemaining)일 남았습니다."
}

상태에 따라 다른 동작

notStarted → 예측 불가능
error → 시스템 오류 이유
inTransit → 남은 일수를 이용해 메시지 반환


main.swift

let testAddress = "서울시 강남구"
let statuses: [DeliveryStatus] = [
    .inTransit(daysRemaining: 3),
    .notStarted,
    .error
]

테스트용 주소와 상태 3가지를 배열로 설정

inTransit(daysRemaining: 3) → 정상 배송 중
notStarted → 아직 배송 시작 전
error → 시스템 오류

상태별 예측 실행 및 에러 처리

for status in statuses {
    do {
        let result = try predictDeliveryDay(for: testAddress, status: status)
        print("결과 :", result)
    } catch let error as DeliveryError {
        switch error {
        case .invalidAddress:
            print("주소가 올바르지 않습니다.")
        case .notStarted:
            print("배송이 아직 시작되지 않았습니다.")
        case .systemError(let reason):
            print("\(reason)")
        }
    } catch {
        print("알 수 없는 에러 \(error)")
    }
}

predictDeliveryDay(for:status:) 함수를 호출하고
do-catch로 에러를 나눠서 처리

0개의 댓글