[iOS] TIL

Zoe·2023년 10월 23일
0

iOS

목록 보기
25/39

✅ 멀티 쓰레드로 동작하는 앱

사실 작업(Task)을 대기열(Queue)에 보내기만 하면,
iOS(운영체제시스템)가 알아서 여러 쓰레드로 나눠서 분산처리를 한다. ( * 여기서 Queue는 항상 선입선출 (FIFO)로 동작한다. )
우리가 할 일은 이제 작업(Task)을 Queue 로 보내면 된다.

여기서 대기열(Queue)에는 크게 2가지가 있다.

DispatchQueue, OperationQueue 이다.
DispatchQueue는 우리가 아는 GCD이다.

대기열(Queue)
🌟 직접적으로 쓰레드를 관리하는 개념이 아닌, 대기열(Queue)의 개념을 이용해서, 작업을 분산처리하고, OS에서 알아서 쓰레드 숫자(갯수)를 관리
🌟 (쓰레드 객체를 직접 생성시키거나 하지 않는) 쓰레드 보다 더 높은 레벨/차원에서 작업을 처리
🌟 메인쓰레드(1번)가 아닌 다른 쓰레드에서 오래걸리는 작업(예: 네트워크 처리)들과 같은 작업들이 쉽게 비동기적으로 동작하도록 함

동시성(Concurrency)은 메인 쓰레드가 아닌 다른 소프트웨어적인 쓰레드에서 동시에 일을 하는 개념이다. 바로 이 동시성이 우리가 신경써야 하는 영역이다.

비동기 / 동시성으로 화면의 버벅거림을 해결할 수 있다.

먼저 비동기(Asyc)란 일을 시작 시키고, 작업이 끝날때까지 “안 기다린다.”는 개념이다.
만약 작업을 처리하는 시간 자체가 더 오래걸린다고 가정한다면, 작업이 끝나길 기다리지 않는다는 것은 큰 의미가 있다. 반대로 동기(Sync) 처리란 작업을 시작 시키고, 뿐만아니라 작업이 끝날때까지 “기다린다.”는 개념이다. 작업을 처리하는 시간 자체가 더 오래걸린다고 가정한다면, 메인쓰레드에서 다른 작업이 시작하질 못한다.

다음 동시성에서 동시(concurrent)큐란 (보통 메인에서) 분산처리 시킨 작업을 “다른 여러개의 쓰레드에서” 처리하는 큐이다. 반대로 직렬(serial)큐란 (보통 메인에서) 분산처리 시킨 작업을 “다른 한개의 쓰레드에서” 처리하는 큐이다.

분산 처리하려면 동시큐만 필요할 것 같은데,
직렬(Serial)큐가 필요할까?
순서가 중요한 작업을 처리할때 직렬큐가 반드시 필요하다!!

그렇다면 동시성 프로그래밍과 관련된 문제점은 ?
멀티 쓰레드의 환경에서, 같은 시점에 여러개의 쓰레드에서 하나의 메모리에 동시접근 하는 문제
즉, race condition이 발생한다.
따라서,(메모리에) 쓰고 있는 동안에는 여러쓰레드에서 접근 못하도록 Lock(잠금) 처리를 해야 한다.

또 다른 문제는 2개이상의 쓰레드가 서로 배타적인 메모리의 사용으로 인해(서로 잠그고 점유하려고 하면서) 메서드의 작업이 종료도 못하고 일의 진행이 멈춰버리는 상태인 Deadlock 상태이다.

위와 같은 문제를 해결하기 위해서 동시큐에서 직렬큐로 보내는 작업 처리를 해야 Thread-safe하게 동작시킬 수 있다.

✅ MVC 구조

Model은 앱의 데이터와 비즈니스 로직을 갖고 있다.
View는 사용자에게 데이터를 보여주거나 UI를 담당한다.
Controller는 Model과 View의 중간다리 역할로 View로부터 사용자의 action을 받아 Model에게 어떤 작업을 해야 하는지 알려주거나, Model의 데이터 변화를 View에게 전달하여 View를 어떻게 업데이트할지 알려준다.

🌟 View와 Controller 사이 관계는 ?

