탈출 클로저에서 값 타입 self 캡처하기

권승용(Eric)·2025년 1월 2일

TIL

목록 보기
26/38

배경

  • 탈출 클로저에서 참조 타입 self에 접근할 땐 순환 참조 방지를 위해 weak self를 사용하라고 배운다.
  • 그런데 값 타입 self에 접근할 땐 어떤 일이 일어날까?

실험

  • 아래 코드로 값 타입 self를 탈출 클로저 내부에서 캡처해 보았다.
struct Task {
    var count = 0

    func execute(action: @escaping () -> Void) {
        action()
    }

    func start() {
        execute(action: {
            print(count) // self 읽기 접근
        })
    }
}
  • non-mutating 함수 내부에서 self에 대해 읽기 접근만 하는 것은 문제없이 가능하다.

  • 그러나 self 내부의 프로퍼티(count)에 쓰기 작업을 추가하면 아래와 같은 에러가 발생한다.

struct Task {
    var count = 0

    func execute(action: @escaping () -> Void) {
        action()
    }

    func start() {
        execute(action: {
            count += 1 // self 쓰기 접근
        })
    }
}

  • non-mutating 함수 내부에서 접근하는 self는 immutable하기 때문에, self에 대한 쓰기 연산이 불가능하다는 에러이다.
  • start 함수를 mutating 함수로 변경해 mutating self를 사용, 쓰기 연산을 가능하게 만들어보자.
struct Task {
    var count = 0

    func execute(action: @escaping () -> Void) {
        action()
    }

    mutating func start() {
        execute(action: {
            count += 1
        })
    }
}
  • 그러면 아래와 같은 에러가 발생한다.

  • 탈출 클로저가 mutating self 파라미터를 캡처한다는 에러이다.
  • 왜 탈출 클로저에서 mutating self 파라미터를 캡처하면 안 될까?

메모리 접근 충돌

  • 코드의 여러 부분에서 동시에 같은 메모리 주소에 접근하게 되는 현상을 메모리 접근 충돌이라고 한다.
  • 이는 비일관적이고 예상이 어려운 결과를 만들어내기 때문에 피해야 한다.
  • 아래 조건들을 모두 만족하는 두 접근이 있을 때 충돌이 발생한다.
    • 접근들이 둘 다 읽기가 아니고, 둘 다 atomic하지 않다.
    • 같은 메모리 주소를 접근한다.
    • duration이 겹친다.
  • 언제 실행될 지 모르는 탈출 클로저 안에서 mutating self를 캡처하게 된다면 메모리 충돌 발생 가능성이 있음
  • 따라서 컴파일 단계에서 에러 발생시켜 예방한다.

해결법

struct Task {
    var count = 0

    func execute(action: @escaping () -> Void) {
        action()
    }

    mutating func start() {
        execute(action: { [self] in
            print(self.count)
        })
    }
}
  • 캡처 리스트를 사용해 self의 복사본을 사용
  • 이 방법을 통해서 값을 가져올 수는 있지만, 캡처한 self는 immutable하기 때문에 값을 설정할 수는 없다.

정리

  • 값 타입의 mutating self를 탈출 클로저에서 캡처할 땐 메모리 접근 충돌 방지를 위해 캡처 리스트를 사용
  • 이 때 캡처 리스트를 사용해도 self에 대한 수정은 불가능하다.
profile
ios 개발자에용

0개의 댓글