iOS

[iOS] 성능 최적화: @inlinable과 lazy

양밀루 2025. 6. 25. 21:00

✅ @inlinable이란?

@inlinable은 컴파일러에게 특정 함수나 계산 속성을 인라인 최적화할 수 있다고 알려주는 키워드

인라인 최적화: 함수 호출 대신 함수의 실제 코드를 호출 지점에 직접 삽입하는 것

// 일반적인 함수 호출
func square(_ x: Int) -> Int {
    return x * x
}
let result = square(5) // 함수 호출 오버헤드 발생

// @inlinable로 최적화된 경우
@inlinable
func square(_ x: Int) -> Int {
    return x * x
}
let result = 5 * 5 // 컴파일러가 직접 코드 삽입

 

 

✅ @inlinable의 특징

  1. 성능 향상: 함수 호출 오버헤드 제거
  2. 모듈 경계 최적화: 다른 모듈에서도 인라인 최적화 가능
  3. ABI 안정성: 함수 구현이 공개 인터페이스의 일부가 됨

 

✅ 언제 사용해야 할까?

 사용하기 좋은 경우

// 1. 작고 자주 호출되는 함수
@inlinable
public func square(_ x: Int) -> Int {
    return x * x
}

// 2. 단순한 계산 프로퍼티
@inlinable
public var isEven: Bool {
    return self % 2 == 0
}

// 3. 수학적 연산
@inlinable
public func distance(_ a: CGPoint, _ b: CGPoint) -> CGFloat {
    let dx = a.x - b.x
    let dy = a.y - b.y
    return sqrt(dx * dx + dy * dy)
}

❌ 사용하지 말아야 하는 경우

// 1. 복잡한 비즈니스 로직
func loadBooks() {
    let result = bookRepository.loadBooks()
    // 복잡한 로직들...
} // @inlinable 부적합

// 2. 상태 변경이 있는 메서드
func selectBook(at index: Int) {
    selectedBookIndex = index
    // side effect 존재
} // @inlinable 부적합

// 3. 큰 함수들
func processLargeData() {
    // 100줄 이상의 코드
} // 코드 크기 증가로 성능 저하 가능

 

✅ 실제 프로젝트에서의 적용

// BookViewModel에서 적용 가능한 예시
final class BookViewModel {
    
    // ✅ 단순한 계산만 하므로 적용 가능
    @inlinable
    var totalBooksCount: Int {
        return books.count
    }
    
    @inlinable
    var bookImageName: String {
        return "harrypotter\(selectedBookIndex + 1)"
    }
    
    // ❌ 복잡한 로직이므로 부적합
    var releaseDate: String {
        return DateFormatter.enDateFormatter.string(from: currentBook?.releaseDate ?? Date())
    }
}

 


✅ lazy란?

lazy는 프로퍼티가 처음 접근될 때까지 초기화를 지연시키는 키워드

lazy의 동작 원리

class Example {
    // 즉시 초기화 (객체 생성 시점)
    let immediateProperty = ExpensiveObject()
    
    // 지연 초기화 (처음 접근할 때)
    lazy var lazyProperty = ExpensiveObject()
}

let example = Example()
// 이 시점에서 immediateProperty는 이미 생성됨
// lazyProperty는 아직 생성되지 않음

let value = example.lazyProperty // 이 시점에서 생성됨

 

✅ 언제 사용해야 할까?

lazy 사용이 적합한 경우

1. UI 컴포넌트 (가장 일반적)

class ViewController: UIViewController {
    
    private lazy var titleLabel = UILabel().then {
        $0.textAlignment = .center
        $0.font = .systemFont(ofSize: 24, weight: .bold)
        $0.numberOfLines = 0
    }
    
    private lazy var tableView = UITableView().then {
        $0.delegate = self
        $0.dataSource = self
        $0.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    private lazy var customButton = UIButton().then {
        $0.setTitle("Tap Me", for: .normal)
        $0.backgroundColor = .blue
        $0.layer.cornerRadius = 8
    }
}

 

2. 포맷터 및 헬퍼 객체

class DataManager {
    
    private lazy var dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter
    }()
    
    private lazy var numberFormatter: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        formatter.locale = Locale.current
        return formatter
    }()
    
    private lazy var jsonDecoder: JSONDecoder = {
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        return decoder
    }()
}

 

3. 네트워크 및 데이터베이스 연결

class NetworkManager {
    
    private lazy var urlSession: URLSession = {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        return URLSession(configuration: configuration)
    }()
    
    private lazy var coreDataStack: CoreDataStack = {
        return CoreDataStack(modelName: "DataModel")
    }()
}

 

4. 복잡한 계산이나 데이터 변환

class AnalyticsManager {
    
