본문 바로가기

iOS_기타

면접 실제 받았던 질문 정리 + 공부

Swift의 장점

  1. 안정성 Safe
    • 스위프트는 안전한 프로그래밍을 지향하기 때문에 프로그래머가 저지를 수 있는 실수를 엄격한 문법을 통하여
      버그를 비연에 방지하고자 노력한다.
    • 때로는 강제적이라고 느껴질 수 있지만 문법적 제재는 실수를 줄이는데 도움이 된다.
    • 옵셔널이라는 기능을 비롯하여 guard구문, 오류처리, 강력한 타입통제 등을 통해 안전한 프로그래밍을 구현
  2. 신속성 Fast
    • 스위프트는 C언어를 기반으로 한 C, C++, Objective- C와 같은 프로그래밍 언어를 대체하려는 목적으로 개발되었습니다
    • 애초에 설계를 성능을 최대한 C언어에 가깝게 맞추려고 했습니다
    • 실행속도의 최적화 뿐만 아니라 컴파일러의 지속된 개량을 통해 더 빠른 컴파일 성능을 구현
  3. 더 나은 표현성 Expressive
    • 스위프트는 그간 발전된 프로그래밍 언어를 모두 참고하여 사용하기 편하고 보기좋은 문법을 구사하려 노력했습니다
    • 개발자들이 원하던 현대적이고 세련된 문법을 구현
    • 스위프트는 다중 프로그래밍 패러다임을 채용한 다중 패러다임 프로그래밍 언어입니다.
    • 다중패러다임 안에는크게 3가지가 있습니다
      • 명령형 객체지향 프로그래밍
      • 함수형 프로그래밍 
      • 프로토콜 지향 프로그래밍

hugging, resistance에 대해서 설명 & Intrinsic Size에 대해서 설명

Auto layout을 통해 constraint를 잡을 때 반드시 해당 view의 위치(position)와 크기(size) 정보가 필요합니다.

하나라도 모르면 뷰가 어그러지거나, 화면 상에 보이지 않는 등 예상했던 것과 다르게 표현이 되곤하죠!

그런데 모든 뷰가 width/height 정보를 명시해 줄 필요는 없어요.

 

Intrinsic Content Size

특정 뷰들은 자신들이 현재 갖고 있는 content에 따라 본질적으로 크기가 정해지기도 하는데 이 때 그 크기를 intrinsic content size

라고 해요. 아래 표에도 나와 있지만 조금 덧붙여보면

  • UIView, NSView : intrinsic content size 없음
  • Sliders : width 정해짐 (OS X 에서는 slider 종류에 따라 height도 정해지기도 함)
  • Label : 내부 텍스트 크기에 따라 width, height가 정해짐 (폰트도 당연히 영향 미침)
  • Button : button의 title과 갖고있는 margin에 따라 width, height 정해짐
  • image view : (image가 있다면) image 크기가 intrinsic content size가 됨
  • text view : intrinsic content size에 영향을 미치는 요소들이 많음(ex. content 길이, scroll 가능 여부 등)
    • scoll 이 가능하면 intrinsic content size 없음
    • scroll 이 불가능하면 line wrapping 적용 안된 text의 크기가 intrinsic content size

Content Hugging & Compression Resistance

Intrinsic content size와 함께 content hugging 과 compression resistance는 함께 나오는 개념이에요.

Intrinsic content sizie에 대해 제약조건을 추가할 수 있는 것이죠.

  Content Hugging Compression Resistance
Intrinsic content size의 최대값을 지정 최소값을 지정
존재 이유 view가 content를 알맞게 감싸기 위해 view가 content를 가리지 않게 하기 위해
default priority 250 750

 

위의 표에서 알 수 있다시피 compression resistance의 priority가 더 높아요.

그렇기 때문에 compression resistance를 통해 UI component를 늘리는 것이 더 권장됩니다.

물론 priority는 필요에 따라 수정이 가능하기 때문에 상황에 맞게 적절히 사용하면 되죠.


struct와 class와 enum의 차이를 설명

Struct

  • Value Type
    • 할당된 메모리를 값으로 해석
    • 복사 시 값 복사 발생
  • 컴파일 타임에 컴파일러가 언제 메모리를 할당 및 해제해야 하는지 알고 있음
    • 레퍼런스 카운팅을 사용하지 않음
  • 상속 불가능
    • Swift는 프로토콜 지향 프로그래밍을 지향하여 클래스의 상속 관계 대신 구조체와 프로토콜의 합성 관계를 지향한다.
    • 상속이 필요하지 않고 모델의 크기가 크지 않을 때 사용하기 좋음

Class

  • Reference Type
    • 할당된 메모리를 주소로 해석
    • 복사 시 참조 복사 발생
  • 힙에 할당됨
  • 런타임에 할당되고 레퍼런스 카운팅을 통해 참조 해제 관리
  • 상속 가능

이 차이점을 가지고 심화질문을 받았는데.. 답변이 어려웠다..

 

A. 클래스 안에 구조체의 인스턴스가 있는 경우

import Foundation
 
struct ValueType {
    var number = 2
}
 
class ReferenceType {
    var number = 1
    var structInstance = ValueType()
}
 
let classInstance = ReferenceType()
let classInstanceCopy = classInstance
 
classInstanceCopy.number = 0
classInstanceCopy.structInstance.number = 0
 
print(classInstanceCopy.number)
print(classInstanceCopy.structInstance.number)
print()
print(classInstance.number)
print(classInstance.structInstance.number)

 

위 코드를 실행하면 결과는 다음과 같습니다.

0 // classInstance.number
0 // classInstance.structInstance.number
 
0 // classInstanceCopy.number
0 // classInstanceCopy.structInstance.number

