Memory

메모리 누수와 메모리 증가의 차이

메모리 그래프가 올라간다고 전부 누수는 아니다. "정상 증가"와 "해제되지 않는 누수"를 구분해야 정확한 대응이 가능하다.

학습 날짜

2026-04-30

핵심 정의

구분의미시간 경과 패턴
메모리 증가작업량에 따라 일시적으로 메모리가 늘어나는 상태작업 종료/압박 시 다시 내려갈 수 있음
메모리 누수더 이상 필요 없는 객체가 참조 때문에 해제되지 않는 상태반복할수록 계단식/지속 상승, 회복 안 됨

정상 증가 예

  • 이미지 리스트 진입 시 캐시 워밍으로 메모리 증가
  • 대형 JSON 파싱 중 임시 버퍼 증가
  • 스크롤 중 prefetch로 일시적 객체 증가

중요한 건 "나중에 회수되는가"다.

누수 예

  • 강한 순환 참조(closure-capture, delegate strong)
  • 해제 안 되는 observer/timer
  • 전역 저장소에 불필요 객체 누적

판별 방법

  1. 동일 시나리오를 5~10회 반복한다.
  2. 각 반복 후 메모리 baseline이 회복되는지 본다.
  3. 회복되지 않고 baseline이 계속 올라가면 누수 의심.
단발성 피크보다 "반복 후 기준선"이 더 중요한 지표다.

Before (누수 패턴)

final class A {
    var onDone: (() -> Void)?
}

final class B {
    let a = A()

    func bind() {
        a.onDone = {
            self.doWork() // self 강한 캡처
        }
    }

    func doWork() {}
}

After (해제 가능)

final class B {
    let a = A()

    func bind() {
        a.onDone = { [weak self] in
            self?.doWork()
        }
    }

    deinit {
        a.onDone = nil
    }

    func doWork() {}
}

Observer 예시

Before (해제 누락)

final class FeedViewModel {
    init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(onRefresh),
            name: .init("refresh_feed"),
            object: nil
        )
    }

    @objc private func onRefresh() { }
}

After (해제 보장)

final class FeedViewModel {
    private var token: NSObjectProtocol?

    init() {
        token = NotificationCenter.default.addObserver(
            forName: .init("refresh_feed"),
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.onRefresh()
        }
    }

    private func onRefresh() { }

    deinit {
        if let token {
            NotificationCenter.default.removeObserver(token)
        }
    }
}

Timer 예시

Before (invalidate 누락 + 강한 캡처)

final class CountdownController {
    private var timer: Timer?

    func start() {
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.tick() // self 강한 캡처
        }
    }

    private func tick() { }
}

After (weak self + 정리)

final class CountdownController {
    private var timer: Timer?

    func start() {
        timer?.invalidate()
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
            self?.tick()
        }
    }

    private func tick() { }

    deinit {
        timer?.invalidate()
        timer = nil
    }
}

실무 체크리스트

관련 문서

retain cycle이 생기는 이유와 끊는 방법
ARC가 객체 생명주기를 어떻게 관리하는지
대형 이미지가 메모리를 많이 쓰는 이유