Skip to content

ChaminLee/ios-bank-manager

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

62 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿฆ ์€ํ–‰ ๋งค๋‹ˆ์ € ํ”„๋กœ์ ํŠธ

  • ํŒ€ ํ”„๋กœ์ ํŠธ (2์ธ)
  • ๊ตฌํ˜„ ๊ธฐ๊ฐ„ : 2021.12.20 ~ 31 (2 weeks)
STEP 1 STEP 2 STEP 3 STEP 4
โœ… โœ… โœ… โœ…

๋ชฉ์ฐจ


๊ณ ๊ฐ ์ถ”๊ฐ€/์ดˆ๊ธฐํ™” ๊ณ ๊ฐ ์ฒ˜๋ฆฌ
Simulator Screen Recording - iPhone 13 Pro - 2022-02-25 at 18 46 25 Simulator Screen Recording - iPhone 13 Pro - 2022-02-25 at 18 45 30

๐Ÿค” STEP 1

๐Ÿ“ฑ ๊ตฌํ˜„ ๋‚ด์šฉ

1๏ธโƒฃ Linked List

์ž๋ฃŒ ๊ตฌ์กฐ LinkedList
์žฅ์  1. ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€ ๋ฐ ์‚ญ์ œ๊ฐ€ ๋น ๋ฅด๋‹ค
2. ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€/์‚ญ์ œ์‹œ ๋ฉ”๋ชจ๋ฆฌ ์žฌ๋ฐฐ์น˜๊ฐ€ ํ•„์š”์—†๊ธฐ์— ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋‚ฎ๋‹ค
๋‹จ์  1. ์ธ๋ฑ์‹ฑ(๊ฒ€์ƒ‰)์ด ๋Š๋ฆฌ๋‹ค(O(n))

ํ”„๋กœ์ ํŠธ์—์„œ ์š”๊ตฌ๋˜๋Š” LinkedList ์ž๋ฃŒ๊ตฌ์กฐ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์€ํ–‰์— ๋„์ฐฉํ•œ ๊ณ ๊ฐ์€ ์ˆœ์„œ๋Œ€๋กœ ์ฒ˜๋ฆฌ๋  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์—ฌ Singly Linked List๋ฅผ ๊ตฌํ˜„ํ•œ ํ›„ ์ด๋ฅผ ํ™œ์šฉํ•˜์—ฌ Queue๋ฅผ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ Node ํƒ€์ž…์„ nested type์œผ๋กœ ๊ตฌํ˜„

Node ํƒ€์ž…์˜ ๊ฒฝ์šฐ Linked List์™€ ์ง์ ‘์ ์œผ๋กœ ๊ด€๋ จ๋œ ํƒ€์ž…์œผ๋กœ, ์™ธ๋ถ€์—์„œ๋Š” ์‚ฌ์šฉ๋  ์ผ์ด ์—†์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•˜์—ฌ nested type์œผ๋กœ ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

class LinkedList<Element> {
    class Node {
        var value: Element
        var next: Node?
        
        init(value: Element) {
            self.value = value
        }
    }
}

3๏ธโƒฃ Queue์˜ items ์ธ์Šคํ„ด์Šค

Queueํƒ€์ž…์„ ์ดˆ๊ธฐํ™” ํ•  ๋•Œ๋Š” items์— ์š”์†Œ๊ฐ€ ์—†๋Š” ๋นˆ ๊ฒฝ์šฐ์ผ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์—ฌ items๋ฅผ ์˜ต์…”๋„์ด ์•„๋‹Œ ๋นˆ ๊ฐ’์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”ฅ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„

