호댕의 iOS 개발

[TWL] 21.12.20 ~ 21.12.24 (프레임워크와 라이브러리, 의존성 관리도구, POP, DispatchQueue 등) 본문

Software Engineering/TIL

[TWL] 21.12.20 ~ 21.12.24 (프레임워크와 라이브러리, 의존성 관리도구, POP, DispatchQueue 등)

호르댕댕댕 2021. 12. 26. 14:32

이번 주는 프레임워크와 라이브러리, POP를 중점으로 학습했다. 

또한 프로젝트에서 동시성 프로그래밍을 녹여보기 위해 노력했다. 아직 동시성 프로그래밍은 다른 사람에게 설명해줄 정도로 명확하게 알지는 못하는 것 같아 추가적인 학습의 필요성을 느꼈다. 

 

그럼 이번 주는 뭘 배웠을까?

 

프레임워크와 라이브러리

구분을 할 수 있는 기준점: 이게 없어도 iOS 앱을 만들 수 있는가? (필수적인가)

  • 프레임워크: 없으면 만들 수 없다 -> 기반이 되는 코드의 묶음 (ex: UIKit, Foundation)
  • 라이브러리: 없어도 만들 수 있다 -> 모듈화된 기능으로 가져와서 쓸 수 있다.

그렇다면 RXSwift는 프레임워크일까? 아니면 라이브러리일까?

‘RxSwift is a library for composing asynchronous and event-based code by using observable sequences and functional style operators, allowing for parameterized execution via schedulers.’

일단, RXSwift는 라이브러리라고 명시하고 있다.

그 이유는 간단하다. RXSwift가 없더라도 iOS 앱을 만들 수 있기 때문이다. 즉, 앱을 만드는데 도움은 주지만 필수적인 것은 아니기 때문에 라이브러리라고 볼 수 있다.

하지만 앱을 만들 때 빠르고 도움이 된다고 무작정 라이브러리를 사용하는 것은 지양해야한다!! 라이브러리 내부 코드도 정확히 이해하지 못한 상태에서 무작정 사용한다면 분명 왜 이 라이브러리를 썼는지 부터 시작해서 면접에서 많은 공격을 받을 것이다. 따라서 라이브러리를 이해하고, 최소 반 정도는 어떻게 구현할 지 안 상태가 아니라면 라이브러리를 사용하는 것은 지양하는 것이 좋다.

 

의존성 관리 도구

의존성 관리도구는 애플리케이션 기능을 개발하기 위해 외부 라이브러리를 사용할 때 프로젝트와 해당 라이브러리의 상관관계를 편하게 관리해줄 수 있도록 하는 툴이다.

단지 하나의 라이브러리를 가져다 쓰기 위한 용도라기 보단 라이브러리의 버전을 쉽게 관리해줄 수 있다. 또한 라이브러리 내에서도 다른 라이브러리를 의존하고 있는 관계가 있을 수 있는데 이런 경우에도 편하게 의존성 관리를 해줄 수 있도록 해준다.

  • Cocoapods

 

장점 단점
1. Dynamic, Static 라이브러리를 모두 지원한다

2. 사용하기 쉽다

3. 의존성의 의존성까지 자동으로 관리해준다

4. 누구나 쉽게 어떤 의존성이 애플리케이션에 있는지 알 수 있다

5. pod outdated 명령어로 쉽게 새로운 버전이 있는지 알 수 있다

6. 나온 지가 가장 오래되서 대부분의 라이브러리를 지원한다 
1. 프로젝트를 빌드할 때마다 모든 종속 패키지를 빌드해서 빌드 속도가 느리다

2. 라이브러리를 다운 받아 설치(pod install, update)하는데 오랜 시간이 걸린다
  • Carthage
장점 단점
1. Dynamic, Static 라이브러리를 모두 지원한다

2. 빌드될 때마다 모든 라이브러리를 빌드하지 않아 빌드 속도가 빠르다

3. 의존성의 의존성까지 자동으로 관리해준다

4. 누구나 쉽게 어떤 의존성이 애플리케이션에 있는지 알 수 있다

5. carthage outdated 명령어로 쉽게 새로운 버전이 있는지 체크할 수 있다

