Swift문법 종합반 기초 과제로 출제되었던 'Calculator' 만들기의 해설 영상이 제공되었다.
과제 출제의 의도는 무엇이고, 정석적인 방법은 어떤 것인지 해설 영상을 보며 내용을 복습해 보았다.
Lv.1 필수 구현
- 더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스 만들기
생성한 클래스를 이용하여 연산을 진행하고 출력- 더하기, 빼기, 나누기, 곱하기 연산을 수행할 수 있는 Calculator 클래스 만들기
생성한 클래스를 이용하여 연산을 진행하고 출력
나는 과제를 보고 Calculator 클래스에서 메소드 하나로 모든 연산을 수행하고자 했다.
class Calculator {
func calcurate(operator: String, firstNumber: Double, secondNumber: Double) -> Double {
var result: Double = 0
if `operator` == "+" {
result = firstNumber + secondNumber
} else if `operator` == "-" {
result = firstNumber - secondNumber
} else if `operator` == "*" {
result = firstNumber * secondNumber
} else if `operator` == "/" {
result = firstNumber / secondNumber
}
return result
}
}
과제 해설에서는 더하기, 빼기, 곱하기, 나누기 등의 연산을 각각의 메소드를 만들어 진행하였다.
여기서부터 내 과제풀이와 과제 해설이 큰 차이가 발생한 것 같다.
class Calculator {
func add(num1: Double, num2: Double) -> Double {
return num1 + num2
}
func sub(num1: Double, num2: Double) -> Double {
return num1 - num2
}
func mul(num1: Double, num2: Double) -> Double {
return num1 * num2
}
func div(num1: Double, num2: Double) -> Double {
return num1 / num2
}
}
Lv.2 필수구현
- Lv.1 에서 만든 Calculator 클래스에 "나머지 연산"이 가능하도록 코드를 추가하고, 연산 진행 후 출력
오류가 날 수 있는 예외처리 상황에 대해 고민해보기 + 구현하기
나는 나머지 연산도 역시 if 조건문에 새로운 조건을 추가하는 것으로 마무리 했는데, 과제 해설에서는 새로운 메소드를 생성하였다.
// 본인의 과제 구현
class Calculator {
var result: Double = 0
func calculate(operator: String, firstNumber: Double, secondNumber: Double) - > Double {
if `operator` == "+" {
// 생략...
} else if `operator` == "%" {
result = firstNumber % secondNumber
}
return result
}
// 해설의 과제 구현
class Calculator {
// 생략...
func rem(num1: Double, num2: Double) -> Double {
return num1 % num2
}
}
게다가 해설에서는 여기서 발생할 수 있는 예외사항으로, 나머지를 구할 때 오른쪽에 오는 값으로 0이 온다면?
이라는 예외사항에 대해 처리를 하였다.
확실히, 오른쪽 값이 0일 경우 반환되는 값은 infinity 가 되어버릴테니 문제라고 할 수 있다.
생각해보지 못했던 문제였기에 다음부터는 사소한 것도 잘 찾아보며 고려해봐야겠다고 생각했다.
class Calculator {
// 생략...
func rem(num1: Double, num2: Double) -> Double {
// num2가 0이라면?
if num2 == 0 {
return 0
} else {
return num1 % num2
}
}
}
Lv3 필수구현
- 아래의 각각의 클래스들을 만들고 클래스간의 관계를 고려하여 Calculator 클래스와 관계 맺기
- AddOperation(더하기)
- SubstractOperation(빼기)
- MultiplyOperation(곱하기)
- DivideOperation(나누기)
- Calculator 클래스의 내부코드를 변경
- 관계를 맺은 후 필요하다면 별도로 만든 연산 클래스의 인스턴스를 Calculator 내부에서 사용
나는 사실 이 문제를 보고 '관계를 맺는다'라는 것을 상속이라고 생각했다.
그렇기 때문에 별도의 클래스를 만든 후 Calculator 클래스를 상속시켜 주었는데, 해설에서는 완전 다르게 사용하였다...
별도로 만든 클래스를 Calculator 클래스 내부에 인스턴스화 시켜서 사용했는데,
Calculator내에 있던 연산 메소드를 각 연산자 클래스로 옮기고 사용했다...
관계를 맺는다는게 꼭 상속을 의미하는게 아니라 저런 식으로 사용하는 것도 관계를 맺는다고 할 수 있다는걸 깨닫게 되었다.
class AddOperation {
func add(num1: Double, num2: Double) -> Double {
return num1 + num2
}
}
// 생략...
class Calculator {
// 관계맺기 - 인스턴스화
let addOperation = AddOperation()
// 생략...
}
// 계산 사용
let calculator = Calculator()
print(calculator.addOperation.add(num1: 10, num2: 20)) // 출력 - 30
Lv4 선택구현
- AdstractOperation라는 추상화된 프로토콜 만들기
- 기존에 구현한 AddOperation(더하기), SubstractOperation(빼기), MultiplyOperation(곱하기), DivideOperation(나누기) 클래스들과 관계를 맺고 Calculator 클래스의 내부 코드를 변경
- 스위프트의 어떤 문법을 이용하여 추상화할 수 있을지 생각해 봅시다
나는 Lv3에서 이미 과정을 틀렸기 때문에 Lv4도 완전히 틀렸다...
그래서 과제에서 진행한 방식만을 적는다.
// 프로토콜 만들기
protocol AdstractOperation {
func operate(_ num1: Double, _ num2: Double) -> Double
}
// 프로토콜 채택하기
class AddOperation: AdstractOperation {
// 프로토콜 준수하기
func operate(num1: Double, num2: Double) -> Double {
return num1 + num2
}
}
// 사용하기
let addOperation = AddOperation()
print(addOperation.operate(num1: 10, num2: 20)) // 출력 - 30
AddOperation라는 클래스에서 AdstractOperation 프로토콜을 채택한다. 프로토콜에서는 operate라는 메소드를 준수하게 시키기 때문에 기존에 add, sub 등... 제각각이었던 메소드들을 통일할 수 있게 된다.
프로토콜을 사용하면 어떠한 타입을 정의할 때 규칙을 정해줄 수 있기 때문에, 공통되는 기능을 가져야 하는 클래스나 구조체를 구현할 때 활용하면 좋다. 이번 과제는 문제를 잘못 파악한 탓에 여러모로 실수를 했지만... 다음 과제에서는 실수하지 않게 문제의 설명을 잘 읽고 진행해야겠다고 생각했다.
개념정리
Generic이란, 함수나 타입을 정의할 때 구체적인 데이터 타입을 명시하지 않고, 나중에 사용하는 시점에서 타입을 지정할 수 있게 해주는 기능이다.
제네릭을 사용하면 Swift에서 다양한 타입에 대해 유연하고 재사용 가능한 코드를 작성할 수 있다.
제네릭은 <> 기호를 사용해서 사용한다. 제네릭의 이름은 사용자가 자유롭게 변경할 수 있고, 제네릭을 함수 내부에서 사용할 수도 있다.
// inount - copy-In, copy-Out
// 매개변수의 값을 복사하여 함수 내에서 변화를 줄 수 있게하고, 변경된 값을 다시 복사해서 반환할 수 있게 도와주는 코드
// 즉, 매개변수의 값을 수정할 수 있게 도와준다
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let num = a
a = b
b = num
}
var firstNumber = 1
var secondNumber = 2
swapTwoValues(&firstNumber, &secondNumber)
print("\(firstNumber), \(secondNumber)") // 출력 - 2, 1
var firstString = "Hello"
var secondString = "World"
swapTwoValues(&firstString, &secondString)
print("\(firstString), \(secondString)") // 출력 - World, Hello
또, 제네릭의 타입을 특정 타입을 준수하도록 제한할 수도 있다. 이를 제네릭 제약이라고 한다.
where을 사용하거나 제네릭에 타입을 지정해주면 된다. 단, 이 때 특정 프로토콜을 준수하거나 특정 클래스의 하위 클래스에서만 허용할 수 있다.
// 제네릭을 Comparable 프로토콜을 준수하는 타입만 사용하도록 제약
func findMinimumValue2<T>(array: [T]) -> T? where T : Comparable {
guard !array.isEmpty else { return nil }
return array.min()
}
// 제네릭을 Comparable 프로토콜을 준수하는 타입만 사용하도록 제약
func findMinimumValue<T: Comparable>(in array: [T]) -> T? {
guard !array.isEmpty else { return nil }
return array.min()
}
let intArray = [3, 1, 4, 1, 5, 9]
if let minValue = findMinimumValue(in: intArray) {
print("Minimum value: \(minValue)") // 출력 - 1
}
let stringArray = ["Apple", "Banana", "Cherry"]
if let minValue = findMinimumValue2(array: stringArray) {
print("Minimum value: \(minValue)") // 출력 - Apple
}
구조체에서는 선언시 <>기호 안에 제네릭 이름을 선언하여 사용할 수 있다. 또, 이 제네릭을 내부 프로퍼티나 메소드에서 활용할 수 있다.
// 제네릭으로 Element 선언
struct Stack<Element> {
var items: [Element] = []
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element? {
return items.popLast()
}
}
// Stack구조체를 선언하고 제네릭을 Int타입으로 설정
var intStack = Stack<Int>()
intStack.push(10)
intStack.push(20)
intStack.push(30)
print(intStack.pop() ?? 0) // 출력 - 30
// Stack구조체를 선언하고 제네릭을 String타입으로 설정
var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print(stringStack.pop() ?? "") // 출력 - World
클래스에서 제네릭을 사용하는 방법은 구조체와 다르지 않다.
class Stack2<Element> {
var items: [Element] = []
init(_ items: [Element]) {
self.items = items
}
func push(_ item: Element) {
items.append(item)
}
func pop() -> Element? {
return self.items.popLast()
}
}
// 인스턴스화 하는 2가지 방법
var classStack = Stack2([1, 2, 3])
var classStack2 = Stack2<String>(["Apple", "Banana", "Cherry"])
classStack.push(10)
classStack.push(20)
print(classStack.items) // 1, 2, 3, 10, 20
print(classStack.pop() ?? 0) // 출력 - 20
classStack2.push("Person")
classStack2.push("Cat")
print(classStack2.items) // 출력 - "Apple", "Banana", "Cherry", "Person", "Cat"
print(classStack2.pop() ?? "") // 출력 - Cat
열거형에서는 제네릭을 case의 연관값으로 사용하거나 메소드, 선언자로 활용할 수 있다.
enum Stack3<Element> {
case empty
case value(Element)
init(_ items: Element) {
self = .value(items)
}
func printElement() {
switch self {
case .empty:
print("This case is empty")
case .value(let message):
print("value: \(message)")
}
}
}
var enumStack = Stack3.value(1)
enumStack.printElement()
// 한번 제네릭 타입을 선언하면 바꿀 수 없다.
// enumStack = Stack3.value("value") - Cannot convert value of type 'String' to expected argument type 'Int'
또, 열거형에서 제네릭을 활용하면 에러를 검출하게 하여 Result<>처럼 사용할 수 있다.
// 열거형을 Result<>처럼 활용
enum Error<Success, Failure> {
case success(Success)
case failure(Failure)
func pritStatus() {
switch self {
case .success(let message):
print(message)
case .failure(let message):
print(message)
}
}
}
// 반환타입으로 제네릭 열거형을 선언
func setNetwork(_ choice: Bool) -> Error<String, String> {
if choice {
return .success("통신 성공")
} else {
return .failure("통신 실패")
}
}
// 반환타입이 열거형이기 때문에 메소드 사용 가능
setNetwork(false).pritStatus()
// Result?
/*
enum Result<Success, Failure> where Failure : Error {
case success(Success)
case failure(Failure)
}
*/
오늘은 이전에 제출했던 과제의 해설을 보며 내 부족함을 깨닫는 시간이었다...
과제의 해설을 보며 내 잘못된 점을 파악하고, 과제를 다시 만들어보며 반성하는 시간을 가졌다.
그래도 덕분에 프로토콜이나 클래스에 대해 여러모로 확인해볼 수 있는 시간이었다고 생각한다.