참조 값이 복사된 클래스 인스턴스의 구조체 멤버 변수를 변경하면, 기존 클래스 인스턴스의 구조체 멤버 변수도 함께 변경됩니다. 그리고 number 멤버 변수도 함께 변경되었습니다. 이렇게 동작하는 이유는 struct는 값 타입이지만 RefereceType 인스턴스가 힙에 포함되어서 참조 타입인 ReferenceType 인스턴스가 해제될 때까지 메모리에 함께 남아있게 되기 때문입니다.

 

즉, 참조 타입 인스턴스를 다른 변수에 할당할 때는 참조 값만 전달되기 때문에 내부의 값 타입 인스턴스는 새로 복사되지 않고 기존 인스턴스를 따라갑니다! 생각해보면 너무나 당연하죠? 참조 타입 인스턴스를 새로 어딘가에 할당한다고 해서 새로운 인스턴스가 생성되는 건 아니니까요.

 

B. 구조체 안에 클래스 인스턴스가 있는 경우

그럼 반대로 구조체 안에 클래스 인스턴스가 있을 때는 어떨까요? 이번에도 코드로 한 번 알아볼까요?

import Foundation
 
struct ValueType {
    var number = 2
    var classInstance = ReferenceType()
}
 
class ReferenceType {
    var number = 1
}
 
var structInstance = ValueType()
var structInstanceCopy = structInstance
 
structInstanceCopy.number = 0
structInstanceCopy.classInstance.number = 0
 
print(structInstance.number)
print(structInstance.classInstance.number)
print()
print(structInstanceCopy.number)
print(structInstanceCopy.classInstance.number)

이 코드의 결과는 다음과 같습니다.

2 // structInstance.number
0 // structInstance.classInstance.number
 
0 // structInstanceCopy.number
0 // structInstanceCopy.classInstance.number

클래스 내부에 구조체 인스턴스가 있을 때와는 조금 다르죠?

 

먼저, 값 타입 할당이 일어났기 때문에 구조체 인스턴스가 새로운 변수에 복사됩니다. 이때, 내부에 있는 값 타입 멤버 변수는 별개의 변수로 복사되고, 참조 타입 클래스 인스턴스는 참조 값이 복사됩니다. 따라서 두 인스턴스의 멤버 변수인 number는 복사본에만 업데이트되고, 클래스 인스턴스는 참조값이 복사되었기 때문에 양쪽 인스턴스에 모두 업데이트됩니다.


Protocol Oriented Programming과 Object Oriented Programming의 차이점을 설명

OOP는 상속을 기본 전제로 가져가기 때문에 Super Class와 Sub Class가 생성됩니다. 이 때 Sub Class는 Super Class를 그대로 상속받기 때문에 자신에게 필요하지 않은 property나 method를 가질수 있습니다. 하지만 POP는 protocol에 정의된 인터페이스를 직접구현하는 것이 전제입니다. 따라서 필요하지 않은 property나 method를 갖지 않도록 구현할 수 있습니다. 또한 상속에서는 하나의 Sub Class는 하나의 Super Class만을 가질 수 있지만 Protocl은 그렇지 않습니다.


Delegate 패턴을 활용하는 경우를 예를 들어 설명

Protocol

프로토콜이란, 선언된 프로퍼티, 메소드, 기타 요구사항 등을 직접 구현하지 않고 특정 역할을 수행하고자, 조건만 제시한 규약입니다.

Delegate란?

Protocol를 이용하여 권한을 위임하고 일을 처리하는 방식의 디자인 패턴입니다.

다시말해 delegate pattern은 클래스나 구조체의 인스턴스에 특정 행위에 대한 책임을 다른 타입의 인스턴스에게 넘기는 방식입니다.

( 하나의 특정 객체가 모든일을 처리하는 것이 아닌, 처리해야할 일 중 일부 작업을 다른 객체에 넘기는 것을 의미합니다. )

그럼, 어떻게 권한을 위임할까? 아마도 같은 프로토콜을 채택한 클래스에게 권한을 위임해주는 것 같습니다.

protocol ADelegate {
}

class A {
    weak var delegate: ADelegate
}

class B: ADelegate {
    let a = A()
    a.delegate = self
}

Delegate Pattern

  • delegation된 기능을 제공할 수 있도록 delegation된 책임을 캡슐화하는 프로토콜을 정의하는 것으로 구현
  • 클래스나 구조체가 자신의 책임이나 임무를 다른 타입의 인스턴스에게 위임하는 디자인 패턴.
  • 책무를 위임하기 위해 정의한 프로토콜을 준수하는 타입은 자신에게 위임될 일정 책무를 할 수 있다는 것을 보장

Singleton 패턴을 활용하는 경우를 예를 들어 설명

Singleton

singleton pattern

싱글톤 패턴은 특정 용도로 객체를 하나 생성해서 공용으로 사용하고 싶을 때 사용하는 방법입니다.

즉, 인스턴스가 하나만 존재하는 것을 보증하고 싶을 경우 에 사용하게 되는데, 주로 환경설정, 네트워크 객체, 로그인 정보 등을 특정 용도로 생성해둔 객체에 넣어두고 필요할때마다 여러 객체에서 접근 가능하도록 하여 데이터를 사용합니다.

메모리 낭비를 방지 할 수 있고 데이터를 공유 할 수 있다는 대표적인 장점이 있습니다.

예제

class SingleTon {
    private static let sharedInstance = SingleTon()
   
    private init() { }
}

싱글톤 패턴은 특정 용도로 객체를 하나 생성해서 공용으로 사용하고 싶을 때 사용하는 방법입니다.

  • static을 이용해 Instance를 저장할 프로퍼티를 하나 생성
  • Init 함수를 호출해 Instance를 또 생생하는 것을 막기 위해, init() 함수 접근 제어자를 private로 지정
  • 아까 생성해뒀던 static 프로퍼티를 이용하됨
  • 즉, 어느 클래스에서든 생성해준 static 프로퍼티를 접근하면 하나의 instance를 공유하게 됨으로, 메모리 낭비를 방지해줄 수 있다는 장점이 있습니다.
  • Singleton Instance는 전역 Instance로 다른 클래스들과 자원 공유가 쉬움
  • 공통된 객체를 여러개 생성해서 사용해야하는 상황에서 많이 사용

