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. 앱이 백그라운드에서 동작할 때 수행할 작업 정의

View Controller의 생명주기

  1. loadView()
    • 컨트롤러가 관리하는 뷰를 만든다.
      • view 프로퍼티가 nil일 때 호출하여 뷰를 만들고 프로퍼티에 할당한다.
    • 직접 호출하면 안된다.
  2. viewDidLoad()
    • 컨트롤러의 뷰가 메모리에 로드된 후 호출된다.
    • 한 번만 수행되어야 하는 초기화 작업을 위해 사용할 수 있다.
  3. viewWillAppear(_:)
    • 뷰 컨트롤러의 뷰가 뷰 계층에 추가되려 함을 알린다.
  4. viewDidAppear(_:)
    • 뷰 컨트롤러의 뷰가 뷰 계층에 추가되었음을 알린다.
  5. viewWillDisappear(_:)
    • 뷰 컨트롤러의 뷰가 뷰 계층에서 제거되려 함을 알린다.
  6. viewDidDisappear(_:)
    • 뷰 컨트롤러의 뷰가 뷰 계층에서 제거되었음을 알린다.

Optional

enum Optional<Wrapped>: ExpressibleByNilLiteral {
case some(Wrapped)
case none
}
  • 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) : 값을 다른 타입, 또는 같은 타입의 다른 값으로 변환하는 함수
  • Sequence에 정의되어 있는 각 함수는 Sequence.Element를 변환하거나 Sequence를 변환한다.
  • Optional에 정의되어 있는 각 함수는 Wrapped를 변환하거나 Optional을 변환한다.

map

// Sequence.map
func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]
// Optional.map
func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
// Result.map
func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure>
  • 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
func compactMap<ElementOfResult>(_ transform: (Self.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
  • 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
func flatMap<SegmentOfResult: Sequence>(_ transform: (Self.Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element]
// Optional.flatMap
func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
// Result.flatMap
func flatMap<NewSuccess>(_ transform: (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>
  • 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) } // nil
    • Wrapped == 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, FailureResult<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)
    • 위의 예제에서 closureself를 강한참조하여 해당 클로저를 호출할 때 self의 레퍼런스 카운트가 증가한다. a가 참조하는 A 객체를 소멸시킬 방법이 없으므로 순환 참조에 의한 메모리 누수가 발생함

함수형 프로그래밍

  • 순수 함수의 조합 / 공유 상태, 변경 가능한 데이터, 사이드 이펙트 없애기
  • 명령형이 아닌 선언형. how 대신 what을 명시함
  • 애플리케이션의 상태는 순수 함수를 통해 전달됨

일급 객체

  • 변수나 자료구조 안에 담을 수 있음
  • 매개변수로 전달될 수 있음
  • 반환값으로 사용할 수 있음
  • 고유한 식별이 가능함
  • 동적으로 프로퍼티 할당이 가능함

고차 함수

  • 매개변수로 전달할 수 있는 함수
  • 반환형으로 사용될 수 있는 함수
  • 일급 객체의 부분집합
    • Swift에서 함수는 일급 객체라고 불리나, 완벽한 일급 객체는 아님 (고유한 식별이 불가능함)

불변성

  • 변하지 않는 데이터
  • 데이터 변경이 필요한 경우 원본을 변경하는 대신 해당 데이터의 복사본을 만들고 그것을 활용하여 작업함

순수 함수

  • 동일한 입력에 항상 동일한 값을 반환함
  • 외부 상태에 영향을 받지도, 주지도 않음
  • 프로그램에 변화가 없고, 입력 값에 대한 출력 값을 예상하기 쉬워 테스트에 용이함

합성

  • 둘 이상의 함수를 조합함. 여러 개의 순수 함수를 조합함
  • 어떠한 함수의 출력이 다른 함수의 입력이 됨

커링

  • 여러 개의 매개변수를 가진 함수를, 하나의 매개변수를 갖는 여러 개의 함수로 쪼개기

재귀 함수

  • 함수가 자기 자신을 호출함
    • 함수 종료 조건 제시
  • 절차 지향적으로 작성되는 코드를 함수형으로 작성할 수 있음

탈출 클로저

  • 함수 스코프 밖에서 사용 가능한 클로저
  • @escaping 키워드 명시
  • 함수 스코프 밖에서 사용 가능해야 하므로, 현재 스코프를 참조하기 위해 self를 명시적으로 사용함
    • 이 때 일반적으로 self를 강한참조함 : 순환 참조의 원인
    • 캡쳐 리스트를 사용하여 self를 약한참조하는 것으로 해결 가능

strong, weak, unowned

strong

  • 강한참조
  • 참조와 동시에 객체 소유권 획득. 참조하는 객체의 참조 카운트 1 증가

weak, unowned

  • 약한참조
  • 참조만 하고 객체 소유권은 획득하지 않음. 참조하는 객체의 참조 카운트에 변화 없음
  • nil이 될 수 있으므로 옵셔널로 선언되어야 함

weak과 unowned의 차이

  • 객체가 메모리에 존재하는 라이프타임의 관점에서 바라보아야 함
    • 객체 간 관계에서 참조가 nil이 되는 것이 말이 될 때 약한참조 사용
    • 객체 간 관계에서 참조가 nil이 되는 것이 말이 되지 않을 때 미소유참조 사용
      • 미소유참조는 참조하는 인스턴스가 같은 라이프타임 또는 더 긴 라이프타임을 가지고 있을 때 사용한다.