6. 처음 프레임워크를 추가하는 것 외에 프로젝트 설정이 바뀌지 않는다
1. 아직 지원하지 않는 라이브러리가 존재할 수 있다

2. 의존성이 추가될 때마다 해줘야 하는 번거로운 작업이 있다
  • Swift Package Manager (SPM)
장점 단점
1. Dynamic, Static 라이브러리를 모두 지원한다

2. 애플에서 공식으로 지원하는 패키지 매니저이다. (Xcode에 포함되어 있어 별도의 설치가 필요없다)

3. 의존성의 의존성까지 자동으로 관리해준다

4. 누구나 쉽게 어떤 의존성이 애플리케이션에 있는지 알 수 있다.

5. Package.swift 파일 이외에 수행할 설정이 없다

6. Xcode의 GUI 환경에서 관리가 가능하다
1. 아직 지원하지 않는 라이브러리가 존재할 수 있다

2. 나온 지(WWDC 2018)가 얼마되지 않아 해결되지 않은 이슈들이 있다.

 

Xcode 13.0 이상부터 SPM 사용 방법

기존에는 file > Packages에 들어가 Package를 추가해주면 됐다.

하지만 Xcode 13.0 이상부턴 그림에서 보이는 것처럼 들어가 Package Dependencies에서 추가를 해주면 된다.

참고: https://iiroalhonen.medium.com/adding-a-swift-package-dependency-in-xcode-13-937b2caaf218

또한 설치를 할 때 executable에 해당하는 파일은 체크를 하지 않고 받아야 한다.

(아직 정확하게 이해는 못했지만 이를 체크하면 오류가 발생했다)

 

Git 명령어

  • git push --set-upstream origin [branch 명] 사실 이전에는 해당 명령어가 어떤 기능을 하는지는 정확히 모르고 단순히 이 명령어를 적으면 git push를 할 때 굳이 origin [branch 명]을 안써줘도 되나보다 정도로만 이해를 했었다. 오늘 이게 원격 저장소에 branch를 지정해서 올려주는 기능을 한다는 것을 알았다... 🥲
  • git config --global -e 이 명령어는 기존에 깃헙 id와 패스워드를 작성한 것을 확인할 수 있다. 여기서 아래 내용을 붙여넣으면 push를 할 때에 기본적으로 현재 위치한 branch에서 push를 해주게 된다.
[push]
default = current
[pull]
rebase = true

 

XCTUnwrap

UnitTest 코드를 작성할 때 setUpWithError()에서 sut에 값을 할당해주고 tearDownWithError에서 sut에 nil을 할당해주게 된다. 따라서 sut을 옵셔널로 설정해둬야 한다.

이 때 강제 추출(!)을 통해 값을 빼오지 않는다면 따로 if let이나 guard let으로 옵셔널 바인딩을 해줘야 한다.

매번 이렇게 옵셔널 바인딩을 해주는 것이 번거롭다고 생각했었는데 XCTUnwrap이라는 것이 있었다. 강제 추출을 통해 옵셔널 바인딩을 하게 되면 값이 없을 경우 앱 자체가 종료되는 일이 발생하게 된다. 하지만 XCTUnwrap을 사용하면 테스트 실패로 나와서 보다 안전한 방법이란 생각이 들었다.

func test_enqueue_shouldAddNode() throws {
    var linkedList = try XCTUnwrap(sut)
    linkedList.enqueue(1)
    linkedList.enqueue(2)
    linkedList.enqueue(3)

    XCTAssertEqual(linkedList.peek(), 1)
}

새롭게 알게 된 내용

  • ctrl + cmd + e -> rename 단축키
  • retainCount를 확인하는 LLDB 명령어 : po CFGetRetainCount()

 

Protocol Oriented Programming

P.O.P와 protocol+extension이 정확히 같은 개념은 아니다.

원래 Objective-C에선 프로토콜이 단순히 청사진 역할만 했다. 하지만 Swift에선 이런 역할을 포함해서 Default Implementation이 가능하다. 즉, Swift에선 프로토콜이 단순히 메서드를 정의해놓는 것이 아니라 스스로 기능을 가질 수 있는 것이다.

<클래스 상속의 단점>

  • 클래스에서만 사용 가능하다.
  • 성능 관리에 비용이 값 타입에 비해 많이 든다. (항상 많이 드는 것은 아니다)
  • 상속을 받으면 Super Class의 메서드(불필요한 메서드 포함)까지 모두 받아야 한다.
  • 다중 상속이 불가능하다.

