rxSwift와 combine의 비동기 흐름은 아래와 같다

 

Observable     →     이벤트 생성     →     전달     →     Observer

 

RxSwift의 경우

Observable     →     이벤트 생성     →     전달     →     Observer
                              subscribe(on:)        observe(on:)

 

이벤트가 생성되는 쪽의 실행 스레드를 지정해주는 것이 subscribe(on:)

생성된 이벤트를 전달받아 처리하는 쪽의 실행 스레드를 지정해주는 것이 observe(on:)이다.

Observable<Int>
     .create { observer in
         // Subscription 영역
     }
     .subscribe(on:) // Subscription 영역의 실행 스레드 지정 (업스트림)
     .observe(on:) // Observing 영역을 지정 (다운 스트림)
     .subscribe { _ in
         // Observing 영역
     }
     .disposed(by: disposeBag)

 

 

subscribe(on:)은 어느곳에서 호출되더라도 이벤트를 방출하는 Subscription영역의 실행 스레드를 변경시키고

observe(on:)은 Observing 영역의 실행스레드를 변경시키기 때문에 호출된 곳의 아래 있는 실행스레드들이 바뀌게 된다

 

 

subscribe(on:)은 여러번 호출되더라도 Subscription영역은 한군데이기 때문에 위치와 관계 없이 가장 처음 호출된것이 적용된다.

observe(on:)은 다른 observe(on:)을 만날때 까지 적용된다

Observable<Int>
    .create { observer in
        // Subscription 영역
        print("create 스레드: \(Thread.current)")
        observer.onNext(1)
        observer.onCompleted()
        return Disposables.create()
    }
    .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background))  // 이것만 적용    
    .observe(on: ConcurrentDispatchQueueScheduler(qos: .background)) // 아래 전부 적용
    .map {
        // Observing 영역
        print("Map 스레드: \(Thread.current)")
        return $0
    }
    .subscribe(on: MainScheduler.instance)
    .observe(on: MainScheduler.instance) // 아래 전부 적용
    .subscribe { _ in
        // Observing 영역
        print("subscribe 스레드: \(Thread.current)")
    }
    .disposed(by: disposeBag)
    
    
Observable<Int>
    .create { observer in
        // Subscription 영역
        print("create 스레드: \(Thread.current)")
        observer.onNext(1)
        observer.onCompleted()
        return Disposables.create()
    }
    .subscribe(on: ConcurrentDispatchQueueScheduler(qos: .background))  // 이것만 적용
    .subscribe(on: MainScheduler.instance)
    .observe(on: ConcurrentDispatchQueueScheduler(qos: .background)) // 아래 전부 적용
    .map {
        // Observing 영역
        print("Map 스레드: \(Thread.current)")
        return $0
    }
    .observe(on: MainScheduler.instance) // 아래 전부 적용
    .subscribe { _ in
        // Observing 영역
        print("subscribe 스레드: \(Thread.current)")
    }
    .disposed(by: disposeBag)
    

// 결과
create 스레드: <NSThread: 0x600001750c00>{number = 5, name = (null)}
Map 스레드: <NSThread: 0x600001750c00>{number = 5, name = (null)}
subscribe 스레드: <_NSMainThread: 0x60000170c000>{number = 1, name = main}

 

 

Combine의 경우

Observable     →     이벤트 생성     →     전달     →     Observer
                              subscribe(on:)       receive(on:)

 

combine도 rxSwift와 동일하다 다만 이름이 observe(on:) -> receive(on:)로 변경된것 뿐이다.

Just(1)
    .subscribe(on: DispatchQueue.global()) // 이것만 적용
    .subscribe(on: RunLoop.main)
    .handleEvents(receiveSubscription: { _ in
        print("subscription 스레드 \(Thread.current)")
    })
    .receive(on: RunLoop.main)
    .map { (value) -> Int in
        print("map 스레드 \(Thread.current)")
        return value
    }
    .receive(on: DispatchQueue.global())
    .sink { value in
        print("received 스레드 \(Thread.current)")
    }
    
Just(1)
    .handleEvents(receiveSubscription: { _ in
        print("subscription 스레드 \(Thread.current)")
    })
    .receive(on: RunLoop.main)
    .map { (value) -> Int in
        print("map 스레드 \(Thread.current)")
        return value
    }
	.subscribe(on: DispatchQueue.global()) // 이것만 적용
    .subscribe(on: RunLoop.main)
    .receive(on: DispatchQueue.global())
    .sink { value in
        print("received 스레드 \(Thread.current)")
    }
    
// 결과
subscription 스레드 <NSThread: 0x6000017bf700>{number = 9, name = (null)}
map 스레드 <_NSMainThread: 0x600001710000>{number = 1, name = main}
received 스레드 <NSThread: 0x600001718300>{number = 7, name = (null)}

 

 

 

요약

이벤트 생성 시점 스레드 지정 (가장 처음 호출된 것만 적용 / 업스트림)

  • subscribe(on:) 

이벤트 처리 시점 스레드 지정 (호출된곳에서부터 아래로 전부 영향을 받음 / 다운스트림)

  • RxSwift - observe(on:)
  • Combine - receive(on:) 

combine에서 @Published는 값이 바뀔때마다 방출을 하는데 이때 방출되는 시점은 언제일까 ??

당연히 didSet이 호출되어 값이 바뀌고 난 뒤라고 생각했는데 실제 값과 receive된 값이 다르다는 걸 알게 되었다.

//
//  ContentView.swift
//  Test
//
//  Created by 이준복 on 6/20/25.
//

import SwiftUI
import Combine

struct ContentView: View {
    
    @StateObject
    private var viewModel = ViewModel()
    
    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
                .font(.largeTitle)
            
            Button("Increment") {
                viewModel.increment()
            }
        }
        .padding()
    }
    
}

#Preview {
    ContentView()
}


class ViewModel: ObservableObject {
    
    @Published
    var count: Int = 0 {
        willSet {
            print("willSet: \(count)")
        }
        
        didSet {
            print("didSet: \(count)")
        }
    }
    
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        $count
            .sink { [weak self] value in
                print("receive: \(value)", "real: \(self?.count ?? -1)")
            }
            .store(in: &cancellables)
    }

    func increment() {
        print("변경 전: \(count)")
        count += 1
        print("변경 후: \(count)")
    }
    
}

 

 

코드를 실행하기 전 결과를 예상 했을땐 아래라고 생각했는데 실제 결과 값은 그렇지 않았다.

변경 전: 0
willSet: 0
didSet: 1
receive: 1 real: 0
변경 후: 1

 

 

실제 결과 값

 

조금 찾아보니 willSet에서 .send(newValue)를 호출하여 값을 방출하는 것 같았다. 실제 메모리 반영되고 값이 바뀐 시점은 didSet 시점이니 이 부분을 알고 있으면 좋을 것 같다.

일급 객체

일급객체, 일급함수란 객체나 함수가 값으로 취급될 수 있다는 것이다.

값으로 취급 할 수 있다는 것의 의미는 변수를 할당받거나, 파라미터로 전달되거나 리턴 값으로 취급 될 수 있다는 것이다.

설명으로만 보면 이해하기 힘든데 코드를 보면 쉽게 이해가 가능하다

// 반환 값
func getName() -> String {
    "Name"
}

// 파라미터로 전달
func add(_ a: Int, _ b: Int) -> Int {
    a + b
}

// 변수에 저장
struct Person {
    let name: String
    let age: Int
}