한번 할당된 메모리는 끝날때까지 할당되어 있습니다. ( 프로세스 종료될때까지 남아있음 → 메모리 낭비가 존재 )

언제 최초로 할당? : 레이즈가 접근하는 시점에 초기화가 됩니다.


탈출 클로저에 대하여 설명

정의

함수의 인자로 전달된 클로저가 함수가 반환된 후 실행 되는 클로저

전달인자로 받은 클로저가 함수 종료 후에 호출될 경우 "클로저가 함수를 탈출한다"로 표현합니다.

사용법

클로저를 매개변수로 갖는 함수를 선언할 때 매개변수 이름 콜론 뒤에 @escaping 키워드를 사용하여 명시

언제 탈출할까?

아래와 같은 경우에 @escaping 을 붙여줘야 하고 그렇지 않을 경우에는 compile 에러가 납니다.

  • 전달 받은 클로저가 클로저 함수 외부로 다시 반환되는 경우
  • 외부 글로벌 변수에 저장되는 경우

이는 기존에 우리가 알고 있던 변수의 scope 개념을 무시하는 것으로 함수에서 선언된 로컬 변수가 로컬 변수의 영역을 뛰어넘어 함수 밖에서도 유효하기 때문입니다.

예시

매개변수로 불러온 클로저를 해당 함수에서 사용하는 것은 전혀 문제가 되지 않습니다.
하지만 이 클로저를 저장하고 다른 곳에서 호출하려고 한다면 컴파일 에러가 발생합니다.
이를 다른 곳에서 사용이 가능하도록 만들기 위해서 @escaping 키워드를 사용해 탈출 클로저로 만듭니다.
즉, 해당 클로저를 어떤 구문 밖으로 탈출시켜서 사용한다는 의미입니다.

아래와 같은 경우 completion을 함수 구문 밖인 completionHandlers 배열에 넣는 것을 볼 수 있습니다.
이를 통해 함수 구문 밖으로 탈출시켜 나중에 언제든 배열에서 꺼내서 사용하겠다를 의미합니다.

// @escaping 선언이 없다면 구문 밖에서 사용이 불가능하기 때문에, 배열에 할당이 불가능합니다.
var completionHandlers: [() -> Void] = []

func withEscaping(completion: @escaping () -> Void) {
    completionHandlers.append(completion)
}

왜 탈출 시킬까?

보통 비동기 작업 을 하기 위해서 클로저를 탈출 시킵니다.

보통 클로저를 가장 자주 사용하는 경우가 Completion Handler 입니다.
예를 들어, 네트워크 요청 작업이 있고 비동기적으로 이를 처리하고 이 처리가 끝난 후 동작하는 것을 Completion Handler에 명령하는 것입니다.

예로 로그인하는 경우
completion을 언제 호출하는지가 관건인데, statusCode==200이란 조건을 만족했을 경우, completion(.success("object"))로 선언하는 것은 통신이 성공했으니 200에 맞는 데이터를 넣어주고 탈출 클로저를 실행해라는 것입니다. 만약 실패했을 경우엔 실패했다는 것을 알 수 있는 데이터를 넣고 탈출 클로저를 실행시켜줍니다.

private func requestSignup(_ url: String, _ headers: HTTPHeaders?, _ parameters: Parameters?, _ completion: @escaping (NetworkResult<Codable>) -> Void) {
    guard let url = try? url.asURL() else { return }

    AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers)
        .validate(statusCode: 200...500)
        .responseDecodable(of: APIResponseData<APICantSortableDataResult<SignupResponse>, APIError>.self) { response in
            switch response.result {
                case .success(let signupResponse):
                    guard let statusCode = response.response?.statusCode else { return }
                    if statusCode == 200 {
                        completion(.success(signupResponse.data?.result)) }
                    else { completion(.requestErr(signupResponse.error?.errorMessage)) }
                case .failure:
                    completion(.networkFail)
            }
    } 
}

이렇듯 탈출 클로저를 사용해 결과를 받아오고 결과에 맞는 분기처리로 해당 함수 구문 밖에서도 적절한 동작을 할 수 있도록 설계할 수 있습니다.

주의

만약, 탈출 클로저임을 명시했을 경우 클로저 내부에서 프로퍼티, 메서드, 서브스크립트 등에 접근하기 위해서는 반드시 self 키워드를 넣어주어야 합니다.


앱의 콘텐츠나 데이터 자체를 저장/보관하는 특별한 객체

UserDefaults

키-값 형태로 데이터를 저장하고, 사용할 수 있는 데이터 저장소

사용자 기본 설정과 같은 단일 데이터 값에 적합함

UserDefaults에서 Get해오는 객체(Object)는 Codable을 채택하고 있음

TODO: CoreData, Realm 공부하기


NSOperationQueue 와 GCD Queue 의 차이점을 설명

쉽고 편한 멀티 스레딩 처리를 위해 애플은 두가지의 API를 제공하고 있다.

GCD(Grand Central Dispatch)라는 C기반의 저수준 API와 NSOperation이라는 Obj-C 기반으로 만들어진 고수준 API가 있다. NSOperation은 GCD보다 약간의 오버헤드가 더 발생되고 느리지만 GCD에서는 직접 처리해야 하는 작업들을 지원 하고 있기 때문에 (KVO관찰, 작업취소 등등) 그정도는 감수하고 사용할만하다.

GCD(Grand Central Dispatch)

GCD는 백그라운드에서 스레드를 관리하면서 동시적으로 작업을 실행시키는 저수준 API를 제공하는 라이브러리이다.

DispatchQueue

