평소와 같이 9시부터 알고리즘을 풀고 있었는데 오류가 발생했다.
왜 이런 오류가 발생하는지 궁금해서 찾아보니 처음 알게되는 사실들이 있었다.
문제 설명
정수 n을 입력받아 n의 약수를 모두 더한 값을 리턴하는 함수, solution을 완성해주세요.
제한 사항
n은 0 이상 3000이하인 정수입니다.
| 입출력 | 예 |
|---|---|
| n | return |
| 12 | 28 |
| 5 | 6 |
문제를 보고 for-in문으로 풀면 어렵지 않게 풀 수 있을 것이라고 생각했다.
입력받은 n을 %로 나누었을 때 값이 0인 값만 더하도록, 반복 횟수는 1~n 만큼 반복하도록 설정했다.
func solution(_ n:Int) -> Int {
// 제한사항
guard (n >= 0) && (n <= 3000) else {
return 0
}
var answer = 0
for index in 1...n {
if n % index == 0 {
answer += index
} else {
continue
}
}
return answer
}
위의 코드로 테스트를 했을 때는 통과했고, 제출을 할 때도 거의 통과가 나와서 무난히 넘길 수 있다고 생각했는데 돌연 실패가 발생했다.
실패 코드는 (signal: illegal instruction (core dumped)) 이었는데 무슨 소리인지 알 수 없었다.
혹시 if-else 때문일까 싶어서 지우고 다시 시도해보았지만 여전히 같은 에러가 발생했다.
계속 같은 에러가 발생하는 탓에 원인을 해결하고자 인터넷이 에러코드를 검색해 보았다.
검색결과를 보니 나와 비슷하게 알고리즘에서 Swift로 문제를 풀다가 같은 에러를 마주한 사람이 꽤 있는 듯 했다.
에러가 발생하는 원인은 크게 두가지라고 한다.
나의 경우 옵셔널이 나올 수는 없었기 때문에 첫번째 경우에 해당한다고 생각했다.
index out of range가 무슨 뜻일까?
Closed Range Operator
The closed range operator(a...b) defines a range that runs from a to b,
and includes the values a and b. The value of a must not be greater than b.
Closed Range Operator - Basic Operators | Documentation
애플의 공식문서를 보면 범위 연산자를 사용할 때 b가 a보다 반드시 큰 값이어야 한다는 내용이 적혀있다.
(a...b) 일 때
a > b = Error
a < b = Success
즉, 내가 겪은 에러는 범위 연산자를 사용할 때 index(b) 값이 범위를 초과했다는 오류인 듯 했다.
처음 알게된 사실이었다. 사실 범위 연산자를 쓸 때 a에서 b까지의 값 등 당연히 b가 더 큰 값이라고 별 생각없이 사용했었는데...
어쨌든 이제 원인을 알았으니 해결을 할 차례이다.
위의 코드에서 나는 for-in 반복문에 반복 조건으로 1...n 범위 연산자를 사용했다.
이 때 나는 당연히 n이 1보다 클 것이라고 멋대로 생각하고 진행했었는데, 이 알고리즘의 제한사항은 'n이 0보다 크고 3000보다 작은 수' 이다.
즉, n의 값은 0일 수도, 1일 수도 있다는 뜻이다.
그래서 나는 if 조건문을 하나 추가하여 n이 1보다 클 경우에만 for-in 반복문을 사용하도록 하였다.
n이 0이면 답은 0이 되고, 1일 경우에는 1이 되기 때문에 n이 1보다 크지 않을 경우에는 n을 반환하도록 했다.
func solution(_ n:Int) -> Int {
guard (n >= 0) && (n <= 3000) else {
return 0
}
var answer = 0
// n이 1보다 클 경우에만 반복문 사용
if n > 1 {
for index in 1...n {
if n % index == 0 {
answer += index
}
}
// n이 1보다 작을 경우 n을 반환
} else {
return n
}
return answer
}
이렇게 제출하니 에러없이 무사히 통과할 수 있었다.
정말 간단한 문제였는데 이런 사소한 부분에서도 문제가 발생할 수 있다는 것을 알 수 있었다.
덕분에 범위 연산자에 대해 찾아볼 수 있는 시간을 가져서 유익했다고 생각한다.
개념정리
Type Casting이란 인스턴스의 타입을 확인하거나 특정 타입으로 변환하는 방법이다. 타입을 확인할 때는is를 사용하고 타입을 변환하고 싶을 때는as,as?,as!를 사용한다.
주로class에서 상속 관계를 이용한 타입캐스팅에 사용되며,struct나enum에서는 상속을 통한 타입캐스팅은 지원하지 않는다.
코드를 작성하다보면 인스턴스의 타입을 확인해야 할 때가 있다.
is는 인스턴스의 타입을 확인할 수 있는 코드로, 인스턴스의 타입이 특정 클래스이거나 하위클래스인 경우 true를 반환하고 아니라면 false를 반환한다.
// is의 사용 방법
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Person: Animal {
var age: Int
init(name: String, age: Int) {
self.age = age
super.init(name: name)
}
}
class Car { }
let person = Person(name: "crois", age: 30)
let animal = Animal(name: "Cat")
print(person is Person) // true
print(person is Animal) // true
print(person is Car) // false
print(animal is Person) // false
print(animal is Animal) // true
print(animal is Car) // false
코드를 작성하다보면 타입의 변환이 필요할 때가 온다.
UIKit으로 UI를 구현하다가 StoryBoard와 연결된 뷰 컨트롤러를 가져오거나 guard 조건문 등에서 타입 캐스팅을 사용할 수 있다.
as - 슈퍼클래스로 변환(업캐스팅)as? - 서브클래스로 변환, 옵셔널 값(다운캐스팅)as! - 서브클래스로 변환, 변환 실패시 크래시 발생(다운캐스팅)// as, as?, as! 사용 예시
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Person: Animal {
var age: Int
init(name: String, age: Int) {
self.age = age
super.init(name: name)
}
}
class Car { }
// 업캐스팅
let person = Person(name: "crois", age: 20)
let personToAnimal = person as Animal // person을 Animal 타입으로 업캐스팅
print(personToAnimal.name) // 출력 - crois
// 다운캐스팅
// Person 인스턴스를 생성했지만 타입을 Animal로 명시했기 때문에 Animal 타입
let person2: Animal = Person(name: "sparta", age: 25)
let animalToPerson = person2 as? Person
print(animalToPerson?.age) // 출력 - Optional(25)
let animal = Animal(name: "cat")
let animalToPerson2 = animal as? Person
print(animalToPerson2) // 출력 - nil
// Person 타입에 있는 age 프로퍼티가 없기 때문에 다운캐스팅 실패
// animal as! Person을 시도하면 캐스팅 실패로 크래시 발생
이전 온보딩주차에서 진행했던 미니프로젝트에서 나는 UIKit의 코딩으로만 UI를 만들었고, 다른 팀원은 스토리보드로 UI를 구현해서 연결하는 과정에서 문제가 발생했었다. 그 때도 타입캐스팅을 이용해서 문제를 해결했었는데, 타입캐스팅에 대해 배운 김에 복습해보려고 한다.
// 문제의 코드
if pageIndex == 0 {
let vc =
UIStoryboard(name: "Introduction", bundle: nil).instantiateViewController(identifier: "IntroductionViewController")
as! IntroductionViewController
// setupCustomView - UIController의 오토레이아웃을 적용하는 함수
setupCustomView(vc)
// 생략...
}
먼저 if 조건문으로 페이지뷰의 인덱스값을 확인하고 인덱스 값에 따라 다른 뷰를 보여주도록 한다. 이 때, vc라는 상수를 만들고 여기에 ViewController타입의 값을 넣어 오토레이아웃을 적용해주는 함수인 setupCustomView에 매개변수 값으로 사용하려고 한다.
그러나 vc에 IntroductionViewController라는 뷰컨트롤러를 그냥 넣게 되면 빌드 후 런타임에러가 발생하게 된다. 왜냐하면 해당 뷰는 스토리보드로 UI를 구현했기 때문에 그냥 불러오면 @IBOulet의 값이 묵시적 옵셔널 추출로 되어있어 nil 을 반환하기 때문이다.
때문에 스토리보드로 되어있는 UI들을 뷰컨트롤러 타입으로 변환해 줄 필요가 있다.
UIStoryboard(name: "Introduction", bundle: nil)는 지정한 리소스파일의 스토리보드를 생성하고 반환한다. 매개변수인 name은 스토리보드의 파일명이고, bundle은 스토리보드 파일과 관련된 리소스를 알려주는 것이다. bundle의 경우 nil을 입력하면 현재 프로젝트의 기본 번들(main)이 할당된다.
instantiateViewController(identifier: "IntroductionViewController") 는 스토리보드에서 지정된 뷰 컨트롤러를 생성하고 초기화하는 코드이다. identifier를 통해 뷰 컨트롤러를 지정해줄 수 있다. 이 때 반환타입이 독특했다.
ViewController where ViewController : UIViewController
이는 제네릭 타입이라고 하는데... 아직은 잘 모르겠다.
where을 통해 ViewController가 UIViewController를 상속해야 함을 명시하고 있고, 이 때 반환하려는 클래스(뷰컨트롤러)가 UIViewController의 서브 클래스여야 하고 반드시 기본 이니셜라이저를 갖고 있어야 한다.
만약 특정 초기화 옵션이나 매개변수가 있는 경우에는 매개변수를 추가로 받아 초기화할 수도 있다고 한다.
이제 오늘 배운 타입캐스팅을 이용해서 UIViewController타입으로 스토리보드의 인스턴스를 변환해준다.
as! IntroductionViewController
이 때는 as!를 사용하는 다소 위험한 방법을 사용했는데, 만약 as?를 사용하면 어떻게 될까.

