호댕의 iOS 개발

[TWL] 21 . 11 .22 ~ 21 .11 .28 (ARC, App Life Cycle, reversed 시간 복잡도, localizedError, git stash) 본문

Software Engineering/TIL

[TWL] 21 . 11 .22 ~ 21 .11 .28 (ARC, App Life Cycle, reversed 시간 복잡도, localizedError, git stash)

호르댕댕댕 2021. 11. 29. 15:58

이번 주는 NumberFormatter, ARC, App Life Cycle에 대해 중점적으로 배웠다. 

 

ARC

ARC는 뭘 자동으로 해주는 것일까?

컴파일 타입에 retain을 통해 reference count가 올라가고 release를 통해 reference count가 내려가는 것에 대한 코드를 자동으로 넣어준다.

이를 통해 reference counting이 0이 되면 메모리에서 해제시킨다. (deinit을 호출한다)

그렇다면 ARC 이전에는 어떻게 메모리 관리를 해줬을까?

MRC를 사용해서 직접 retain과 release 코드를 작성해주고 reference count를 올리고 내려줬다. 따라서 이 때는 참조 카운트를 개발자의 실수로 잘못 더해주거나 빼주는 경우도 있었다.

ARC를 이해해야 하는 이유는 무엇무엇이 있을까?

가장 큰 이유는 메모리 관리를 잘 해줘서 메모리 누수를 방지하기 위함이다. 만약 강한 참조로 인스턴스 간 참조를 하고 있다면 각 인스턴스에 nil을 할당해주더라도 인스턴스 간 참조는 남아 있는 강한 참조 순환 문제가 발생할 수 있다. 이 때 인스턴스를 해제해주었지만 deinit은 실행이 되지 않아 메모리 누수가 발생하게 된다. 따라서 ARC가 어떻게 동작하고 참조 카운팅을 세주는 지를 파악하는 것이 이런 메모리 누수가 발생하는 상황을 방지할 수 있기 때문에 ARC를 이해해야 한다.

메모리 구조 (메모리의 논리적 영역)

  • 코드 영역: 컴파일이 될 때 소스코드를 로우레벨의 언어로 컴파일하여 코드 영역에 저장된다. 컴파일 될 때부터 끝까지 메모리에 남아있게 된다.
  • 데이터 영역: 컴파일 시점부터 전역변수와 정적 변수가 할당되어 프로그램이 종료될 때까지 남아있게 된다. 즉, 코드에서 전역적으로 사용할 수 있는 데이터가 저장된다. -> 그렇다면 싱글톤은 어떨까?
  • 스택 영역: 함수 내부의 지역변수나 매개변수가 호출 시점부터 할당되었다가 호출이 종료되면 할당 해제가 된다. 컴파일되는 시점에 데이터의 크기와 처리 시간이 확실한 것들이 스택 영역에 올라가게 된다.
  • 힙 영역: reference 타입, 컴파일 시점에 정확한 크기를 알 수 없는 collection 타입들의 내부 데이터 등이 힙 영역에 할당된다.

하지만 C를 제외한 다른 언어들의 경우 정확히 어떤 영역에 들어가게 될 지 불확실한 경우도 많다.

추가적인 궁금증

  1. 싱글톤은 타입 프로퍼티로서 자기 자신의 인스턴스를 가진 채 있는데, 싱글톤이 데이터 영역에 올라갈 때 주소 값만 가지게 되고 인스턴스 자체는 힙 영역에 올라가는 것일까?? (맞다)
  2. 스택 영역에 데이터의 크기가 확실히 정해진 데이터가 올라가게 된다면 mutating을 사용해도 스택 영역에 올라갈 수 있을까? 값 타입에서 mutating을 사용하면 값을 바꿔서 다시 값을 덮어씌우는 방식으로 값이 변경된다.

계산기 프로젝트 합치기를 하며 새롭게 안 내용

  • maximumintegerdigit의 기본값은 20억이다. 즉, 20억 자리까진 표현할 수 있다.
  • maximumfractiondigit의 기본값은 3이다. 즉, 소수점 3째 자리까지 표현할 수 있다.

숲재의 코드를 보며 Linked List에 대해 직접 코드를 보며 설명을 들었다. 물론 한 번 직접 만들어봐야겠지만 추상적으로 개념만 파악하고 있었는데 Linked List가 어떻게 구현되어 있는지 직접 확인해보니 좋았다.

방학을 이용해서 Linked list는 직접 구현을 해봐야겠다.

 

객체지향의 사실과 오해 3장

추상화: 복잡한 것들을 이해하기 쉬운 수준으로 단순화하는 것

  1. 구체적인 사물들 간의 공통점은 취하고 차이점은 버리는 일반화를 통해 단순하게 만든다.
  2. 중요한 부분을 강조하기 위해 불필요한 부분을 제거하여 단순하게 만든다.