1๏ธโƒฃ Struct vs Class

  • Node๋ฅผ ํด๋ž˜์Šค๋กœ ๋งŒ๋“  ์ด์œ 

    • Node ํƒ€์ž…์˜ ํ”„๋กœํผํ‹ฐ์ธ next๊ฐ€ Node ํƒ€์ž…์ด์–ด์•ผ ํ•˜๋Š” ์ƒํ™ฉ, ์ฆ‰ recursiveํ•œ ๊ด€๊ณ„๋ฅผ ๊ฐ€์ ธ์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— class๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ๋‹ค์Œ ๋…ธ๋“œ์— ๋Œ€ํ•œ ์ฐธ์กฐ๊ฐ€ ํ•„์š”ํ•˜๊ธฐ ๋•Œ๋ฌธ์— class๋ฅผ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.
  • LinkedList, Queue๋ฅผ ํด๋ž˜์Šค๋กœ ๋งŒ๋“  ์ด์œ 

    • struct๋กœ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ ๋‹จ์ 
      1. ๋ณ€๊ฒฝ์œผ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•˜์ง€ ์•Š๋‹ค
        • ์šฐ์„  struct๋กœ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ์ด์œ ๋Š”, ๋‚ด๋ถ€์˜ ํƒ€์ž…์ด class์ด๊ธฐ ๋•Œ๋ฌธ์—, ๊ฐ’ ํƒ€์ž…์ž„์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ณ€๊ฒฝ์œผ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.
        • struct ํƒ€์ž…์œผ๋กœ ๊ตฌํ˜„๋œ Linked List์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค๊ณ  ์ด๋ฅผ ๋ณต์‚ฌํ•œ ํ›„, ๋ณต์‚ฌ๋ณธ ๋‚ด node์˜ ๊ฐ’์„ ๋ณ€๊ฒฝํ•˜๊ฒŒ ๋˜๋ฉด ์›๋ณธ์˜ ๋…ธ๋“œ ๊ฐ’์—๋„ ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.
        • node๊ฐ€ class๋กœ ๊ตฌํ˜„๋˜์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋™์ผํ•œ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๋ฅผ ๊ฐ€๋ฆฌํ‚ค๊ธฐ ๋•Œ๋ฌธ์ด๊ธฐ์— ๋ฐœ์ƒํ•œ ๋ฌธ์ œ๋กœ, ํ•ด๋‹น ํƒ€์ž…์„ struct๊ฐ€ ์•„๋‹Œ class๋กœ ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.
        • struct์•ˆ์— class ํƒ€์ž…์„ ๊ตฌํ˜„ํ•˜๊ฒŒ ๋˜๋ฉด struct์˜ ์žฅ์ ์ธ '๋ณ€๊ฒฝ์œผ๋กœ๋ถ€ํ„ฐ ์•ˆ์ „ํ•˜๋‹ค'๋Š” ๊ฒƒ์ด ์‚ฌ๋ผ์ง‘๋‹ˆ๋‹ค.
      2. ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ
        • ๋˜ํ•œ ํ˜น์‹œ๋‚˜ ์žˆ์„ ์ธ์Šคํ„ด์Šค ๋ณต์‚ฌ์— ๋Œ€ํ•ด์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ๋ฆฌ ๋ฌธ์ œ์— ์œ ์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.
        • ์˜ˆ์‹œ๋กœ, Linked List๊ฐ€ struct์ผ ๋•Œ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค๋กœ ๋ณต์‚ฌ ํ›„ ๋‚ด๋ถ€ node๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ•˜๋Š” ๊ฒฝ์šฐ, node์˜ retain count๋งŒ 1๋งŒํผ ๋‚ด๋ ค๊ฐ€๊ณ , node์˜ deinit์ด ํ˜ธ์ถœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ฅผ class๋กœ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ, Linked List ์›๋ณธ๊ณผ ๋ณต์‚ฌ๋ณธ์˜ ๋ฉ”๋ชจ๋ฆฌ ์ฃผ์†Œ๊ฐ€ ๊ฐ™์•„, ์–ด๋Š ํ•œ ๊ตฐ๋ฐ์„œ node๋ฅผ ์ง€์šฐ๋”๋ผ๋„ ๋ชจ๋‘ ์ œ๊ฑฐ๋˜์–ด node์˜ deinit์ด ์ •์ƒ์ ์œผ๋กœ ํ˜ธ์ถœ๋จ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.
    • ์ด๋Ÿฌํ•œ ์ด์œ ์— ๊ทผ๊ฑฐํ•˜์—ฌ class๋กœ ํ•ด๋‹น ํƒ€์ž…์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ LinkedList์˜ removeAll() ๋ฉ”์„œ๋“œ

Linked List์˜ ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ์ œ๊ฑฐํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋Šฅ์œผ๋กœ ์•„๋ž˜์™€ ๊ฐ™์ด ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

//์ตœ์ข… ์ฝ”๋“œ
func removeAll() {
    head = nil
    tail = nil
}