상속을 제한하는 방법도 존재하긴 하지만 이렇게 하면 SOLID 리스코프 치환 원칙을 위배하게 된다.

<프로토콜 기본 구현의 단점>

  • extension에 저장 프로퍼티를 따로 만들 수 없다. <br>

새롭게 알게 된 내용

  • 프로토콜 기본 구현을 할 때에도 매개변수를 사용할 수 있다.
extension Ridable {
    func move(to destination: String) {
        print("\\\\(destination)로 이동합니다.")
    }
}

  • 프로토콜에 메서드를 정의해두지 않고 extension에 메서드를 정의해놓는다면 인스턴스 메서드처럼 작동을 하게 된다.
  • 프로토콜로 정의하지 않고 익스텐션으로 기본 구현만 해놓으면 재정의를 할 수 없다.
  • 상속받은 저장 프로퍼티의 경우 override가 불가능했다.

Cannot override with a stored property 'passenger'

class Car: Ridable {
    var passenger: Int = 2
}

let a = Car()
a.move(to: "집")

class Sedan: Car {
    var a = 4
    override var passenger: Int {
        get {
            return a
        }
        set {
            a = newValue
        }
    }
}

let b = Sedan()
print(b.passenger) // 4
b.passenger = 5
print(b.passenger) // 5

따라서 SuperClass에서 정의한 프로퍼티를 재정의하려면 연산 프로퍼티를 활용해야 했다.

Custom Type의 접근 제어

private class Node<Type> {
    var data: Type
    var next: Node<Type>?

    init(data: Type) {
        self.data = data
    }
}

struct LinkedList<Type> {
    private var head: Node<Type>?
    private var tail: Node<Type>?

    var isEmpty: Bool {
        return head == nil
    }

    func peek() -> Type? {
        return head?.data
    }
}

위 코드에서 분명 Node를 private으로 접근제어를 설정했는데, peek 메서드에서 어떻게 data에 접근할 수 있는지 궁금했다. 또한 어떻게 head의 타입을 Node로 선언할 수 있는지 궁금했다.

일단 AccessControl 공식 문서 Custom Types 부분을 보면 다음과 같이 나와있다.

if you define a file-private class, that class can only be used as the type of a property, or as a function parameter or return type, in the source file in which the file-private class is defined.

Class를 file-private로 설정하면 타입의 프로퍼티, 함수 파라미터 혹은 반환 타입은 file-private 클래스가 정의된 소스 파일 내에서만 사용할 수 있다고 하고 있다. 즉, 타입에 설정한 접근제어를 타입 내부 프로퍼티와 메서드에도 적용하는 것이다.

하지만 밑에 이런 내용이 있다.

If you define a type’s access level as private or file private, the default access level of its members will also be private or file private.

file private와 private가 명확하게 구분되어 있지 않고 해당 접근 제어를 가진다면 이 멤버들 또한 private 혹은 file private 접근 제어를 가지게 된다고 하고 있다.

그렇다면 다시 예제 코드로 돌아가보자.

Node의 타입을 갖는 head, tail에 접근제어자를 붙이지 않으면 다음과 같은 컴파일 에러가 발생한다.

Property must be declared fileprivate because its type uses a private type

type이 private이기 때문에 프로퍼티는 fileprivate로 선언해줘야 한다고 하고 있다. 기존에는 private로 타입에 접근 제어를 설정하여 프로퍼티도 private로 선언해야 한다고 생각했는데 fileprivate로 선언해도 괜찮았다.

이는 Node 타입의 인스턴스를 생성할 때에도 동일했다.

따라서 타입에 private로 접근제어 설정을 하게 된다면 일단 fileprivate 수준으로 접근 제어가 설정된다고 판단했다. (Node의 프로퍼티 data, next에 명시적으로 private로 접근 제어를 해준다면 LinkedList에서도 접근을 할 수 없다)

 

Protocol Oriented Programming 적용

목요일 활동학습에서 P.O.P에 대해 공부한 것을 바탕으로 프로젝트에도 사용해보았다. BankClerk 처럼 일을 하는 객체(특히 반복적으로 손님들에게 서비스를 제공하는 경우)의 경우 내부에 일을 시작했고 종료했다는 것을 반복적으로 사용할 수 있다고 생각했다.