여기서 공통점을 기반으로 객체들을 묶기 위한 것이 바로 개념이다.-> 개념이 객체에 적용됐을 때 객체를 개념의 인스턴스라고 할 수 있다.

타입: 개념의 정의와 동일-> 타입은 추상화이고 이를 실체화한 인스턴스가 바로 객체이다.

객체를 창조할 때 가장 중요한 것은 객체의 상태가 아니라 다른 객체와 협력하기 위해 어떤 행동을 할지 결정하는 것이다. 또한 객체의 내부적인 표현은 외부로 부터 은닉화되게 된다.

궁금했던 점

  • 그렇다면 extension은 타입으로 볼 수 있을까?
  • 정적 모델은 무엇일까?

bounds.size.height vs bounds.height

사실 두 값의 차이는 존재하지 않는다. 다만 bounds.height는 으로 쓰기를 할 순 없다.bounds.size.height의 경우 쓰기도 할 수 있다.

CGPoint의 좌표

private func scrollToBottom() {
    formulasScrollView.layoutIfNeeded()
    let bottomOffset = CGPoint(x: 0,
                               y: self.formulasScrollView.contentSize.height
                                - self.formulasScrollView.bounds.size.height)
    self.formulasScrollView.setContentOffset(bottomOffset, animated: true)
}

Swift

여기서 CGPoint의 좌표 값이 정확히 어떤 의미인지가 궁금했다.

처음에는 (0,0)이 왼쪽 상단이 기준이라고 생각했다.

그런데 좌표 x 값이 0이었는데 왼쪽이 아닌 오른쪽에서 로그가 뜨는 이유가 궁금했다.

스토리보드에서 UIScrollView의 설정을 보면 Alignment가 trailing으로 되어 있다. 여기서 leading으로 Alignment를 변경하면 좌측에 로그가 뜨게 된다.

그렇다면 x축에 값을 넣으면 어떻게 될까?Alignment가 leading으로 되어 있는 경우 왼쪽을 기준으로 값을 넣은 만큼 떨어지게 됐다.Alignment가 trailing으로 되어 있는 경우 동일한 값을 넣으면 오른쪽을 기준으로 값을 넣은 만큼 떨어졌다.

즉, (0, 0)은 항상 왼쪽 상단에 고정된 것이 아니라 Alignment에 따라 조정되는 것이다.

 

reversed의 시간 복잡도

저번에 TIL로 쓰긴 했지만 reversed의 경우 배열을 보는 순서를 바꾸는 것으로 O(1)의 시간 복잡도를 가지고 있다.

하지만 reversed를 한 배열을 다시 할당을 하게 될 때에는 이야기가 달라진다.

일단 반환되는 타입에서도 차이가 있다.

  • enqueueStack.reversed() -> ReversedCollection<Array>
  • dequeueStack = enqueueStack.reversed() -> [Element]

단순히 reversed를 했을 때에는 위에서 언급했던 것처럼 O(1)의 시간 복잡도를 가지지만, reversed를 한 배열을 다른 변수나 상수에 할당하게 된다면 O(n)의 시간 복잡도를 갖게 된다.

즉, doubleStack을 활용해 Queue를 구현하게 된다면 dequeue를 위한 배열이 비어있을 때마다 O(n)의 시간 복잡도를 갖고 배열을 할당해주는 것이다.

또한 배열을 append할 때에도 종종 O(n)의 시간 복잡도를 갖곤 한다.재할당을 위해 appending을 하기 전에 미리 다른 copy를 만들어놓게 되는데 이 때 O(n)의 시간 복잡도가 발생하게 된다.

참고자료: https://developer.apple.com/documentation/swift/array/3126937-append

Double이 정확하게 표현할 수 있는 한계

Double has a precision of at least 15 decimal digits, whereas the precision of Float can be as little as 6 decimal digits.

Swift The Basics 공식 문서 Floating-Point Numbers를 보게 되면 Double은 64 bit Floating-point number로 표현되며, Float의 경우 32 bit Floating-point number로 표현된다고 하고 있다.

또한 Double의 경우 최소 15자리까지만 정확하게 표현할 수 있다고 하고 있다.

그래서 종종 계산기 프로젝트를 할 때 15자리를 넘어가게 되면 알아서 반올림이 되거나 하는 오차가 발생했다.

참고자료: https://docs.swift.org/swift-book/LanguageGuide/TheBasics.html

LocalizedError

기존에는 enum을 통해 에러 케이스를 만들어 직접 사용하는 방법만 이용했었다.그런데 숲재와 프로젝트를 하며 LocalizedError에 대해 배웠다.

이는 에러에 대해 좀 더 구체적으로 표현할 수 있도록 하는 프로토콜이다.공식문서에는 아래처럼 설명하고 있다.

A specialized error that provides localized messages describing the error and why it occurred.