let person = Person(name: "준", age: 20)

위와 같이 처럼 정수형, 배열, 객체, 클래스 등 변수에 할당받을 수 있는 타입을 일급객체라고 한다.

 

일급함수

일급함수란 일급객체로 취급되는 함수를 일급 함수라고 한다.

즉 함수를 값처럼 다를 수 있다는 이야기. Swift에선 함수(클로저)를 일급객체로 취급한다.

// 매개변수
func run(_ completion: ((String) -> Void)?) {
    completion?("Finish")
}

// 변수에 저장
func helloWorld() {
    print("Hello World!")
}

let task1 = helloWorld
let task2 = {
    print("Good Bye World!")
}

var tasks: [() -> Void] = [task1, task2]

// 리턴 값
func plus(_ x: Int) -> (Int) -> Int {
    { y in x + y }
}

let plus5 = plus(5)
print(plus5(10)) // 15

 

고차함수

고차함수는 다른 함수를 인자를 받거나 함수를 결과값으로 반환하는 함수를 말한다.

그러므로 모든 고차함수는 일급함수(일급객체)이다.

당연하게도 모든 일급함수가 고차함수인것은 아니다

- 모든 일급함수가 다른 함수를 인자로 받거나 함수를 결과값으로 반환하는게 아니기 때문

 

Swift에선 클로저가 일급객체 취급이므로 고차 함수는 클로저를 인자로 받거나 반환할 수 있다

 

고차함수의 장점

  • 코드 가독성 향상
  • 재사용성 증가
  • 유연성 및 확장성 증가
// 코드 가독성 향상
let numbers = [1, 2, 3, 4, 5]

var evens: [Int] = []
for number in numbers {
    if number % 2 == 0 {
        evens.append(number)
    }
}

let evens2 = numbers.filter { $0 % 2 == 0 }


// 재사용성 증가
func performCalculation(_ a: Int, _ b: Int, using operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let sum = performCalculation(3, 5, using: +) // 8
let product = performCalculation(3, 5, using: *) // 15


// 유연성 및 확장성 증가
// completion 구현에 따른 확장성
func reqeust(completion: (() -> Void)?) {
    DispatchQueue.global().async {
        completion?()
    }
}


reqeust {
    print("do SomeThing")
}


reqeust {
    print("do SomeThing 2")
}

 

 

하지만 단점도 존재하는데 대표적인것이 콜백 지옥이다

func getUserID(_ completion: @escaping (Result<String, Error>) -> Void) {
    ... 비동기처리 ...
    isSuccess ? completion(.success("ID")) : completion(.failure(SomeError))
}

func getUserName(id: String, _ completion: @escaping (Result<String, Error>) -> Void) {
    ... 비동기처리 ...
    isSuccess ? completion(.success("Name")) : completion(.failure(SomeError))
}


getUserID { result in
    switch result {
    case .success(let id):
        getUserName(id: id) { result in
            switch result {
            case .success(let name):
                print(name)
            case .failure(let error):
                print(error)
            }
        }
    case .failure(let error):
        print(error)
    }
}

 

순수함수

순수함수는 두가지 조건을 만족해야 한다

  • 동일한 input엔 항상 동일한 output
  • Side Effect가 없다 (함수 외부에서 변경이 일어나지 않고 함수 실행하는 것 외에 다른 외부 상호작용이 없다)

순수함수는 수학의 함수와 동일하다고 생각하면 된다

함수 f(x) = x + 5 에서 f(5)는 항상 10 이다. f(6)은 항상 11이다

 

func add(_ a: Int, _ b: Int) -> Int {
    a + b
}
add(10, 5) // 항상 10


func minus(_ a: Int, _ b: Int) -> Int {
    a - b
}
minus(10, 5) // 항상 5


// Side Effect가 존재
var multiple = 2
func increment(_ a: Int) -> Int {
    a * multiple
}

increment(5) // 10
multiple = 10
increment(5) // 100

 

즉 Side Effect가 존재하지 않으면 동일한 input에 동일한 output이 나온다

 

간혹가다 아래 처럼 Date(), random() 처럼 input이 없고 동일한 함수를 호출하는데 다른 output이 나오니까

앞에 말한 조건에 위배되는것 아닌가라고 생각할 수 있는데

엄연히 사이드 이팩트가 존재하는 불순 함수들이다

// 외부 환경인 현재 시간에 의존
func getCurrentTime() -> Date {
    return Date() 
}

// 외부 환경인 난수 생성기에 의존
func getRandomNumber() -> Int {
    return Int.random(in: 1...100)
}

Key의 개념

  • Swift의 KeyPath, KVC, KVO 자꾸나오는 Key의 개념은?
    • 문자열(Key)를 의미
    • 이 key값을 통해 instance의 property value에 간접적으로 접근하게 해주는 Objective-C에서 나온 개념

KVC, KVO가 생기게 된 이유

Objective - C로 개발하던 시절에는 MVC 패턴이 정답이라고 생각하던 시기

MVC에서 가장 중요한것은 Model과 View의 Sync를 맞추는 일

여기서 Controller는 2가지 역할을 수행하여야 함

  1. Model의 변화를 View에 반영
  2. View의 Interaction을 Model에 반영

View나 Model에서 Action이 일어날 때마다 이 2가지 과정을 거쳐

일일이 상태값을 업데이트 해주고 View와 Model을 동기화 해줘야하는 문제에 직면

이를 나누지 말고 묶어서 서로 업데이트 해주면 되지 편리하지 않을까 ? 라는 관점에서 나온 것

Key Value Coding

  • 객체의 값을 직접 가져오지 않고 Key 또는 KeyPath를 이용해서 간접적으로 데이터를 가져오거나 수정하는 방법
  • Objective-C의 것이기 때문에 NSObject가 지고 있으므로 NSObject의 서브클래스여야 가능
  • KVC는 Objective-C Dynamic Dispatch 특성에 의존하므로 프로퍼티 앞에 @objc 어노테이션을 붙여서 사용
  • key는 String
  • key는 일반적으로 객체에 정의된 accessor method 또는 인스턴스 변수의 "이름"
  • key는 특정 규칙을 따름
    • ASCII로 인코딩 되어야 함
    • 소문자로 시작해야함
    • 공백이 없어야함

KVC는 NSKeyValueCoding 을 준수

NSKeyValueCoding | Apple Developer Documentation

 

NSKeyValueCoding | Apple Developer Documentation

A mechanism by which you can access the properties of an object indirectly by name or key.

developer.apple.com

 

NSKeyValueCoding에는 다양한 getter와 setter가 있는데 가장 많이 사용하는 4가지의 사용법을 알아보자

get method

set method

KVC 예제

  • objctive-C 에서 생긴 개념이기에 NSObject를 상속하고 프로퍼티에 @objc 어노테이션을 사용

ForKey

class Point: NSObject {
    @objc var x: Double
    @objc var y: Double
}

let point = Point(x: 0, y: 0)
point.value(forKey: "x") // 0
point.setValue(5, forKey: "x")
point.value(forKey: "x") // 5

key값을 이용해 value에 접근할 수 있다. 그렇다면 keypath는 언제 쓸까 ?

ForKeyPath

객체안에 객체가 들어가 있을 때 사용한다. Key로는 객체 내부의 프로퍼티만 접근가능하기 때문

class Point: NSObject {
    @objc let x: Double
    @objc let y: Double
}

class Size: NSObject {
    @objc var width: Double
    @objc var height: Double
}

class Rect: NSObject {
    @objc var point: Point
    @objc var size: Size
    @objc var min: Double
    @objc var max: Double
}

let rect = Rect(
    point: .init(x: 1, y: 2),
    size: .init(width: 3, height: 4),
    min: 5,
    max: 6
)

rect.value(forKey: "point") // <Key.Point: 0x60000183b060>

위와 같이 선언되어 있을 때 Rect객체의 Point의 x값을 확인하려면 KeyPath를 이용할 수 밖에 없다.

rect.value(forKeyPath: "point.x") // 1
rect.value(forKeyPath: "point.y") // 2
rect.value(forKeyPath: "size.width") // 3
rect.value(forKeyPath: "size.height") // 4
rect.value(forKeyPath: "min") // 5
rect.value(forKeyPath: "max") // 6

rect.setValue(100, forKeyPath: "point.x") // rect.point.x = 100
rect.setValue(15, forKeyPath: "min") // rect.min = 15

rect.setValue(Point(x: 10, y: 20), forKey: "point") // rect.point = Point(10, 20)

KVO - Key Value Observing

  • NSObject를 상속하고 프로퍼티에 @objc dynamic 을 붙혀 사용
class MyPoint: NSObject {
    @objc dynamic var x: Double
    @objc dynamic var y: Double
}

let myPoint = MyPoint(x: 0, y: 0)

myPoint의 x,y의 값이 변경될 때마다 특정 작업을 하기위해 observing 하려면 observe 함수를 정의한다.

var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.old, .new]) { myPoint, change in
	print("myPoint.x의 현재 값 \\(change.oldValue!)가 \\(change.newValue!)로 변경")
}
myPoint.x = 2 // 위 코드로 인해 "myPoint.x의 현재 값 0.0가 2.0로 변경" 출력
observer = nil
  1. myPoint의 프로퍼티 x에 observer 추가
  2. 프로퍼티가 변경됨
  3. observer의 changeHandler 호출
  4. handler 내의 클로저 실행

