12월 21, 2025
47 분 읽기

고급 iOS 모바일 개발자 면접 질문 완벽 가이드

interview
career-advice
job-search
고급 iOS 모바일 개발자 면접 질문 완벽 가이드
MB

Milad Bonakdar

작성자

고급 iOS 개발 마스터를 위한 필수 면접 질문 가이드: 아키텍처 패턴, 성능 최적화, 동시성, Core Data 및 시스템 설계 등 시니어 개발자를 위한 핵심 내용들을 다룹니다.


소개

숙련된 iOS 개발자는 높은 코드 품질과 성능을 유지하면서 견고하고 확장 가능한 애플리케이션을 설계할 수 있어야 합니다. 이를 위해서는 iOS 프레임워크, 디자인 패턴, 메모리 관리 및 정보에 입각한 아키텍처 결정을 내릴 수 있는 능숙한 지식이 필요합니다.

이 종합 가이드는 고급 Swift 개념, 아키텍처 패턴, 성능 최적화, 동시성 및 시스템 설계에 걸쳐 숙련된 iOS 개발자를 위한 필수 면접 질문을 다룹니다. 각 질문에는 자세한 답변, 희소성 평가 및 난이도 등급이 포함되어 있습니다.


고급 Swift & 언어 기능 (6개 질문)

1. Swift의 메모리 관리 및 ARC (자동 참조 카운팅)에 대해 설명하십시오.

답변: ARC는 클래스 인스턴스에 대한 참조를 추적하고 관리하여 메모리를 자동으로 관리합니다.

  • 작동 방식: 각 클래스 인스턴스에는 참조 횟수가 있습니다. 횟수가 0에 도달하면 인스턴스가 할당 해제됩니다.
  • 강한 참조: 기본값입니다. 참조 횟수를 증가시킵니다.
  • 약한 참조: 참조 횟수를 증가시키지 않습니다. 인스턴스가 할당 해제되면 자동으로 nil이 됩니다.
  • 미소유 참조: 참조 횟수를 증가시키지 않지만 인스턴스가 항상 존재한다고 가정합니다.
  • 순환 참조: 두 객체가 서로에 대한 강한 참조를 유지하여 할당 해제를 막을 때 발생합니다.
class Person {
    var name: String
    var apartment: Apartment?
    
    init(name: String) { self.name = name }
    deinit { print("\(name)이(가) 할당 해제되고 있습니다.") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // 순환 참조를 끊기 위해 weak 사용
    
    init(unit: String) { self.unit = unit }
    deinit { print("아파트 \(unit)이(가) 할당 해제되고 있습니다.") }
}

희소성: 매우 흔함 난이도: 어려움


2. Swift의 제네릭이란 무엇이며 왜 유용한가요?

답변: 제네릭을 사용하면 모든 유형에서 작동할 수 있는 유연하고 재사용 가능한 함수와 유형을 작성할 수 있습니다.

  • 장점: 코드 재사용성, 타입 안전성, 성능 (런타임 오버헤드 없음)
  • 타입 제약 조건: 제네릭 타입을 특정 프로토콜 또는 클래스로 제한합니다.
  • 연관 타입: 프로토콜에서 자리 표시자 타입을 정의하는 데 사용됩니다.
// 제네릭 함수
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}

// 제네릭 타입
struct Stack<Element> {
    private var items: [Element] = []
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

// 타입 제약 조건
func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

희소성: 흔함 난이도: 중간


3. escaping 클로저와 non-escaping 클로저의 차이점을 설명하십시오.

답변:

  • Non-escaping (기본값): 클로저는 함수가 반환되기 전에 실행됩니다. 컴파일러가 더 잘 최적화할 수 있습니다.
  • Escaping (@escaping): 클로저는 함수보다 오래 지속됩니다 (속성에 저장되거나 비동기적으로 호출됨). self를 명시적으로 캡처해야 합니다.
class NetworkManager {
    var completionHandlers: [() -> Void] = []
    
    // Escaping 클로저 - 나중에 사용하기 위해 저장됨
    func fetchData(completion: @escaping (Data?) -> Void) {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            completion(data)  // 함수가 반환된 후에 호출됨
        }.resume()
    }
    