์ฒ˜์Œ์—๋Š” ์œ„์ฒ˜๋Ÿผ ๊ตฌํ˜„ํ•  ๊ฒฝ์šฐ head์™€ tail ์‚ฌ์ด์˜ ๋…ธ๋“œ๊ฐ€ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ appendํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ณ€์ˆ˜์— Node๋ฅผ ๋„ฃ์–ด ์„ ์–ธํ•˜๋Š” ๊ฒƒ์ด ๋ณ€์ˆ˜์™€ Node ์ธ์Šคํ„ด์Šค ์‚ฌ์ด์— ์ฐธ์กฐ ๊ด€๊ณ„๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋ฉ”๋ชจ๋ฆฌ์— ํ• ๋‹น๋œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ชจ๋“  ๋…ธ๋“œ๋“ค์„ ์ˆœํšŒํ•˜๋ฉฐ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ•ด์ œํ•ด์ฃผ๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

func append(value: Element) {
    let node = Node(value: value)

    if let tailNode = tail {
        tailNode.next = node
    } else {
        head = node
    }

    tail = node
}
//๊ณ ๋ฏผํ–ˆ๋˜ ์ฝ”๋“œ
func removeAll() {
    while let next = head?.next {
        head = nil
        head = next
    }
    
    head = nil
    tail = nil
}

ํ•˜์ง€๋งŒ ๋ฉ”์„œ๋“œ ๋‚ด์˜ ์ง€์—ญ๋ณ€์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— ํ˜ธ์ถœ ์ข…๋ฃŒ์™€ ๋™์‹œ์— ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์ธ์ง€ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด์— Node ํƒ€์ž…์— deinit์„ ๊ตฌํ˜„ํ•˜์—ฌ ํ™•์ธํ•ด๋ณด๋‹ˆ ๊ธฐ์กด removeAll๋ฉ”์„œ๋“œ ์ฝ”๋“œ๋งŒ์œผ๋กœ๋„ ๋ชจ๋“  node๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ ์‹œํ‚ค๊ณ  ์žˆ์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

head๋ฅผ nil๋กœ ๋งŒ๋“ค์–ด ์ค„ ๊ฒฝ์šฐ, ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰ ์ด์ „ ๋…ธ๋“œ๊นŒ์ง€์˜ ๋…ธ๋“œ๋ฅผ ์‚ญ์ œํ•ด์ฃผ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. head๊ฐ€ ์‚ฌ๋ผ์ง€๋ฉด์„œ head.next์— ๋Œ€ํ•œ ์ฐธ์กฐ ๋˜ํ•œ ๊นจ์ง€๊ธฐ ๋•Œ๋ฌธ์— ์—ฐ์‡„์ ์œผ๋กœ ๋…ธ๋“œ๋“ค์ด ์‚ฌ๋ผ์ง€๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด์— ๋งˆ์ง€๋ง‰์œผ๋กœ tail = nil์„ ํ•ด์ฃผ์–ด ๋งˆ์ง€๋ง‰ ๋…ธ๋“œ๋„ ์ •์ƒ์ ์œผ๋กœ ์ œ๊ฑฐํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

3๏ธโƒฃ Unit Test๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ ๋ฐฉ๋ฒ•

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ์ข‹์€ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•ด์•ผํ•˜๋Š”์ง€์— ๋Œ€ํ•œ ๊ณ ๋ฏผ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์ฒ˜์Œ์—๋Š” ์ข‹์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์œ„ํ•ด, XCTestCase์—์„œ sut ํ”„๋กœํผํ‹ฐ๋ฅผ ์•”์‹œ์  ์ถ”์ถœ ์˜ต์…”๋„ ํƒ€์ž…์œผ๋กœ ์ƒ์„ฑํ•˜๊ธฐ๋ณด๋‹ค๋Š”, ์˜ต์…”๋„ ํƒ€์ž…์œผ๋กœ ์ƒ์„ฑํ•˜์—ฌ ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค guard ๋ฌธ์œผ๋กœ ์˜ต์…”๋„์„ ์–ธ๋ž˜ํ•‘ํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ž‘์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜, guard ๋ฌธ์œผ๋กœ ์–ธ๋ž˜ํ•‘ํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋…์„ฑ ์ธก๋ฉด์—์„œ ์ข‹์ง€ ์•Š๊ณ  ํ…Œ์ŠคํŠธ ๋ชฉ์ ์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋Š” ์ข‹์€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค๋Š” ํ…Œ์ŠคํŠธ ๋กœ์ง ์ž์ฒด์— ์ง‘์ค‘ํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ฐ•์ œ ์–ธ๋ž˜ํ•‘์„ ์‚ฌ์šฉํ•ด๋„ ๋œ๋‹ค๋Š” ๊ฒฐ๋ก ์„ ๋‚ด๋ ธ์Šต๋‹ˆ๋‹ค.