KeyPath

  • 어떤 값을 observe할 것인지를 정함
  • 위 예시에선 x를 observe 하기 때문에 x 이외의 값은 아무리 바꾸어도 observer가 실행되지 않는다.
let myPoint = MyPoint(x: 0, y: 0)

var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.old, .new]) { myPoint, change in
    print("myPoint.x의 현재 값 \\(change.oldValue!)가 \\(change.newValue!)로 변경")
}

myPoint.x = 2 // myPoint.x의 현재 값 0.0가 2.0로 변경

myPoint.y = 1 // 아무일 없음
myPoint.y = 2 // 아무일 없음
myPoint.y = 3 // 아무일 없음

observer = nil

options

old, new

new와 old는 willSet, didSet의 oldValue, newValue를 생각하면 됨

old, new만 주었을 때는 초기화 후 x의 값에 새로운 값을 주었을 때만 handler 호출

var myPoint = MyPoint(x: 0, y: 0)
var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.old, .new]) { myPoint, change in
    print(change.oldValue, change.newValue)
		// Optional(0.0) Optional(1.0)
}

myPoint.x = 1

observer = nil

initial

초기화 시에도 호출 할것인지를 묻는 것

initial을 주면 초기화 시에도 handler 호출

var myPoint = MyPoint(x: 0, y: 0)
var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.initial, .old, .new]) { myPoint, change in
    print(change.oldValue, change.newValue)
		// nil Optional(0.0)
		// Optional(0.0) Optional(1.0)
}

myPoint.x = 1

observer = nil

prior

이전 상태의 값과 현재 상태의 값을 둘 다 주는 것

var myPoint = MyPoint(x: 0, y: 0)
var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.prior, .old, .new]) { myPoint, change in
    print(change.oldValue, change.newValue)
	  // Optional(0.0) nil <- myPoint.x = 1 이 실행되기 전의 상태 값 newValue가 없기에 nil
		// Optional(0.0) Optional(1.0)
}

myPoint.x = 1

observer = nil

initial을 안주어서 초기화 상태 말고 x가 1로 변경될 때 변경되기 전의 상태 값과 변경되고 난 후의 상태값 을 호출

initial을 주게되면 ?

var myPoint = MyPoint(x: 0, y: 0)
var observer: NSKeyValueObservation? = myPoint.observe(\\.x, options: [.initial, .prior, .old, .new]) { myPoint, change in
    print(change.oldValue, change.newValue)
		// nil Optional(0.0)
		// Optional(0.0) nil 
		// Optional(0.0) Optional(1.0) 
}

myPoint.x = 1

observer = nil

initial은 prior을 벗어난것을 확인 할 수 있다.

KVO의 장단점

장점

  • Model, View 두 객체간의 동기화 가능
  • 외부 라이브러리나 다른 사람이 작성한 객체의 경우 코드 변경 없이 내부의 값 관찰 가능
    • 단 해당 객체가 NSObject를 무조건 상속받고 있어야 하며 @objc dynamic도 붙어 있어야 함 …
    • 장점맞나 ?
  • KeyPath를 사용하여 프로퍼티를 관찰하므로 중첩된 프로퍼티도 관찰 가능
    • ex) rect.point.x

단점

  • NSObject를 상속받아야 함
    • object-c 런타임에 의존하게 됨
    • static dispatch가 dynamic dispatch가 된다는 단점

번외

setValue시 this class is not key value coding-compliant for the key 에러가 뜨는 경우가 있는데

이 경우는 말 그대로 key값으로 value를 변경할 수 없을 때 뜨는 에러인데

object.setValue(value, key)일 때 보통 아래 경우라 볼 수 있다.

  • object가 nil
  • class Point: NSObject { @objc var x: Double @objc var y: Double } let point: Point? point.setValue(1, "x")
  • key값이 잘못 됨
  • class Point: NSObject { @objc var x: Double @objc var y: Double } let point = Point(x: 0, y: 0) point.setValue(1, "XX")
  • value 타입이 맞지 않음
  • class Point: NSObject { @objc var x: Int @objc var y: Int } let point = Point(x: 0, y: 0) point.setValue(1.4, "x")
  • let 으로 선언되어 있음
  • class Point: NSObject { @objc let x: Double @objc let y: Double } let point = Point(x: 0, y: 0) point.setValue(1, "x")

참고

Using Key-Value Observing in Swift | Apple Developer Documentation

 

Using Key-Value Observing in Swift | Apple Developer Documentation

Notify objects about changes to the properties of other objects.

developer.apple.com

NSKeyValueObserving | Apple Developer Documentation

 

NSKeyValueObserving | Apple Developer Documentation

An informal protocol that objects adopt to be notified of changes to the specified properties of other objects.

developer.apple.com

Key-Value Coding Programming Guide: About Key-Value Coding

 

Key-Value Coding Programming Guide: About Key-Value Coding

Key-Value Coding Programming Guide

developer.apple.com

NSKeyValueObservingOptions | Apple Developer Documentation

 

NSKeyValueObservingOptions | Apple Developer Documentation

The values that can be returned in a change dictionary.

developer.apple.com

[iOS] KVO(Key-Value Observing)

 

[iOS] KVO(Key-Value Observing)

KVO(Key-Value-Observiing)란? KVO는 A객체에서 A의 변경사항을 B객체에게 알리기 위해 사용하는 코코아 프로그래밍 패턴이다. KVO를 사용하면 다른 개체의 특정 속성이 변경될 때 알림을 받도록 객체를