GCD를 사용하기전에 먼저 알아야 할 클래스가 있다. 바로 DispatchQueue라는 클래스이다.

실제로 해야할 Task를 담아두면 선택된 스레드에서 실행을 해주는 역할을 한다.

DispatchQueue는 2가지로 종류로 나눌수 있다.

  1. Serial Dispatch Queue: 등록된 작업을 한번에 하나씩 차례대로 처리 한다. 처리중인 작업이 완료되면 다음 작업을 처리!
  2. Concurrent Dispatch Queue: Concurrent Queue는 등록된 작업을 한번에 하나씩 처리 하지 않고 여러 작업들을 동시에 처리
let serialQueue = DispatchQueue(label: "zehye.serial")
print(serialQueue)	// Serial Dispatch Queue

let concurrentQueue = DispatchQueue(label: "zehye.concurrent",
                                    attributes: .concurrent)
print(concurrentQueue)	// Concurrent Dispatch Queue

앱 실행시 시스템에서 기본적으로 2개의 Queue를 만들어 준다.

  1. Main Queue: 메인 스레드(UI 스레드)에서 사용 되는 Serial Queue로 모든 UI 처리는 메인 스레드에서 처리를 해야한다.
  2. Global Queue: 편의상 사용할수 있게 만들어 놓은 Concurrent Queue로 Global Queue는 처리 우선 순위를 위한 qos(Quality of service) 파라메터를 제공하여 병렬적으로 동시에 처리를 하기때문에 작업 완료의 순서는 정할수 없지만 우선적으로 일을 처리하게 할수 있다.
let mainQueue = DispatchQueue.main
print(mainQueue)	// Main Queue
let globalQueue = DispatchQueue.global(qos: .background)
print(globalQueue)	// Global Queue

QOS 우선순위는 아래와 같다.

userInteractive → userInitiated → default → utility → background → unspecified

sync / async

Dispatch Queue는 sync와 async라는 메소드를 가지고 있는데, sync는 동기 처리 메소드로 해당 작업을 처리하는 동안 다음으로 진행되지 않고 계속 머물러 있다.

DispatchQueue.main.sync {
  print("value: 1")
}
print("value: 2")

// 결과
/*
  value: 1
  value: 2
*/

 

반면 async는 비동기 처리 메소드로 sync와는 다르게 처리를 하라고 지시후 다음으로 넘어가 버린다.

let globalQueue = DispatchQueue.global(qos: .background)
globalQueue.async {
  print("value: 1")
}
print("value: 2")

// 결과
/*
  value: 2
  value: 1
*/

보통 스레드 처리를 하는 작업들은 시간이 꽤나 걸리는 큰 작업이거나 언제 끝날지 알수 없는 작업에 사용 되는데 (ex: 네트워크, 파일로딩) 작업이 처리 되는동안 아무것도 하지 못하고 멈춰 있으면앱이 렉이 걸리거나 아무 반응이 없는거처럼 보인다. 그래서 보통 동기 처리 메소드인 sync는 잘 사용하지 않는다.

NSOperation

  • NSOperation: GCD와 비교했을땐 추가적인 오버해드가 있으나, 다양한 작업들 가운데 의존성을 추가할 수 있고, 재사용, 취소, 중지시킬 수 있다.
  • NSOperationQueue: NSOperation들을 만들어서 병렬로 실행시키는 스레드 풀을 제공한다. Operation queue가 GCD의 일부는 아니다.
  • NSBlockOperation: 하나 혹은 그 이상의 클로저에서 NSOperation을 생성할 수 있게 해준다. NSBlockOperation들은 동시적으로 실행하는 다중 블락을 가질 수 있다.

GCD API 동작 방식과 필요성에 대해 설명

동작 방식

해야 할 일(코드)을 Operation으로 Wrapping한 다음에, Queue에 넣는다. Queue에서 남는 스레드에 작업을 배분한다.

필요성

웹에서 이미지를 다운 받아서 사용자에게 보여준다고 했을 때, 비동기로 처리하지 않는다면 이미지를 다운받는 동안 다른 작업을 할 수 없기 때문에 앱이 멈춘다. 이렇게 비용이 많이 들어가는? 작업을 메인 스레드에서 진행하면 사용자가 다른 작업을 할 수 없기 때문에 필요하다고 생각한다.

DispatchQueue

class DispatchQueue: DispatchObject

DispatchQueue는 애플리케이션이 블록 객체 형태로 작업을 제출할 수 있는 FIFO Queue

  • DispatchQueue는 작업을 직렬 또는 동시에 실행함
  • DispatchQueue에 제출된 작업은 시스템에서 관리하는 스레드 풀에서 실행됨
  • 앱의 기본 스레드를 나타내는 DispatchQueue를 제외하고 시스템은 작업을 실행하는 데 사용하는 스레드에 대해 보장하지 않음

main queue에서 작업 항목을 동기적으로 실행하려고 하면 교착 상태(deadlock)가 발생

동작 방식

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

DispatchQueue.main

  • main thread queue
  • 기본적으로 Serial Queue(직렬 큐)로 동작

DisptchQueue.global

  • background thread queue
  • 기본적으로 Concurrent Queue(병렬 큐)로 동작
  • QoS를 이용해 우선 순위를 정해줄 수 있음

작업을 main 스레드 혹은 global 스레드에서 할 지와 동기, 비동기를 지정하여 DispatchQueue에 보낸다. 이렇게 보내진 작업은 시스템에 의해 각 스레드로 보내져서 작업이 이루어진다.

필요성

기존에 스레드를 사용하려면 개발자가 직접 스레드를 생성하고 관리해야 했다. GCD를 사용하면 스레드 생성, 유지, 삭제 등을 개발자가 신경쓸 필요 없이 해야할 작업(코드)를 큐에 예약하기만 하면 된다.


