IOS면접 질문정리
작성일
Bounds 와 Frame 의 차이점을 설명하시오.
frame
- 상위뷰의 좌표 시스템에서 뷰의 위치와 사이즈를 나타낸다.
bounds
- 자신의 좌표 시스템에서 뷰의 위치와 크기를 나타낸다.
언제 사용?
-
frame은 UIView의 위치나 크기를 설정할 때 사용한다. 스토리보드에서 우측에 X좌표와 Y좌표가 frame의 좌표이다.
-
bounds는 View의 크기를 알고 싶거나 View내부에 그림을 그릴 때 사용한다.
실제 디바이스가 없을 경우 개발 환경에서 할 수 있는 것과 없는 것을 설명하시오.
하드웨어
- 가속도 센서, 가압계 센서, 주변광 센서, GPS 센서 기능 불가
- 마우스로 터치를 하기에 두 손가락으로 하는 줌인, 줌아웃을 할 수 없음
- 카메라 불가
- 마이크 불가
- 전화기능 불가
API
- Apple의 푸시 알림 받기, 보내기 불가
- 사진, 연락처, 캘린더에 엑세스하기 위한 개인 정보 보호 알림 지원안함
- Handoff 기능 불가
- MessageUI 기능 불가
그 외
- 맥의 성능이 아이폰보다 뛰어나 CPU나 메모리에 얼마나 부담되는지 알 수 없음
- 네트워크 속도 체크 불가
- 페이스 아이디는 직접 얼굴 인식이 안되지만 인식됨, 안됨 처리는 가능
앱의 콘텐츠나 데이터 자체를 저장/보관하는 특별한 객체를 무엇이라고 하는가?
UserDefaults
- 키-값 쌍으로 저장하는 인터페이스이다. 런타임시 개체를 이용하여 기본 데이터베이스에서 사용하는 기본값을 읽어오기 때문에 값이 필요할 때마다 데이터베이스를 열 필요가 없다.
- 대용량의 데이터보다 자동로그인 여부, 아이디, 환경설정에서의 설정 데이터 값과 같은 단일 데이터 등을 보관한다.
CoreData
- 객체 그래프를 관리하기 위한 Framework이다.
- SQLite와 같이 테이블을 이용하지 않고 객체를 생성하여 데이터를 운영하기에 더 많은 저장공간과 메모리를 필요로 한다. 그렇지만 더욱 빠르게 데이터를 가져온다.
- Data Model을 생성한 후 Entity를 생성한다.
SQLite
- Swift에는 특별한 설치없이 바로 사용할 수 있다.
- C언어로 작성되어 있기에 매우 가벼운 것이 특징이며, 전체 데이터베이스를 디스크 파일 1개에 저장하고, 설정 자체가 매우 간편하기에 관리하기가 수월하다.
- SQLite는 iOS, Android, Linux, Window 등과 같이 다양한 운영체제에서 사용된다.
- 수많은 프로세스와 스레드의 접근으로부터 안전하다.
Realm
- SQLite와 같이 오픈소스이며, 모바일에 최적화된 라이브러리이다. SQLite, Core Data보다 속도가 빠르고 성능면에서 더 우수하다.
- 많은 작업들을 처리하기 위해 코드가 많이 필요하지 않으며, 메인 스레드에서 데이터의 읽기, 쓰기 작업을 모두 할 수 있어 편리하다.
- 대용량의 데이터에 대해 무료로 사용할 수 있으며, 용량이 적고 큼에 상관없이 속도와 성능이 유지된다.
앱 화면의 콘텐츠를 표시하는 로직과 관리를 담당하는 객체를 무엇이라고 하는가?
UIViewController
- UIKit 앱의 뷰 계층 구조를 관리하는 객체이다뷰의 사용자 상호 작용에 응답한다전체 인터페이스의 레이아웃을 관리한다앱에서 다른 ViewController를 포함한 다른객체들과 조정을 한다데이터가 변경되면 뷰의 콘텐츠를 업데이트할 수 있다
App thinning에 대해서 설명하시오.
- 디바이스에 애플리케이션이 설치될때, 앱스토어와 운영체제가 디바이스에 맞게 설치되도록 설치 최적화 기술을 의미한다. 최소한의 디스크 사용과 빠른 다운로드가 가능하다
- 슬라이싱
- 앱이 지원하는 여러 디바이스에 각각의 조각 애플리케이션 번들 생성, 해당 디바이스에 적합한 조각을 전달에 설치한다.
- 개발자가 앱스토어 커넥트에 앱을 업로드 하면, 앱스토어에서 다양한 버전의 조각들을 생성하고 사용자가 가장 알맞는 조각을 다운로드 하게 해준다
- 비트코드
- 기계 언어로 번역되기 전단계이다
- 비트코드를 사용해서 업로드 하면 애플에서 애플리케이션을 다시 컴파일해서 앱 바이너리 생성한다
- 비트코드를 사용하지 않아도, fat binary가 업로드 되기는 하지만 비트코드를 사용하면 다시 컴파일 할 때 최적화 할 수 있다
- 주문형 리소스
- 필요할 때만 다운로드 받을 수 있다
- 예를 들어 체험판 -> 본판 or 게임에서 저레벨에서 고레벨로 갈 때
앱이 시작할 때 main.c 에 있는 UIApplicationMain 함수에 의해서 생성되는 객체는 무엇인가?
- UIApplication 싱글턴 객체가 생성됨
@Main에 대해서 설명하시오.
- 프로그램 실행을 시작하기 위한 진입점
앱이 foreground에 있을 때와 background에 있을 때 어떤 제약사항이 있나요?
- Foreground mode는 메모리 및 기타 시스템 리소스에 높은 우선순위를 가지며 시스템은 이러한 리소스를 사용할 수 있도록 필요에 따라 background 앱을 종료합니다.
- Background mode는 가능한 적은 메모리공간을 사용해야함(시스템 리소스 해제, 메모리에서 해제 후 데이터를 디스크에 작성) 사용자 이벤트를 받기 어렵고 공유 시스템 리소스를 해제하고 이미지 객체 참조 등 메모리 제한
상태 변화에 따라 다른 동작을 처리하기 위한 앱델리게이트 메서드들을 설명하시오.
applicationWillResignActive
애플리케이션이 InActive 상태로 전환되기 직전에 호출applicationDidEnterBackground
애플리케이션이 백그라운드 상태로 전환된 뒤 호출applicationWillEnterForeground
애플리케이션이 Active상태가 되기 전에 호출applicationDidBecomeActive
애플리케이션이 Active상태로 전환된 후 호출applicationWillTerminate
애플리케이션이 종료되기 직전에 호출
앱이 In-Active 상태가 되는 시나리오를 설명하시오.
- 전화나 메시지 같은 인터럽트가 발생하거나, 미리알림 같은 특정 알림창이 화면을 덮어서 앱이 실질적으로 event를 받지 못하는 상태 In-Active 상태가 된다. 앱을 처음켜거나, foreground에서 background, background에서 foreground 상태가 될 때도 in-Active 상태를 거쳐간다.
scene delegate에 대해 설명하시오.
- 애플리케이션이 화면에 표시되는 방식과 애플리케이션의 생명주기를 담당한다
UIApplication 객체의 컨트롤러 역할은 어디에 구현해야 하는가?
- UIApplicationMain 함수
App의 Not running, Inactive, Active, Background, Suspended에 대해 설명하시오.
1. Not Running
- Not Running은 앱이 아직 실행되지 않았거나, 완전히 종료되어 동작하지 않는 상태.
2. Foreground - Inactive
- Inactive는 app이 실행중이지만 사용자로부터 event를 받을 수 없는 상태. multitasking window로 진입하거나 app 실행중 전화, 알림 등에 의해 app을 사용할 수 없게 되는 경우 이 상태로 진입.
3. Foreground - Active
- Active는 app이 실제 실행중이고 사용자 event를 받아서 상호작용할 수 있는 상태.(바로 Active가 되지 않고 Inactive 상태를 거쳐 Active상태가 된다.)
4. Backgound - Running
- Background는 홈화면으로 나가거나 다른 app으로 전환되어 현재 app이 실질적인 동작을 하지 않는 상태. 예를 들어 Music app을 닫아도 재생한 음악이 계속 실행되는 경우. 사용자가 app을 사용하지 않는 동안 서버와 데이터를 동기화하거나 타이머가 실행되어야 하는 등의 작업을 이 상태에서 할 수 있습니다.
5. Backgound - Suspended
- Suspended는 app을 다시 실행했을 때 최근 작업을 빠르게 로드하기 위해 메모리에 관련 데이터만 저장되어있는 상태. app이 background 상태에 진입했을 때 다른 작업을 하지 않으면 Suspended 상태로 진입하게 됩니다.(보통 2~3초 이내) Suspended 상태의 app들은 iOS의 메모리가 부족해지면 가장 먼저 메모리에서 해제됨. 따라서 app을 종료시킨 적이 없더라도 app을 다시 실행하려고 하면 처음부터 다시 실행됩니다.
NSOperationQueue 와 GCD Queue 의 차이점을 설명하시오.
NSOperationQueue
- Objective-C 기반 고수준 API
- GCD보다 약간의 오버헤드가 더 발생되고 느리다. But KVO 지원 및 작업취소등을 지원
- 다양한 작업들 중에서 의존성을 추가할 수 있다
- 재사용, 취소, 중지 가능하다
- NSOperation을 만들어서 병렬 or 직렬로 스레드 풀을 사용가능하다.
GCD Queue
- C기반 로우레벨의 API
- Global Queue에서 QOS 우선순위를 줄 수 있다.
- Main Queue: 메인 스레드에서 사용될 것 들을 처리, UI코드
GCD API 동작 방식과 필요성에 대해 설명하시오.
동작 방식
- 해야 할 일(코드)을 Operation으로 Wrapping한 다음에, Queue에 넣는다. Queue에서 남는 스레드에 작업을 배분한다.
필요성
- 웹에서 이미지를 다운 받아서 사용자에게 보여준다고 했을 때, 비동기로 처리하지 않는다면 이미지를 다운받는 동안 다른 작업을 할 수 없기 때문에 앱이 멈춘다. 이렇게 비용이 많이 들어가는? 작업을 메인 스레드에서 진행하면 사용자가 다른 작업을 할 수 없기 때문에 필요하다고 생각한다.
iOS 앱을 만들고, User Interface를 구성하는 데 필수적인 프레임워크 이름은 무엇인가?
UIKit
Foundation Kit은 무엇이고 포함되어 있는 클래스들은 어떤 것이 있는지 설명하시오.
- Cocoa Touch framework에 포함되어 있는 프레임워크 중 하나입니다. String, Int 등의 원시 데이터 타입과 컬렉션 타입 및 운영체제 서비스를 사용해 앱의 기본적인 기능을 관리하는 프레임워크입니다.
Delegate란 무엇인지 설명하고, retain 되는지 안되는지 그 이유를 함께 설명하시오.
- delegate란 객체 지향 프로그래밍에서 하나의 객체가 모든 일을 처리하는 것이 아니라 처리해야 할 일 중 일부를 다른 객체에게 넘기는 것을 의미한다.
- retain(유지하다) : 메모리가 해제되지 않아서 낭비되는 현상을 의미 (Memory Leak, 메모리 누수)
- Delegate는 객체 간의 작업이여서 참조 값을 사용하기 때문에 retain 현상이 일어난다.
NotificationCenter 동작 방식과 활용 방안에 대해 설명하시오.
동작 방식
- 특정 객체가 NotificationCenter에 등록된 Event를 발생시키면, NotificationCennter에 등록된 Observer들 중 해당 Event를 담당 중인 Observer가 그 Event에 대한 행동을 취하는 것(#selector)이 NotificationCenter가 동작하는 방식이다
활용 방안
- 동작방식에서 알 수 있듯이 특정 Event의 실행을 감지할 수 있기 때문에, 특정 Event의 실행에 따라 동작해야하는 것 또는 동시적으로 여러 View에서 동작해야하는 것 등을 처리할 때에 활용할 수 있다.
UIKit 클래스들을 다룰 때 꼭 처리해야하는 애플리케이션 쓰레드 이름은 무엇인가?
Main Thread
- Cocoa Touch 앱에서는 UIApplication의 인스턴스가 main thread에 붙기 때문입니다. UIApplication은 앱을 시작할 때 인스턴스화 되는 앱의 첫번째 부분인데 앱의 run loop를 포함하여 main event loop를 설정하고 event 처리를 시작합니다. 앱의 main even loop는 touch, gesture등의 모든 UI Event를 수신합니다.
App Bundle의 구조와 역할에 대해 설명하시오.
- 하나의 앱을 구성하는 여러가지 요소(실행파일, 리소스, 컨피규레이션 파일 등)를 한 곳에 묶어서 관리하는 하나의 디렉토리, 모음
모든 View Controller 객체의 상위 클래스는 무엇이고 그 역할은 무엇인가?
UIViewController
- UIViewController는 뷰의 내용을 업데이트하고, 뷰와 사용자의 상호 작용에 응답하고, 기본 데이터의 변경에 대한 응답으로 뷰의 콘텐츠를 업데이트하고, 뷰 크기 조정 및 전체 인터페이스의 레이아웃을 관리합니다.
자신만의 Custom View를 만들려면 어떻게 해야하는지 설명하시오.
1. xib 이용해서 별도의 Stroyboard 처럼 관리 가능
- Xib을 생성하고 또한 별도의 UIView을 상속받은 Class을 생성한다. 그리고 Xib에서 owner로 해당 클래스를 임명하고 커스텀 클래스 내에서 초기화 시, Xib 파일을 불러와 view로 임명하는 코드 추가를 하고 원하는 작업들을 Storyboard와 동일하게 수행하면 된다.
2. UIView을 상속해서 코드로만 구현
- UIView을 상속받는 클래스를 생성해 정말 코드로만 원하는 작업들을 설정합니다.
View 객체에 대해 설명하시오.
- 화면에 content 표시, 그리기 및 애니메이션, 오토레이아웃, 제스처 인식 등 화면에 관한 것들을 담당하는 객체입니다. view는 사용자 인터페이스의 기본 구성 요소이며 모든 조작은 main thread에서 해야합니다.
UIView 에서 Layer 객체는 무엇이고 어떤 역할을 담당하는지 설명하시오.
- 렌더링에 사용되는 뷰의 CoreAnimation계층 UIView를 지원해주고 실질적으로 뷰위에 컨텐츠와 애니메이션을 그리는 담당
UIWindow 객체의 역할은 무엇인가?
- 사용자 인터페이스에 배경(backdrop)을 제공하고, 중요한 이벤트 처리 행동(behaviors)을 제공하는 객체
UINavigationController 의 역할이 무엇인지 설명하시오.
- UINavigationController는 앱의 화면 이동에 대한 관리와 그에 연관된 처리를 담당해주는 컨트롤러입니다. 이는 네비게이션 바와 툴바를 두 가지를 보여주는 역할을 합니다. 네비게이션 바에서는 뒤로 가기 버튼과 커스터마이징한 버튼을 추가할 수 있으며 옵션으로 툴바 뷰도 제공합니다
TableView를 동작 방식과 화면에 Cell을 출력하기 위해 최소한 구현해야 하는 DataSource 메서드를 설명하시오.
numberOfRowInSection
섹션에 표시할 행의 개수cellForRowAt
특정 위치에 표시할 셀을 요청하는 메서드
하나의 View Controller 코드에서 여러 TableView Controller 역할을 해야 할 경우 어떻게 구분해서 구현해야 하는지 설명하시오.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
에서 파라미터로 받는tableView
를 객체 비교를 통해 구분한다.- 테이블 뷰의
Tag
를 등록, 비교해서 구분한다.
setNeedsLayout와 setNeedsDisplay의 차이에 대해 설명하시오.
setNeedsLayout()
메소드와setNeedsDisplay()
메소드 모두 호출 즉시 실행되지 않고 다음 update cycle에 변경사항이 적용됩니다.setNeedsLayout
은 layoutSubview메소드를setNeedsDisplay
는 draw메소드를 시스템이 호출하게끔 유도한다.setNeedsLayout()
메소드는 모든 핸들러가 종료되고 권한이 main run loop로 돌아오는 시점에 view의 position이나 layout에 관한 변화를 적용시키고setNeedsDisplay()
메소드는 다음 드로잉 사이클이 오면 그 때 쌓여있는 그려야할 컨텐츠들을 동시에 적용시킵니다.
NSCache와 딕셔너리로 캐시를 구성했을때의 차이를 설명하시오.
딕셔너리
는 메모리가 부족하면 값을 삭제하는 코드를 작성해야 하지만NSCache
는 메모리가 자동으로 관리된다.NSCache
는Thread-safe하다. 데이터를 쓸때마다 lock을 해줄 필요가 없다.
URLSession에 대해서 설명하시오.
- 앱과 서버 간 데이터를 주고 받기 위해 사용하는 Apple의 기본 API URLSessionConfiguration 설정하고 URLSession·URLComponents 생성, 이를 통해 URLSessionDataTask 생성 한 뒤, 마지막으로 테스크를 실행하는 과정을 거친다.
prepareForReuse에 대해서 설명하시오.
- tableView의 cell을 재사용하기 위해 셀 속성을 reset하는 함수입니다. dequeueReusableCell 메서드에서 객체가 반환되기 직전에 호출됩니다.
ViewController의 생명주기를 설명하시오.
init
ViewController 객체가 생성됩니다.loadView
View를 메모리에 로드합니다.viewDidLoad
View의 Controller가 메모리에 로드된 뒤 호출됩니다.viewWillAppear
View가 표시되기 직전에 호출됩니다.viewDidAppear
View가 표시된 후 호출됩니다.viewWillDisappear
View가 사라지기 직전 호출됩니다.viewDidDisappear
View가 사라지기 직후 호출됩니다.viewDidUnload
View가 메모리에서 해제된 뒤 호출됩니다.
TableView와 CollectionView의 차이점을 설명하시오.
테이블뷰
에서는 하나의 열에 여러 행의 cell을 표현하는 방식이기때문에, cell의 모양은 항상 row에 맞춰서 디자인 해야한다.컬렉션뷰
는 행과 열을 모두 만들 수 있기 때문에 cell의 모양을 자유롭게 디자인이 가능하다.
오토레이아웃을 코드로 작성하는 방법은 무엇인가? (3가지)
1. NSLayoutConstraint 사용
2. Visual Format Language 사용
3. Anchor 사용
hugging, resistance에 대해서 설명하시오.
hugging
우선순위가 클수록 자신의 크기를 유지하려하고, 우선순위가 작을수록 크기가 늘어나는 속성resistance
우선순위가 클수록 자신의 크기를 유지하려하고, 우선순위가 작을수록 크기가 작아지려는 속성
Intrinsic Size에 대해서 설명하시오.
- Intrinsic Size는 콘텐츠의 본질적인 크기입니다. 모든 view가 Intrinsic Size를 갖는 것은 아니고, 대표적인 예로는 UILabel, UIButton을 들 수 있습니다. 또한, 각각의 view마다 Intrinsic Size가 적용되는 방식이 다릅니다.
스토리보드를 이용했을때의 장단점을 설명하시오.
장점
- UI 구성을 한눈에 확인할 수 있다
- View에 어떤 속성과 값을 설정했는지 확인하기 쉽다
단점
- StoryBoard 구현을 위해 Xcode 메모리가 올라간다. (이 부분은 기능별로 StoryBoard를 분리하면 해결 가능)
- 협업시 StoryBoard 충돌 혹은 이슈가 발생할 수 있다
- 인터페이스빌드와 StoryBoard 간 연결을 추적하기 어려울 수 있다
코드 기반 UI의 장단점
장점
- 스토리보드와 비교했을 때 Xcode가 가벼워진다
- 협업시 충돌 위험도가 낮아진다
- 스토리보드에 비해 View의 재사용성이 좋다
단점
- View가 어떤 모습을 갖고 있을지 한눈에 파악하기 어렵다
- View의 속성 및 기능을 숙지하고 있어야한다 (스토리보드처럼 기능에 대한 표시를 해주지 않음)
- NSLayoutConstraint를 통해 AutoLayout을 설정해줘야 하는데 귀찮고 복잡하다 (SnapKit 라이브러리를 통해 극복 가능)
Safearea에 대해서 설명하시오.
- 핸드폰의 전체적인 UI를 통틀어서 컨텐츠가 제대로 보일수 있는 부분에만 우리 뷰를 놓을 수 있도록 도와주는 기능.
Left Constraint 와 Leading Constraint 의 차이점을 설명하시오.
Left Constraint
는 어떤 객체의 왼쪽을 뜻하고Leading Constraint
는 어떤 객체의 앞쪽 가장자리를 뜻한다.
class와 struct와 enum의 차이를 설명하시오.
class
참조 타입
- 실제 인스턴스(데이터)는 힙에 저장되고, 그 인스턴스를 가리키는 변수에 메모리 주소가 담겨 스택에 저장됨
- 일반적으로 단일 상속이 가능하지만, 프로토콜을 사용하면 다중 상속도 가능
struct
값 타입
- 실제 인스턴스의 데이터 자체가 스택에 저장됨
- 상속이 불가능
enum
값 타입
- 상속 불가
- 유사한 종류의 여러 값을 유의미한 이름으로 한 곳에 모아 정의한 것
- 열거형 자체가 하나의 데이터 타입
class의 성능을 향상 시킬수 있는 방법들을 나열해보시오.
- heap보다는 stack메모리를 할당하려고 노력한다.
- referecn counting을 적게 만든다.
- 클래스를 선언할 때 상속되지 않는 클래스에 final을 붙이면 성능이 향상됩니다.
Copy On Write는 어떤 방식으로 동작하는지 설명하시오.
- Copy On Write는 A라는 변수에 B라는 변수를 할당해주었을 때, 새로 메모리에 할당하는 것이 아니라, B의 메모리를 A가 공유하는 형태로 구성됩니다. 그러다가 A가 값이 수정될 때 새로 메모리에 할당이 되는 식으로 동작합니다.
- Convenience init에 대해 설명하시오.
- 보조 이니셜라이저로, 클래스의 원래 이니셜라이저인 init을 도와주는 역할을 합니다.
- convenience init은 같은 클래스에서 다른 이니셜라이저를 호출해야한다는 규칙이 있습니다.
AnyObject에 대해 설명하시오.
- AnyObject는 모든 클래스 타입의 인스턴스를 나타낼 수 있는 프로토콜입니다.
- AnyObject로 선언 시, “클래스 타입”만 저장할 수 있습니다.
Optional 이란 무엇인지 설명하시오.
- 래핑된 값 또는 값이 없는 nil 상태를 표현하는 형식이며, enum 구조를 뛰고 있다. 값이 있고 래핑된 some case 제네릭 형태 그리고 값이 없는 nil상태 none 케이스
Struct 가 무엇이고 어떻게 사용하는지 설명하시오.
- 인스턴스의 값(프로퍼티)을 저장하거나, 기능(메소드)를 제공하고 이를 캡슐화할 수 있도록 스위프트가 제공하는 타입입니다.
struct [구조체 이름] {
[프로퍼티와 메서드들]
}
Subscripts에 대해 설명하시오.
- Class, Struct, Enum에서 collection, 순열, list, sequence 등 집합의 특정 멤버 요소에 쉽게 접근하기 위한 방법입니다.
- 이것을 이용하면 추가적인 메소드없이 특정 값을 할당(set)하거나 가져(get)올 수 있습니다.
- 인스턴스 이름 뒤에 대괄호로 감싼 값을 써줌으로써 인스턴스 내부의 특정 값에 접근할 수 있습니다.
String은 왜 subscript로 접근이 안되는지 설명하시오.
- Swift에서 Character는 1개 이상의 Unicode Scalar로 이루어져 있다. 즉 크기가 가변적이다.
instance 메서드와 class 메서드의 차이점을 설명하시오.
Instance Method
클래스를 통해 호출할 수 없고, 클래스의 인스턴스를 만들어 실체하여 생성된 인스턴스를 통해서 호출할 수 있는 메소드 입니다.Class Method
인스턴스를 만들어 실체화 하지 않아도 클래스를 통해 직접적으로 호출 할 수 있습니다.
class 메서드와 static 메서드의 차이점을 설명하시오.
- class메서드는 오버라이딩 가능하지만 static메서드는 오버라이딩이 불가능합니다.
Delegate 패턴을 활용하는 경우를 예를 들어 설명하시오.
- 객체지향 프로그래밍에서 하나의 객체가 모든 일을 처리하는 것이 아니라 해야할일 중 일부를 다른 객체에게 넘기는 것(위임하는 것)
- 대표적으로 TableViewDelegate가 있다 뷰컨트롤러에 딜리게이트 함수를 정의하고 테이블뷰의 동작이 일어나면 해당 딜리게이트 함수를 호출하고 뷰컨트롤러가 대신 처리해줌
Singleton 패턴을 활용하는 경우를 예를 들어 설명하시오.
- 인스턴스가 하나만 존재하는 것을 보증하고 싶을 경우에 사용
- 메모리 낭비를 방지할 수 있고 데이터를 공유할 수 있다는 대표적인 장점
URLSession.shared
네트워크 처리를 할 때 URLSession 객체를 이용하는데, 이미 만들어져있는 shared 객체에 접근해 메서드를 수행한다.
KVO 동작 방식에 대해 설명하시오.
- 모델 객체의 어떤 값이 변경되었을 경우 이를 UI에 반영하기 위해서 컨트롤러는 모델 객체에 Observing을 도입하여 델리게이트에 특정 메시지를 보내 처리할 수 있도록 하는 것
- 즉, 변수에 코드를 붙여 변수가 변경될 때마다 코드가 실행되도록 하는 방법을 의미
Delegates와 Notification 방식의 차이점에 대해 설명하시오.
- 가장 큰 차이점은 Notification과 다르게 Delegate의 경우에는 수신자가 발신자의 정보를 알고 있어야한다는 것이다.
- delegate 방식은 주로 이벤트를 1:1로 전달할 때 많이 사용됩니다. 주로 protocol을 정의하고 이를 이벤트를 대신 처리할 객체가 채택하여 사용하게 됩니다. 이에 따라 제 3 객체를 필요로 하지 않으며 확실한 처리가 가능하지만, 많은 줄의 코드를 필요로 하며 많은 객체에게 이벤트를 알리고 싶을 경우 비효율적이라는 단점이 있습니다.
- notification 방식은 이벤트를 1:N으로 전달할 때 용이합니다. NotificationCenter라는 싱글톤객체를 기반으로 이벤트 발생여부를 옵저버를 등록한 객체에게 전달하는 방식으로 구성됩니다. 따라서 다수의 객체에게 손쉽게 이벤트 전달이 가능합니다. 하지만 제 3 객체를 필수적으로 필요로 하며, key값으로 Notification의 이름을 사용하기 때문에 컴파일 중 구독이 제대로 이루어져있는지 확인할 수 없다는 단점이 존재합니다.
멀티 쓰레드로 동작하는 앱을 작성하고 싶을 때 고려할 수 있는 방식들을 설명하시오.
- UI업데이트에 관한 작업들은 메인스레드
- 동기로 할지 비동기로 할지를 명확하게 정의해야함
- 어떤 작업을 글로벌 큐에 넣어야 할지 정확히 알아둬야 함
- 글로버 큐에 작업을 배치할 때, 작업에 따라 QoS를 적절하게 사용해야함
- 상황에 따라 작업간의 인과관계를 설정하거나 특정 시간 이후에 처리하도록 설정해야함
MVC 구조에 대해 블록 그림을 그리고, 각 역할과 흐름을 설명하시오.
MVC 패턴이란?
- 프로그래밍을 할 때 전체적인 구조에 관련된 여러 디자인 패턴 중 하나
- model, view, controller의 약자인 MVC 패턴은 하나의 어플리케이션, 프로젝트를 구성할때 그 구성요소를 세가지의 역할로 구분한 디자인 패턴입니다.
- 이 패턴을 성공적으로 사용시, 사용자 인터페이스로부터 비즈니스 로직을 분리하여 애플리케이션의 시각적 요소나 그 이면에서 실행되는 비즈니스 로직을 서로 영향 없이 독립적으로 유지보수를 할 수 있습니다.
M
- Model
- 프로그램이 작업하는 세계관의 요소들을 개념적으로 정의한 것
- 프로그램이 목표하는 작업을 원활하게 수행하기 위해 필요한 물리적 개체, 규칙, 작업 등의 요소들을 구분하는역할
- 앱의 데이터와 비즈니스 로직 가지고 있습니다.
-
결과적으로 Model을 잘 설계한다라는 것은!
→ 해당 도메인 세계를 얼마만큼 이해하고 있는지와도 밀접한 연관이 있습니다.
→ 물리적인 요소뿐아니라 추상적인 요소 또한 해당 작업을 수행하는데 특정 책임과 역할로서 구분될 수 있다면 최대한 구체적이고 작은 entitiy를 유지하면서 Model 설계하는 것이 중요합니다.
V
- View
- 사용자가 보는 화면에 입출력 과정 및 결과를 보여주기 위한 역할 ( UI 담당 )
- 입출력의 순서나 데이터 양식은 Controller에 종속되어 결정됩니다. 이때 View는 도메인 모델의 상태를 변환하거나, 받아서 렌더링하는 역할을 합니다.
- 객체를 전달받아 상태를 바로 출력하는 역할만을 담당해야 합니다.
- view에서는 도메인 객체의 상태를 따로 저장하고 관리하는 클래스 변수나 인스턴스 변수가 필요 없습니다.
C
- Controller
- model과 view를 연결시켜주는 다리 역할
- 도메인 객체들의 조합을 통해 프로그램의 작동 순서나 방식을 제어합니다.
- view와 model이 각각 어떤 역할과 책임이 있는지 알고 있어야합니다.
- View로부터 사용자의 action을 받아 Model에게 어떤 작업을 해야하는지 알려주거나 Model의 데이터 변화를 View에게 전달하여 View를 어떻게 업데이트할지 알려줍니다.
- Model, View에게 직접 지시 가능
프로토콜이란 무엇인지 설명하시오.
- 프로토콜은 class나 struct의 행동을 정의하는 역할을 합니다. 프로토콜은 행동을 정의하기만 할 뿐 구현하지 않습니다. 어떠한 클래스나 구조체가 해당 프로토콜을 따른다는 것은 프로토콜에 정의된 행동들을 구현해야함을 의미합니다.
POP(프로토콜 중심 프로그래밍)과 OPP(객체 중심 프로그래밍)의 차이점을 설명하시오.
- POP(Protocol Oriented Programming)은 프로토콜 중심 프로그래밍으로, 슈퍼클래스와 서브클래스의 사이가 독립적이고 reference 타입과 value 타입을 모두 지원하여 OOP의 단점들을 모두 보완한다고 말할 수 있습니다. 또한, 합성으로 객체를 묘사하는 수평적인 구조를 가지고 있어 상속과는 다르게 다수의 프로토콜을 가지는 것이 가능합니다.
- OOP(Object Oriented Programming)은 객체중심 프로그래밍으로, 상속을 통해 타입을 확장하는 수직적인 구조를 가지고 있습니다. 그렇기때문에 슈퍼클래스를 그대로 상속받아 필요없는 메소드와 변수를 모두 물려받아야한다는 단점이 있습니다. 또한 상속구조를 사용하기위해서는 value type으로 정의해도 되는 모델들을 reference 타입으로 정의해야하는 불편함이 있습니다.
Hashable이 무엇이고, Equatable을 왜 상속해야 하는지 설명하시오.
- Hashable은 “정수 hash 값을 제공하는 타입”으로 정의된 프로토콜입니다.
- hash란, 해시 함수에 의해 얻어지는 값으로 해시값, 해시코드, 해시 체크섬으로도 불립니다.
- 해시 함수 - 임의의 길이의 데이터를 고정된 길이의 데이터로 매핑하는 함수
- hashValue - 어떠한 데이터를 Hash 함수에 넣게 되면 반환해주는 값
- 해쉬값은 고유값이어야 하므로 고유값인지 식별해 줄 수 있는 “==”함수가 필요합니다. 이 함수는 Equatable 프로토콜 안에 정의되어 있으니 hashable은 equatable프로토콜을 따라야합니다. 기본 데이터 타입도 ==을 통해 비교가 가능하므로 equatable프로토콜을 따릅니다.
- HashTable에서 hash값을 찾기 위해선 key가 필요하고 이 key는 식별할 수 있도록 unique 해야 합니다.
mutating 키워드에 대해 설명하시오.
- 값 타입의 속성은 기본적으로 인스턴스 메서드 내에서 수정할 수 없습니다.
- mutating을 선언한 메서드는 메서드 내에서 프로퍼티를 변경할 수 있고, 메서드가 종료될 때 변경한 모든 내용을 원래 struct에 다시 기록합니다. 또한 메서드는 self property에 새 인스턴스를 할당할 수도 있습니다.
탈출 클로저에 대하여 설명하시오.
- 비동기 작업을 하기 위해서 탈출 시키는 클로저
- 함수의 인자로 전달된 클로저가 함수가 반환된 후 실행 되는 클로저
- 전달인자로 받은 클로저가 함수 종료 후에 호출될 경우 “클로저가 함수를 탈출한다”로 표현합니다.
Extension에 대해 설명하시오.
- 수평적 확장이다.
- extension은 기존 존재하는 클래스,구조체,열거형,프로토콜 타입에 새로운 기능을 추가할 수 있게 해준다.
- 내부 소스를 접근할 수 없는 원본 타입들에 대해 새로운 기능을 부여할 수 있고, 특정 타입의 기능 및 준수하는 프로토콜별 구현부를 분리해서 보다 코드 가독성을 높일 수 있습니다.
Extension 내부에서 함수를 override할 수 있는지 설명하시오.
- 새로운 기능을 정의하는 것은 가능하지만 override할수는 없습니다.
override는 부모클래스의 메서드를 자식클래스가 재정의하여 사용하는 것을 의미하지만, extension은 수평적인 확장이기 때문에 override라는 개념이 어울리지 않습니다.
접근 제어자의 종류엔 어떤게 있는지 설명하시오.
- open - 다른 모듈에서도 접근가능 / 상속 및 재정의도 가능 (제한 낮음)
- public - 다른 모듈에서도 접근가능(상속/재정의불가)
- internal - 같은 모듈 내에서만 접근가능(기본 설정 값)
- fileprivate - 같은 파일 내에서만 접근가능
- private - 같은 scope내에서만 접근가능 (제한 높음)
defer란 무엇인지 설명하시오.
- 현재 코드 블록을 나가기 전에 꼭 실행해야 되는 코드를 작성하여
코드가 블록을 어떻게 빠져 나가든 꼭 마무리해야 되는 작업을 할 수 있게 도와줍니다.
defer가 호출되는 순서는 어떻게 되고, defer가 호출되지 않는 경우를 설명하시오.
- defer가 호출되는 경우
swift
func test() {
defer {
print("3")
}
print("2")
}
print("1")
test()
// 출력결과
1
2
3
1, 2, 3 호출
- defer가 호출되지 않는 경우
swift
var a = "Hello"
func b() -> String {
defer { a.append(" World") }
retrun a
}
a = "Hello"
print(b())
// 출력결과
Hello
출력한 후 a에다가 World를 더해줌 따라서 출력 결과는 Hello
property wrapper에 대해서 설명하시오.
- 연산 프로퍼티(Computed property)의 getter, setter를 기본으로 가진 구조체(Property)를 Property wrapper 라고 합니다.
- 프로퍼티 래퍼를 사용하면 코드의 재사용성을 높여 코드가 짧아지는 효과를 나타낼 수 있습니다.
Generic에 대해 설명하시오.
- 제네릭이란 타입에 의존하지 않는 범용 코드를 작성할 때 사용한다
- 제네릭을 사용하면 중복을 피하고, 코드를 유연하게 작성할 수 있다
some 키워드에 대해 설명하시오.
- some키워드는 return값에 불투명한 타입이 있음을 나타냅니다. 불투명한 타입이란 어떠한 타입을 리턴할 지 모른다는 것입니다. some키워드를 사용하면 함수 내부의 리턴 값에 따라 리턴 타입이 지정됩니다.
Result타입에 대해 설명하시오.
- Result 타입은 Generic Enumeration로 선언되어 있고, 경우에 따른 연관값을 포함하여, 성공과 실패를 나타내는 값입니다.
Codable에 대하여 설명하시오.
- 기존의 Encodable과 Decodable이 합쳐진 프로토콜 입니다.
- Encodable -> data를 Encoder에서 변환해주려는 프로토콜로 바꿔주는 것
- Decodable -> data를 원하는 모델로 Decode 해주는 것
Closure에 대하여 설명하시오.
- 참조타입이다.
- 클로저는 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 중괄호“{}”로 구분된 코드의 블럭이며, 일급 객체의 역할을 할 수 있다.
Closure와 함수와의 관계에 대해 설명하시오.
- 함수는 클로저의 한 형태로, 이름이 있는 클로저이다.
ARC
ARC란 무엇인지 설명하시오.
- 참조(레퍼런스)메모리 관리를 자동으로 해주는 기능이다.
- 인스턴스가 참조되거나 참조 해제될 때 레퍼런스를 카운팅하고, 레퍼런스 카운트가 0이 되면 인스턴스를 메모리에서 해제하는 방식으로 메모리를 관리한다.
Retain Count 방식에 대해 설명하시오.
- 객체가 메모리에서 해제되지 않도록 이 함수를 호출하여 카운트를 증가시키는것
Strong 과 Weak 참조 방식에 대해 설명하시오.
Strong (강한 참조)
- 해당 인스턴스의 소유권을 가진다.
- 자신이 참조하는 인스턴스의 retain count를 증가시킨다.
- 값 지정 시점에 retain이 되고, 참조가 종료되는 시점에 release가 된다.
- 선언할 때 아무것도 적어주지 않으면 default로 strong이 된다.
var test = Test() // retain count 1 증가 test = nil // retain count가 1 감소 되어 0 이 되면서 메모리 해제됨
Weak (약한 참조)
- 해당 인스턴스의 소유권을 가지지 않고, 주소값만을 가지고 있는 포인터 개념이다.
- 자신이 참조하는 인스턴스의 retain count를 증가시키지 않는다. release도 발생하지 않는다.
- 자신이 참조는 하지만 weak 메모리를 해제시킬 수 있는 권한은 다른 클래스에 있따.
- 메모리가 해제될 경우 자동으로 레퍼런스가 nil로 초기화를 해준다.
- weak 속성을 사용하는 객체는 항상 optional타입이어야 한다.(해당객체가 nil일 수 있기때문)
var test = Test() // retain count 1 증가 test = nil // retain count가 1 감소 되어 0 이 되면서 메모리 해제됨
순환 참조에 대하여 설명하시오.
class ClassA {
var objB: ClassB!
deinit { print("A 객체 해제") }
}
class ClassB {
var objA: ClassA!
deinit { print("B 객체 해제") }
}
var a: ClassA! = ClassA() // -> A 객체 R.C = 1
var b: ClassB! = ClassB() // -> B 객체 R.C = 1
여기서 ClassA의 인스턴스인 a가 ClassA 객체를 참조하고 있기 때문에 A객체의 R.C가 1 증가하고ClassB의 인스턴스인 b가 ClassB 객체를 참조하고 있어서 B객체의 R.C가 1 증가합니다.
a = nil // -> A 객체 R.C = 0
b = nil // -> B 객체 R.C = 0
그런데 프로퍼티 a와 b에 각각 nil을 대입 소유권을 해제 즉, 객체를 해제시켜줬기 때문에 ClassA와 ClassB 객체의 R.C가 0이 됩니다. 이를 순환 참조라고 합니다.
강한 순환 참조 (Strong Reference Cycle) 는 어떤 경우에 발생하는지 설명하시오.
class ClassA {
var objB: ClassB!
deinit { print("A 객체 해제") }
}
class ClassB {
var objA: ClassA!
deinit { print("B 객체 해제") }
}
var a: ClassA! = ClassA() // -> A 객체 R.C = 1
var b: ClassB! = ClassB() // -> B 객체 R.C = 1
만약 여기서 끝난다고 해도 강한 순환 참조가 됩니다. 왜냐하면 ClassA와 ClassB 객체가 해제되지 않아 Reference Count가 1로 남아있기 때문입니다.
// 서로 객체 소유
**a.objB = b** // -> B 객체 R.C = 2
**b.objA = a** // -> A 객체 R.C = 2
여기서 ClassA 객체의 인스턴스인 a가 objB라는 프로퍼티를 통해서 ClassB 객체를 참조하고 있어 ClassB 객체의 R.C가 1 증가하고 ClassB 객체의 인스턴스인 b가 objA라는 프로퍼티를 통해서 ClassA 객체를 참조해 ClassA 객체의 R.C가 1씩 증가합니다.
a = nil // -> A 객체 R.C = 1
b = nil // -> B 객체 R.C = 1
nil을 대입해 해제하려고 해도 두 객체 모두 현재 R.C가 1씩 남아있기 때문에 객체가 해제되지 않는데 이 상황을 강한 순한 참조 라고 합니다.
Functional Programming
순수함수란 무엇인지 설명하시오.
- 외부 상태에 의존적이지 않고, 어떠한 사이드이펙트도 발생시키지 않는 함수입니다.
- 언제 얼마나 많이 호출해도 항상 같은
input
에 대해 같은output
을 반환합니다. - 테스트가 용의하고, 재사용성이 올라감
- 예측가능성이 높아짐
함수형 프로그래밍이 무엇인지 설명하시오.
- 값이나 상태의 변화보다는 함수 자체의 응용을 중요하게 여긴다.
- 프로그램이 상태의 변화 없이 데이터 처리를 수학적 함수 계산으로 취급하고자 하는 프로그래밍
- 코드 이해와 실행 결과의 관점에서, 순수하게 함수에 전달된 인자 값만 결과에 영향을 주기 때문에 상태 값을 갖지 않고 순수하게 함수만으로 동작
- 어떤 상황에서 프로그램을 실행하더라도 일정하게 같은 결과를 도출할 수 있다.
- 프로그램 동작 과정에서 상태(값)가 변하지 않으면 함수 호출이 각각 상호 간섭 없이
실행되므로 병렬처리를 할 때 부작용이 거의 없다는 장점이 있다.
- 필요한 만큼 함수를 나누어 처리할 수 있도록 스케일업할 수 있기 때문에 대규모 병렬처리에 큰 강점을 가짐.
고차 함수가 무엇인지 설명하시오.
- 고차 함수는 함수형 프로그래밍에 기반을 두고 있다.
- 함수를 매개변수로 전달받거나 함수를 결과로 반환하는 함수
- 고차 함수는 매개변수로 받은 함수를 필요한 시점에 호출하거나 클로저를 생성하여 반환한다.
- 스위프트에서 유용한 고차함수는 대표적으로 map, filter, reduce가 있다.
Swift Standard Library의 map, filter, reduce, compactMap, flatMap에 대하여 설명하시오.
1) map 함수
- 기존 배열 등의 각 아이템을 새롭게 매핑해서 새로운 배열을 리턴하는 함수
- 각 아이템을 매핑해서, 새로운 배열을 만들때 주로 사용한다.
let numbers = [1, 2, 3, 4, 5]
// 이렇게 클로져로 사용할 수있다(여기서는 numbers의 타입이 Int여서 클로져에서 Int로 나온다)
numbers.map(transform: (Int) throws -> T)
// int값을 받아서 이렇게 String 값으로 바꿀수 있다.
numbers.map { num in
return "숫자: \(num)"
}
// 클로져형태라서 이렇게 줄여서 사용할 수도 있다. 위 코드랑 같은 코드다.
// nubmers.map { "숫자: \($0)" }
//출력
print(numbers)
["숫자: 1", "숫자: 2", "숫자: 3", "숫자: 4", "숫자: 5"]
2) filter 함수
- 기존 배열 등의 각 아이템을 조건을 확인한 후, Bool값으로 판단한후, 새로운 배열을 리턴해주는 함수
let names = ["Apple", "Black", "Circle", "Dream", "Blue"]
// 이렇게 클로져로 사용할 수있다(여기서는 names의 타입이 String여서 클로져에서 String으로 나온다)
names.filter(isIncluded: (String) throws -> Bool)
// names 배열에서 "B"가 포함되어 있으면(true면) return해준다.
var aaa = names.filter { str in
return str.contains("B")
}
// 출력
print(aaa)
["Black", "Blue"]
//여기서 contains는 값이 포함되어 있는지, 이미 String에 구현되어있는 기능이다. 결과값으로 true or false를 return 해준다.
// ex) "Black".contains("B") // true가 return된다.
3) reduce 함수
- 기존 배열 등의 각 아이템을 클로져가 제공하는 방식으로 결합해서 마지막 결과값을 리턴해준다.
- 누적적으로 계속 반복하기 때문에 초기값이 필요하다.
var numbersArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbersArray.reduce(initialResult: Result, nextPartialResult: (Result, Int) throws -> Result)
// 여기서 초기값은 0이다. (0 + 1)더한값이 그다음 초기값으로 사용된다.(1 + 2)...반복한다.
var aaa = numbersArray.reduce(0) { a, b in
return a + b
}
//클로져 형태라서 압축할수 있다.
var aaa = numbersArray.reduce(0) { $0 + $1 } //위에랑 같은 코드이다.
//출력
print(aaa)
55
4) compactMap 함수
- 기존 배열 등의 각 아이템을 새롭게 매핑해서 변형하되, 옵셔널 요소는 제거하고, 새로운 배열을 리턴해준다.
- 옵셔널 바인딩의 기능까지 내장되어 있다.
let stringArray: [String?] = ["A", nil, "B", nil, "C"]
var newStringArray = stringArray.compactMap { $0 }
//출력
print(newStringArray)
["A", "B", "C"] // 여기서 newStringArray의 타입을 보면 [String]타입으로 바뀌어있다.
5) flatMap 함수
- 중첩된 배열의 각 배열을 새롭게 매핑해서 내부 중첩된 배열을 제거하고 리턴해준다.
MVVM에 대해서 설명하시오
MVVM
MVVM은 iOS 에서 가장 많은 인기를 누리고 있다. Model - View - ViewModel 의 줄임표현이다.
ViewModel
은 테스트가 가능하기에 꽤나 쓸만하고, 이 모델 내부엔 UIKit 관련 코드가 없다.
즉, 뷰 모델은 ViewController
로딩과 의존성이 없기에 ViewModel
에서 비즈니스 로직 테스트가 가능하다.
UIViewController
와 UIView
는 함께 View
로 분류된다.
MVVM을 꼭 FRP(Function Reactive Programming) 구조로 작성할 필요는 없다.
즉, MVVM 아키텍쳐에게 Rx
, ReactiveCocoa
가 필수 라이브러리는 아니다.
그러나 이것의 부재가 MVVM의 단점인 많은 기반 코드 작성으로 귀결된다.
대체로Rx
가 바인딩을 돕기 위한 라이브러리로 사용된다.
MVVM의 가장 큰 장점은 테스트의 용이성에 있다. 다만 VIPER의 Router 역할 없이, 어떻게 뷰 컨트롤러와 바인딩할 수 있을지가 문제로 남는다. Apple은 이를 위해 의존성 주입을 권장했으나, 이는 꽤나 문제적이다.
MVVM 의존성 주입의 문제
만약 뷰컨 A에 뷰컨 B의 의존성을 주입하고 다시 뷰컨 C의 의존성을 주입하면, 뷰컨 A는 모든 뷰컨의 의존성을 갖게 된다. 앱 하나가 갖는 NavigationController 의 수만큼이나 이런 현상은 많이 발생할 수 있다. 가독성이 매우 떨어지고 실제로 필요한 것과 필요 없는 것이 무엇인지 구분하기에 어렵다.
버튼 하나만 눌렀는데, 그 내부에 Child 뷰컨이 또 다른 객체 의존성을 갖고 생성되고
if
문으로 디바이스 상태에 따라 또 서로 다른 뷰컨과 보여주기 방식이 작성되고 있다.
이처럼 Router
의 부재는 MVVM의 테스트 용이성에 걸림돌이 된다.
func doneButtonTapped() {
let vc = ChildViewController(prepareNeccessaryState())
if Device.isIPad() {
navigationController.pushViewController(vc, animated: true, completion: nil)
} else {
var nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(500,600)
popover.delegate = self
popover.sourceView = self.view
popover.sourceRect = CGRectMake(100, 100, 0, 0)
presentViewController(nav, animated: true, completion: nil)
}
}
MVVM과 Router의 부재
Router
하나의 부재로 인해 적어도 2개의 문제가 발생하고 있다.
VIPER에서 Router 는 자기 자신 내부에서 각 뷰로 넘어가기 위한 로직을 소화한다.
필요한 만큼의 의존성 주입이 이루어진다.
그러나 MVVM은 뷰를 어떻게 넘기고 보여줄지를 담당하는 Router가 없기에 아래와 같은 문제가 발생한다.
- 불필요한 의존성 발생 :
- 대체 하나의 뷰컨이 몇 개의 뷰컨과 관계를 맺고 있는 것인가.
- 테스트 용이성 저하 :
- 뷰 모델 테스트를 위해 뷰 컨을
stub
해야 한다.
- 뷰 모델 테스트를 위해 뷰 컨을
- 재사용성 저하
- 유지보수 어려움
MVVM의 한계 극복
Router
가 없다면, 만들면 된다.
이를 위한 Flow Coordinators
패턴이 준비되어 있다.
이제 MVVM에서도 하나의 뷰 컨트롤러를 이곳저곳에서 더 편하게 재활용할 수 있다.
위에서 나온 코드를 이제 다 지워버리고 이렇게 해보자.
이제 함수를 통해서 의존성을 필요한 만큼만 주입할 것이다.
class MyViewController {
let onDone = (Void -> Void)?
func doneButtonTapped() {
onDone?(prepareNeccesaryState())
}
}
뷰컨, 뷰모델은 다른 화면을 참조하지 않을 것이고, UIKit 뷰를 그리는 객체나 메소드를 쓸 필요가 없어진다.
Delegate
처럼 작동할 수 있을 것이며, 싱글톤 참조도 필요하지 않다.
테스트 코드도 아래처럼 간단해질 것이다.
의존성 주입에 대하여 설명하시오.
의존성 주입
의존성이란 특정 객체 A가 변할 때, 객체 B도 함께 변화함을 의미한다. 따라서 객체 서로 간 영향력을 행사하는 정도가 클수록 의존성이 크다. 의존성이 커질수록 코드 작성 중 발생하는 애로사항이 많아지는데, 이를 극복하기 위해 의존성 주입 패턴이 등장한다.
iOS에서 대표적으로 의존성과 밀접하게 등장하는 개념은 싱글톤 패턴 이라 할 수 있겠다. 싱글톤 패턴은 단 하나의 객체 인스턴스에 여러 객체가 동시에 접근하는 상황을 야기한다. 그래서 과도한 싱글톤 패턴의 남발은 테스트 주도 개발의 장애물이 될 수 있고, 객체 간 의존성을 복잡하게 증가시킬 수 있다. 아래 예시는 간략한 싱글톤 객체의 사용 형태이다.
뷰컨트롤러 내부에서 직접 Service
의 전역 인스턴스 shared
에 접근하고 있다.
이러한 뷰컨트롤러가 10개, 20개가 된다고 생각해보면 단 하나의 전역 인스턴스와 관계를 맺고 있는 객체가 얼마나 많아지고 복잡해질지 감이 온다.
핵심은 대체 왜 뷰컨트롤러가 Service
라는 객체 인스턴스를 생성하고, 그 책임을 갖고 있느냐에 있다.
// MARK: Singleton Object
struct Service {
static let shared = Service()
func curlData() {
// code
}
private init() {
// code
}
}
// MARK: Random Object
final class RandomViewController: UIViewController {
private let serviceManager = Service.shared
// code
override func viewDidLoad() {
super.viewDidLoad()
getData()
}
func getData() {
Service.shared.curlData() ...
}
}
의존성 주입 활용
의존성 주입이라는 표현이 되게 복잡한 표현이지만, 생각해보면 그 표현에 답이 있다(이렇게 당연한 말을?).
위 싱글톤 패턴 예시에서는 뷰컨트롤러 자신이 외부 객체 인스턴스에 직접 접근해서 그것을 소유하게 된다.
의존성 주입은 뷰컨트롤러가 외부 객체 인스턴스를 직접 소유하도록 하는 것을 방지하고 다른 방법으로 뷰컨트롤러에게 외부 객체 인스턴스를 제공한다.
이를 코드로 구현하는 일반적인 방법에는 3가지 정도가 있으며, 쓰기 편한 순서대로 예시 코드를 설명하고자 한다(주관적). 차례대로 속성 주입, 생성자 주입, 메소드 주입이다.
속성 주입
속성 주입 방식은 꽤 편리하지만 의존성의 변형이 일어날 수 있다.
var
로 선언되는 serviceManager
는 변수인 동시에 public
하기도 하기 때문.
다만 스토리보드를 쓴다면 아래에 소개할 생성자 의존성 주입 패턴 은 사용할 수 없다.
스토리보드로는 생성자를 내 마음대로 커스텀하여 구현할 수 없기 때문이다.
// MARK: 속성 주입방법
struct Service {
// code
init() { ... }
}
// MARK: UIViewController
final class RandomViewController: UIViewController {
var serviceManager: Service?
}
// MARK: 인스턴스 생성을 담당하는 외부 객체
let randomViewController = RandomViewController()
randomViewController.serviceManager = Service()
randomViewController.serviceManager = Network() // 상수면 불가능
생성자 의존성 주입
생성자 의존성 주입 패턴은 굉장히 쓰임이 많고 유연하다. 생성되는 과정 중에는 내가 원하는 조건이나 사전 설정이 가능하며, 이렇게 정의되는 속성과 메소드는 상수로 선언할 수 있다. 여기서부터 프로토콜이 가져다주는 이점이 동시에 활용될 수 있는데, 특정 프로토콜을 채택한 객체에 대해서만 의존성을 주입하는 방식의 응용이 가능하다.
예시에서는 Items
라는 프로토콜이 기준이 된다.
이 프로토콜을 채택하는 Beginners
는 요구사항인 weapon
과 armor
를 구체화하고 있다.
그후 Warrior
클래스는 Items
프로토콜을 채택한 equipment
라는 속성을 갖는데, 이 속성은 생성자를 통해 구체화 된다.
또한 이 클래스는 Items
프로토콜을 채택하는 인스턴스 생성 책임 객체가 된다.
이 equipment
가 의존 속성이 되며, 이것은 private let
하기 때문에 이 클래스가 한 번 생성된 후에는 변하지 않는다.
protocol Items {
var weapon: (String, Int) { get }
var armor: (String, Int) { get }
}
struct Beginners: Items {
var weapon: (String, Int) {
return ("Beginners Sword", 10)
}
var armor: (String, Int) {
return ("Beginners Armor", 15)
}
}
class Warrior {
private let equipment: Items
init(_ equipment: Items) {
self.equipment = equipment
print(self.equipment.weapon, self.equipment.armor)
}
}
let warrior = Warrior(Beginners())
메소드 의존성 주입
마지막은 메소드 의존성 주입 방식이다. 위에서 언급한 생성자 의존성 주입 방식에서 생성자가 메소드로 바뀐 것이 전부다. 인스턴스 초기화 시점이 아닌 내가 원하는 시점에 의존성을 주입할 수 있다. 또한 어떤 아규먼트를 전달할 것인지도 조금 더 유연하게 결정할 수 있다는 장점도 있다.
class Warrior {
private var equipment: Items?
func setWarrior(_ equipment: Items) {
self.equipment = equipment
}
}
let warrior = Warrior()
warrior.setWarrior(Beginners())
정리
- 싱글톤 패턴의 과도한 의존성 커플링을 해제할 수 있는 방안으로 의존성 주입 패턴을 활용할 수 있다.
- 의존성 주입 패턴은 특정 객체 내부에서 타 객체의 인스턴스를 생성하지 않고, 외부에서 인스턴스를 생성하여 주입하는 방식으로 의존성을 주입하는 패턴을 의미한다.
- 의존성 주입 패턴에는 대략 3가지 방법이 있는데 각각 속성 주입, 생성자 주입, 메소드 주입 방식이다. 생성자 주입 방식은 스토리보드로 UI를 구성할 때는 활용할 수 없다.
- 의존성 주입 패턴을 활용하여 코드의 재사용성과 유연성, 테스트 용이성을 향상할 수 있다.
- 의존성을 주입하는 컨테이너는 앱 내의 모든 객체 간 의존성을 담당하게 된다.