엄청나게 에러가 발생했다...
에러내용을 확인해보면 옵셔널 값인 UIViewController?를 UIViewController 값으로 만들어야 하는 것 같다.
그렇다면 요구대로 옵셔널 바인딩을 해보자!
복잡하게 할 것 없이 if문을 사용해서 간단히 옵셔널 바인딩을 해줬다.
if pageIndex == 0 {
let vc =
UIStoryboard(name: "Introduction", bundle: nil).instantiateViewController(identifier: "IntroductionViewController")
as? IntroductionViewController
// vc를 옵셔널 바인딩
if vc {
setupCustomView(vc)
}
// 생략...
}
이렇게 하니 오류가 해제되었다. 다른 방법으로는 vc를 묵시적 옵셔널 추출로 지정해도 에러는 사라진다 vc!
스토리보드의 이름만 잘 입력하면 값이 nil인 경우는 웬만하면 없을 것이라고 생각하기 때문에 이 경우 as!를 사용해도 별 문제 없을 것이라고 생각하고 실제로도 문제가 발생하지 않았지만, 안전을 생각한다면 역시 as?를 사용하여 다운캐스팅을 하는 편이 좋을 것 같다.
개념정리
동시성 프로그래밍이란 한번에 여러 작업을 수행하는 것을 목표로 하는 프로그래밍 방식을 말한다. 하나의 프로세스에는 여러 개의 쓰레드가 존재하며, 각 쓰레드에서 병렬 작업을 수행할 수 있다.
만약 쓰레드를 별도로 지정하지 않는다면 메인 쓰레드에서 작업이 진행된다.
메인 쓰레드는 UI 작업을 할 수 있는 유일한 쓰레드이다. 기본적으로 모든 작업은 메인 쓰레드에서 실행되지만, 정말 모든 작업을 메인 쓰레드에서 진행할 경우 속도 지연 및 성능 저하 문제가 발생할 수 있기 때문에 주의하여야 한다.
DispatchQueue를 사용하여 여러 개의 쓰레드에 업무를 분담DispatchQueue는 주로 2가지 큐를 제공하며 Main Queue, Global Queue로 구성메인 쓰레드로 작업을 전달하는 Queue이다. 할당된 일을 하나씩 처리한다고 하여 Serial Queue(직렬 큐)라고 불린다.
DispatchQueue.main.async {
// 작업
}
할당된 여러가지 작업을 동시에 처리할 수 있어서 Concurrent Queue(병렬 큐)라고 불린다. Global Queue에 할 일을 추가하면 여러 개의 쓰레드에 작업을 나누어 처리한다. 이 때, 작업의 우선순위를 지정하는 Quality of Service(QoS)를 설정할 수 있다.
.userInteractive:.userInitiated:.default:.utility:.background:.unspecified:DispatchQueue.global(qos: .background).async {
// 사용자에게 보이지 않거나 중요하지 않은 작업
}
async는 비동기작업, sync는 동기 작업으로 분류한다.
import UIKit
DispatchQueue.main.sync {
print("Hello")
}
print("world")
// sync의 작업이 끝나기전에 print("world")가 호출되지 않음
// "Hello" "World"가 출력됨
import UIKit
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 1)
print("Hello")
}
print("World")
// async이기 떄문에 바로 "World"가 호출된 후 1초뒤에 "Hello"가 호출됨
오늘은 알고리즘을 풀면서 발생한 문제와 타입캐스팅, 그리고 동시성에 대해 정리를 해보았다.
알고리즘은 풀면 풀수록 수학실력이 요구되는 것 같아서 어렵다...
타입캐스팅은 지난번 문제였던 것을 되돌아보며 복습하니 더 잘 이해가 되고 좋았던 것 같다.
동시성은 아직 어떻게 활용하면 좋을지, 어떤 상황에 사용하면 좋을지에 대해서 고민이 더 필요할 것 같다.
오늘 복습을 하며 제네릭 등의 처음 보는 단어들도 나왔는데, 다행히 다음 강의에서 배울 수 있는 것 같아서 안심했다.
이제부터 어려운 공부만 남은 것 같아서 걱정이다.