-Today's Learning Content-

  • Algorithm
  • Type Casting
  • 동시성

1. Algorithm

평소와 같이 9시부터 알고리즘을 풀고 있었는데 오류가 발생했다.
왜 이런 오류가 발생하는지 궁금해서 찾아보니 처음 알게되는 사실들이 있었다.

1) 알고리즘 문제

문제 설명
정수 n을 입력받아 n의 약수를 모두 더한 값을 리턴하는 함수, solution을 완성해주세요.
제한 사항
n은 0 이상 3000이하인 정수입니다.

입출력
nreturn
1228
56

2) 풀이 중 에러 발생

문제를 보고 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 때문일까 싶어서 지우고 다시 시도해보았지만 여전히 같은 에러가 발생했다.

3) 문제의 원인 찾기

계속 같은 에러가 발생하는 탓에 원인을 해결하고자 인터넷이 에러코드를 검색해 보았다.
검색결과를 보니 나와 비슷하게 알고리즘에서 Swift로 문제를 풀다가 같은 에러를 마주한 사람이 꽤 있는 듯 했다.

에러가 발생하는 원인은 크게 두가지라고 한다.

  • index out of range (활용한 index 범위 연산자가 초과함.)
  • 옵셔널에서 nil 값에 의한 오류

나의 경우 옵셔널이 나올 수는 없었기 때문에 첫번째 경우에 해당한다고 생각했다.
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가 더 큰 값이라고 별 생각없이 사용했었는데...
어쨌든 이제 원인을 알았으니 해결을 할 차례이다.

4) 문제 해결하기

위의 코드에서 나는 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
}

이렇게 제출하니 에러없이 무사히 통과할 수 있었다.
정말 간단한 문제였는데 이런 사소한 부분에서도 문제가 발생할 수 있다는 것을 알 수 있었다.
덕분에 범위 연산자에 대해 찾아볼 수 있는 시간을 가져서 유익했다고 생각한다.


2. Type Casting

개념정리

Type Casting 이란 인스턴스의 타입을 확인하거나 특정 타입으로 변환하는 방법이다. 타입을 확인할 때는 is를 사용하고 타입을 변환하고 싶을 때는 as, as?, as! 를 사용한다.
주로 class에서 상속 관계를 이용한 타입캐스팅에 사용되며, structenum 에서는 상속을 통한 타입캐스팅은 지원하지 않는다.

1) 인스턴스의 타입 확인(is)

코드를 작성하다보면 인스턴스의 타입을 확인해야 할 때가 있다.
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 

2) 타입 변환(as, as?, as!)

코드를 작성하다보면 타입의 변환이 필요할 때가 온다.
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을 시도하면 캐스팅 실패로 크래시 발생

3) 되돌아보기

이전 온보딩주차에서 진행했던 미니프로젝트에서 나는 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에 매개변수 값으로 사용하려고 한다.

그러나 vcIntroductionViewController라는 뷰컨트롤러를 그냥 넣게 되면 빌드 후 런타임에러가 발생하게 된다. 왜냐하면 해당 뷰는 스토리보드로 UI를 구현했기 때문에 그냥 불러오면 @IBOulet의 값이 묵시적 옵셔널 추출로 되어있어 nil 을 반환하기 때문이다.
때문에 스토리보드로 되어있는 UI들을 뷰컨트롤러 타입으로 변환해 줄 필요가 있다.

a. 스토리보드 불러오기

UIStoryboard(name: "Introduction", bundle: nil)는 지정한 리소스파일의 스토리보드를 생성하고 반환한다. 매개변수인 name은 스토리보드의 파일명이고, bundle은 스토리보드 파일과 관련된 리소스를 알려주는 것이다. bundle의 경우 nil을 입력하면 현재 프로젝트의 기본 번들(main)이 할당된다.

b. 스토리보드 인스턴스화 하기

instantiateViewController(identifier: "IntroductionViewController") 는 스토리보드에서 지정된 뷰 컨트롤러를 생성하고 초기화하는 코드이다. identifier를 통해 뷰 컨트롤러를 지정해줄 수 있다. 이 때 반환타입이 독특했다.

ViewController where ViewController : UIViewController

이는 제네릭 타입이라고 하는데... 아직은 잘 모르겠다.
where을 통해 ViewControllerUIViewController를 상속해야 함을 명시하고 있고, 이 때 반환하려는 클래스(뷰컨트롤러)가 UIViewController의 서브 클래스여야 하고 반드시 기본 이니셜라이저를 갖고 있어야 한다.
만약 특정 초기화 옵션이나 매개변수가 있는 경우에는 매개변수를 추가로 받아 초기화할 수도 있다고 한다.

c. 타입 변환하기

이제 오늘 배운 타입캐스팅을 이용해서 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?를 사용하여 다운캐스팅을 하는 편이 좋을 것 같다.


3. 동시성

개념정리