    // Non-escaping 클로저 - 즉시 실행됨
    func processData(_ data: Data, transform: (Data) -> String) -> String {
        return transform(data)  // 함수가 반환되기 전에 호출됨
    }
}

희소성: 흔함 난이도: 중간


4. map, flatMapcompactMap의 차이점은 무엇입니까?

답변: 이들은 컬렉션을 변환하기 위한 고차 함수입니다.

  • map: 각 요소를 변환하고 결과 배열을 반환합니다.
  • compactMap: map과 같지만 nil 값을 필터링합니다.
  • flatMap: 중첩된 배열을 단일 배열로 평탄화합니다.
let numbers = [1, 2, 3, 4, 5]

// map
let doubled = numbers.map { $0 * 2 }
// [2, 4, 6, 8, 10]

// compactMap - nil 제거
let strings = ["1", "2", "three", "4"]
let validNumbers = strings.compactMap { Int($0) }
// [1, 2, 4]

// flatMap - 배열 평탄화
let nested = [[1, 2], [3, 4], [5]]
let flattened = nested.flatMap { $0 }
// [1, 2, 3, 4, 5]

희소성: 흔함 난이도: 쉬움


5. Swift의 속성 래퍼에 대해 설명하십시오.

답변: 속성 래퍼는 속성이 저장되는 방식을 관리하는 코드와 속성을 정의하는 코드 사이에 분리 계층을 추가합니다.

  • 기본 제공 예: SwiftUI의 @State, @Published, @AppStorage
  • 사용자 지정 래퍼: 재사용 가능한 속성 동작을 정의합니다.
@propertyWrapper
struct Capitalized {
    private var value: String = ""
    
    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

struct User {
    @Capitalized var firstName: String
    @Capitalized var lastName: String
}

var user = User()
user.firstName = "john"
print(user.firstName)  // "John"

// UserDefaults 래퍼
@propertyWrapper
struct UserDefault<T> {
    let key: String
    let defaultValue: T
    
    var wrappedValue: T {
        get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
        set { UserDefaults.standard.set(newValue, forKey: key) }
    }
}

struct Settings {
    @UserDefault(key: "username", defaultValue: "Guest")
    static var username: String
}

희소성: 중간 난이도: 어려움


6. Result 타입이란 무엇이며 어떻게 사용됩니까?

답변: Result는 성공 또는 실패를 나타내는 열거형으로, 오류 처리를 더 명시적으로 만듭니다.

  • 정의: enum Result<Success, Failure: Error>
  • 장점: 타입 안전 오류 처리, 더 명확한 API 계약, 비동기 코드에 대한 함수 throw보다 더 좋습니다.
enum NetworkError: Error {
    case invalidURL
    case noData
    case decodingError
}

func fetchUser(id: Int, completion: @escaping (Result<User, NetworkError>) -> Void) {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        completion(.failure(.invalidURL))
        return
    }
    
    URLSession.shared.dataTask(with: url) { data, response, error in
        guard let data = data else {
            completion(.failure(.noData))
            return
        }
        
        do {
            let user = try JSONDecoder().decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(.decodingError))
        }
    }.resume()
}

// 사용법
fetchUser(id: 1) { result in
    switch result {
    case .success(let user):
        print("사용자: \(user.name)")
    case .failure(let error):
        print("오류: \(error)")
    }
}

희소성: 흔함 난이도: 중간


아키텍처 패턴 (5개 질문)

7. MVVM (Model-View-ViewModel) 패턴에 대해 설명하십시오.

답변: MVVM은 UI 로직을 비즈니스 로직에서 분리하여 코드를 더 테스트 가능하고 유지 관리 가능하게 만듭니다.

Loading diagram...
  • Model: 데이터 및 비즈니스 로직
  • View: UI (UIViewController, SwiftUI View)
  • ViewModel: 프레젠테이션 로직, 뷰를 위한 모델 데이터 변환
  • 장점: 테스트 가능 (ViewModel에는 UI 종속성이 없음), 재사용 가능한 ViewModels, 명확한 관심사 분리
// 모델
struct User {
    let id: Int
    let firstName: String
    let lastName: String
}

// ViewModel
class UserViewModel {
    private let user: User
    