class QueueTest: XCTestCase {
    var sut: Queue<Int>!
}
func test_1์ด_๋“ค์–ด์žˆ๋Š”_ํ์—_peekํ–ˆ์„๋•Œ_๊ฐ’๋งŒ_๋ฐ˜ํ™˜๋˜๊ณ _1์ด_์‚ฌ๋ผ์ง€์ง€์•Š๋Š”์ง€() {
    sut.enqueue(value: 1)
    let result = sut.peek()

    XCTAssertEqual(result, 1)
    XCTAssertFalse(sut.isEmpty)
}

sut ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณณ์—์„œ ๊ตณ์ด ์–ธ๋ž˜ํ•‘ํ•ด์ฃผ์ง€ ์•Š๊ณ  ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•ด์กŒ์Šต๋‹ˆ๋‹ค.

4๏ธโƒฃ super.xxx๋Š” ๋งค๋ฒˆ ํ˜ธ์ถœํ•ด์ค˜์•ผ ํ•˜๋Š”๊ฐ€?

override๋ฅผ ํ•˜๋Š” ๊ฒฝ์šฐ์— ํ•˜์œ„ ํด๋ž˜์Šค๊ฐ€ ์ƒ์œ„ ํด๋ž˜์Šค์—์„œ ์ง€์›ํ•˜๋Š” ํ•ด๋‹น ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก super.xxx๋ฅผ ํ˜ธ์ถœํ–ˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋ถ€๋ถ„์ด SOLID ์›์น™ ์ค‘ LSP์™€ ๊ด€๋ จ๋œ ๊ฒƒ์ด๋ผ๋Š” ์ถ”๊ฐ€์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๊ณ  OOP์ ์ธ ๊ด€์ ์—์„œ ๋‹ค์‹œ๊ธˆ ์ƒ๊ฐํ•ด๋ณด๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์•ž์„œ ์–ธ๊ธ‰๋œ ๋ถ€๋ถ„์ฒ˜๋Ÿผ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ์—์„œ๋Š” OOP ๊ด€์ ์œผ๋กœ ์ƒ๊ฐํ•˜์ง€ ์•Š๊ณ  ๋ฌด๊ฒฐ์„ฑ์„ ์šฐ์„ ์‹œํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๊ด€๋ จ๋œ ๋ฉ”์„œ๋“œ์—์„œsuper.xxx๋ฅผ ํ˜ธ์ถœํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค๋Š” ๊ฒƒ์„ ์ธ์ง€ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค” STEP 2

๐Ÿ“ˆ ๊ตฌํ˜„ ํ๋ฆ„ (UML)

๐Ÿ”ฅ ๊ตฌํ˜„ ๋‚ด์šฉ

1๏ธโƒฃ ๊ฐ์ฒด ์ง€ํ–ฅ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์— ๋Œ€ํ•ด

๊ฐ์ฒด๋“ค์€ ๊ฐ๊ฐ ์—ญํ• ๊ณผ ์ฑ…์ž„์„ ๊ฐ–๊ณ , ์„œ๋กœ ๋ฉ”์‹œ์ง€(์š”์ฒญ)๋ฅผ ๋ณด๋‚ด ํ˜‘๋ ฅํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด๊ณ ์ž ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • BankClerk (= ์€ํ–‰์›)
    • ๊ณ ๊ฐ ์—…๋ฌด ์ฒ˜๋ฆฌ ์ง„ํ–‰
    • ์€ํ–‰์—๊ฒŒ ์—…๋ฌด ๋งˆ๊ฐ์„ ์•Œ๋ฆผ
  • Bank (= ์€ํ–‰)
    • ๊ณ ๊ฐ queue ๋ณด์œ 
    • ์€ํ–‰์›์—๊ฒŒ ์ผ์„ ์‹œ์ž‘ํ•  ๊ฒƒ์„ ์•Œ๋ฆผ
    • ์€ํ–‰ ์—…๋ฌด ์ข…๋ฃŒ ๊ด€๋ฆฌ
  • BankManager
    • ์€ํ–‰ ๊ฐœ์ 
    • ์€ํ–‰์˜ ๊ณ ๊ฐ queue๋ฅผ ์ฑ„์šฐ๋Š” ๊ธฐ๋Šฅ
    • ์‚ฌ์šฉ์ž์—๊ฒŒ ์ถœ๋ ฅ๋˜๋Š” ๊ธฐ๋Šฅ ๋‹ด๋‹น
  • Customer (= ๊ณ ๊ฐ)
    • ๋Œ€๊ธฐ์—ด ๋ฒˆํ˜ธ ๋ณด์œ 
    • ์—…๋ฌด ์ฒ˜๋ฆฌ์— ์†Œ์š”๋˜๋Š” ์‹œ๊ฐ„ ๋ณด์œ 