Controller는 View에서 발생할 수 있는 action에 대한 target을 만들어둔다.
그래서 View에서 유저의 action이 발생할 경우 Controller에 있는 target이 이를 받아들이고 작 업을 수행한다.
또한 View는 delegate 패턴의 delegate와 datasource를 이용하여 Controller에게 어떤 작업을 수행해야하는지 알리기도 한다.
대표적인 예로 UITableView의 UITableViewDelegate와 UITableViewDatasource를 들 수 있다.

🌟 Model과 Controller 사이 관계는 ?

Model은 Observer 패턴의 Notification과 KVO(Key Value Observation)를 통해 Controller에게 알린다.
Notification과 KVO는 일을 수행하는 객체(publisher)가 진행하던 작업이 끝나면 자신들을 구독 중인 객체들(subscribers)에게 신호를 보내는 방식이다.
간단하게 설명하자면, 작업이 완료됐을 때 라디오 센터에서 전파를 보내는 것처럼 Controller에게 신호를 보낸다.

🌟 단점

View와 Controller가 너무 밀접하게 연결되어 있다.
Controller가 View의 Life Cycle까지 관리하기 때문에 View와 Controller를 분리하기 어렵다.
이렇게 되면 재사용성이 떨어지고, 유닛 테스트를 진행하기 힘들어진다.
또한 대부분의 코드가 Controller에 밀집될 수 있다.

따라서, SwiftUI에서는 이를 개선하여 MVVM 패턴을 주로 사용하고 있다.

✅ POP, OOP

OOP
독립된 객체를 만들고, 서로 협력을 통해 다른 객체와 Message를 주고 받으며 소프트웨어를 구성하는 것

본질설명
Messaging다른 객체의 데이터나 프로시져가 필요할 때는 메세지로 요청을 하고, 메세지를 받는 객체는 스스로 처리 방법을 선택한다.
Hiding of state-process관련있는 데이터와 프로시져를 찾아서 묶고, 다른 객체가 내부를 건드리지 못하게 한다. (private을 우선 적용하라는 이유)
Extreme late-binding메세지를 받는 객체는 그때 그때 달라질 수 있다.

OOP를 활용한다면,

🌟 변경 가능한 공유 데이터가 최소로 줄어든다.

  • 캡슐화를 통해 다른 객체가 접근할 수 없게 하기 때문이다.
  • 다른 객체의 상태를 알아내거나 바꾸려면, 해당 객체가 정해놓은 형식으로 메세지를 보내야한다.

🌟 구현 부분을 쉽게 바꿀 수 있다.

  • 메세지를 받는 부분만 일관되게 유지된다면, 실제로 그걸 처리하는 코드는 바뀌어도 실행에 문제가 없다.
  • 메세지를 보내는 것 ≠ 메서드 호출
    다른 객체에게 “돈을 줘” 라는 메세지를 받았을 때, 지갑에서 돈을 꺼내주던 이체를 해주던 응답 하는 방식은 객체가 스스로 결정

🌟 메세지를 실제로 처리하는 객체를 쉽게 변경할 수 있다.

  • 동적 바인딩은 다용도 드라이버 같은 느낌이다.
  • 손잡이는 하나지만, 그때 그때 새로운 드라이버를 바꿔끼면서 ‘다용도’를 만들어 낸다.
  • 그때 그때 동적 바인딩을 해야 하니까, 객체의 생성과 사용을 분리해야한다.

POP
OOP의 단점을 보안한, Protocol을 통한 상속의 한계점을 탈피한 프로그래밍

POP를 활용하면,

필요한 부분만 프로토콜로 분리할 수 있고, 다중 프로토콜을 구현할 수도 있다.
이 규칙을 Class / Struct / Enum 에 적용할 수 있어 OOP보다 유연한 프로그래밍이 가능하다.

또한, Swift의 기본 타입(String, Int, Float 등)은 대부분 구조체로 구현이 되어있는데,
Protocol과 Extension 사용 시, 위처럼 상속이 되지 않는 타입들에게 공통된 기능을 줄 수 있다.

Class는 하나의 상속만 가능하고 수직적인 구조를 고려하여야 하지만, Protocol은 마치 블럭처럼 기능을 추가할 수 있다.