따라서 이에 대응하기 위해 프로토콜을 사용했고 extension을 통해 기본 구현까지 해보았다.

protocol Workable {
    func work(for client: Int)
}

extension Workable {
    func work(for client: Int) {
        print("\\\\(client)번 고객 업무 시작")
        Thread.sleep(forTimeInterval: 0.7)
        print("\\\\(client)번 고객 업무 완료")
    }
}

이렇게 하다보니 정작 BankClerk 객체 내부에는 구현한 내용이 아무 것도 없게 되었다. 이런 경우에 내부 구현을 아무것도 안해주고 빈 타입으로 놔두어도 되는지 궁금하다.. 😵‍💫

Dispatch Queue

프로젝트를 진행하며 Dispatch Queue를 어디에 생성해야 할지 고민했다. 처음에는 직접 일을 하는 BankClerk가 Dispatch Queue를 생성하도록 구현했었다. 하지만 이야기를 나눠보며 Dispatch Queue가 일을 시켜야 된다는 것을 깨달았고, BankManager에서 Dispatch Queue를 생성하도록 수정했다.

이 때도 프로토콜과 extension을 활용하여 해당 프로토콜을 채택한 경우 직원에게 일을 시킬 수 있는 dispatch Queue를 생성할 수 있도록 구현했다.

protocol Managable {
    func giveWork(to clerkNumber: Int) -> DispatchQueue
}

extension Managable {
    func giveWork(to clerkNumber: Int) -> DispatchQueue {
        return DispatchQueue(label: "clerk\\\\(clerkNumber)")
    }
}

Dispatch Queue를 다루며 아직 동시성 프로그래밍이나 쓰레드 등의 개념이 미흡하다는 것을 많이 느꼈다. 주말 동안 부족한 부분을 채울 수 있도록 해야겠다!!

CFAbsoluteTime

코드 실행에 따른 소요 시간을 어떻게 계산할 지에 대해 고민했다. 처음에는 실행에 걸리는 0.7초의 시간에 고객 수를 곱하여 계산하려고 했으나, 이렇게 계산할 경우 약간의 연산 딜레이를 포함하지 못했고, 여러 쓰레드가 일을 하게 된다면 이에 대응할 수 없었다.

따라서 업무를 시작하고 끝나는 시간을 CFAbsoluteTime 을 통해 받아온 뒤 이 둘의 차이를 계산할 수 있도록 구현했다.

mutating private func dequeueWaitingLine() {
    let startTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()

    while clients.startTask() > Bank.emptyWaitingLine {
        manageClerk(clerkNumber, for: clients.startTask())
        clients.completeTask()
    }

    let endTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
    duration = endTime - startTime
}

CFAbsoluteTime은 현재 시스템의 절대 시간을 가지고 오는 메서드이다.

 

타입 내 프로퍼티를 생성하면 바로 사용을 못하는 이유

구조체나 클래스 같은 타입을 생성하고 내부에 프로퍼티를 구현했을 때 메서드를 따로 만들지 않는 이상 타입 내에서 바로 프로퍼티를 사용할 수 없다. 만약 사용하려고 한다면 다음과 같은 오류를 만나게 된다.

Cannot use instance member 'clients' within property initializer; property initializers run before 'self' is available

즉 아직 인스턴스가 생성이 되어 있지 않은 상황이라, 프로퍼티 또한 초기화가 되어 있지 않은 상황에서 바로 프로퍼티에 접근하려고 해서 생긴 오류인 것이다.

따라서 이 땐 해결할 수 있는 방법이 3가지 정도 있다.

  1. 외부에 전역으로 필요한 변수를 생성한다. 이미 외부에 생성이 되어 있다면 사용이 가능하다. 하지만 전역 변수를 사용해야 하기 때문에 깔끔한 해결 방안은 아니다.
  2. 메서드를 만들어 할당해준다.
  3. lazy를 활용한다.
var clients = Clients()
lazy var number = clients.makeWaitingLine()

이런 식으로 하면 number 변수는 나중에 생성이 되면서 오류가 사라진다. (lazy 키워드는 좀 더 공부해보도록 하자)

 

Comments