์š”๊ตฌ์‚ฌํ•ญ์—์„œ๋Š” ์€ํ–‰/๊ณ ๊ฐ ํƒ€์ž…๋งŒ ์žˆ์—ˆ์ง€๋งŒ, ์€ํ–‰ ๋‚ด์—์„œ ์‹ค์งˆ์ ์œผ๋กœ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์€ํ–‰์› ํƒ€์ž…์„ ์ถ”๊ฐ€ํ•ด๋ดค์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์ตœ๋Œ€ํ•œ ์œ„ ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฝ”๋“œ๋กœ ์˜ฎ๊ฒจ๋ณด๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ Delegate ํŒจํ„ด ๊ตฌํ˜„์— ๋Œ€ํ•œ ๋ถ€๋ถ„

๋‘ ๊ฐ€์ง€ ๋ถ€๋ถ„์— ๋Œ€ํ•ด Delegate ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

  1. Bank <-> BankClerk : ์„œ๋กœ ์ž‘์—…์„ ์š”์ฒญํ•˜๊ณ , ์ž‘์—…์ด ์ข…๋ฃŒ๋จ์„ ์•Œ๋ฆฌ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค
  2. Bank์™€ BankClerk -> BankManager (BankDelegate, BankClerkDelegate ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒ) : Bank์™€ BankClerk๊ฐ€ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ค„ ๋ฉ”์„ธ์ง€๋ฅผ ์ง์ ‘ ํ”„๋ฆฐํŠธํ•˜์ง€ ์•Š๊ณ  BankManager์—์„œ ๋Œ€์‹  ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. (์ถ”ํ›„ UIApp์œผ๋กœ ์ „ํ™˜์‹œ, ํ˜„์žฌ print๋˜๊ณ  ์žˆ๋Š” ๋ถ€๋ถ„๋“ค์ด UI์š”์†Œ์™€ ์—ฐ๊ฒฐ๋  ๋ถ€๋ถ„์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์˜€์Šต๋‹ˆ๋‹ค.)

3๏ธโƒฃ Delegate ํŒจํ„ด์˜ ์ˆœํ™˜์ฐธ์กฐ ๋ฐฉ์ง€

Bank์™€ BankClerk๊ฐ€ ์„œ๋กœ ์ฐธ์กฐํ•˜๊ณ  ์žˆ๋Š” ๊ตฌ์กฐ๊ฐ€ ๋˜์–ด ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์„๊ฑฐ๋ผ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด์— BankClerk๊ฐ€ ๊ฐ€์ง€๋Š” bank๋ฅผ weak๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜๋ฅผ ๋ฐฉ์ง€ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿค” STEP 3

๐Ÿ”ฅ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„

1๏ธโƒฃ ๋™์‹œ์„ฑ ํ”„๋กœ๊ทธ๋ž˜๋ฐ

์€ํ–‰์›์˜ ์ˆ˜๋ฅผ ์“ฐ๋ ˆ๋“œ๋ผ๊ณ  ์ƒ๊ฐํ•˜์˜€๊ณ , ๊ณ ๊ฐ์˜ ์—…๋ฌด ์ข…๋ฅ˜์— ๋”ฐ๋ผ depositQueue, loanQueue๋ฅผ ๋งŒ๋“ค์–ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. depositQueue๋Š” concurrentํ•˜๊ฒŒ loanQueue๋Š” serialํ•˜๊ฒŒ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

while๋ฌธ์˜ ์กฐ๊ฑด์œผ๋กœ customerQueue์—์„œ customer์„ dequeueํ•˜๋Š” ๊ฒƒ์„ ์„ค์ •ํ•˜์—ฌ ๊ณต์œ ์ž์›์—๋Š” ํ•œ ๋ฒˆ์”ฉ๋งŒ ์ ‘๊ทผํ•˜๋„๋ก ์„ค๊ณ„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

