-Today's Learning Content-

  • Calculator 과제 돌아보기
  • 제네릭(Generic)

1. Calculator 과제 돌아보기

Swift문법 종합반 기초 과제로 출제되었던 'Calculator' 만들기의 해설 영상이 제공되었다.
과제 출제의 의도는 무엇이고, 정석적인 방법은 어떤 것인지 해설 영상을 보며 내용을 복습해 보았다.

1) Lv1 구현하기

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
    }
}

2) Lv2 구현하기

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
        }
    }
}

3) Lv3 구현하기

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

4) Lv4 구현하기

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 등... 제각각이었던 메소드들을 통일할 수 있게 된다.
프로토콜을 사용하면 어떠한 타입을 정의할 때 규칙을 정해줄 수 있기 때문에, 공통되는 기능을 가져야 하는 클래스나 구조체를 구현할 때 활용하면 좋다. 이번 과제는 문제를 잘못 파악한 탓에 여러모로 실수를 했지만... 다음 과제에서는 실수하지 않게 문제의 설명을 잘 읽고 진행해야겠다고 생각했다.


2. 제네릭(Generic)

개념정리

Generic 이란, 함수나 타입을 정의할 때 구체적인 데이터 타입을 명시하지 않고, 나중에 사용하는 시점에서 타입을 지정할 수 있게 해주는 기능이다.
제네릭을 사용하면 Swift에서 다양한 타입에 대해 유연하고 재사용 가능한 코드를 작성할 수 있다.

1) 함수에서 제네릭 사용하기

제네릭은 <> 기호를 사용해서 사용한다. 제네릭의 이름은 사용자가 자유롭게 변경할 수 있고, 제네릭을 함수 내부에서 사용할 수도 있다.

// 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
}

2) 구조체에서 제네릭 사용하기

구조체에서는 선언시 <>기호 안에 제네릭 이름을 선언하여 사용할 수 있다. 또, 이 제네릭을 내부 프로퍼티나 메소드에서 활용할 수 있다.

// 제네릭으로 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

3) 클래스에서 제네릭 사용하기

클래스에서 제네릭을 사용하는 방법은 구조체와 다르지 않다.

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

4) 열거형에서 제네릭 사용하기

열거형에서는 제네릭을 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)
 }
 */

-Today's Lesson Review-

오늘은 이전에 제출했던 과제의 해설을 보며 내 부족함을 깨닫는 시간이었다...
과제의 해설을 보며 내 잘못된 점을 파악하고, 과제를 다시 만들어보며 반성하는 시간을 가졌다.
그래도 덕분에 프로토콜이나 클래스에 대해 여러모로 확인해볼 수 있는 시간이었다고 생각한다.
profile
이유있는 코드를 쓰자!!

0개의 댓글