Global DispatchQueue 의 Qos 에는 어떤 종류가 있는지, 각각 어떤 의미인지 설명

  • User Interactive
    • 즉각 반응해야 하는 작업으로, 반응성과 성능에 중점을 둡니다.
    • Main Thread 에서 동작하는 인터페이스 새로고침, 애니메이션 작업 등 즉각 수행되는 user와의 상호작용 작업에 할당합니다.
  • User Initiated
    • 몇 초 이내의 짧은 시간 내 수행해야 하는 작업으로, 반응성 및 성능에 중점을 둡니다.
    • 문서를 열거나, 버튼을 클릭하여 액션을 수행하는 것처럼 빠른 결과를 요구하는 user와의 상호작용 작업에 할당합니다.
  • Default
    • Qos를 별도로 지정하지 않으면 기본값으로 사용되는 형태이며, GCD global queue의 기본 동작 형태입니다.
  • Utility
    • 수초에서 수분에 걸쳐 오랜시간 수행되는 작업으로 반응성, 성능, 그리고 에너지 효율성 간에 균형을 유지하는 데에 중점을 둡니다.
    • background에서 동작하며 색인 생성, 동기화, 백업 등과 같이 user가 볼 수 없는 작업에 할당합니다.
    • 주의 : 저전력 모드에서는 네트워킹을 포함하여 background 작업은 일시 중지합니다.
  • Background
    • 사용자가 직접 알지 못하는 작업을 나타냄. 유저 관리 및 사용자 상호 작용이 필요하지 않고 시간에 민감하지 않은 기타 작업에 사용.
  • Unspecified
    • Qos 정보가 없으므로 시스템이 Qos를 추론해야 한다는 것을 의미합니다.

ARC란 무엇인지 설명

ARC(Automatic Reference Counting)는 참조(레퍼런스)메모리 관리를 자동으로 해주는 기능이다. 인스턴스가 참조되거나 참조 해제될 때 레퍼런스를 카운팅하고, 레퍼런스 카운트가 0이 되면 인스턴스를 메모리에서 해제하는 방식으로 메모리를 관리한다.


Retain Count 방식에 대해 설명

📌 retain, release

각 단어의 의미를 살펴보자.

 

👉  retain

객체의 reference count(retain count)를 증가시킨다.

객체가 메모리에서 해제되지 않도록 이 함수를 호출하여 카운트를 증가시키는것.!

 

👉  release

객체의 reference count(retain count)를 감소시킨다.

객체를 더이상 필요로 하지 않을 때 이 함수를 호출하여 카운트를 감소시키는 것.!

 

이렇게 ARC는 retain, release의 호출을 통해 메모리 관리를 수행하는 것이고,

이러한 메모리 관리 방법을 Reference Counting(Retain Counting)이라고 한다.💥

📎 레퍼런스 카운팅 (Reference Counting)

위에서 말했듯 단순히 생각하면 레퍼런스 카운트를 증감시키는 일이지만

조금 더 자세하게..! 보자면 다음과 같이 정의할 수 있다.

'compile time에 자동으로 retain, release를 적절한 위치에 삽입하는 방식'

 

위의 정의를 뒷받침하기 위해 간단히 그림을 그렸다.

compile time과 run time으로 나눠서 표현했는데

1) compile time에는 코드를 분석하고 예측하여 적절한 위치에 retain, release를 삽입해준다.

2) run time에 삽입된 코드가 실행되면서 retain, release에 의해 reference count를 증감하고

count가 0이 되었을 때 메모리에서 제거하는 것!


prepareForReuse에 대해서 설명

재사용 되는 cell을 dequeue 할 때 이전에 사용 되었던 흔적이 남을 때 prepareForReuse를 통해 해결을 하곤 했는데요,

정확히 어떠한 이유로, 어떤 목적을 위해 만들어진 메소드이지 알아보도록 하겠습니다.

prepareForReuse에 대한 문서는 2개가 존재해요.

UITableViewCell 하위에도 하나 있고, UICollectionReusableView 밑에도 하나가 있죠.

(UICollectionViewCell은 UICollectionReusableView를 상속하는데, 이 외에도 UICollectionViewListCell

 

UITableViewCell > prepareForReuse

UITableView의 delegate가 reusable cell이 재사용 될 수 있도록 준비할 수 있는 메소드에요.

tableView(_:cellForRowAt:)에서 원래 해당 cell의 content를 reset을 하기 때문에,

성능을 위해서 prepareForReuse 메소드 안에서는 content와 무관한 property들만 reset 하는게 좋습니다.

content와 무관한 property를 초기화하는 예시는 아래와 같아요.

override func prepareForReuse() {
   super.prepareForReuse()
   
   isHidden = true
   subview1.removeFromSuperview()
}

성능 이슈로 하지 말라고 하는 예시는 아래와 같아요.

override func prepareForReuse() {
   super.prepareForReuse()
   
   imageView?.image = nil
}

 

그리고 보시다시피 override 해서 사용할 경우 부모의 행동을 따라줘야 하니 잊지 않고 불러줘야 하고,

위에서 언급했듯이 content와 관련된 reset은 cellForRowAt에서 가능하기 때문에, 여기서 해주는게 좋답니다!

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   if let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) {
      cell.imageView?.image = image ?? defaultImage
      return cell
   }
   
   return UITableViewCell()
}

 

UICollectionReusableView > prepareForReuse

UICollectionReusableView도 별반 다르지 않습니다.

view를 재사용할 때 필요한 정리 작업을 하고 초기값들을 정해줄 수 있고, 새로운 data(위에서 content)를 배정해줄 떄 쓰면 안됩니다.

만약 UICollectionReusableView를 상속하는 클래스를 만들었다면 역시 super를 통해 부모 메소드 호출해야합니다.

위에서 설명이 안되었던 것 중 하나가 불리는 시점이에요.

위의 코드를 예로 그대로 써보면, deque를 진행하고 return cell을 하기 전에 먼저 불립니다.


