iOS 면접 출제 빈도 높은 질문들
정답이 아닌, 저의 지식과 경험, 검색에 기반한 답입니다.
Swift에서 Struct와 Class의 차이
Struct
- Value Type
- 할당된 메모리를 값으로 해석함
- 복사 시 값 복사 발생
- 공유 데이터로 인한 문제를 일으킬 확률이 적음
- 함수형 프로그래밍을 지향하는 Swift는 스위프트 표준 라이브러리의 대부분을 Struct로 구현함
- 컴파일 타임에 컴파일러가 언제 메모리를 할당 및 해제해야 하는지 알고 있음
- 스택 포인터를 조절하면 되므로 단편화 문제 없음
- 레퍼런스 카운팅을 사용하지 않음
- 상속 불가능
- Swift는 프로토콜 지향 프로그래밍을 지향하여 클래스의 상속 관계 대신 구조체와 프로토콜의 합성 관계를 지향한다.
- 공간에 한계가 있음
- 스택오버플로우
- 정적 메소드 디스패치
- 컴파일 타임에서 메소드의 위치를 특정할 수 있음
- 상속이 필요하지 않고 모델의 크기가 크지 않을 때 사용하기 좋음
Class
- Reference Type
- 할당된 메모리를 주소로 해석함
- 복사 시 참조 복사 발생
- 힙에 할당됨
- 빈 공간을 Atomic하게 찾아야 하므로 동기화 작업 필요
- 단편화 문제
- 런타임에 할당되고 레퍼런스 카운팅을 통해 참조 해제 관리
- 레퍼런스 카운팅은 Atomic하게 일어나야 하므로 동기화 작업 필요
- 상속 가능
- 동적 메소드 디스패치
- 런타임에서 메소드의 위치를 판별함
- Objective-C 호환이 필요하다면 Class를 사용해야 함
Rx의 장단점
장점
- Rx는 관찰 가능한 시퀀스를 사용하여 비동기 및 이벤트 기반 프로그램을 구성하기 위한 라이브러리.
- 여러 개의 아이템에 대한 비동기 작업을 쉽게 할 수 있게 해줌
- 네트워킹으로 대표되는 비동기 작업을 쉽게 처리할 수 있음
- iOS에서 비동기 처리를 위해 사용한 컴플리션 핸들러, 델리게이트 패턴,
NotificationCenter
등 대신 Rx를 사용하여 처리할 수 있음- 콜백 지옥, 가독성 저하 등의 문제 해결 가능
- 변환, 결합 등 오퍼레이터 체인을 잘 구성하여 강력한 비동기 프로그래밍을 할 수 있음
단점
- 러닝 커브가 크다.
- 기존의 명령형 프로그래밍에서, 데이터가 흐르는 스트림의 개념을 생각할 수 있어야 한다.
- 많은 오퍼레이터들을 잘 이해해야 잘 사용할 수 있다.
- 제대로 이해하지 못한다면 디버깅이 힘들 수 있고, 기존 방식으로 구현하는 것보다 더 힘들어질 수 있으며, 가독성 개선 효과도 잘 경험할 수 없을 것이다.
MVC, MVP, MVVM 등 아키텍쳐 패턴
MVC
- Model-View-Controller
- Model : 데이터, 비즈니스 로직
- View : UI (View)
- Controller : Model과 View의 중재자, 유저 입력을 받고 처리함 (View Controller)
- View와 Model 간 의존 발생
- Cocoa MVC는 전통적인 MVC와 다름
- View Controller가 View와 Controller를 모두 담당할 수 있음
- View Controller의 코드가 비대해짐 : Massive View Controller
- Massive View Controller의 문제를 해결하기 위한 다양한 아키텍쳐 패턴의 시도가 있음
MVP
- Model-View-Presenter
- Model : 데이터, 비즈니스 로직
- View : UI, 유저 입력 (View / View Controller)
- Presenter : View가 요청한 데이터를 Model에서 가공하여 View로 전달
- Presenter는 View를 참조함
- Model과 View는 Presenter를 통해 동작하여 서로 의존하지 않음
- View와 Presenter 간 의존 발생
MVVM
- Model-View-View Model
- Model : 데이터, 비즈니스 로직
- View : UI, 유저 입력 (View / View Controller)
- View Model : View가 요청한 데이터를 Model에서 받아오고 가공하여 저장
- Data Binding으로 인해 View는 View Model의 변화에 따라 자동 갱신됨
- Model과 View는 View Model을 통해 동작하여 서로 의존하지 않음
- View Model은 View를 모르므로 View와 View Model 간 서로 의존하지 않음
평가 기준
- 책임 분리
- 테스트 용이
- 사용 용이
GCD
- Grand Central Dispatch
Dispatch
프레임워크- 시스템이 관리하는 디스패치 큐에 작업을 제출하여 멀티코어 하드웨어에서 코드를 병렬 실행
- 멀티쓰레드 프로그래밍을 쉽게 사용하기 위한 API이며, 기반은 C로 구현되어 있음
DispatchQueue
/DispatchWorkItem
/DispatchGroup
/DispatchSemaphore
등
serial / concurrent
- serial : 큐에서 한 번에 하나의 작업만 실행할 수 있음
- concurrent : 큐에서 여러 개의 작업을 동시에 실행할 수 있음
sync / async
- sync : 작업을 실행한 후 해당 작업이 끝날 때까지 기다림. 제어권은 callee에게 있음
- async : 작업을 실행한 후 해당 작업이 끝나는 것을 기다리지 않음. 제어권은 caller에게 있음
DispatchQueue
- main 타입 프로퍼티를 통해 접근 가능한 메인 큐는 메인 스레드(UI 스레드)에서 사용되는 serial 큐
- 일반적으로
DispatchQueue.main.async
를 사용하여 메인 스레드에서 실행할 작업을 정의함
- 일반적으로
- global 타입 메소드를 통해 만들 수 있는 시스템이 제공하는 글로벌 큐는 메인 스레드에서 사용하지 않는 concurrent 큐
- 인자로 QoS를 정의할 수 있음
- userInteractive : 유저 인터랙티브 작업을 위한 QoS. 애니메이션, 이벤트 처리, UI 갱신 등
- userInitiated : 유저가 앱을 액티브하게 사용하지 못하게 하는 작업을 위한 QoS. 결과를 즉시 제공해야 하는 작업 정의
- default : 디폴트 QoS
- utility : 유저가 액티브하게 추적하지 않는 작업을 위한 QoS. 사용자가 추적하지 않는 오래 걸리는 작업 정의
- background : 유지나 클린업 작업을 위한 QoS. 앱이 백그라운드에서 동작할 때 수행할 작업 정의
- 인자로 QoS를 정의할 수 있음
View Controller의 생명주기
loadView()
- 컨트롤러가 관리하는 뷰를 만든다.
view
프로퍼티가nil
일 때 호출하여 뷰를 만들고 프로퍼티에 할당한다.
- 직접 호출하면 안된다.
- 컨트롤러가 관리하는 뷰를 만든다.
viewDidLoad()
- 컨트롤러의 뷰가 메모리에 로드된 후 호출된다.
- 한 번만 수행되어야 하는 초기화 작업을 위해 사용할 수 있다.
viewWillAppear(_:)
- 뷰 컨트롤러의 뷰가 뷰 계층에 추가되려 함을 알린다.
viewDidAppear(_:)
- 뷰 컨트롤러의 뷰가 뷰 계층에 추가되었음을 알린다.
viewWillDisappear(_:)
- 뷰 컨트롤러의 뷰가 뷰 계층에서 제거되려 함을 알린다.
viewDidDisappear(_:)
- 뷰 컨트롤러의 뷰가 뷰 계층에서 제거되었음을 알린다.
Optional
enum Optional<Wrapped>: ExpressibleByNilLiteral { |
Optional
은 열거형으로 구현되어 있으며, 제네릭 타입을 하나 받고,some
케이스와none
케이스를 갖는다.some
케이스는 연관 값으로Wrapped
값을 받는다.none
케이스는 값이 없음을 나타낸다.
ExpressibleByNilLiteral
프로토콜을 채택하여nil
을 사용하여 인스턴스를 만들 수 있다.
Monad
Functor
- 값을 담은 Context의 구조를 가짐
- Context의 Content를 변환할 수 있음 (map)
Monad
- 값을 담은 Context의 구조를 가짐
- Context를 변환할 수 있음 (flatMap)
map, compactMap, flatMap
- 셋 모두 Context가 있는 타입에 대해 정의되어 호출 가능
- Context(Container) :
Collection
,Optional
,Result
처럼 값을 포함하는 구조의 자료형 - Value(Content) : 실제 Context에 들어 있는 값
- Transform(Function) : 값을 다른 타입, 또는 같은 타입의 다른 값으로 변환하는 함수
- Context(Container) :
Sequence
에 정의되어 있는 각 함수는Sequence.Element
를 변환하거나Sequence
를 변환한다.Optional
에 정의되어 있는 각 함수는Wrapped
를 변환하거나Optional
을 변환한다.
map
// Sequence.map |
Context에서 Content를 빼내어 Transform을 적용하여 Content를 변환한 후 다시 Context에 담는다.
let a: [Int] = [1, 2]
a.map { "\($0)" } // ["2", "4"]Element
==Int
,T
==String
이므로, 반환형[T]
은[String]
이 된다.
let a: String? = "가"
a.map { Int($0) } // Optional(nil)Wrapped
==String
,U
==Int?
이므로, 반환형U?
은Int??
가 된다.
let a = Result<Int, Never>.success(0)
a.map { "\($0)" } // success("0")Success
==Int
,Failure
==Never
,NewSuccess
==String
이므로, 반환형Result<NewSuccess, Failure>
는Result<String, Neer>
가 된다.
compactMap
// Sequence.compactMap |
Context에서 Content를 빼내어 Transform을 적용하여 Content를 변환한 후 nil이 아닌 것들만 다시 Context에 담는다.
let a = [1, 2, nil]
a.compactMap { $0 } // [1, 2]Element
==Int?
,ElementOfResult?
==Int?
이므로, 반환형[ElementOfResult
은[Int]
가 된다.
flatMap
// Sequence.flatMap |
Context에서 Content를 빼내어 Transform을 적용하여 Context에 감싸진 Content로 변환한 후 평평하게 만든다.
let a = [1, 2]
a.flatMap { [$0, $0 + 1] } // [1, 2, 2, 3]Element
==Int
,SegmentOfResult
==[Int]
이므로, 반환형[SegmentOfResult.Element]
는[Int]
가 된다.
let a: String? = "가"
a.flatMap { Int($0) } // nilWrapped
==String
,U?
==Int?
이므로, 반환형U?
은Int?
가 된다.
let a = Result<Int, Never>.success(0)
a.flatMap { Result<String, Never>.success("\($0)") } // success("0")Success
==Int
,Failure
==Never
,NewSuccess
==String
이므로, 반환형Result<NewSuccess, Failure
는Result<String, Never>
가 된다.
순환 참조
A 객체와 B 객체가 서로를 강한참조하고 있을 때 순환 참조 문제 발생
- A 객체의 프로퍼티가 B 객체를 강한참조하고, B 객체의 프로퍼티가 A 객체를 강한참조하는 경우
class A {
var instance: B?
}
class B {
var instance: A?
}
let a = A() // a(1)
let b = B() // b(1)
a.instance = b // b(2)
b.instance = a // a(2)
a = nil // a(1)
b = nil // b(1)- 위의 예제에서
a
가 참조하는A
객체의 레퍼런스 카운트와,b
가 참조하는B
객체의 레퍼런스 카운트가 0이 되지 않았고, 이 객체들을 소멸시킬 방법이 없으므로 순환 참조에 의한 메모리 누수 문제가 발생함
클로저가 클로저를 포함하는 인스턴스(
self
)를 강한참조할 때class A {
var b = 0
lazy var closure: () -> Void = {
print(self.b)
}
}
var a: A? = A() // A(1)
a?.closure() // A(2)
a = nil // A(1)- 위의 예제에서
closure
는self
를 강한참조하여 해당 클로저를 호출할 때self
의 레퍼런스 카운트가 증가한다.a
가 참조하는A
객체를 소멸시킬 방법이 없으므로 순환 참조에 의한 메모리 누수가 발생함
- 위의 예제에서
함수형 프로그래밍
- 순수 함수의 조합 / 공유 상태, 변경 가능한 데이터, 사이드 이펙트 없애기
- 명령형이 아닌 선언형. how 대신 what을 명시함
- 애플리케이션의 상태는 순수 함수를 통해 전달됨
일급 객체
- 변수나 자료구조 안에 담을 수 있음
- 매개변수로 전달될 수 있음
- 반환값으로 사용할 수 있음
- 고유한 식별이 가능함
- 동적으로 프로퍼티 할당이 가능함
고차 함수
- 매개변수로 전달할 수 있는 함수
- 반환형으로 사용될 수 있는 함수
- 일급 객체의 부분집합
- Swift에서 함수는 일급 객체라고 불리나, 완벽한 일급 객체는 아님 (고유한 식별이 불가능함)
불변성
- 변하지 않는 데이터
- 데이터 변경이 필요한 경우 원본을 변경하는 대신 해당 데이터의 복사본을 만들고 그것을 활용하여 작업함
순수 함수
- 동일한 입력에 항상 동일한 값을 반환함
- 외부 상태에 영향을 받지도, 주지도 않음
- 프로그램에 변화가 없고, 입력 값에 대한 출력 값을 예상하기 쉬워 테스트에 용이함
합성
- 둘 이상의 함수를 조합함. 여러 개의 순수 함수를 조합함
- 어떠한 함수의 출력이 다른 함수의 입력이 됨
커링
- 여러 개의 매개변수를 가진 함수를, 하나의 매개변수를 갖는 여러 개의 함수로 쪼개기
재귀 함수
- 함수가 자기 자신을 호출함
- 함수 종료 조건 제시
- 절차 지향적으로 작성되는 코드를 함수형으로 작성할 수 있음
탈출 클로저
- 함수 스코프 밖에서 사용 가능한 클로저
@escaping
키워드 명시- 함수 스코프 밖에서 사용 가능해야 하므로, 현재 스코프를 참조하기 위해
self
를 명시적으로 사용함- 이 때 일반적으로
self
를 강한참조함 : 순환 참조의 원인 - 캡쳐 리스트를 사용하여
self
를 약한참조하는 것으로 해결 가능
- 이 때 일반적으로
strong, weak, unowned
strong
- 강한참조
- 참조와 동시에 객체 소유권 획득. 참조하는 객체의 참조 카운트 1 증가
weak, unowned
- 약한참조
- 참조만 하고 객체 소유권은 획득하지 않음. 참조하는 객체의 참조 카운트에 변화 없음
nil
이 될 수 있으므로 옵셔널로 선언되어야 함
weak과 unowned의 차이
- 객체가 메모리에 존재하는 라이프타임의 관점에서 바라보아야 함
- 객체 간 관계에서 참조가
nil
이 되는 것이 말이 될 때 약한참조 사용 - 객체 간 관계에서 참조가
nil
이 되는 것이 말이 되지 않을 때 미소유참조 사용- 미소유참조는 참조하는 인스턴스가 같은 라이프타임 또는 더 긴 라이프타임을 가지고 있을 때 사용한다.
- 객체 간 관계에서 참조가