OOP는 상속을 통해 타입을 확장하기 때문에 슈퍼클래스를 그대로 상속받아, 불필요한 프로퍼티들을 갖게 된다. 상속 구조를 사용하기 위해, 값 타입으로 정의해도 되는 모델들을 참조타입으로 정의해야 하기도 한다.
하지만!! POP는 슈퍼클래스와 서브클래스의 사이가 독립적이다. 값/참조 타입을 모두 사용할 수 있다.

// 아래와 같은 프로토콜 구현시
protocol Cafe {
    func coffee()
    func dessert()
} 

// 공통된 기능을 줄 수있다.
class CafeOne: Cafe {
    func coffee() {
        print("아메리카노와 라떼가 있습니다.")
    }
    func dessert() {
        print("케이크와 마카롱이 있습니다.")
    }
}

class CafeTwo: Cafe {    
    func coffee() {
        print("아메리카노와 라떼, 바닐라 라떼가 있습니다.")
    }
    func dessert() {
        print("휘낭시에와 르뱅쿠키가 있습니다.")
    }   
}
protocol Receiveable {
    func received(data: Any, from: Sendable)
}

extension Receiveable {
    func received(data: Any, from: Sendable) {
        print("\(self) received \(data) from \(from)")
    }
}

protocol Sendable {
    var from: Sendable { get }
    var to: Receiveable? { get }
    
    func send(data: Any)
}

extension Sendable {
    var from: Sendable { return self }
    
    func send(data: Any) {
        guard let reciver: Receiveable = self.to else { return }
        
        reciver.received(data: data, from: self.from)
    }
}

// 수신, 발신 모두 가능한 메일
class Mail: Receiveable, Sendable {
    var to: Receiveable?
}

let myMail = Mail()
let yourMail = Mail()

myMail.to = yourMail
myMail.send(data: "안녕!") // myMail 이 yourMail 에게 "안녕!" 이라는 메세지 전송

✅ Hashable

어떤 타입이 Hashable하다는 뜻은 해당 타입을 해시함수의 input값으로 사용가능하다는 뜻이다. (즉, 해시함수를 사용해 유일한 값으로 변환이 가능한 타입인지의 여부를 묻는 것!)
Swift에서는 String, Int, Double 등 기본타입이 모두 Hashble한 타입이다.
값의 유일성 보장하고, 검색 속도의 빠르다는 장점이 있다.
해시 밸류가 서로 다른 값이니, 서로 다른 input값이다라는 것이 보장되는 것이다.

originalhash value
aaa12333
ccc12344
vvv12567

Equatable을 왜 상속해야 하는지 ?

기본적으로 우리는 int형이나, double, string과 같은 타입은 비교 연산이 가능하다.
그렇지만 struct나 class의 인스턴스는 비교 연산자가 불가능한데 그 이유는 기본적으로 사용하는 Int, string, double이라는 형 내부에서는 진작에 Equatable이라는 프로토콜이 채택되어 있고 이로 인하여 구현까지 되어있기 때문이다.

Equatable: '==' 으로 비교 가능
Comparable: '<, >, <=, >=' 으로 비교 가능
Hashable: 해시값 만들어줌

결론적으로, hashValue가 같아서 해시 충돌이 일어날 가능성이 있기 때문에,
hashValue로만은 정확도가 떨어져서 추가로 동일한지 확인하는 Equatable이 필요한 것이다.

✅ mutating

인스턴스 메서드에서...

  • 메서드이기 때문에 인스턴스에 메모리 공간이 할당되어 있지 않음
  • 메서드 접근 시, 인스턴스 이름으로 접근 해야함 ➡ instance.method( )
  • 메서드 실행시, 스택프레임을 만들고 인스턴스의 데이터를 사용 ➡ 메서드 종료시 스택프레임 사라짐
  • 값 타입(구조체/열거형)의 인스턴스 메서드에서 인스턴스 고유의 (저장)속성을 수정할 수 없음(수정하려면 명시적으로 mutating 키워드 필요)
struct Point {
    var x = 0.0, y = 0.0
    func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX // ERROR
        y += deltaY // ERROR
    }
}

