ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 좀 더 Low 하게 가보자~ Swift 저수준 메모리 관리
    Apple🍎/Swift 2025. 4. 25. 20:25

    ARC(Automatic Reference Counting)의 작동 원리

    ARC는 Swift에서 메모리를 자동으로 관리하는 메커니즘입니다.

    • 참조 카운팅 기본 원리: 각 참조 타입 객체(클래스 인스턴스)는 자신을 가리키는 참조의 개수를 추적하는 참조 카운트를 가집니다.
    • 카운트 증감 동작
      • 객체에 새 참조가 생성될 때 → 카운트 증가
      • 참조가 범위를 벗어날 때 → 카운트 감소
      • 카운트가 0이 되면 → 메모리에서 해제
    • 참조 타입
      • 강한 참조(strong): 기본값, 참조 카운트를 증가시킴
      • 약한 참조(weak): 참조하는 객체가 메모리에서 해제될 수 있음, 참조 카운트 증가 안 함, 자동으로 nil이 됨
      • 미소유 참조(unowned): weak과 유사하지만 nil이 되지 않음, 참조 객체가 반드시 자신보다 오래 살아있다고 가정
    • 순환 참조 문제: 두 객체가 서로를 강하게 참조하면 참조 카운트가 0이 되지 않아 메모리 누수 발생
    class Person {
        var apartment: Apartment?
    }
    
    class Apartment {
        weak var tenant: Person? // weak로 순환 참조 해결
    }
    • 클로저와 캡처 리스트: 클로저는 객체를 강하게 참조하므로 순환 참조가 발생할 수 있음
    class Example {
        var closure: (() -> Void)?
        
        func setup() {
            closure = { [weak self] in
                // self에 약한 참조 사용
            }
        }
    }

     

    수동 메모리 관리(MRR)와 retain/release 패턴

    Objective-C의 MRR은 "Manual Retain-Release"의 약자로, ARC가 도입되기 전 Objective-C에서 사용되던 수동 메모리 관리 방식입니다.

    MRR의 핵심 개념

    • 수동 참조 카운팅: 개발자가 직접 객체의 참조 카운트를 관리해야 함
    • 메모리 관리 규칙: "소유권 규칙"이라고도 하며, 객체를 생성하거나 보관할 때 명시적으로 관리해야 함

    주요 메서드

    • retain: 객체의 참조 카운트를 수동으로 1 증가시킴
    • release: 객체의 참조 카운트를 수동으로 1 감소시킴
    • autorelease: 현재 autorelease pool이 drain될 때 release가 호출되도록 예약

    Objective-C에서의 MRR 예시

    // 객체 생성 (참조 카운트 1)
    NSString *str = [[NSString alloc] initWithString:@"Hello"];
    
    // 객체 보관 (참조 카운트 +1)
    [str retain];  // 명시적으로 참조 카운트 증가
    
    // 객체 사용
    NSLog(@"%@", str);
    
    // 객체 해제 (참조 카운트 -1)
    [str release];  // 명시적으로 참조 카운트 감소
    
    // 또 다른 해제 (참조 카운트 0 → 메모리 해제)
    [str release];
    

    Autorelease 패턴

    // 자동 해제 풀 생성
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    // 객체를 자동 해제 풀에 추가
    NSString *str = [[[NSString alloc] initWithString:@"Hello"] autorelease];
    
    // 풀 해제 (풀 내의 모든 객체에 release 호출)
    [pool drain];
    

    MRR의 문제점

    1. 개발자 실수 가능성
      • retain 없이 release 호출 → 크래시 발생
      • release 누락 → 메모리 누수
      • 과도한 retain → 메모리 누수
    2. 코드 복잡성 증가: 메모리 관리 코드가 비즈니스 로직과 섞여 가독성 저하
    3. 디버깅 어려움: 메모리 관련 버그는 발견과 수정이 어려움

    Unsafe 포인터 

    Swift에서 Unsafe 포인터는 메모리를 직접 관리할 수 있게 해주는 저수준 기능입니다. Swift는 기본적으로 메모리 안전성을 보장하는 언어이지만, C나 C++과 같은 저수준 API와 상호작용하거나 성능 최적화가 필요한 경우 Unsafe 포인터를 사용할 수 있습니다.

    Swift에서 제공하는 주요 Unsafe 포인터 타입들은 다음과 같습니다.

    1. UnsafePointer<T>: 특정 타입 T에 대한 읽기 전용 포인터입니다.
    2. UnsafeMutablePointer<T>: 특정 타입 T에 대해 읽기와 쓰기가 모두 가능한 포인터입니다.
    3. UnsafeRawPointer: 타입이 지정되지 않은 읽기 전용 바이트 포인터입니다.
    4. UnsafeMutableRawPointer: 타입이 지정되지 않은 읽기와 쓰기가 모두 가능한 바이트 포인터입니다.
    5. UnsafeBufferPointer<T>: 연속된 T 타입 요소들에 대한 읽기 전용 접근을 제공합니다.
    6. UnsafeMutableBufferPointer<T>: 연속된 T 타입 요소들에 대한 읽기와 쓰기가 모두 가능한 접근을 제공합니다.

    Unsafe 포인터의 특징

    1. 메모리 관리 책임: Unsafe 포인터를 사용할 때는 메모리 할당, 해제, 접근 범위 체크 등의 책임이 프로그래머에게 있습니다.
    2. 수명 관리: 포인터가 가리키는 메모리의 수명을 관리해야 합니다. 이미 해제된 메모리에 접근하면 프로그램이 충돌할 수 있습니다.
    3. 타입 안전성: Raw 포인터를 사용할 때는 타입 안전성이 보장되지 않습니다. 타입 변환 시 주의가 필요합니다.

    사용 예시

    // UnsafeMutablePointer 사용 예시
    let count = 3
    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
    
    // 메모리 초기화
    pointer.initialize(repeating: 0, count: count)
    
    // 값 설정
    pointer[0] = 10
    pointer[1] = 20
    pointer[2] = 30
    
    // 값 읽기
    for i in 0..<count {
        print(pointer[i])  // 10, 20, 30 출력
    }
    
    // 메모리 해제
    pointer.deinitialize(count: count)
    pointer.deallocate()
    
    // UnsafeRawPointer 사용 예시
    let value: Int = 42
    withUnsafeBytes(of: value) { rawPointer in
        for i in 0..<MemoryLayout<Int>.size {
            print(rawPointer[i])  // Int의 바이트 표현 출력
        }
    }

    자세한 설명

    let count = 3
    let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
    
    • count 변수에 3을 저장합니다.
    • UnsafeMutablePointer<Int>.allocate(capacity: count)는 3개의 Int 값을 저장할 수 있는 메모리 블록을 힙(heap)에 할당합니다.
    • 이 메모리 주소를 pointer 상수에 저장합니다. 여기서 pointer는 해당 메모리의 시작 주소를 가리킵니다.
    pointer.initialize(repeating: 0, count: count)
    
    • 할당된 메모리를 사용하기 전에 초기화합니다.
    • initialize 메서드는 할당된 메모리 블록의 각 요소를 0으로 초기화합니다.
    • Swift에서는 메모리를 할당한 후 반드시 초기화해야 합니다. 초기화하지 않고 사용하면 정의되지 않은 동작이 발생할 수 있습니다.
    pointer[0] = 10
    pointer[1] = 20
    pointer[2] = 30
    
    • 포인터를 배열처럼 접근하여 각 위치에 값을 할당합니다.
    • pointer[0]은 할당된 메모리의 첫 번째 Int 위치에 10을 저장합니다.
    • pointer[1]은 두 번째 위치(첫 번째 위치 + Int 크기만큼 떨어진 곳)에 20을 저장합니다.
    • pointer[2]는 세 번째 위치에 30을 저장합니다.
    for i in 0..<count {
        print(pointer[i])  // 10, 20, 30 출력
    }
    
    • 0부터 2까지 반복하면서 각 인덱스에 해당하는 메모리 위치의 값을 읽어 출력합니다.
    • 결과적으로 10, 20, 30이 차례로 출력됩니다.
    pointer.deinitialize(count: count)
    pointer.deallocate()
    
    • deinitialize는 메모리에 저장된 값들의 소멸자를 호출합니다. Int는 단순 값 타입이라 특별한 소멸 작업이 없지만, 클래스나 복잡한 구조체의 경우 중요합니다.
    • deallocate는 이전에 할당했던 메모리 블록을 운영체제에 반환합니다.
    • 이 두 단계가 없으면 메모리 누수가 발생할 수 있습니다.
    let value: Int = 42
    withUnsafeBytes(of: value) { rawPointer in
        for i in 0..<MemoryLayout<Int>.size {
            print(rawPointer[i])  // Int의 바이트 표현 출력
        }
    }
    
    • value라는 Int 변수에 42를 저장합니다.
    • withUnsafeBytes(of:)는 value의 메모리 표현을 바이트 단위로 안전하게 접근할 수 있게 해주는 함수입니다.
    • 이 함수는 클로저를 받아 실행하며, 클로저가 종료되면 자동으로 리소스를 정리합니다.
    • 클로저 내부에서 rawPointer는 UnsafeRawBufferPointer 타입으로, value의 메모리 시작 주소를 가리킵니다.
    • MemoryLayout<Int>.size는 Int 타입의 크기를 바이트 단위로 반환합니다(64비트 시스템에서는 일반적으로 8바이트).
    • 반복문에서는 첫 바이트부터 Int 크기만큼의 각 바이트 값을 출력합니다.
    • 42의 바이트 표현은 시스템의 엔디안(endianness)에 따라 달라집니다:
      • 리틀 엔디안 시스템(인텔 CPU 등)에서는: 42, 0, 0, 0, 0, 0, 0, 0
      • 빅 엔디안 시스템에서는: 0, 0, 0, 0, 0, 0, 0, 42

    댓글

Designed by Tistory.