동시성 프로그래밍 이란 한번에 여러 작업을 수행하는 것을 목표로 하는 프로그래밍 방식을 말한다. 하나의 프로세스에는 여러 개의 쓰레드가 존재하며, 각 쓰레드에서 병렬 작업을 수행할 수 있다.
만약 쓰레드를 별도로 지정하지 않는다면 메인 쓰레드에서 작업이 진행된다.

1) 메인 쓰레드

메인 쓰레드는 UI 작업을 할 수 있는 유일한 쓰레드이다. 기본적으로 모든 작업은 메인 쓰레드에서 실행되지만, 정말 모든 작업을 메인 쓰레드에서 진행할 경우 속도 지연 및 성능 저하 문제가 발생할 수 있기 때문에 주의하여야 한다.

2) GCD(Grand Central Dispatch)

  • GCD는 iOS에서 제공하는 동시성프로그래밍을 쉽게 처리하기 위한 도구 중 하나
  • 작업을 Queue에 추가하면 OS가 적절한 Thread에게 일을 할당하여 처리
  • DispatchQueue를 사용하여 여러 개의 쓰레드에 업무를 분담
  • DispatchQueue는 주로 2가지 큐를 제공하며 Main Queue, Global Queue로 구성
  • Queue를 사용하기 때문에 먼저 들어온 업무를 먼저 Thread에 전달

a. Main Queue

메인 쓰레드로 작업을 전달하는 Queue이다. 할당된 일을 하나씩 처리한다고 하여 Serial Queue(직렬 큐)라고 불린다.

DispatchQueue.main.async {
	// 작업 
}

b. Global Queue

할당된 여러가지 작업을 동시에 처리할 수 있어서 Concurrent Queue(병렬 큐)라고 불린다. Global Queue에 할 일을 추가하면 여러 개의 쓰레드에 작업을 나누어 처리한다. 이 때, 작업의 우선순위를 지정하는 Quality of Service(QoS)를 설정할 수 있다.

  • .userInteractive:
    가장 높은 우선순위, 사용자에게 즉각적인 반응을 줘야할 때 사용(애니메이션, 터치 이벤트 등)
  • .userInitiated:
    문서열기, 이미지 로드 등
  • .default:
    기본 설정 값
  • .utility:
    긴 시간이 걸릴 수 있는 작업(백그라운드에서 데이터 동기화, 파일 다운로드 등)
  • .background:
    사용자에게 보이지 않거나 중요하지 않은 작업(데이터 백업, 데이터 정리 등)
  • .unspecified:
    QoS가 지정되지 않은 상태로, 시스템이 우선순위를 정하게 함
    사용되지 않음
DispatchQueue.global(qos: .background).async {
	// 사용자에게 보이지 않거나 중요하지 않은 작업 
}

3) async와 sync

async는 비동기작업, sync는 동기 작업으로 분류한다.

a. sync(동기)

  • 작업이 순차적으로 실행
  • 현재 작업이 끝날 때 까지 다음 작업이 시작되지 않음
  • 작업이 끝날때까지 대기하기때문에 대기시간이 발생할 수 있음
  • 작업이 순서대로 실행되므로, 코드의 흐름을 이해하기 편하고 디버깅이 간편하지만, 성능 저하가 발생할 수 있음
import UIKit

DispatchQueue.main.sync {
	print("Hello")
}

print("world")

// sync의 작업이 끝나기전에 print("world")가 호출되지 않음
// "Hello" "World"가 출력됨

b. async(비동기)

  • 작업을 시작한 후, 작업이 완료될때까지 기다리지 않고 다음 코드를 바로 실행
  • 여러 작업이 병행되어 빠른 처리가 가능하지만, 결과를 기다리는 구조로 인해 코드가 복잡해 질 수 있음
import UIKit

DispatchQueue.global().async {
    Thread.sleep(forTimeInterval: 1)
		print("Hello")
}

print("World")

// async이기 떄문에 바로 "World"가 호출된 후 1초뒤에 "Hello"가 호출됨

-Today's Lesson Review-

오늘은 알고리즘을 풀면서 발생한 문제와 타입캐스팅, 그리고 동시성에 대해 정리를 해보았다.
알고리즘은 풀면 풀수록 수학실력이 요구되는 것 같아서 어렵다...
타입캐스팅은 지난번 문제였던 것을 되돌아보며 복습하니 더 잘 이해가 되고 좋았던 것 같다.
동시성은 아직 어떻게 활용하면 좋을지, 어떤 상황에 사용하면 좋을지에 대해서 고민이 더 필요할 것 같다.
오늘 복습을 하며 제네릭 등의 처음 보는 단어들도 나왔는데, 다행히 다음 강의에서 배울 수 있는 것 같아서 안심했다.
이제부터 어려운 공부만 남은 것 같아서 걱정이다.
profile
이유있는 코드를 쓰자!!

0개의 댓글