12월 21, 2025
47 분 읽기

시니어 iOS 개발자 면접 질문과 답변

interview
career-advice
job-search
시니어 iOS 개발자 면접 질문과 답변
Milad Bonakdar

Milad Bonakdar

작성자

Swift, SwiftUI, 아키텍처, 동시성, 성능, Core Data, 오프라인 동기화, 보안을 다루는 실전형 시니어 iOS 면접 질문입니다.


소개

시니어 iOS 면접은 Swift와 Apple 프레임워크 지식을 실제 제품 의사결정에 연결할 수 있는지 확인합니다. 아키텍처, 상태 관리, 동시성, 성능, 영속성, 보안, 실제 앱에서의 트레이드오프를 설명해야 합니다.

이 가이드는 결정을 먼저 말하고, 피하려는 실패 모드를 설명하며, Xcode 도구나 코드 기법까지 제시하는 답변을 연습하도록 구성되어 있습니다. 시니어 답변은 문서 암기가 아니라 코드 리뷰에서 신뢰할 수 있는 실무성이 있어야 합니다.


고급 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

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

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

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

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

무료로 시작하기

이 게시물 공유

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

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