clamp-coding.tistory.com

[iOS - swift] KVC (Key Value Coding), KVO (Key Value Observing), value(forKey:), setValue(_:forKey:) 개념

 

[iOS - swift] KVC (Key Value Coding), KVO (Key Value Observing), value(forKey:), setValue(_:forKey:) 개념

Key의 개념 스위프트의 KeyPath, KVC, KVO 자꾸나오는 Key의 개념은? 문자열(Key)를 의미하고 이 key값을 통해 인스턴스의 프로퍼티 값(Value)에 간접적으로 접근하게 해주는 Objective-C에서 나온 개념 KVC (Key

ios-development.tistory.com

Key-Value Coding(KVC) / KeyPath in Swift

 

Key-Value Coding(KVC) / KeyPath in Swift

안녕하세요 :) Zedd입니다. 오늘은 KVC에 대해서 공부해보겠습니다. # KVC - Key-Value Coding 의 약자 - 객체의 값을 직접 가져오지않고, Key 또는 KeyPath 를 이용해서 간접적으로 데이터를 가져오거나 수정

zeddios.tistory.com

Key-Value Observing(KVO) in Swift

 

Key-Value Observing(KVO) in Swift

안녕하세요 :) Zedd입니다. 오늘은 KVO에 대해서 공부! # KVO - Key-Value Observing의 약자 - 객체의 프로퍼티의 변경사항을 다른 객체에 알리기 위해 사용하는 코코아 프로그래밍 패턴 - Model과 View와 같이

zeddios.tistory.com

KVC/KVO in Objective-C

 

KVC/KVO in Objective-C

KVC/KVO 는 Apple Framework에서 중요한 부분을 담당한다. 한번 공부해보자.

velog.io

[iOS] KVC, KVO

 

[iOS] KVC, KVO

Key-Value Coding 객체의 값을 직접 가져오지 않고, Key 또는 KeyPath를 이용해서 간접적으로 데이터를 가져오거나 수정하는 방법\\BaseType.ProperyName으로 만들어준다.KeyPath : Read onlyWritableKeyPath : v

velog.io

KVC와 KVO의 활용 및 목적 - 야곰닷넷

 

KVC와 KVO의 활용 및 목적 - 야곰닷넷

최근에 KVC와 KVO에 대해서 검색을 했습니다. 각각에 대한 개념이나 예시 같은 것은 많이 나와있지만 왜 […]

yagom.net

 

 

Concurrency(동시성) 프로그래밍

왜 동시성 프로그래밍인가 ??

우리가 은행을 갔다고 해보자. 아래와 같이 은행원1에게만 고객이 줄을 선다고 하면

대기중인 고객은 자신의 앞에 있는 고객이 끝나기 전까지는 계속해서 기다려야 한다.

하지만 옆에 있는 은행원들 에게 고객을 분산시켜주면 아주 빠르게 고객들의 업무를 볼 수 있다.

즉 은행원1에게 쌓인 일을 분산시킨다면 고객들이 아무리 은행에 많이 와도 아주 빠르게 업무를 해결할 수 있는것이다.

 

 

은행원 1에 고객이 몰려서 대기하고 있는경우 앞의 고객이 끝나아만 내 차례가온다.

 

다른 은행원들에게 고객들을 분배하여 고객들이 많이 몰리 더라도 동시에 빠르게 일을 처리할 수 있다.

 

 

 

즉 우리는 지금까지 작성한 코드들은 위와 같이 은행원1(메인스레드) 에게만 일을 맡겼던 것이다.

 

 

MainThread만 일을 하는 모습. 다른 스레드들은 놀고 있다.

 

 

func Task1() {
		...
}

func Task2() {
		...
}

func Task3() {
		...
}

Task1()
Task2()
Task3()

 

 

Task를 어떻게 다른 Thread에게 분산 시킬 수 있을까 ?

iOS에서는 Task를 Queue에 보내기만 하면 된다

다양한 예시

let queue = DispatchQueue.global()

//클로저를 해당 큐에 비동기적으로 보낸다
queue.async {
	// 클로저
}
//클로저를 해당 큐에 동기적으로 보낸다
queue.sync {
	// queue
}

// 당연히 클로저 내부는 순차적으로 실행
DispatchQueue.global().async {
	...  // 클로저 내부가 작업의 한 단위가 됨 Task1
}

DispatchQueue.global().async {
	...  // 클로저 내부가 작업의 한 단위가 됨 Task2
}

DispatchQueue.global().async {
	...  // 클로저 내부가 작업의 한 단위가 됨 Task3
}

queue로 Task를 작업을 보낸 상황

 

queue가 알아서 스레드로 task를 분배한다

 

 

GCD(Grand Central Dispatch) / Operation

iOS에서는 Queue가 두가지가 있는데 DispatchQueue(GCD)와 OperationQueue라는 대기 행렬이 있다.

직접적으로 스레드를 관리하지 않고 큐에 작업을 넣으면 시스템에서 알아서 스레드들을 관리한다

GCD

  • 간단한 일과 함수를 사용하는 작업. 즉 메소드 위주

Operation

  • GCD를 기반으로 발전된 큐이다
  • 복잡한일과 데이터와 기능을 캡슐화한 객체
  • 취소 / 순서지정 / 일시중지 사용 가능

 

동기 (Synchronous) VS 비동기(Asynchronous)

우리가 컵라면을 끓여 먹는다고 했을 때 뜨거운 물을 붓고 3분동안 기다려야 한다.

라면이 익기를 3분동안 기다리는 동안 우리는 김치를 꺼내거나 젓가락을 세팅하는 등 다른 일들을 할 수 있다.

이것이 비동기라고 생각하면 이해하기 쉽다.

 

일을 시키지만 그동안 기다리지 않는 것이 핵심이다.

 

반대로 동기라면 라면에 물을 붓고 라면이 익을 때 까지 아무것도 하지 못하고

3분이 지난 다음에서야 김치를 꺼내고 그 다음 젓가락을 꺼낼 수 있는 것이다

 

 

동기 (Synchronous)

작업을 시작시키고 작업이 끝날 때 까지 기다린다

Task1, Task2, Task3가 있고 메인스레드가 Task1를 스레드1로 보낸다고 하였을 때 동기이기 때문에

Task1은 스레드1에서 실행되고 메인스레드는 아무것도 하지 않고 있지만 메인 스레드는 스레드1이 Task1을 다 끝내는 10초동안 대기하고 있어야 한다. 결국 메인스레드에서 Task1을 실행하는것과 다르지 않게 된다.

 

 

비동기(Asynchronous)

작업을 시작시키고 작업이 끝날 때 까지 기다리지 않는다

비동기이기 때문에 메인스레드가 Task1을 스레드1로 보내도 Task1이 끝나는 것을 기다리지 않고 곧바로 다음 Task2를 실행한다.

코드 예시

이제 위에서 작성했던 코드의 의미를 알 수 있다.

// 작업을 큐에 보내고 작업이 끝날때 까지 기다리지 않는다
DispatchQueue.global().async {
    
}

// 작업을 큐에 보내고 작업이 끝날때 까지 기다린다
DispatchQueue.global().sync {
    
}

 

비동기일 때

func task1() {
    DispatchQueue.global().async {
        print("task1 시작")
        ... 작업 ...
        print("task1 종료")
    }
}