    var fullName: String {
        "\(user.firstName) \(user.lastName)"
    }
    
    var displayText: String {
        "사용자 #\(user.id): \(fullName)"
    }
    
    init(user: User) {
        self.user = user
    }
}

// 뷰
class UserViewController: UIViewController {
    private let viewModel: UserViewModel
    private let label = UILabel()
    
    init(viewModel: UserViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        label.text = viewModel.displayText
    }
}

희소성: 매우 흔함 난이도: 중간


8. 코디네이터 패턴이란 무엇이며 왜 사용해야 합니까?

답변: 코디네이터 패턴은 탐색 로직을 뷰 컨트롤러에서 분리합니다.

  • 문제: UI 로직과 혼합된 탐색 로직이 있는 거대한 뷰 컨트롤러
  • 해결책: 코디네이터는 탐색 흐름을 처리합니다.
  • 장점: 재사용 가능한 뷰 컨트롤러, 테스트 가능한 탐색, 명확한 앱 흐름
protocol Coordinator {
    var navigationController: UINavigationController { get }
    func start()
}

class AppCoordinator: Coordinator {
    let navigationController: UINavigationController
    
    init(navigationController: UINavigationController) {
        self.navigationController = navigationController
    }
    
    func start() {
        showLogin()
    }
    
    func showLogin() {
        let loginVC = LoginViewController()
        loginVC.coordinator = self
        navigationController.pushViewController(loginVC, animated: true)
    }
    
    func showHome() {
        let homeVC = HomeViewController()
        homeVC.coordinator = self
        navigationController.pushViewController(homeVC, animated: true)
    }
}

희소성: 중간 난이도: 어려움


9. iOS에서 의존성 주입에 대해 설명하십시오.

답변: 의존성 주입은 객체가 내부적으로 생성하는 대신 의존성이 객체에 제공되는 디자인 패턴입니다.

  • 장점: 테스트 가능성 (모의 객체 주입), 유연성, 느슨한 결합
  • 유형:
    • 생성자 주입: 이니셜라이저를 통해 의존성을 전달합니다 (가장 일반적).
    • 속성 주입: 초기화 후 의존성을 설정합니다.
    • 메서드 주입: 의존성을 메서드 매개변수로 전달합니다.
// 추상화를 위한 프로토콜
protocol NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void)
}

// 구체적인 구현
class URLSessionNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        // 실제 네트워크 호출
    }
}

// 테스트를 위한 모의 객체
class MockNetworkService: NetworkService {
    func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
        completion(.success(Data()))
    }
}

// 의존성 주입을 사용하는 ViewModel
class UserViewModel {
    private let networkService: NetworkService
    
    // 생성자 주입
    init(networkService: NetworkService) {
        self.networkService = networkService
    }
    
    func loadUsers() {
        networkService.fetchData { result in
            // 결과 처리
        }
    }
}

// 사용법
let viewModel = UserViewModel(networkService: URLSessionNetworkService())

// 테스트
let testViewModel = UserViewModel(networkService: MockNetworkService())

희소성: 흔함 난이도: 중간


10. 리포지토리 패턴이란 무엇입니까?

답변: 리포지토리 패턴은 데이터 액세스 로직을 추상화하여 데이터 작업에 대한 깔끔한 API를 제공합니다.

  • 장점: 중앙 집중식 데이터 로직, 데이터 소스 전환 용이 (API, 데이터베이스, 캐시), 테스트 가능
  • 구현: 리포지토리는 여러 데이터 소스 간에 조정합니다.
protocol UserRepository {
    func getUser(id: Int) async throws -> User
    func saveUser(_ user: User) async throws
}

class UserRepositoryImpl: UserRepository {
    private let apiService: APIService
    private let cacheService: CacheService
    
    init(apiService: APIService, cacheService: CacheService) {
        self.apiService = apiService
        self.cacheService = cacheService
    }
    
    func getUser(id: Int) async throws -> User {
        // 캐시를 먼저 확인합니다.
        if let cachedUser = try? await cacheService.getUser(id: id) {
            return cachedUser
        }
        
        // API에서 가져옵니다.
        let user = try await apiService.fetchUser(id: id)
        
        // 캐시 업데이트
        try? await cacheService.saveUser(user)
        
        return user
    }
    