struct Point {
    var x = 0.0, y = 0.0
    mutating func moveBy(x deltaX: Double, y deltaY: Double) {
        x += deltaX 
        y += deltaY 
    }
}

🌟 Swift의 Value Type 은 기본적으로 내부에서 인스턴스 메서드를 통해 내부 값을 수정할 수 없다.

그렇다면 왜 값 타입의 인스턴스 메소드는 내부 프로퍼티를 변경할 수 없으며 mutating 은 왜 사용해야 하는 것일까?

Swift 에서 값 타입이 복사되었을 때는 같은 주소를 가리키다가 변경이 있을 때는 복사하여 수정하게 되는 COW(Copy on Write)가 있다.
값 타입의 인스턴스 메소드로 내부 프로퍼티가 변경될 수 있는지 없는지 알 수 없기 때문에 어느 시점에 복사를 해야하는지 알 수 없다.

그래서 mutating 키워드를 가진 메소드가 호출된다면 실제적인 복사를 해야한다고 컴파일러에게 알릴 수 있다.

struct에서 변수의 값이 변경되면 struct 전체가 복사되고 주소값이 변경이 된다.

값이 변경될 때 struct 전체가 복사되는 이유는 ?

swift가 함수형 언어이고 불변성이 중요하기 때문에,
전체가 복사가 되고 주소값이 변경이 된다.(이를 통해 멀티 쓰레드에서 safe하게 작동할 수 있게 된다.)

struct Model {
    var value: Int?
    static var value1: Int?
    
    mutating func updateValue(_ value: Int?) {
        self.value = value
    }
    
    static func updateValue1(_ value1: Int?) {
        self.value1 = value1
    }
}

class ViewModel {

    func test() {
        var example = Example()
        withUnsafePointer(to: example) {
            print($0)
        }
        example.value = 5
        withUnsafePointer(to: example) {
            print($0)
        }
        example.updateValue(5)
        withUnsafePointer(to: example) {
            print($0)
        }
        Example.value1 = 5
        withUnsafePointer(to: example) {
            print($0)
        }
        Example.updateValue1(5)
        withUnsafePointer(to: example) {
            print($0)
        }
    }

}
메모리 주소
0x00007ffee440f108
0x00007ffee440f0f0
0x00007ffee440f0e0
0x00007ffee440f0b8
0x00007ffee440f0a8

*** 주소값을 보려면 withUnsafePointer 를 사용하면 된다.

✅ @escaping

closure는 heap 영역에 저장된다. (클로저(함수)가 실제 실행되는 건, 스택프레임에서 동작하고, heap 영역의 메모리 주소를 가리키는 것.)
실행 스택에서 일이 종료되어서, 일을 더이상 할 필요가 없어지면 실제 힙에 저장되어 있던 클로저도 사라진다.

@escaping이란 키워드가 파라미터 타입 앞에 붙으면 함수가 끝난 이후에도 클로저를 실행할 수 있다.
그런 이 클로저는 요긴하게 쓰이는 부분은 Network 작업을 할 때 종종 사용한다.
왜냐하면 콜백 함수로 비동기 작업을 수행할 때에는 함수의 동작이 이미 끝난 상태이므로..

func fetchMovies(from term: String, completion: @escaping () -> Void) {
    var components = URLComponents(string: "https://itunes.apple.com/search")!

    let movieName = URLQueryItem(name: "term", value: term)
    let media = URLQueryItem(name: "media", value: "movie")
    let entity = URLQueryItem(name: "entity", value: "movie")
    let limit = URLQueryItem(name: "limit", value: "20")

    components.queryItems = [ movieName, media, entity, limit ]

    let url = components.url!

    var request = URLRequest(url: url)
    request.httpMethod = "GET"

    URLSession.shared.dataTask(with: request) {[weak self] data, response, error in
        guard let self = self else { return }
        if let error = error {
            print(error.localizedDescription)
            return
        }
        
        DispatchQueue.main.async {
            do {
                let object = try JSONDecoder().decode(Result.self, from: data!)
                self.items = object.results

                completion()
            } catch let error {
                print(error.localizedDescription)
            }
        }
    }
    .resume()
}