func task2() {
    DispatchQueue.global().async {
        print("task2 시작")
        ... 작업 ...
        print("task2 종료")
    }
}

func task3() {
    DispatchQueue.global().async {
        print("task3 시작")
        ... 작업 ...
        print("task3 종료")
    }
}

print("비동기 시작")
task1()
task2()
task3()
print("비동기 끝")

// 비동기 시작
// 비동기 끝
// task1 시작
// task3 시작
// task2 시작
// task2 종료
// task3 종료
// task1 종료

 

task1,2,3이 먼저 시작되었지만 비동기이기 떄문에 비동기 끝이 먼저 출력된다.

task의 시작종료는 그때그때 먼저 시작되는 task가 달라진다.

비동기 처리는 우리가 시작시키는것이 아니라 운영체제가 알아서 해주기 때문에 순서가 변경될 수 있다

 

동기일 때

func task1() {
    DispatchQueue.global().sync {
        print("task1 시작")
        ... 작업 ...
        print("task1 종료")
    }
}

func task2() {
    DispatchQueue.global().sync {
        print("task2 시작")
        ... 작업 ...
        print("task2 종료")
    }
}

func task3() {
    DispatchQueue.global().sync {
        print("task3 시작")
        ... 작업 ...
        print("task3 종료")
    }
}

print("비동기 시작")
task1()
task2()
task3()
print("비동기 끝")

// 비동기 시작
// task1 시작
// task1 종료
// task2 시작
// task2 종료
// task3 시작
// task3 종료
// 비동기 끝

 

동기이기 때문에 앞의 작업이 끝나야만 그 다음 작업을 시작할 수 있다.

 

 

직렬(Serial) VS 동시(Concurrent)

큐의 특성중 하나 serial큐의 경우 하나의 스레드를 사용하고 concurrent큐는 여러개의 스레드를 사용한다.

직렬(Serial)

하나의 스레드를 사용한다. 한개의 스레드를 사용하지만 어떤 스레드를 사용할지는 알 수 없고 운영체제가 알아서 스레드를 하나 사용한다

분산시킨 작업을 다른 한개의 스레드에서 처리하는 큐

운영체제가 메인스레드를 선택하여 메인스레드에서만 Task들이 실행된다
직렬큐이기 때문에 한개의 스레드만 사용하는 모습

 

 

동시(Concurrent)

여러개의 스레드를 사용한다. 몇개의 스레드를 사용하여 작업을 분배할지는 운영체제가 알아서 정한다.

2개의 스레드를 사용할 수 도 5개의 스레드를 사용할 수 도 있다.

분산시킨 작업을 다른 여러개의 스레드에서 처리하는 큐

운영체제가 3개의 스레드에 작업을 분할한다
몇개의 스레드를 사용할지 각 스레드에 작업을 어떻게 분배할지는 운영체제가 알아서 한다.

 

 

Concurrent 큐가 분산 처리에 더 좋아보이는데 Serial 큐가 필요한 이유

직렬큐는 순서가 중요한 작업을 할 때 필요하기 때문이다.

예를 들어 이미지를 가져온 뒤 그 이미지를 전달 받아 테이블뷰 셀에 표시한다고 했을 때

이미지를 먼저 받아야 이미지를 표시할 수 있기 때문에

이미지를 다운받는 작업이 실행되고 난 후 이미지를 표시할 작업을 해야하기 때문이다.

// 동시 큐라면 작업1과 작업2가 동시에 실행될수도 있다.
// 하지만 직렬큐라면 작업1이 실행된 뒤 작업2가 실행된다.
var 이미지: UIImage

func 작업1() {
    이미지 = 이미지 다운로드
}
func 작업2() {
    테이블뷰 셀의 이미지 = 이미지
}

DispatchQueue.global().async {
    작업1()
}

DispatchQueue.global().async {
    작업2()
}

Serial 큐. 작업1이 실행되고 난 뒤 작업2가 실행된다

 

 

반대로 독립적이나 유사한 여러개의 작업을 하는 경우엔 동시큐가 더 좋다.

예를 들어 테이블 뷰에서 각 셀에 이미지를 다운받아 표시해야 한다면 동시큐로 진행한다.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    DispatchQueue.global().async {
        셀.이미지 = 이미지 다운로드
    }
}

Concurrent 큐

 

큐의 종류

SerialQueue.sync

  • 메인 스레드의 작업흐름이 큐에 넘긴 작업이 끝날때까지 멈춰(sync)있다
  • 넘겨진 작업은 큐에 먼저 담겨져있던 작업들과 같은 스레드에 보내지기 때문에 큐에 작업들이 모두 끝나야 실행 가능하다.
    하나의 스레드에서만 실행된다.
    할당되는 스레드는 운영체제가 결정한다.
    큐에 있는 작업들은 앞에 있는 작업들이 끝나야 스레드로 할당된다.

 

 

 

CocurrentQueue.sync

  • 메인 스레드의 작업흐름이 큐에 넘긴 작업이 끝날 때까지 멈춰(sync)있다
  • 넘겨진 작업들은 큐에 먼저 담겨있던 작업들이 보내진 스레드와 다른 스레드에 보내질 수 있지만
    sync이기 때문에 큐에 있는 작업들이 끝나야 실행 가능하다

 

 

 


SerialQueue.async

  • 메인 스레드의 작업흐름이 작업을 큐에 넘기자마자 반환된다(async)
  • 넘겨진 작업들은 큐에 먼저 담겨 있던 작업들과 같은 스레드에 보내지기 때문에 큐에 있는 작업들이 모두 끝나야 실행 가능하다
  • 하나의 스레드에서만 실행된다.
    할당되는 스레드는 운영체제가 결정한다.
    비동기 이기 때문에 큐에 들어오면 바로 다음 작업이 스레드에 할당된다.
    하지만 하나의 스레드에만 작업이 할당되기 때문에 앞의 작업이 끝나야 다음 작업이 실행된다.

 

 

 

ConcurrentQueue.async

  • 메인 스레드의 작업흐름이 작업을 큐에 넘기자마자 반환된다(async)
  • 앞의 작업을 스레드에 보낸뒤 끝날때 까지 기다리지 않기 때문에 바로 다음 작업들이 다른 스레드로 분배된다.
    단 메인스레드처럼 동일한 스레드에 작업들이 할당되면 앞의 작업이 끝나야 뒤의 작업이 실행가능하다.
    Task1, Task2, Task3는 동시에 실행될 수 있지만 Task4는 Task이 끝나야만 실행된다.

 

 

 

 

비동기란 말과 동시란 말이 같은 말인가?

이제 완전 다른말이라는 것을 알 수 있다

async vs sync

작업을 보내는 시점에서 기다릴지 말지에 대해 다루는 것

concurrent vs serial

Queue로 보내진 작업들을 여러개의 스레드로 보낼 것인지 한개의 스레드로 보낼 것인지에 대해 다루는 것

 

결국 사용하는것은 비동기코드이다.

동기코드는 실행하는것을 기다리기 때문에 메인스레드에서 실행하는 것과 큰 차이가 없기 때문이다.

Thread Pool

  • 병렬 처리가 많아지면 스레드 개수가 무한히 증가된다.
  • 스레드의 생성과 스케줄링으로 인한 오버헤드가 커져 성능 저하로 이어진다.
  • 스레드의 폭증을 막기위해 Thread Pool을 이용한다.
  • Thread Pool은 작업 처리에 사용 되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리한다.
  • 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.
  • 따라서 작업 처리 요청이 폭증해도 작업 큐라는 곳에 작업이 대기하다가 여유가 있는 스레드가 그것을 처리하므로 스레드의 전체 개수는 일정하며 애플리케이션의 성능도 저하되지 않는다.
  • Swift에서는 DispatchQueue가 스레드를 관리한다