    func saveUser(_ user: User) async throws {
        try await apiService.updateUser(user)
        try await cacheService.saveUser(user)
    }
}

희소성: 중간 난이도: 중간


11. MVC, MVP 및 MVVM의 차이점을 설명하십시오.

답변:

  • MVC (Model-View-Controller):
    • Apple의 기본 패턴
    • 컨트롤러는 Model과 View 사이를 중재합니다.
    • 문제: 거대한 뷰 컨트롤러
  • MVP (Model-View-Presenter):
    • Presenter는 모든 UI 로직을 처리합니다.
    • View는 수동적입니다 (데이터만 표시).
    • MVC보다 더 나은 테스트 가능성
  • MVVM (Model-View-ViewModel):
    • ViewModel은 데이터 스트림을 노출합니다.
    • View는 ViewModel에 바인딩됩니다.
    • 반응형 프로그래밍에 가장 적합합니다 (Combine, RxSwift).

희소성: 흔함 난이도: 어려움


성능 및 최적화 (5개 질문)

12. 테이블 뷰 및 컬렉션 뷰 성능을 어떻게 최적화합니까?

답변: 여러 전략이 스크롤 성능을 향상시킵니다.

  1. 셀 재사용: dequeueReusableCell을 올바르게 사용하십시오.
  2. 무거운 작업 피하기: cellForRowAt에서 비용이 많이 드는 계산을 수행하지 마십시오.
  3. 이미지 최적화:
    • 이미지를 표시 크기로 조정합니다.
    • 이미지 처리에 백그라운드 스레드를 사용합니다.
    • 디코딩된 이미지를 캐시합니다.
  4. 미리 가져오기: UITableViewDataSourcePrefetching을 구현합니다.
  5. 높이 캐싱: 계산된 셀 높이를 캐시합니다.
  6. 투명도 피하기: 불투명 뷰가 더 빠르게 렌더링됩니다.
class ImageTableViewCell: UITableViewCell {
    static let identifier = "ImageCell"
    private let customImageView = UIImageView()
    
    override func prepareForReuse() {
        super.prepareForReuse()
        customImageView.image = nil  // 이전 이미지 지우기
    }
}

// 뷰 컨트롤러에서
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: ImageTableViewCell.identifier, for: indexPath) as! ImageTableViewCell
    
    // 이미지를 비동기적으로 로드합니다.
    DispatchQueue.global(qos: .userInitiated).async {
        let image = self.loadAndResizeImage(at: indexPath)
        DispatchQueue.main.async {
            cell.imageView?.image = image
        }
    }
    
    return cell
}

희소성: 매우 흔함 난이도: 중간


13. Instruments와 성능 프로파일링에 사용하는 방법에 대해 설명하십시오.

답변: Instruments는 Xcode의 성능 분석 도구입니다.

  • 일반적인 Instruments:
    • Time Profiler: CPU 집약적인 코드를 식별합니다.
    • Allocations: 메모리 할당 및 누수를 추적합니다.
    • Leaks: 메모리 누수를 감지합니다.
    • Network: 네트워크 활동을 모니터링합니다.
    • Energy Log: 배터리 사용량을 분석합니다.
  • 워크플로:
    1. 앱 프로파일링 (Cmd+I)
    2. instrument 선택
    3. 앱 기록 및 상호 작용
    4. 호출 트리 및 타임라인 분석
    5. 병목 현상 식별

희소성: 흔함 난이도: 중간


14. 메모리 누수를 감지하고 수정하는 방법은 무엇입니까?

답변: 메모리 누수는 더 이상 필요하지 않은 객체가 할당 해제되지 않을 때 발생합니다.

  • 일반적인 원인:
    • 순환 참조 (강한 참조 순환)
    • self를 강하게 캡처하는 클로저
    • weak로 표시되지 않은 대리자
  • 감지:
    • Instruments Leaks 도구
    • Xcode의 메모리 그래프 디버거
    • 증가하는 메모리 사용량 감시
  • 수정:
    • 대리자에 weak 또는 unowned 사용
    • 클로저에서 [weak self] 또는 [unowned self] 사용
    • 순환 참조 끊기