deposit ์ผ€์ด์Šค์—์„œ๋Š” ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” concurrent ํ์—์„œ semaphore๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ์— ์ ‘๊ทผ๊ฐ€๋Šฅํ•œ ์Šค๋ ˆ๋“œ์˜ ์ˆ˜๋ฅผ 2๋กœ ๋‘์–ด์„œ deposit์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์€ํ–‰์›์ด ๋™์‹œ์— ๋‘๋ช…์ด ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด์— loan ์ผ€์ด์Šค์—์„œ๋Š” ์ผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์€ํ–‰์›์ด ํ•œ ๋ช…์ด๊ธฐ๋•Œ๋ฌธ์— ๋น„๋™๊ธฐ์ ์œผ๋กœ ๋™์ž‘ํ•˜๋Š” serialํ๋ฅผ ์‚ฌ์šฉํ•˜์˜€์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ fatalError๋ฅผ ์‚ฌ์šฉํ•œ ์ด์œ 

static func createRandomTask() -> Self {
    guard let task = Self.allCases.randomElement() else {
        fatalError("๋žœ๋คํ•œ Task๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
    }
    return task
}

Task enum๋‚ด์— ๋žœ๋คํ•œ task๋ฅผ ์—ด๊ฑฐํ˜•์˜ ๋ชจ๋“  case์—์„œ ๊ณจ๋ผ์„œ ๋ฆฌํ„ดํ•ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ์—์„œ fatalError()์„ ์‚ฌ์šฉํ•˜์˜€๋Š”๋ฐ์š”, ์ฒ˜์Œ์—๋Š” error enum์„ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด์ค˜์„œ ์—๋Ÿฌ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ๋ ค๊ณ  ํ–ˆ์ง€๋งŒ, ๋กœ์ง์ƒ ์ ˆ๋Œ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์—†๋Š” ์—๋Ÿฌ๋ผ๊ณ  ํŒ๋‹จํ•˜์—ฌ fatalError๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. (apple์ด ๊ฒ€์ฆํ•œ ๋ฉ”์„œ๋“œ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.)

3๏ธโƒฃ BankClerk์˜ ์—ญํ• 

์•ž์„œ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„์— ๋”ฐ๋ผ ๊ตฌํ˜„ํ•˜๋‹ค ๋ณด๋‹ˆ BankClerk๊ฐ€ delegate ํŒจํ„ด์„ ํ†ตํ•ด Bank๋ฅผ ์•Œ๊ฒŒํ•˜๋Š” ๋ฐฉ์‹์ด ํ•„์š”์—†๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ์ด ๊ด€๊ณ„๋ฅผ ์ œ๊ฑฐํ•ด์ฃผ์—ˆ๊ณ , ๊ธฐ์กด customer queue์—์„œ dequeueํ•˜๋˜ ๋ถ€๋ถ„์„ Bank๊ฐ€ ํ•˜๋„๋ก ์ด์ „ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

4๏ธโƒฃ ์ด ์—…๋ฌด ์‹œ๊ฐ„ ์ธก์ •ํ•˜๋Š” ๋ฐฉ์‹

๊ธฐ์กด์—๋Š” BankClerk๊ฐ€ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณ ๊ฐ์˜ ์†Œ์š” ์‹œ๊ฐ„์„ ์ผ์ผ์ด ํ•ฉํ•˜์—ฌ ์ด๋ฅผ ๊ณ„์‚ฐํ–ˆ์—ˆ๋Š”๋ฐ, ์‹œ์Šคํ…œ ์‹œ๊ฐ„์„ ํ™œ์šฉํ•˜์—ฌ ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. CFAbsoluteTimeGetCurrent() ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ์ž‘/๋ ์‹œ๊ฐ„์„ ๊ธฐ๋กํ•˜๊ณ  ์ด ์‹œ๊ฐ„๋“ค์˜ ์ฐจ๋ฅผ ์ด ์—…๋ฌด์‹œ๊ฐ„์œผ๋กœ ํ•˜์˜€์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜, ์‹œ์Šคํ…œ ์‹œ๊ฐ„์€ ์™ธ๋ถ€ ์‹œ๊ฐ„ ์ฐธ์กฐ์™€์˜ ๋™๊ธฐํ™” ๋˜๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์‹œ์Šคํ…œ ์‹œ๊ฐ„์„ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ ๊ฐ์†Œํ•  ์ˆ˜ ์žˆ๊ธฐ๋•Œ๋ฌธ์— DispatchTime ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// ๊ธฐ์กด ๋ฐฉ์‹
let startTime = CFAbsoluteTimeGetCurrent()
let endTime = CFAbsoluteTimeGetCurrent()

let totalProcessingTime = endTime - startTime
        
// ๊ฐœ์„  ๋ฐฉ์‹
let startTime = DispatchTime.now()
let endTime = DispatchTime.now()

let totalProcessingTime = Double(endTime.uptimeNanoseconds - startTime.uptimeNanoseconds)

5๏ธโƒฃ Bank ํƒ€์ž…์˜ open ๋ฉ”์„œ๋“œ ๊ตฌํ˜„ ๋ฐฉ์‹

Queue์—์„œ ๋งจ ์ฒ˜์Œ ์š”์†Œ๋ฅผ ์ œ๊ฑฐํ•˜์ง„ ์•Š๊ณ  ํ™•์ธ๋งŒ ํ•˜๋Š” peek()๋ฅผ ํ†ตํ•ด customer์˜ task๋ฅผ ํ™•์ธํ•˜๊ณ  ๊ฐ๊ฐ์˜ queue์— ํ• ๋‹น ํ•œ ํ›„ ๊ฐ ์“ฐ๋ ˆ๋“œ ๋‚ด์—์„œ customerQueue์— ์ ‘๊ทผํ•˜์—ฌ dequeueํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„ํ•ด๋ณด๋ ค ์‹œ๋„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‹ค๊ฐ€ while๋ฌธ์ด ๋น„์ •์ƒ์ ์œผ๋กœ ๋งŽ์ด ๋Œ๋ฉฐ ํ”„๋กœ๊ทธ๋žจ์ด ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์•„์„œ, ๊ฐ queue์—์„œ customerQueue์— ๋™์‹œ์— ์ ‘๊ทผํ•˜๋Š” ์ƒํ™ฉ์ด ์•„์˜ˆ ์ƒ๊ฒจ๋‚˜์ง€ ์•Š๋„๋ก customerQueue.dequeue() ๋กœ ๋ฆฌํ„ดํ•œ ์š”์†Œ๋ฅผ ๋ฐ”๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง์œผ๋กœ ๊ฐœ์„ ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ฐœ์„  ์ „

func open() {
    let group = DispatchGroup()
    let depositQueue = DispatchQueue(label: "deposit", attributes: .concurrent)
    let loanQueue = DispatchQueue(label: "loan")
    let semaphore = DispatchSemaphore(value: 2)

    while !customerQueue.isEmpty {
        switch customerQueue.peek()?.task {
        case .deposit:
            depositQueue.async(group: group) {
                semaphore.wait()
                self.bankClerk.work() //์—ฌ๊ธฐ์„œ dequeue๋ฅผ ํ•œ๋‹ค.
                semaphore.signal()
            }
        case .loan:
            loanQueue.async {
                self.bankClerk.work() //์—ฌ๊ธฐ์„œ dequeue๋ฅผ ํ•œ๋‹ค.
            }

        default:
            return
        }
    }

    group.wait()
}

๊ฐœ์„  ํ›„

func open(timer: BankTimer) {
    timer.start()

    let group = DispatchGroup()
    let semaphore = DispatchSemaphore(value: 2)
    let depositQueue = DispatchQueue(label: "deposit", attributes: .concurrent)
    let loanQueue = DispatchQueue(label: "loan")
    let bankGroup = DispatchGroup()

    while let customer = customerQueue.dequeue() {
        switch customer.task {
        case .deposit:
            depositQueue.async(group: group) {
                semaphore.wait()
                self.bankClerk.work(with: customer)
                semaphore.signal()
            }
        case .loan:
            loanQueue.async(group: group) {
                self.bankClerk.work(with: customer)
            }
        }
    }

    group.notify(queue: DispatchQueue.main) {
        timer.stop()
    }
}

๐Ÿค” STEP 4

๐Ÿ”ฅ ๊ณ ๋ฏผํ–ˆ๋˜ ๋ถ€๋ถ„

1๏ธโƒฃ UI ์—…๋ฐ์ดํŠธ

UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋Š” ์ฝ”๋“œ๋“ค์€ ๋ชจ๋‘ main ์“ฐ๋ ˆ๋“œ ๋‚ด์—์„œ ์ง„ํ–‰ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์— ๋ฌธ์ œ์—†์ด ์ž‘๋™ํ•˜๋Š” ์  ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ์€ํ–‰ ์—…๋ฌด๊ฐ€ ์ง„ํ–‰๋˜๋Š” ์“ฐ๋ ˆ๋“œ๋Š” custom queue(serial/concurrent)์ด๊ธฐ์— UI์˜ ๊ฒฝ์šฐ ๋‹ค๋ฅธ ์“ฐ๋ ˆ๋“œ์ธ ๋ฉ”์ธ ์“ฐ๋ ˆ๋“œ์—์„œ ์ •์ƒ์ ์œผ๋กœ ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

2๏ธโƒฃ CustomView์˜ ํ™œ์šฉ

์ž์ฃผ ์“ฐ์ด๋Š” UIStackView๋‚˜ UILabel, UIButton๋“ค์„ ์ปค์Šคํ…€ ๋ทฐ๋กœ ๊ตฌํ˜„ํ•˜์—ฌ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค. ์ปค์Šคํ…€ ๋ทฐ๋ฅผ ๋งŒ๋“ค ๋•Œ ํ•„์š”ํ•œ ์†์„ฑ์„ ์ด๋‹ˆ์…œ๋ผ์ด์ €๋ฅผ ํ†ตํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ํ†ตํ•ด ์ปค์Šคํ…€ ๋ทฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ๋ณด๋‹ค๋Š” ํด๋กœ์ € ๋ฐฉ์‹์œผ๋กœ UI์ธ์Šคํ„ด์Šค๋ฅผ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ฐฉ์‹์ด ๋” ํšจ์œจ์ ์ด๋ผ๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜๊ณ  ์ถ”ํ›„ ๋ฐ˜์˜ํ•ด๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

3๏ธโƒฃ wait() ๊ณผ notify()

Bank ํƒ€์ž…์˜ open() ๋ฉ”์„œ๋“œ์—์„œ DispatchGroup์„ ์‚ฌ์šฉํ•˜์—ฌ group์•ˆ์— ๋“ค์–ด๊ฐ„ ์ž‘์—…๋“ค์ด ๋ชจ๋‘ ๋๋‚  ๋•Œ ๋น„๋™๊ธฐ์ ์œผ๋กœ timer์˜ stop ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ์—๋Š” Bank์˜ open() ๋ฉ”์„œ๋“œ์—์„œ ๋งˆ์ง€๋ง‰ ๋ผ์ธ์— group.wait() ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”์ธ ์“ฐ๋ ˆ๋“œ๋ฅผ ๋ธ”๋กํ–ˆ์—ˆ๋Š”๋ฐ, ์ด ๋•Œ๋ฌธ์— ๋ชจ๋“  ์ž‘์—…์ด ์™„๋ฃŒ๋˜๊ณ  ๋‚˜์„œ์•ผ UI๊ฐ€ ํ•œ๊บผ๋ฒˆ์— ์—…๋ฐ์ดํŠธ ๋˜๋Š” ์ƒํ™ฉ์ด ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค. ์ด์— ๋™์‹œ์— UI ๋˜ํ•œ ์—…๋ฐ์ดํŠธ๋ฅผ ์‹œ์ผœ์ฃผ๊ธฐ ์œ„ํ•ด, ์ž‘์—…์„ ๋™๊ธฐ์ ์œผ๋กœ ๊ธฐ๋‹ค๋ฆฌ์ง€ ์•Š๊ณ  ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ธฐ๋‹ค๋ ค์ฃผ๋„๋ก ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด ๋•Œ dispatchGroup์—์„œ wait()๋Š” synchronous ํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ณ , notify()์˜ ๊ฒฝ์šฐ asynchronous ํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค๋Š” ๊ฒƒ์„ ์ด์šฉํ•˜์—ฌ notify()๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค.

group.notify(queue: DispatchQueue.main) {
    timer.stop()
}

4๏ธโƒฃ UIStackView ํ™œ์šฉ

์ฝ”๋“œ๋กœ ํ™”๋ฉด์„ ๊ทธ๋ฆด ๋•Œ ์ตœ๋Œ€ํ•œ stackView๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ตฌ์„ฑํ•ด๋ณด๋ ค๊ณ  ๋…ธ๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด ๋•๋ถ„์— ๊ฐœ๋ณ„ UI์š”์†Œ์— auto layout์„ ์ ์šฉํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๊ณต์ˆ˜๊ฐ€ ๋œ ๋“ค๊ฒŒ ๋œ ๊ฒƒ ๊ฐ™์•„ ์ด์ ์„ ์ทจํ–ˆ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค! ์ถ”๊ฐ€ ํ”ผ๋“œ๋ฐฑ์„ ํ†ตํ•ด UIStackView, Auto layout์˜ ์„ฑ๋Šฅ์„ ํ™•์ธํ–ˆ์œผ๋ฉฐ ์ฒด๊ณ„์ ์œผ๋กœ ์ž˜ ์‚ฌ์šฉํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%