함수의 인자인 completion이라는 클로저를 콜백 함수 안에서 실행시킨다.
만약 여기서 @escaping이란 키워드를 붙여주지 않으면 오류가 뜬다.
당연하게도 dataTask의 콜백함수로 받는 completionHandler는 메서드가 종료되고 비동기 데이터를 가져오기 때문이다.

✅ Extension

extension SomeType {
//새로운 기능 추가
}

(타입) 계산 속성, (인스턴스) 계산 속성
(타입) 메서드, (인스턴스) 메서드
새로운 생성자 (⭐ 다만, 클래스의 경우 편의생성자만 추가 가능)
*** [예외]: 값타입(구조체, 열거형)의 경우, 지정 생성자 형태로도 (자유롭게) 생성자 구현 가능
서브스크립트
새로운 중첩 타입 정의 및 사용
프로토콜 채택 및 프로토콜 관련 메서드

클래스 / 구조체 / 열거형 타입에 extension이 가능하다.
새로운 메서드(기능)를 추가할 수 있지만 (상속처럼) 본체에 대한 재정의는 불가능하다.
애플이 미리 만들어놓은 원본 소스 코드에는 권한이 없지만, 확장을 사용해서 기능을 확장하는 것은 가능하다!!

extension SomeType:   { // 프로토콜이 원하는 내용 구현
}

계산 속성

계산된 프로퍼티를 추가할 수 있다.
저장된 프로퍼티나 기존 프로퍼티에 관찰자를 추가하는건 불가능.

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// Prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"

✅ Extension 내부에서 함수를 override할 수 있는지 설명하시오.

extension이 기존 타입의 선언을 수평 확장하는 목적을 가지며,
그 구현을 변경하거나 수정하는 것은 허용하지 않기 때문에
override는 할 수 없다.

class MySuperClass {

}

extension MySuperClass {
    @objc func otherNumber() -> Int {
        return 1
    }
}

class MySubclass : MySuperClass {
    @objc override func otherNumber() -> Int {
        return 2
    }
}

var sup = MySuperClass()
var sub = MySubclass()
print(sup.otherNumber()) // 1
print(sub.otherNumber()) // 2

위와 같은 방식으로 부모클래스의 extension 내부에 있는 method는 override 가능하다고 한다.

그리고 objc와 호환되는 함수의 경우도 그냥 extension 내부에서 override가 허용되는 경우도 있다고 한다.

하지만 애플에서 Extension은 기능의 추가를 위해 사용하라고 하니 Extension에서 Override는 지양하자

✅ 접근 제어자

class SomeClass {
	private var name = “이름”
	func nameChange(name: String) { 
		self.name = name
	} 
}

name 속성은 외부에서 볼 수 없다. 이처럼 은닉이 가능하다.

접근 제어가 필요한 이유는 ?
원하는 코드를 감출 수 있고,
코드의 영역을 분리시켜서, 효율적 관리 가능하며,
컴파일 시간이 줄어들기 때문이다. (컴파일러가, 해당 변수가 어느범위에서만 쓰이는지 인지 가능)

종류범위설명
open다른 모듈에서 접근 가능 (상속/재정의 가능)클래스의 가장 넓은 수준(클래스에서만 사용 가능)
public다른 모듈에서 접근 가능 (상속/재정의 불가)구조체/열거형의 가장 넓은 수준(구조체는 상속 불가) 기본 타입의 설정 수준(Int, String 등)
internal같은 모듈에서만 접근 가능 (디폴트 설정)따로 명시하지 않는 경우의 기본 수준
fileprivate같은 파일 내에서만 접근 가능
private같은 scope내에서만 접근 가능

*모듈(module): 프레임워크, 라이브러리, 앱 등 import해서 사용할 수 있는 외부의 코드
*타입(클래스, 구조체, 열거형 등), 변수/속성, 함수/메서드(생성자, 서브스크립트), 프로토콜 등 특정영역에 제한됨 (모든 요소)

private(set) var name: String

name의 getter는 internal이 되고, setter는 private이 된다.

public struct Car {
    fileprivate(set) var engine: String
}

engine의 getter는 자신이 속한 struct를 따라 public이 되지만, setter는 fileprivate이 된다.

profile
iOS 개발자😺

0개의 댓글

관련 채용 정보