class ViewController: UIViewController {
    var closure: (() -> Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 나쁨 - 순환 참조
        closure = {
            self.view.backgroundColor = .red
        }
        
        // 좋음 - weak self
        closure = { [weak self] in
            self?.view.backgroundColor = .red
        }
        
        // 좋음 - unowned self (self가 항상 존재하는 경우)
        closure = { [unowned self] in
            self.view.backgroundColor = .red
        }
    }
}

희소성: 매우 흔함 난이도: 중간


15. 앱 시작 최적화를 위해 어떤 기술을 사용합니까?

답변: 더 빠른 앱 실행은 사용자 경험을 향상시킵니다.

  1. 지연 로딩: 필요할 때만 객체 초기화
  2. Dylib 로딩 줄이기: 동적 라이브러리 최소화
  3. application:didFinishLaunching 최적화:
    • 중요하지 않은 작업을 백그라운드로 이동
    • 무거운 초기화 지연
  4. 바이너리 크기: 더 작은 바이너리가 더 빠르게 로드됩니다.
  5. 무거운 작업 피하기: 메인 스레드를 차단하지 마십시오.
  6. 측정: Instruments의 앱 시작 템플릿 사용
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
    // 중요: 창 및 루트 뷰 컨트롤러 설정
    setupWindow()
    
    // 중요하지 않은 작업 지연
    DispatchQueue.main.async {
        self.setupAnalytics()
        self.setupCrashReporting()
    }
    
    // 백그라운드 작업
    DispatchQueue.global(qos: .background).async {
        self.preloadData()
    }
    
    return true
}

희소성: 흔함 난이도: 중간


16. 이미지 캐싱 및 로딩을 어떻게 처리합니까?

답변: 효율적인 이미지 처리는 성능에 매우 중요합니다.

  • 전략:
    • 메모리 캐시: 빠른 액세스, 제한된 크기
    • 디스크 캐시: 영구적, 더 큰 용량
    • 다운로드: 네트워크에서 가져오기
  • 라이브러리: SDWebImage, Kingfisher (캐싱 자동 처리)
  • 사용자 지정 구현:
class ImageCache {
    static let shared = ImageCache()
    
    private let memoryCache = NSCache<NSString, UIImage>()
    private let fileManager = FileManager.default
    private let cacheDirectory: URL
    
    private init() {
        cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
            .appendingPathComponent("ImageCache")
        try? fileManager.createDirectory(at: cacheDirectory, withIntermediateDirectories: true)
    }
    
    func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
        let key = url.absoluteString as NSString
        
        // 메모리 캐시 확인
        if let cachedImage = memoryCache.object(forKey: key) {
            completion(cachedImage)
            return
        }
        
        // 디스크 캐시 확인
        let fileURL = cacheDirectory.appendingPathComponent(url.lastPathComponent)
        if let diskImage = UIImage(contentsOfFile: fileURL.path) {
            memoryCache.setObject(diskImage, forKey: key)
            completion(diskImage)
            return
        }
        
        // 다운로드
        URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data, let image = UIImage(data: data) else {
                completion(nil)
                return
            }
            
            // 캐시에 저장
            self.memoryCache.setObject(image, forKey: key)
            try? data.write(to: fileURL)
            
            DispatchQueue.main.async {
                completion(image)
            }
        }.resume()
    }
}

희소성: 흔함 난이도: 어려움


동시성 및 비동기 프로그래밍 (4개 질문)

17. Swift에서 async/await에 대해 설명하십시오.

답변: Swift 5.5에 도입된 Swift의 최신 동시성 모델입니다.

  • 장점: 완료 처리기보다 깔끔한 구문, 더 쉬운 오류 처리, 컴파일러 강제 스레드 안전성
  • 키워드:
    • async: 일시 중단될 수 있는 함수를 표시합니다.
    • await: 일시 중단 지점을 표시합니다.
    • Task: 새로운 비동기 컨텍스트를 만듭니다.
    • actor: 스레드 안전 참조 타입
// 완료 처리기를 사용하는 이전 방식
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, response, error in
        // 결과 처리
    }.resume()
}