enum OperationError: Error {
    case devidedByZero
    case invalidFormula
}

extension OperationError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .devidedByZero:
            return "0으로 나눌 수 없습니다."
        case .invalidFormula:
            return "잘못된 계산식입니다."
        }
    }
}

Swift

이런 식으로 각 에러에 대한 설명을 반환할 수 있도록 extension을 구현할 수 있으며

} catch let error {
    showAlert(message: error.localizedDescription)
}

Swift

이런 식으로 에러 처리를 해주어 각 에러에 해당하는 Description을 뱉을 수 있도록 구현했다.호출할 때에는 localizedDescription을 사용하게 된다.

앞으로 에러 처리를 할 때 잊지말고 사용해봐야겠다.

git stash

원래 git stash가 변경 사항에 대해 저장을 해놓는 것이라고 알고는 있었지만 프로젝트를 진행하며 처음 git stash를 적극적으로 사용해봤다.

일단 문제 상황은 pull을 하지 않고 commit을 하고 push를 하려하는 상황에서 발생했다.

  1. 따라서 일단 git reset HEAD~1을 통해 커밋 이전의 로그로 돌릴 수 있도록 했다.
  2. 다음으로 git stash를 통해 변경 사항에 대해 저장을 해놓았다.
  3. git stash list를 통해 잘 저장이 되었는지 확인을 해주었다.
  4. 그 후 pull을 해주었다.
  5. git stash pop 혹은 git stash apply를 통해 변경 사항을 다시 불러 줬다.
  6. 이후 commit을 하고 다시 push를 해줬다.

그렇다면 여기서 git stash pop과 git stash apply의 차이는 무엇일까?

  • git stash pop의 경우 저장되어 있었던 git stash list에서 불러온 내용을 지우고 저장한 내용을 불러와준다.
  • git stash apply의 경우 저장되어 있던 git stash list를 그대로 두고 저장한 내용을 불어온다.

굳이 저장 내용을 계속 가지고 있을 필요가 없다면 git stash pop을 사용하는 것이 나을 것 같다.

지금까지 대부분 변경 내용을 다시 돌려주는 방법으로 git stash를 사용했는데 이번에 처음으로 git stash의 목적에 맞게 사용을 해본 것 같다.

만약 변경 내용을 다시 돌려주려면 git checkout .으로 돌려줄 수 있다.

 

App Life Cycle

일단 App Life Cycle은 앱이 실행되고 죽을 때까지 그 사이에서 일어나는 상태 변화를 관리하고, 상태 변화를 끝내고 나서 앱이 죽기까지의 과정을 의미한다.

iOS 13 이후 App Life Cycle을 관리할 때 SceneDelegate와 App Delegate를 사용하게 된다.

  • SceneDelegate: UI의 상태를 관리하는 역할
  • App Delegate: 앱의 프로세스를 관리하는 역할

iOS 12 이전에는 App Delegate만 사용해서 앱의 프로세스와 UI의 상태를 관리했다.

**그렇다면 build version을 낮췄을 때(iOS 12 이하) SceneDelegate를 사용하면 어떻게 될까?**일단 iOS 12 이하에선 UIScene을 아예 모르기 때문에 컴파일 에러가 발생한다.

따라서 iOS 12에서 원래 사용했던 것처럼 AppDelegate에 UI를 관리할 수 있는 코드를 작성해줘야 12 이하에서도 UI에 대해 제어를 할 수 있다.

또한 iOS 13 이상에선 SceneDelegate에서 동일한 기능을 하는 코드를 중복으로 작성해줘야 한다.

이 때 @available(iOS 13.0, *)으로 iOS 13 이후에 해당 기능을 사용할 수 있다.

왜 iOS 13 이후 SceneDelegate를 만든 것일까?

일단 SceneDelegate가 생긴 이유는 iPad에서 multi scene을 지원하기 위함이다.

이 때 multi scene은 단순히 splitView를 통해 화면을 나눠서 보는 것이 아니라 하나의 앱에 대해 2개 이상의 UI로 독립적으로 볼 수 있는 것을 말한다.

만약 이 때 AppDelegate와 SceneDelegate가 동일한 곳에 있었다면 UI만 종료하고 싶을 때에도 앱이 아예 종료되는 문제가 있을 수 있다 생각한다.(사실 이 부분은 아직 SceneDelegate와 AppDelegate를 써보지 않아 뇌피셜이긴 하다… 추후 직접 실험을 해보며 확인해보자)

Life Cycle에서 Unattached, Suspended, Not Running

  • Unattached: 앱이 메모리를 점유하고 있긴 하나 Foreground, Background 어떤 상태에도 올라와 있지 않은 상태
  • Suspended: 앱이 메모리에 올라와 있지만 실행은 되고 있지 않은 상태
  • Not Running: 앱이 아예 메모리에서 해제되어 있는 상태
Comments