참고

iOS Concurrency(동시성) 프로그래밍, 동기 비동기 처리 그리고 GCD/Operation - 디스패치큐와 오퍼레이션큐의 이해 - 인프런 | 강의

 

iOS Concurrency(동시성) 프로그래밍, 동기 비동기 처리 그리고 GCD/Operation - 디스패치큐와 오퍼레이션

동시성(Concurrency)프로그래밍 - iOS프로그래밍에서 필요한 동기, 비동기의 개념 및 그를 확장한 GCD 및 Operation에 관한 모든 내용을 다룹니다., ✍️ 강의 제작 동기 '왜 동기vs비동기 개념, 직렬vs병

www.inflearn.com

flatMap() 정리하기. map(), compactMap() 과의 차이

 

flatMap() 정리하기. map(), compactMap() 과의 차이

Swift의 고차함수 중 flatMap()이라는 함수가 있다. 이 함수를 공부하면서 비슷한 이름인 map(), compactMap()과 어떤 차이가 있는지도 정리해야겠다. flatMap() 정리하기. map(), compactMap() 과의 차이 flatMap이

ontheswift.tistory.com

 

 

 

왜 RxSwift가 나오게 되었는지 또 RxSwift가 어떤 원리인지 알아봅시다.

비동기 함수의 콜백지옥

아래는 swift에서 json을 다운로드 하는 함수입니다.

func downloadJson(_ url: String, _ completion: ((String?) -> Void)?) {
    DispatchQueue.global().async {
        let url = URL(string: url)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        DispatchQueue.main.async {
            completion?(json)
        }
    }
}

 

주소를 입력받고 데이터를 받아와 Json으로 파싱해주는 간단한 함수입니다.

네트워크 처리를 하기 위해서 비동기로 처리해주어야 하기 때문에

Json 데이터가 파싱이 끝났을 때 completion의 파라미터로 값을 넘겨주는 모습입니다.

 

그런데 여기서 json을 여러번 다운받게 된다면 콜백지옥이 되어 점점 알아보기 힘들게 됩니다.

func bind() {
        downloadJson(ID_LIST_URL) { json in
            self.idListView = json
            
            downloadJson(ADDRESS_LIST_URL) { json in
                self.addressListView = json
                
                downloadJson(NAME_LIST_URL) { json in
                    self.nameListView = json
                }
            }
        }
    }

 

어떻게 하면 콜백지옥을 해결할 수 있을까요 ??

아니 왜 콜백지옥이 생기게 되었을까요 ??

 

어떻게 하면 비동기로 생기는 데이터를 어떻게 하면 일반 함수처럼 리턴값으로 줄 수 있을까 ??

RxSwift가 나오게 된 근본적인 이유입니다.

비동기 함수를 어떻게 해야 일반함수처럼 사용할 수 있을까요 ??

다양한 방법을 시도해봅시다.

먼저 completion을 없애본다면 아래와 같을 겁니다.

func downloadJson(_ url: String) -> String? {
        var json: String?
        
        DispatchQueue.global().async {
            let url = URL(string: url)!
            let data = try! Data(contentsOf: url)
            let jsonString = String(data: data, encoding: .utf8)
            json = jsonString
        }
        
        return json
    }

 

하지만 async이기 때문에 donwloadJson함수를 호출하게 되면 json은 nil을 반환하게 됩니다.

저희가 생각했던 방법이 아니네요.

 

async를 걷어낸다면 ??

func downloadJson(_ url: String) -> String? {
        let url = URL(string: url)!
        let data = try! Data(contentsOf: url)
        let json = String(data: data, encoding: .utf8)
        return json
}

func bind() {
        DispatchQueue.global().async {
            self.idListView =  downloadJson(ID_LIST_URL)
            self.addressListView = downloadJson(ADDRESS_LIST_URL)
            self.nameListView = downloadJson(NAME_LIST_URL)
        }
    }

저희가 생각했던 방법과 비슷하지만 downloadJson을 호출하려면 반드시 비동기 처리를 해주어야 합니다.

실수로 비동기 처리를 해주지 않고 호출한다면 앱이 죽어버리게 되니까 리스크는 증가하게 됩니다.

 

비슷하긴 하지만 마찬가지로 저희가 원하는 방법은 아니네요

RxSwift, Combine의 원리

그렇다면 어떻게 하면 될까요 ?

바로 값을 한번 감싸서 새로운 데이터 타입을 만들어 주는 것 입니다.

class 나중에생기는데이터<데이터타입> {
    
    private let 데이터가생기면할일: (@escaping (데이터타입) -> Void) -> Void
    
    init(데이터가생기면할일: @escaping (@escaping (데이터타입) -> Void) -> Void) {
        self.데이터가생기면할일 = 데이터가생기면할일
    }
    
    func 나중에데이터가생기면(_ 콜백함수: @escaping (데이터타입) -> Void) {
        데이터가생기면할일(콜백함수)
    }
    
}

 

위 클래스를 새로운 데이터 타입으로 만들어주고 함수를 수정하면 아래와 같습니다.

func downloadJson(_ url: String) -> 나중에생기는데이터<String?> {
        return 나중에생기는데이터 { 콜백함수 in
            
            DispatchQueue.global().async {
                let url = URL(string: url)!
                let data = try! Data(contentsOf: url)
                let json = String(data: data, encoding: .utf8)
                
                DispatchQueue.main.async {
                    콜백함수(json)
                }
            }
            
        }
    }

func bind() {
        let idList: 나중에생기는데이터<String?> = downloadJson(ID_LIST_URL)
        idList.나중에데이터가생기면 { json in
            self.idListView = json
        }
        
        let addressList: 나중에생기는데이터<String?> = downloadJson(ADDRESS_LIST_URL)
        addressList.나중에데이터가생기면 { json in
            self.idListView = json
        }
        
        let nameList: 나중에생기는데이터<String?> = downloadJson(NAME_LIST_URL)
        nameList.나중에데이터가생기면 { json in
            self.nameListView = json
        }
    }

 

차근차근 풀어서 봅시다.

bind()함수의 첫번째 줄에서 downloadJson함수를 사용하여 나중에생기는데이터를 생성해줍니다.

 

이 나중에생기는데이터의 데이터가생기면할일은 아래와 같습니다.

DispatchQueue.global().async {
    let url = URL(string: url)!
    let data = try! Data(contentsOf: url)
    let json = String(data: data, encoding: .utf8)
    
    DispatchQueue.main.async {
        콜백함수(json)
    }
}

 

 

그 다음줄에서 나중에데이터가생기면할일을 호출하고 콜백함수를 지정해줍니다.

이제 연쇄호출로 아래와 같은 순서로 함수가 실행됩니다.

 

나중에데이터가생기면 → 데이터가생기면할일 → 콜백함수

 

idList.나중에데이터가생기면(콜백함수) {
	데이터가생기면할일(콜백함수)
}

// 데이터가생기면할일
DispatchQueue.global().async {
    let url = URL(string: url)!
    let data = try! Data(contentsOf: url)
    let json = String(data: data, encoding: .utf8)
    
    DispatchQueue.main.async {
        콜백함수(json)
    }
}