TableView와 CollectionView의 차이점을 설명

🧐 Table View, Collection View 란?

  • UIScrollView의 서브클래스로 무한한 양의(unbounded) 정보에 무한한 접근을 제공하기 위해 사용됩니다. 하지만 자료를 보여주는 방식에서 차이가 발생합니다.
    • Table View: 정보를 하나의 긴 리스트로 보여줍니다(섹션 분리는 가능합니다).
    • Collection View: 다양한 방식이 가능하지만 기본적으로 정보를 2D 포맷으로 보여줍니다(주로 text flow 같은 flowing으로). 여기서 text flow란 글을 읽듯이 왼쪽에서 오른쪽으로, 그리고 다음 줄로 이어서 계속 작성하는 방식을 말합니다. 컬렉션 뷰는 커스터마이징이 가능합니다.
  • 결국 두 View의 차이는 하나는 행으로 나열하여 자료를 보여주는 것이고, 다른 하나는 2차원으로 놓여진 무작위의(arbitrary) 아이템이라는 것입니다.

TableView와 CollectionView에서 Cell의 재사용이 어떻게 이루어 지는지 설명

뷰의 재사용
만약 보여줄 데이터의 양은 많은데 보여주는 뷰의 수가 적은 경우 활용할 수 있습니다
그리고 뷰를 재사용함으로써 메모리를 절약하고 성능을 향상 시킬 수 있습니다

대표적인 예시로 2가지 뷰가 있습니다

- UITableView 의 셀인 UITableViewCell

- UICollectionView의 셀인 UICollectionViewCell

 

재사용의 원리

[부스트코스] iOS 프로그래밍, '뷰의 재사용' 이미지 참조

 

  1. 테이블뷰 및 컬렉션뷰에서 셀을 표시하기 위해 데이터 소스에 뷰(셀) 인스턴스를 요청합니다.
  2. 데이터 소스는 요청마다 새로운 셀을 만드는 대신 재사용 큐 (Reuse Queue)에 재사용을 위해 대기하고있는 셀이 있는지 확인 후 있으면 그 셀에 새로운 데이터를 설정하고, 없으면 새로운 셀을 생성합니다.
  3. 테이블뷰 및 컬렉션뷰는 데이터 소스가 셀을 반환하면 화면에 표시합니다.
  4. 사용자가 스크롤을 하게 되면 일부 셀들이 화면 밖으로 사라지면서 다시 재사용 큐에 들어갑니다.
  5. 위의 1번부터 4번까지의 과정이 계속 반복됩니다.

애플 공식 문서 상에서도 아래 셀 재사용 함수를 찾아보면 내용을 확인할 수 있습니다!!

dequeueReusableCell(withIdentifier:)

 

TableView의 DataSource는 tableView(:cellForRowAt:) 함수를 사용하여 Cell을 TableView의 Row(행)에 넣어줄 때, UITableViewCell 오브젝트를 재사용해야합니다.

 

  • TableView는 DataSource가 재사용을 하도록 표시해둔 UITableViewCell 오브젝트를 Queue 또는 List에 지속적으로 저장해둡니다. TableView의 새로운 Cell을 생성하기 위해서 DataSource 오브젝트로 부터 tableView(:cellForRowAt:) 함수가 실행됩니다.
  • 이 함수는 만약 재사용 큐에 셀이 존재한다면 현재 재사용 큐에 있는 셀을 dequeue(꺼낸다) 해주고, 재사용 큐에 셀이 없으면 새로운 셀을 생성합니다. ( 이 때 생성된 셀은 이전에 클래스나 nib 파일로부터 등록된 UITableViewCell을 활용합니다. )

위에서 설명드린 UITableViewCell의 재사용과 마찬가지로 UICollectionViewCell 또한 재사용 됩니다. 


Block(Obj-C)과 Closure(Swift)의 차이

Swift의 Closure Objective-C의 블록함수 이름이 없는 함수로 개념이 같습니다.

하지만, 둘은 값을 캡쳐하는 방식에 차이가 있습니다. 😭

 

Closure의 캡쳐 방식

: Closure는 값을 캡쳐할 때, Value/Reference 타입에 관계 없이 모두 Reference Capture 한다.

 

원래는 Reference Type인 경우엔 값을 저장할 때 참조(주소값을 저장)하고,

Int, Double, Struct 같은  Value Type의 값은 값을 저장할 때 Copy되어 저장되는 것이 당연합니다.

 

하지만, 위에서 봤듯이 Swift Closure에서는 이 Value Type 값도 캡쳐할 땐 Reference 캡쳐!! 즉, '참조' 합니다.

 

아래 코드에서 closure안에서 num은 Int형(Value Type)임에도 불구하고 Reference 캡쳐를 합니다.

따라서 closure를 실행하기 전에 num 값을 바꾸면 closure에서 실행하는 num의 값도 바뀝니다!!(참조하므로!!)

var num: Int = 0

print("num check #1 = \(num)")

let closure = {
    print("num check #3 = \(num)")
}

closure()
num = 20
print("num check #2 = \(num)")
closure()

위 코드의 결과값

만약, 클로저를 통해 Reference 캡쳐가 아닌 Copy 캡쳐를 하고싶다면, (값 변경 안되게 하고 싶다!)

아래 코드와 같이 copy할 변수를 미리 list로 선언해주면 값이 변경되도 클로저 내부의 값이 변하지 않고 변경이 안됩니다.

var num: Int = 0
print("num check #1 = \(num)")

let closure = { [num] in
    print("num check #3 = \(num)")
}

closure()
num = 20
print("num check #2 = \(num)")
closure()

결과값


Block의 캡쳐 방식

반면에, Block은 값을 캡쳐할 때

Value Type일 경우 값을 복사하여 Capture하고,

Reference Type일 경우 Reference Captrue를 합니다.

 