// async/await를 사용하는 새로운 방식
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    let user = try JSONDecoder().decode(User.self, from: data)
    return user
}

// 사용법
Task {
    do {
        let user = try await fetchUser(id: 1)
        print(user.name)
    } catch {
        print("오류: \(error)")
    }
}

// 병렬 실행
async let user1 = fetchUser(id: 1)
async let user2 = fetchUser(id: 2)
let users = try await [user1, user2]

희소성: 매우 흔함 난이도: 어려움


18. Swift에서 액터란 무엇입니까?

답변: 액터는 데이터 경합으로부터 가변 상태를 보호하는 참조 타입입니다.

  • 스레드 안전성: 한 번에 하나의 작업만 액터의 가변 상태에 액세스할 수 있습니다.
  • 자동 동기화: 컴파일러는 안전한 액세스를 강제합니다.
  • 메인 액터: UI 업데이트를 위한 특수 액터
actor BankAccount {
    private var balance: Double = 0
    
    func deposit(amount: Double) {
        balance += amount
    }
    
    func withdraw(amount: Double) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }
    
    func getBalance() -> Double {
        return balance
    }
}

// 사용법
let account = BankAccount()

Task {
    await account.deposit(amount: 100)
    let balance = await account.getBalance()
    print("잔액: \(balance)")
}

// UI 업데이트를 위한 메인 액터
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [String] = []
    
    func loadData() async {
        let newData = await fetchDataFromAPI()
        // 자동으로 메인 스레드에서 실행
        self.data = newData
    }
}

희소성: 중간 난이도: 어려움


19. Combine 프레임워크에 대해 설명하십시오.

답변: Combine은 Apple의 반응형 프로그래밍 프레임워크입니다.

  • 핵심 개념:
    • Publisher: 시간이 지남에 따라 값을 내보냅니다.
    • Subscriber: 값을 받습니다.
    • Operator: 값을 변환합니다.
  • 장점: 선언적, 구성 가능, 기본 제공 연산자
  • 사용 사례: 네트워킹, 사용자 입력 처리, 데이터 바인딩
import Combine

class UserViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var users: [User] = []
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $searchText
            .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
            .removeDuplicates()
            .filter { !$0.isEmpty }
            .flatMap { query in
                self.searchUsers(query: query)
            }
            .receive(on: DispatchQueue.main)
            .sink { [weak self] users in
                self?.users = users
            }
            .store(in: &cancellables)
    }
    
    func searchUsers(query: String) -> AnyPublisher<[User], Never> {
        // Publisher 반환
        URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: [User].self, decoder: JSONDecoder())
            .replaceError(with: [])
            .eraseToAnyPublisher()
    }
}

희소성: 흔함 난이도: 어려움


20. 직렬 큐와 동시 큐의 차이점은 무엇입니까?

답변: 디스패치 큐는 작업을 직렬 또는 동시적으로 실행합니다.

  • 직렬 큐: FIFO 순서로 한 번에 하나의 작업을 실행합니다. 작업은 이전 작업이 완료될 때까지 기다립니다.
  • 동시 큐: 여러 작업을 동시에 실행합니다. 작업은 FIFO 순서로 시작되지만 어떤 순서로든 완료될 수 있습니다.
  • 메인 큐: UI 업데이트를 위한 특수 직렬 큐
// 직렬 큐
let serialQueue = DispatchQueue(label: "com.app.serial")
serialQueue.async { print("작업 1") }
serialQueue.async { print("작업 2") }
// 출력: 작업 1, 작업 2 (항상 순서대로)

// 동시 큐
let concurrentQueue = DispatchQueue(label: "com.app.concurrent", attributes: .concurrent)
concurrentQueue.async { print("작업 A") }
concurrentQueue.async { print("작업 B") }
// 출력: 작업 A, 작업 B 또는 작업 B, 작업 A (순서 보장되지 않음)

// 쓰기 작업을 위한 배리어
concurrentQueue.async(flags: .barrier) {
    // 독점 액세스 - 다른 작업이 동시에 실행되지 않습니다.
    print("쓰기 작업")
}

희소성: 흔함 난이도: 중간


Core Data & 영구 저장 (3개 질문)