// 콜백함수
{json in
		self.idListView = json
}

 

이렇게 하게되면 함수를 호출하는 곳에서 비동기처리를 하지 않고도 일반 함수처럼 사용할 수 있게 됩니다.

 

RxCocoa 원리 맛보기

우리는 이제 RxSwift, Combine의 원리를 알게되었습니다. 이를 조금 응용하면 값이 변화할 때마다 뷰를 업데이트 해줄 수 도 있습니다.

class 나중에생기는데이터<데이터타입> {
    
    var 데이터값: 데이터타입 {
        didSet {
            데이터가생긴뒤호출될수있는함수?(데이터값)
        }
    }
    
    var 데이터가생긴뒤호출될수있는함수: ((데이터타입) -> Void)?
    
    init(_ 데이터값: 데이터타입) {
        self.데이터값 = 데이터값
    }
    
    func 바인딩하기(_ 콜백함수: @escaping (데이터타입) -> Void) {
        self.데이터가생긴뒤호출될수있는함수 = 콜백함수
    }
    
}

class ViewController {
    
    private var profile: 나중에생기는데이터<Profile> = 나중에생기는데이터(Profile(id: nil, address: nil, name: nil))
        
    
    func fetch(url: String) {
        // 네트워크 처리
        profile.데이터값 = newProfile
    }
    
    private func bind() {
        profile.바인딩하기 { profile in
            self.idLabel.text = profile.id
            self.addressLabel.text = profile.address
            self.nameLabel.text = profile.name
        }
    }
    
}

 

이처럼 다양하게 응용할 수 있으니 RxCocoa와 같은 라이브러리 코드를 한번씩 보는것을 추천드립니다.

참고자료

[SUB] 시즌2 모임 종합편 입니다.

앨런 Swift문법 마스터 스쿨 (15개의 앱을 만들면서 근본원리부터 배우는 UIKit) 강의 - 인프런

 

앨런 Swift문법 마스터 스쿨 (15개의 앱을 만들면서 근본원리부터 배우는 UIKit) 강의 - 인프런

본 강의는 비공개 강의로 기존의 Swift문법 마스터 스쿨 수강자에게 제공되는 무료 강의입니다., 본 강의는 기존 부트캠프 강의 수강자에게 제공되는 무료 비공개 강의입니다.(기존 부트캠프 강

www.inflearn.com

 

정규 표현식 규칙

  • 숫자
    • [0-9]
    • \d
  • 숫자가 아닌 문자
    • \D
  • 문자(숫자, 글자 모두 포함)
    • \w
  • 한글
    • [가-힣] : 한글 모두
  • 영어
    • [A-Z] : 대문자
    • [a-z] : 소문자
    • [A-Za-z] : 대소문자 모두
  • 문자매칭
      • : 앞의 문자가 0번 이상 반복
      • : 앞의 문자가 1번 이상 반복
    • ? : 0 or 1개의 문자 매칭
    • . : 1개의 문자 매칭
  • 시작과 끝
    • ^* : *로 시작
    • *$ : *로 끝난다
  • 공백
    • \r\n : 줄바꿈
    • \s : 공백(space)
    • \t : tab
    • \f : form feed
  • 특수문자
    • [!@#$%^&*()-=+] = 특수문자
  • 특수기호
    • swift에서는 특수기호 앞에 **\**가 붙어야한다
    • \. == .
    • \\ == \
  • 조건문
    • | : or

정규표현식 예시

  • [0-9]* : 숫자가 1개 이상
    • 0, 01, 0123, 0000, 9876 둥
  • ABC[0-9]{2} : ABC로 시작하고 뒤에 숫자가 2개
    • ABC00, ABC11, ABC22 등
  • [0-9]{3}[-][0-9]{4}[-][0-9]{4} : 전화번호 형식
    • 010-1234-5678 등
  • [\w][@][\w][.][\w]* : 이메일
  • 19[0-9]{2}|20[0-9]{2} : 년도 체크
    • 19XX or 20XX
  • 0[1-9]|1[0-9]|2[0-9]|3[0-1] : 일자
    • 0(1~9) or 1(0~9) or 2(0~9) or 3(0~1)

Swift에서의 정규 표현식 사용법

1. Regex

 

 

Regex 응용

두가지 방법이 있다.

String.함수(of: Regex)와 방법과 Regex.함수(in: String/SubString) 이 있다.

 

firstMatch(of/in) - 컬렉션 내에서 지정된 정규식의 첫 번째 일치 항목을 반환

  • Return Value
    • 일치 항목이 있는 경우 : regex 컬렉션의 첫 번째 일치 항목 반환
    • 일치 항목이 없는 경우 : nil 반환

prefixMatch(of/in) - 문자열의 시작이 지정된 정규식과 일치하는 경우 일치 항목을 반환

  • Return Value
    • 일치 항목이 있는 경우 : 일치 항목 반환
    • 일치 항목이 없거거나 변환에서 regex 오류 발생시 : nil

wholeMatch(of/in) - 이 문자열이 처음에 지정된 정규식과 일치하는 경우 일치 항목을 반환

  • Return Value
    • 일치 항목이 있는 경우 : 일치 항목 반환
    • 일치 항목이 없거나 변환에서 regex 오류 발생시 : nil
    • 즉 wholeMatch는 결국 자기자신 or nil을 반환한다고 생각

matches(of:) - 지정된 정규식과 일치하는 모든 항목을 포함하는 컬렉션을 반환

  • ReturnValue
    • 지정된 정규식과 일치하는 모든 항목을 포함하는 Regex<Output>컬렉션
    • 사용하려면 mapping 필요

 

var input = "00002023"
let regex = /19[0-9]{2}|20[0-9]{2}/

input.firstMatch(of: regex) // ["2023"]
try? regex.firstMatch(in: input) // ["2023"]

input.prefixMatch(of: regex) // nil
try? regex.prefixMatch(in: input) // nil

input = "20230000"
input.firstMatch(of: regex) // ["2023"]
try? regex.firstMatch(in: input) // ["2023"]

input.prefixMatch(of: regex) // ["2023"]
try? regex.prefixMatch(in: input) // ["2023"]

input.wholeMatch(of: regex) // nil
try regex.wholeMatch(in: input) // nil

input = "2023"
input.wholeMatch(of: regex) // ["2023"]
try regex.wholeMatch(in: input) // ["2023"]

input = "19972023"
input.matches(of: regex).map { $0.output } // ["1997", "2023"]

2. NSRegularExpression

var input = "19972023"
do {	 
	let isNumber = try NSRegularExpression(
			pattern: "19[0-9]{2}|20[0-9]{2}", 
			options: NSRegularExpression.options
			) //  options는 안써도 됨
} catch {}

참고

Regex | Apple Developer Documentation

 

Regex | Apple Developer Documentation

A regular expression.

developer.apple.com

[Swift] Regex, 정규표현식 사용하기

 

[Swift] Regex, 정규표현식 사용하기

Meet Swift Regex - WWDC22를 보고 추가로 궁금한 부분을 공부하여 정리해보았습니다. 💡 정규표현식이란? 사용자가 규칙을 세워 패턴을 정의해둔 String 정규 표현식은 패턴을 설명하는 간결한 방법으

borabong.tistory.com

Swift에서의 메모리 관리


Swift에서는 ARC(Automatic Reference Counting)를 사용하여 자동으로 할당/해제를 해준다.

BoostCamper 클래스가 있을 때 Swift에서 힙에 메모리를 어떻게 할당하고 해제하는지 알아보자.

class BoostCamper {
  let	id: String
	let name: String
}

var boostcamper1: BoostCamper? = BoostCamper(id: "S029", name: "이준복")
var boostcamper2: BoostCamper? = boostcamper1

boostcamper2 = nil
boostcamper1 = nil

 

var boostcamper1: BoostCamper? = BoostCamper(id: "S029", name: "이준복")
var boostcamper2: BoostCamper? = boostcamper1
boostcamper2 = nil
boostcamper1 = nil
heap에 할당된 boostcamper가 해제된 모습

 

이처럼 Swift는 rc가 0이 되면 자동으로 힙에 할당한 메모리를 해제 해주게 된다.

ARC 덕분에 Swift에서는 메모리를 신경쓰지 않아도 되는것처럼 느껴지지만 그렇지 않다.

 

인스턴스간 강한 참조 순환


Swift에서 메모리에 문제가 발생하는 경우를 알아보자.

swift는 변수를 선언할 때 디폴트로 강한 참조(Strong)를 하는데 이 때 강한 순환 참조 문제가 발생한다.

아래 예시를 보자.

class BoostCamper {
  	let id: String
	var boostCamp: BoostCamp?
}

class BoostCamp {
	let part: String
	var boostCamper: BoostCamper?
}
var boostcamper: BoostCamper? = BoostCamper(id: "S029")
var boostcamp: BoostCamp? = BoostCamp(part: "iOS")

boostcamper.boostcamp = boostcamp
boostcamp.boostcamper = boostcamper

boostcamper = nil
boostcamp = nil

boostcamper.boostcamp = boostcamp / boostcamp.boostcamper = boostcamper

 

boostcamper = nil
boostcamp = nil

 

코드에선 boostcamper와 boostcamp가 nil이 되었지만 내부에서

서로를 순환해서 참조(Strong Reference Cycle)하고 있기 때문에 힙에서 메모리가 해제되지 않아

메모리 누수(Memory Leak)가 발생한다.

swift는 이와 같은 Strong Reference Cycle을 방지해주기 두 가지 방법을 제공해준다.

 

 

 

Weak ReferenceUnowned Reference


weak와 unowned 키워드는 둘 다 가르키는 인스턴스의 RC를 올라가지 않게 한다.

 

Weak Reference (약한 참조)

  • 변수 선언시 옵셔널 타입만 가능
  • 참조하고 있던 인스턴스가 해제되면 nil을 자동 할당
// 옵셔널만 가능
class BoostCamper {
  let	id: String
	weak var boostCamp: BoostCamp?
}

class BoostCamp {
		let part: String
		weak var boostCamper: BoostCamper?
}

 

Unowned Reference (약한 참조)

  • 변수 선언시 옵셔널과 논 옵셔널 타입 둘 다 가능
  • 참조하고 있던 인스턴스가 해제되면 nil을 자동할당 하지 않음
  • 논 옵셔널일 때도 가르키는 곳의 RC가 안올라기 때문에 가르키는 곳이 비어 있다면 에러 발생
// 옵셔널 논옵셔널 둘 다 가능
class BoostCamper {
  let	id: String
// unowned var boostCamp: BoostCamp
	unowned var boostCamp: BoostCamp?
}

class BoostCamp {
		let part: String
//		unowned var boostCamper: BoostCamper
		unowned var boostCamper: BoostCamper?
}

 

약한 참조로 선언한뒤 실행코드를 똑같이 돌려보면 메모리 구조는 아래와 같다.

 

 

boostcamper.boostcamp = boostcamp / boostcamp.boostcamper = boostcamper
boostcamper = nil
boostcamp = nil

 

둘다 RC가 올라가지 않기 때문에 순환참조가 발생하지 않아 힙에서 메모리가 해제되는 것을 볼 수 있다.

 

 

최종 정리


 

 

참고자료


[Swift] Value, Reference type에 관해 알아야 할 10가지

스위프트 타입별 메모리 분석 실험

[iOS] Swift의 Type과 메모리 저장 공간

iOS) 메모리 구조 (Code, Data, Stack, Heap)

iOS) 메모리 관리 (1/3) - ARC(Automatic Reference Counting)