Objective-C의 Block 함수는 Value Type일 경우 참조가 아닌 "복사"로 값을 캡쳐합니다.

 

따라서 같은 코드를 Objective-C로 작성하였을 때, 값이 변하지 않는다는 것을 알 수 있습니다!

- (void)doSomething {
    int num = 0;
    NSLog(@"num check #1 = %d", num);
    
    void (^testBlock)(void) = ^{
        NSLog(@"num check #3 = %d", num);
    };
    
    testBlock();
    num = 20;
    NSLog(@"num check #2 = %d", num);
    testBlock();
}

결과 화면

 

Value Type은 값을 복사하여 캡쳐하므로 블록함수 내부에서 값을 변경할 수 없습니다.

하지만 변경하고 싶을 때, Copy 캡쳐가 아닌 Reference 캡쳐를 하고싶다면, (값 변경 되게 하고 싶다!)

아래 코드와 같이 변경하고자 하는 변수 앞에 __block 키워드를 사용하여 변경 가능합니다.

- (void)doSomething {
    __block int num = 0;
    NSLog(@"num check #1 = %d", num);
    
    void (^testBlock)(void) = ^{
        NSLog(@"num check #3 = %d", num);
    };
    
    testBlock();
    num = 20;
    NSLog(@"num check #2 = %d", num);
    testBlock();
}

결과 화면


Closure vs Block 을 표로 요약하면 다음과 같습니다.😊

  Closure (Swift) Block (Objective-C)
Value Type Reference Capture Value Copy Capture
Reference Type Reference Capture Reference Capture
Value Type일 때
Reference Capture -> Value Copy Capture 변경
Closure List   
Value Type일 때
Value Copy Capture -> Reference Capture 변경
  __block

Type Safety, Type  inference

Swift는 타입 세이프(type-safe) 언어입니다. 타입 세이프 언어는 사용자로 하여금 코드를 작성할 때 사용하는 값의 타입을 명확히 하도록 합니다. 만약 문자열(String) 타입의 값이 필요한 경우 정수(integer) 타입의 값을 사용할 수 없습니다.

var str = "I'm string"
str = 7 // 에러!!

Swift는 타입 세이프 언어이기 때문에 사용자가 작성한 코드를 컴파일할 때 타입 검사(type check)를 진행합니다. 그리고 만약 타입이 불일치하는 곳이 있다면 오류를 표시합니다. 이를 통해 사용자는 개발 과정에서 최대한 빠르게 오류를 발견하고 수정할 수 있습니다.

 

여러 종류의 타입에 해당하는 값을 다룰 때는 타입 검사를 통해 오류를 방지할 수 있습니다. 그러나 그렇다고 해서 모든 상수와 변수를 선언할 때마다 타입을 명확히 표기해야만 하는 것은 아닙니다. 사용자가 어떠한 상수 또는 변수의 타입을 표기하지 않는다면, Swift는 해당 상수 또는 변수에 저장된 값으로부터 적절한 타입을 추론하기 때문입니다. 컴파일러는 사용자가 작성한 코드를 컴파일할 때, 타입 추론(type inference) 기능을 통해 특정한 표현식에 담긴 값을 자세히 살펴봄으로써 해당 값의 타입을 자동으로 추론합니다.

let num1 = 3   // Int형 이구나 추론
let num2 = 3.0 // Double형 이구나 추론

Generic에 대해 설명

 

아래 예제는 Non-Generic한 일반적인 함수다.

func swapInt(_ a:Int, _ b: Int) {
    let tmp = a
    a = b
    b = a
}

위 함수는 정수형 타입의 두 개의 인자를 전달받아 두 값을 서로 변경시킨다.

swapInt 함수는 Int 타입에 한해서만 함수를 실행할 수 있다.

String 타입, Double 타입과 같은 다른 타입은 지원하지 않는다.

그러면 아래와 같이 또 해당 타입에 맞는 함수를 정의해줘야 한다.

func swapString(_ a: String, _ b: String) {
    let tmp = a
    a = b
    b = a
}

func swapDouble(_ a: Double, _ b: Double) {
    let tmp = a
    a = b
    b = a
}

위 3개의 함수는 하는 일은 똑같은데 전달받는 인자의 타입만 다르다.

이러한 경우, 제네릭의 유연성을 통해 단 하나의 함수로 모든 타입을 대체할 수 있다.

func swapValue<T>(_ a: T, _ b: T) {
    let tmp = a
    a = b
    b = a
}

제네릭을 이용한 함수 정의시에는 함수의 이름 뒤에 <T> 타입을 덧붙힌다.

이후 인자 값은 T 를 갖도록 한다.

이를 통해 실제로는 해당 함수가 호출될 떄 마다 전달된 인자에 따라 Type이 결정된다.

위에서 쓰인 T는 타입 인자(Type Parameters) 라고 부른다.


setNeedsLayout & layoutIfNeeded

setNeedsLayout과 layoutIfNeeded를 비교하기 위해서는 먼저 main run loop라는 개념부터 알고있어야 합니다.

Main Run Loop

어플리케이션이 실행되면 iOS의 UIApplication이 매인 스레드에서 main run loop를 실행시킵니다. main run loop는 돌아가면서 터치 이벤트, 위치의 변화, 디바이스의 회전 등의 각종 이벤트들을 처리하게 됩니다. 이러한 처리 과정은 각 이벤트들에 알맞는 핸들러를 찾아 그들에게 처리 권한을 위임하며 진행됩니다.

 

이렇게 발생한 이벤트들을 모두 처리하고 권한이 다시 main run loop로 돌아오게 되고 이 시점을 update cycle이라고 합니다.

Update Cycle

main run loop에서 이벤트가 처리되는 과정에서 버튼을 누르면 크기나 위치가 이동하는 애니메이션과 같이 layout이나 position 값을 바꾸는 핸들러가 실행될 때도 있습니다. 이러한 변화는 즉각적으로 반영되는 것이 아닙니다.

 