21. Core Data 아키텍처와 주요 구성 요소에 대해 설명하십시오.

답변: Core Data는 Apple의 객체 그래프 및 영구 저장 프레임워크입니다.

Loading diagram...
  • NSManagedObjectModel: 스키마 정의 (엔터티, 속성, 관계)
  • NSPersistentStoreCoordinator: 컨텍스트와 저장소 간의 조정
  • NSManagedObjectContext: 객체를 위한 작업 메모리 (스크래치 패드와 유사)
  • NSPersistentStore: 실제 저장소 (SQLite, 바이너리, 메모리 내)

희소성: 흔함 난이도: 중간


22. Core Data에서 동시성을 어떻게 처리합니까?

답변: Core Data 컨텍스트는 스레드 안전하지 않습니다. 적절한 동시성 패턴을 사용하십시오.

  • 컨텍스트 유형:
    • 메인 큐 컨텍스트: UI 작업을 위해
    • 개인 큐 컨텍스트: 백그라운드 작업을 위해
  • 모범 사례:
    • 스레드 간에 관리 객체를 전달하지 마십시오.
    • 컨텍스트 작업에 perform 또는 performAndWait를 사용하십시오.
    • 컨텍스트 간에 객체 ID를 전달하십시오.
class CoreDataManager {
    let persistentContainer: NSPersistentContainer
    
    var viewContext: NSManagedObjectContext {
        return persistentContainer.viewContext
    }
    
    func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
        persistentContainer.performBackgroundTask { context in
            block(context)
            
            if context.hasChanges {
                try? context.save()
            }
        }
    }
}

// 사용법
coreDataManager.performBackgroundTask { context in
    let user = User(context: context)
    user.name = "John"
    // 컨텍스트가 자동으로 저장됩니다.
}

희소성: 중간 난이도: 어려움


23. NSFetchedResultsController란 무엇이며 언제 사용합니까?

답변: NSFetchedResultsController는 테이블/컬렉션 뷰에 대한 Core Data 결과를 효율적으로 관리합니다.

  • 장점:
    • 자동 변경 추적
    • 메모리 효율적 (배치)
    • 섹션 관리
    • 자동 UI 업데이트
  • 사용 사례: 테이블/컬렉션 뷰에서 Core Data 객체 표시
class UsersViewController: UITableViewController, NSFetchedResultsControllerDelegate {
    var fetchedResultsController: NSFetchedResultsController<User>!
    
    func setupFetchedResultsController() {
        let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
        fetchRequest.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]
        
        fetchedResultsController = NSFetchedResultsController(
            fetchRequest: fetchRequest,
            managedObjectContext: context,
            sectionNameKeyPath: nil,
            cacheName: nil
        )
        
        fetchedResultsController.delegate = self
        try? fetchedResultsController.performFetch()
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchedResultsController.sections?[section].numberOfObjects ?? 0
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.reloadData()
    }
}

희소성: 중간 난이도: 중간


시스템 설계 및 모범 사례 (2개 질문)

24. 오프라인 우선 모바일 앱을 어떻게 설계하시겠습니까?

답변: 오프라인 우선 앱은 인터넷 없이 작동하고 연결되면 동기화됩니다.

  • 아키텍처:
    • 진실 소스로서 로컬 데이터베이스 (Core Data, Realm)
    • 서버와 조정하는 동기화 계층
    • 충돌 해결 전략
  • 전략:
    • 낙관적 UI: 변경 사항을 즉시 표시하고 백그라운드에서 동기화합니다.
    • 큐 작업: 실패한 요청을 저장
Newsletter subscription

실제로 효과가 있는 주간 커리어 팁

최신 인사이트를 받은 편지함으로 직접 받아보세요

Decorative doodle

지원을 멈추세요. 채용되기 시작하세요.

전 세계 구직자들이 신뢰하는 AI 기반 최적화로 이력서를 면접 자석으로 변환하세요.

무료로 시작하기

이 게시물 공유

이력서 작성 시간을 90% 단축하세요

평균적인 구직자는 이력서 서식을 작성하는 데 3시간 이상을 소비합니다. 우리 AI는 15분 이내에 완성하여 지원 단계에 12배 더 빠르게 도달할 수 있습니다.