Swift ) (1) Understanding Swift Performance (Swift성능 이해하기)

Swift Memory Management #1 기초 개념

자동 참조 카운트 (Automatic Reference Counting)

'iOS > Swift' 카테고리의 다른 글

[Swift] RxSwift, Combine 원리 이해하기  (0) 2024.01.20
[Swift] 정규표현식  (1) 2024.01.12
[Swift] Copy-on-Write  (1) 2023.12.28
[Swift] Swift에서의 Value Type, Reference Type  (0) 2023.12.28
[Swift] Swift Package 만들기  (0) 2023.12.12

Copy-on-Write


💡 Collection의 Copy -On-Write

예를 들어 변수 A에 들어있는 Collection값을 새로운 변수 B에 복사한다고 할 때 바로 복사본을 만들지 않습니다.

Reference Type 처럼 B는 A의 참조만 공유합니다. 만약 B의 값에 변경이 발생되면 그 시점에서

A에 대한 새로운 복사본을 만들어 B에게 주고 값을 변경합니다.

Copy-On-Write는 Reference type의 효율성과 Value type의 불변성을 이용하는 것 입니다.

할당은 많이 일어나지만 변경은 그것보다 적게 일어나 복사의 비용을 줄일 수 있고 변경이 일어나면

기존의 값을 복사하여 새로운 값을 만든 뒤 변경하므로 기존의 값도 변하지 않는 불변성을 유지 할 수 있습니다.

 

// 예시
var abouSwift: String = """
Swift is a fantastic way to write software, whether it’s for phones, desktops, servers,
or anything else that runs code. It’s a safe, fast, and interactive programming 
language that combines the best in modern language thinking with wisdom 
from the wider Apple engineering culture and the diverse contributions 
from its open-source community. The compiler is optimized for performance 
and the language is optimized for development, without compromising on either.

Swift is friendly to new programmers. 
It’s an industrial-quality programming language 
that’s as expressive and enjoyable as a scripting language. 
Writing Swift code in a playground lets you experiment 
with code and see the results immediately, 
without the overhead of building and running an app.
"""

var str: String = aboutSwift
str += "BoostCamp is the Best !! "

var abouSwift: String = &ldquo;&rdquo;&rdquo; &hellip; &ldquo;&rdquo;&rdquo;
var str: String = aboutSwift
str += "BoostCamp is the Best !! "

 

 

아래 참고 자료를 확인해보면 더욱 잘 이해 할 수 있다.

 

 

스위프트 타입별 메모리 분석 실험

struct와 class 가 메모리 영역을 어디를 사용하는지 분석한 실험 결과

medium.com

이걸 그림으로 그려보면 다음과 같을 것이다.

 

str1 = “abcd”
str1 = “긴문자열”
str2 = str1
str2 = 새로운 문자열

'iOS > Swift' 카테고리의 다른 글

[Swift] 정규표현식  (1) 2024.01.12
[Swift] Swift에서의 메모리 관리  (0) 2023.12.28
[Swift] Swift에서의 Value Type, Reference Type  (0) 2023.12.28
[Swift] Swift Package 만들기  (0) 2023.12.12
[Swift] Optional(옵셔널)  (0) 2023.06.11

+ Recent posts