시스템은 이러한 layout이나 position이 변화되는 View를 체크합니다. 그리고 모든 핸들러가 종료되고 main run loop로 권한이 다시 돌아오는 시점인 update cycle에서 이런 View들의 값을 바꿔주어 position이나 layout의 변화를 적용시킵니다.

 

즉 postion이나 layout 값을 변경하는 코드와 실제로 변경된 값이 반영되는 시점에는 시간차가 존재한다는 뜻입니다.

이러한 시간차가 존재한다는 것을 알고있어야 setNeedsLayout과 layoutIfNeeded의 차이를 알 수 있습니다.

 

이렇게 시간차가 존재하지만 이 시간차는 사용자가 체감할 수 없을 정도로 짧기때문에 사용자는 그러한 시간차를 느끼지 못합니다. 하지만 개발하는 사람은 이러한 시간차를 인지하고 있어야 정확히 원하는 핸들러를 구현할 수 있습니다.

UIView methods

setNeedsLayout과 layoutIfNeeded를 비롯해서 UIView에는 여러 내장 메소드가 존재합니다. 이 두개의 메소드를 들어가기 전에 중요한 메소드 몇 개를 알아보도록 하겠습니다.

 

- layoutSubViews()

 

View의 값을 호출한 즉시 변경시켜주는 메소드입니다. 호출되면 해당 View의 모든 Subview들의 layoutSubViews() 또한 연달아 호출합니다. 그렇기 때문에 비용이 많이 드는 메소드이고 그렇기 때문에 직접 호출하는 것은 지양됩니다. 이는 시스템에 의해서 View의 값이 재계산되어야 하는 적절한 시점(update cycle)에 자동으로 호출됩니다.

 

그렇기 때문에 layoutSubViews를 유도할 수 있는 여러 방법이 존재합니다. 이는 일종의 update cycle에서 layoutSubViews의 호출을 예약하는 행위라고 할 수 있습니다.

 

UIViewController내의 View가 재계산되어 다시 그려지는 행위가 발생하면, 즉 layoutSubViews가 호출되고 View의 값이 갱신되고나면 뒤이어 UIViewController의 메소드인 viewDidLayoutSubviews가 호출됩니다. 그렇기 때문에 갱신된 View 값에 의존하는 행위들은 viewDidLayoutSubviews에 명시를 해주어야 합니다.

예를들어 Layer값은 자동으로 변경되지 않기 때문에 속한 View의 frame이 변경되면 viewDidLayoutSubviews안에 Layer의 frame을 변경하는 코드를 작성해주어야 합니다.

위에서 언급한 것처럼 layoutSubviews를 update cycle에서 호출되게끔 자동으로 예약을 해주는 상황들이 몇 가지 존재합니다. 즉 다음 상황에서는 시스템이 자동으로 size와 position이 변경되어야 하는 View라고 체크를 하고 update cycle에서는 layoutSubviews가 호출되어 체크된 View의 layer와 position에 변경된 값을 반영합니다.

  • View의 크기를 조절할 때
  • Subview를 추가할 때
  • 사용자가 UIScrollView를 스크롤할 때
  • 디바이스를 회전시켰을 때 (Portrait, Landscape)
  • View의 Auto Layout contraint 값을 변경시켰을 때

위에 나열된 시점에는 자동으로 update cycle에서 layoutSubviews를 호출하는 행위를 예약하는 것입니다. 하지만 이렇게 자동으로 예약하는 행위 이외에도 수동으로 예약할 수 있는 메소드도 존재합니다.

 

- setNeedsLayout()

 

layoutSubviews를 예약하는 행위 중 가장 비용이 적게 드는 방법이 setNeedsLayout을 호출하는 것입니다. 이 메소드를 호출한 View는 재계산되어야 하는 View라고 수동으로 체크가 되며 update cycle에서 layoutSubviews가 호출되게 됩니다.

 

이 메소드는 비동기적으로 작동하기 때문에 호출되고 바로 반환됩니다. 그리고 View의 보여지는 모습은 update cycle에 들어갔을 때 바뀌게 됩니다.

 

- layoutIfNeeded()

 

이 메소드는 setNeedsLayout과 같이 수동으로 layoutSubviews를 예약하는 행위이지만 해당 예약을 바로 실행시키는 동기적으로 작동하는 메소드입니다. update cycle이 올 때까지 기다려 layoutSubviews를 호출시키는 것이 아니라 그 즉시 layoutSubviews를 발동시키는 메소드입니다.

 

만일 main run loop에서 하나의 View가 setNeedsLayout을 호출하고 그 다음 layoutIfNeeded를 호출한다면 layoutIfNeeded는 그 즉시 View의 값이 재계산되고 화면에 반영하기 때문에 setNeedsLayout이 예약한 layoutSubviews 메소드는 update cycle에서 반영해야할 변경된 값이 존재하지 않기 때문에 호출되지 않습니다.

 

이러한 동작 원리로 layoutIfNeeded는 그 즉시 값이 변경되어야 하는 애니매이션에서 많이 사용됩니다. 만일 setNeedsLayout을 사용한다면 애니매이션 블록에서 그 즉시 View의 값이 변경되는 것이 아니라 추후 update cycle에서 값이 반영되므로 값의 변경은 이루어지지만 애니매이션 효과는 볼 수 없는 것입니다.

setNeedsLayout과 layoutIfNeeded의 차이점은 동기적으로 동작하느냐 비동기적으로 동작하느냐의 차이입니다.

'iOS_기타' 카테고리의 다른 글

iOS 면접준비 #3  (0) 2022.02.16
iOS 준비#2 (~22.01)  (0) 2022.02.03
iOS 준비#1 (~22.01)  (0) 2022.02.01
DI(Dependency Injection)와 CocoaPods Swinject  (0) 2022.01.14