    private var rawData: [DataPoint] = []
    
    private lazy var processedData: [ProcessedDataPoint] = {
        return rawData.map { dataPoint in
            // 복잡한 변환 로직
            return ProcessedDataPoint(
                value: dataPoint.value * 1.5,
                timestamp: dataPoint.timestamp,
                category: categorize(dataPoint)
            )
        }
    }()
}

❌ lazy 사용을 피해야 하는 경우

// 1. 자주 변경되는 값
var currentTemperature: Double // lazy 부적합

// 2. 가벼운 계산
let spacing: CGFloat = 16 // lazy 불필요

// 3. 항상 필요한 값
let identifier = "CellIdentifier" // lazy 불필요

 

✅ let vs lazy var의 차이점

1. 불변성 (Immutability)

// let - 완전 불변
private let titleLabel = UILabel()
// titleLabel = UILabel() // ❌ 컴파일 에러

// lazy var - 변경 가능
private lazy var titleLabel = UILabel()
// titleLabel = UILabel() // ✅ 가능 (하지만 위험!)

2. Thread Safety

// let - Thread Safe
private let titleLabel = UILabel() // 여러 스레드에서 접근해도 안전

// lazy var - Thread Safe하지 않음
private lazy var titleLabel = UILabel() // 동시 접근 시 위험

3. 메모리 관리

// let - 객체 생성 후 참조 변경 불가
private let titleLabel = UILabel() // 강한 참조 고정

// lazy var - 참조 변경 가능
private lazy var titleLabel = UILabel() // 나중에 다른 객체로 교체 가능

 

✅ 실제 프로젝트 적용 예시

Before: let 사용

class BookViewController: UIViewController {
    // 현재 코드
    private let titleLabel = UILabel().then {
        $0.textAlignment = .center
        $0.font = .systemFont(ofSize: 24, weight: .bold)
        $0.numberOfLines = 0
    }
    
    private let seriesStackView = UIStackView().then {
        $0.axis = .horizontal
        $0.spacing = 8
        $0.distribution = .fillEqually
    }
    
    private let bookDetailView = BookDetailView()
}

After: lazy 적용

class BookViewController: UIViewController {
    // lazy로 변경
    private lazy var titleLabel = UILabel().then {
        $0.textAlignment = .center
        $0.font = .systemFont(ofSize: 24, weight: .bold)
        $0.numberOfLines = 0
    }
    
    private lazy var seriesStackView = UIStackView().then {
        $0.axis = .horizontal
        $0.spacing = 8
        $0.distribution = .fillEqually
    }
    
    private lazy var bookDetailView = BookDetailView()
}

 

✅ 주의사항

1. 실수로 재할당하지 않기

private lazy var titleLabel = UILabel()

func someMethod() {
    titleLabel = UILabel() // ❌ 기존 UI 연결이 끊어짐!
}

 

2. self 참조 시 retain cycle 주의

private lazy var button = UIButton().then { [weak self] in
    $0.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
}

 

 

UI 컴포넌트에서의 메모리 사용 패턴

class ViewController: UIViewController {
    
    // let: 객체 생성 시점에 모든 UI 컴포넌트 생성
    private let label1 = UILabel() // 즉시 생성
    private let label2 = UILabel() // 즉시 생성
    private let label3 = UILabel() // 즉시 생성
    
    // lazy: viewDidLoad에서 실제 사용될 때만 생성
    private lazy var lazyLabel1 = UILabel() // 지연 생성
    private lazy var lazyLabel2 = UILabel() // 지연 생성
    private lazy var lazyLabel3 = UILabel() // 지연 생성
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 이 시점에서 lazy var들이 생성됨
        view.addSubview(lazyLabel1)
    }
}

 

 

✅ 적용 판단 기준

@inlinable 

// ✅ 적용하기
@inlinable var isDataLoaded: Bool { !books.isEmpty }
@inlinable func bookCount() -> Int { books.count }

// ❌ 적용하지 않기
func loadBooks() { /* 복잡한 로직 */ }
func updateUI() { /* 상태 변경 */ }

lazy

// ✅ lazy 적용
private lazy var expensiveFormatter = DateFormatter()
private lazy var complexView = CustomComplexView()

// ❌ lazy 불필요
private let simpleSpacing: CGFloat = 16
private let identifier = "CellID"

 

 

✅ 정리

  @inlinable lazy
목적 함수 호출 오버헤드 제거 메모리 사용 패턴 최적화
적용 대상 작고 자주 호출되는 함수/프로퍼티 생성 비용이 높고 사용되지 않을 수 있는 객체
주의사항 큰 함수에 사용하면 오히려 성능 저하 Thread Safety 없음, 재할당 가능