호댕의 iOS 개발

[TWL] 21.12.27 ~ 21.12.31 (동시성 프로그래밍, responder chain, event, HTTP, HTTPS, RESTFul API, Timer 등) 본문

Software Engineering/TIL

[TWL] 21.12.27 ~ 21.12.31 (동시성 프로그래밍, responder chain, event, HTTP, HTTPS, RESTFul API, Timer 등)

호르댕댕댕 2022. 1. 2. 10:56

이번 주는 약간 체력의 부족함을 느낀 한 주였다. 

유난히 다른 주보단 피곤했던 것 같다. 그래도 이번 주도 많은 것들을 새롭게 배울 수 있었던 한 주였다. 

 


동시성 프로그래밍 복습

아직 동시성 프로그래밍이 완전히 머리 속에 정립되지 않은 것 같아 복습 중이다.

여러가지 작업을 시분할로 나눠서 번갈아가며 처리하는 것이 바로 동시성 프로그래밍이다. 동시성 프로그래밍은 소프트웨어에서의 멀티 스레딩을 이용한 기술로, 여러 개의 스레드를 이용하여 여러 작업을 동시에 처리하게 된다.

 

<중요 개념 정리>

  • 코어: CPU의 핵심으로 CPU에서 실제로 일을 처리하게 된다. 코어는 한 번에 한 가지 일만 처리할 수 있다.
  • 스레드: 프로세스(프로그램) 내부에서의 작업 단위가 되는 가상의 작업 단위를 의미한다. 여기서 스레드는 작업을 처리하는 친구들이다.
  • 병렬 프로그래밍: 여러 개의 코어가 하나의 작업을 분담해서 처리하는 것을 의미한다. 이는 물리적인 개념으로 CPU가 여러 개 있는 경우만 가능하다. (싱글코어에선 병렬 프로그래밍이 불가능하다)
  • 동시성 프로그래밍: 물리적으로 보면 하나의 코어이기 때문에 하나의 일만 처리할 수 있지만 논리적으론 여러 가지 일을 동시에 처리할 수 있다. 즉, 여러 개의 스레드를 사용하여 동시에 여러 작업을 처리하는 것이다. (실제로는 여러 작업을 나열하여 조금씩 번갈아가며 작업을 하는데 그 속도가 빨라 동시에 작업하는 것처럼 보이게 된다. 여기서 번갈아가며 작업을 하는 것을 context switching이라고 한다)
  • 동기: 작업이 끝나기를 기다리고 다음 작업을 하게 된다.
  • 비동기: 작업이 끝나기를 기다리지 않고, 바로 다음 코드 블록을 실행하게 된다.

⛔️ 동시성과 비동기를 헷갈리지 말자!!

  • 동시성: 직렬(Serial)이냐, Concurrent인지는 스레드가 단일 스레드인가, 다중 스레드인가로 구분한다. (동시성이라면 다중 스레드 - Concurrent)
  • 동기/비동기: 스레드 수와는 무관하게 작업이 끝나기를 기다리는지, 기다리지 않는지로 구분된다.

 

코어가 한 번에 하나의 일만 한다고 했는데 그렇다면 일의 단위는 무엇일까?

관점에 따라 다를 수는 있지만 Swift에선 하나의 클로저라고 봐도 좋다.

 

responder chain

UIKit Hierarchy

공식문서 Article을 어떻게 봐야 할까?

일단 문서에는 항상 See Also가 있다. 여기에 있는 링크를 통해 관련 문서도 확인하자.

({} 표시가 되어있는 곳은 예제 코드를 볼 수 있는 곳이다. )

만약 관련 문서도 확인하고 다시 링크에 이전 문서를 보여주고 있다면 해당 문서의 기본 사이클은 봤다고 할 수 있다. 이렇게 보는 문서의 폭을 확장해 나가며 공부를 하자!!

하지만 유의해야 할 것은 너무 범위가 넓어지면 현재 공부해야하는 내용에서 벗어날 수도 있다. 기준을 어느 정도 정해두고 공식문서를 보는 것도 필요하다.

 

사용자가 받아들일 수 있는 이벤트에는 뭐가 있을까?

  • case touches The event is related to touches on the screen.
  • case motion The event is related to motion of the device, such as when the user shakes it.
  • case remoteControl The event is a remote-control event.
  • case presses The event is related to the press of a physical button.

이러한 이벤트들에서 사용자들이 자주하는 것들을 모아놓은 것이 제스쳐이다.

  • Standard Gestures
    • Tap, Drag, Flick, Swipe, Double tap, Pinch, Three-finger pinch, Three-finger swipe, Touch and hold, Rotate, Shake

 

활동학습을 하다 궁금했던 내용


Using Responders and the Responder Chain to Handle Events 문서에 보면 다음과 같이 나와있다.

"Gesture recognizers receive touch and press events before their view does."

따라서 항상 Gesture recognizer가 먼저 호출이 되고 View의 메서드가 호출될 것이라고 생각했다. 하지만 실제 시뮬레이터를 통해 확인해보니 그렇지 않았다.

UITextField를 상속받은 MyField의 touchesBegan이 먼저 호출됐고 그 후 ViewControllergesture recognizer와 관련된 메서드가 호출됐다.

또한 이후 TextField의 입력값을 받기 위한 키보드도 활성화되지 않았다.

 

내가 이해했을 때 그 이유는 일단 화면을 터치하게 되면 MyField의 touchesBegan이 호출되게 된다. 만약 이게 탭으로 인식될 지, 아니면 long press로 인식될 지는 touch를 한 후 손가락을 언제 떼는 지에 따라 달라지게 된다.

그러다가 손가락을 탭을 할 때처럼 빠르게 떼면 이는 탭으로 인식하고 gesture recognizer와 관련된 메서드가 호출되는 것이다. 또한 이미 gesture recognizer가 first responder로 탭 이벤트를 처리했다고 해서 다른 메서드들은 이후 호출되지 않는다. 즉, responder chain이 gesture recognizer에서 끝나게 되는 것이다.

만약 long press로 길게 누르게 되면 tap gesture recognizer가 호출되지 않고 정상적으로 text field가 활성화된다.

 

UIImageView에 Gesture Recognizer 넣어보기!

만약 이미지를 꾹 눌렀을 때 어떤 동작을 하도록 구현하고 싶다면 어떻게 해야할까?

일단 이미지는 원래 user interaction이 안되는 객체라 Attributes inpector에 들어가서

User Interation Enabled

를 체크해줘야 한다.

그 후 적절한 gesture recognizer를 ImageView에 올려주면 이미지를 눌렀을 때 행동을 정의할 수 있다.

User Interaction을 활성화하는 것은 코드로도 간단하게 구현할 수 있다.

@IBOutlet weak var image: UIImageView!

override func viewDidLoad() {
super.viewDidLoad()
image.isUserInteractionEnabled = true
}

위 코드처럼 isUserInteractionEnabled 프로퍼티만 true로 주면 된다.

 

프로젝트 구현

고민했던 부분

💭 은행창구매니저에서 직원이 3명인 것처럼 어떻게 코드를 구현할까?

3명이 은행 창구에서 일하는 것처럼 구현하기 위해선 Dispatch Queue에 Thread가 3개만 접근할 수 있도록 구현해야 했다. 처음에는 Serial Queue를 직원마다 각각 생성해주고 Concurrent로 일을 시키게 되면 가능할 것이라고 생각했다. (하지만 이 방법은 계속 시도를 해보다가 다른 방법으로 선택하게 됐다. 나중에 이 방법으로도 한 번 구현해보면 좋을 것 같다)

그래서 일단 global.async로 Queue를 만들어 taskType에 따라 semaphore를 다르게 주는 방법을 고민했다. 이외에도 다음처럼 구현을 했다.

  • async로 쓰레드에 일을 준 후 바로 종료되었다는 메서드가 실행되지 않도록 DispatchGroup() 을 만들어 작업이 끝날 때까지 기다릴 수 있도록 dispatchGroup.wait()을 해줬다.

🤔 Semaphore를 어떻게 사용하는 것이 좋을까?

처음에는 총 직원 수가 3명인 만큼 Semaphore의 value를 3개로 두고 DispatchQueue의 클로저 내부에서 Semaphore를 대출(1명), 예금(2명)으로 각각 나누어 적용해보려고 했다.

하지만 이렇게 하니 While문 내부에 있는 Semaphore는 작동을 하지 않았다.

아직 그 이유는 잘 모르겠다...

private func startBankTask() {
        let depositSemaphore = DispatchSemaphore(value: Bank.depositClerkCount)
        let loanSemaphore = DispatchSemaphore(value: Bank.loanClerkCount)

        while let taskType = clients.informTaskType(),
              let clientIdentifier = clients.dequeueWaitingLine() {
              /*
              let depositSemaphore = DispatchSemaphore(value: Bank.depositClerkCount)
              let loanSemaphore = DispatchSemaphore(value: Bank.loanClerkCount)
              */

            DispatchQueue.global().async(group: dispatchGroup) {
                switch taskType {
                case .deposit:
                    depositSemaphore.wait()
                    self.clerk.work(for: clientIdentifier, task: taskType)
                    depositSemaphore.signal()
                case .loan:
                    loanSemaphore.wait()
                    self.clerk.work(for: clientIdentifier, task: taskType)
                    loanSemaphore.signal()
                }
            }
        }
    }

Escaping closure captures mutating 'self' parameter

구조체로 타입을 만들어 진행하다가 Escaping closure captures mutating 'self' parameter 에러를 만나게 되었다. 여기서 Escaping Closure는 DispatchQueue를 만드는 클로저라고 판단됐다. 이 안에 BankManager에서 만든 메서드를 사용하려고 했더니 해당 에러가 발생하는 것이었다.

이를 해결하려면 Class로 타입을 변경하거나, 함수를 하나로 합치고 함수 내부에도 인스턴스를 생성해줘야 했다. 아직 Escaping Closure에 대해 정확히 이해를 하지 못하여 그 부분에 대해선 좀 더 공부를 해봐야겠다.

 

 

Request Method

  • GET 은 언제 어떻게 사용되나요? -> 자료를 요청할 때 사용 => URL에 바로 보임
  • POST 는 언제 어떻게 사용되나요? -> 자료의 생성을 요청할 때 사용 => 개발자 디버거를 통해 body에서 확인해야 함
  • PUT 은 언제 어떻게 사용되나요? -> 자료의 전체 수정을 요청할 때 사용
  • PATCH 는 언제 어떻게 사용되나요? -> 자료의 부분 수정을 요청할 때 사용
  • DELETE 는 언제 어떻게 사용되나요? -> 자료의 삭제를 요청할 때 사용

request-query

  • URL에 직접 사용한다.
  • URL에서 ? 뒤에 변수로 넣게 된다. (편지 봉투 역할)
  • 서버에 미리 query가 구현되어 있으며 어떤 데이터를 요청하는지에 대해 작성한다. (key와 value로 찾게 된다)
  • <단점> 누구나 읽을 수 있다. 적는 데도 길이의 제한이 있다.

request-body(payload)

  • 주소에는 확인할 수 없는 XML, JSON 등의 데이터를 담는다. (편지지 역할)
  • 서버에서 정의한 데이터 타입만 받을 수 있다.

 

Request Header

Content-Type

주고 받을 데이터의 타입으로 encoding/deconding할 때의 규칙이다.

파일을 전송할 때 주로 사용되는 content-type

Content-Type 의 종류.

  1. Multipart Related MIME 타입
  • Content-Type: Multipart/related <-- 기본형태
  • Content-Type: Application/X-FixedRecord
  1. XML Media의 타입
  • Content-Type: text/xml
  • Content-Type: Application/xml
  • Content-Type: Application/xml-external-parsed-entity
  • Content-Type: Application/xml-dtd
  • Content-Type: Application/mathtml+xml
  • Content-Type: Application/xslt+xml
  1. Application의 타입
  • Content-Type: Application/EDI-X12 <-- Defined in RFC 1767
  • Content-Type: Application/EDIFACT <-- Defined in RFC 1767
  • Content-Type: Application/javascript <-- Defined in RFC 4329
  • Content-Type: Application/octet-stream : <-- 디폴트 미디어 타입은 운영체제 종종 실행파일, 다운로드를 의미
  • Content-Type: Application/ogg <-- Defined in RFC 3534
  • Content-Type: Application/x-shockwave-flash <-- Adobe Flash files
  • Content-Type: Application/json <-- JavaScript Object Notation JSON; Defined in RFC 4627
  • Content-Type: Application/x-www-form-urlencode <-- HTML Form 형태
  • x-www-form-urlencode와 multipart/form-data은 둘다 폼 형태이지만 x-www-form-urlencode은 대용량 바이너리 테이터를 전송하기에 비능률적이기 때문에 대부분 첨부파일은 multipart/form-data를 사용하게 된다.
  1. 오디오 타입
  • Content-Type: audio/mpeg <-- MP3 or other MPEG audio
  • Content-Type: audio/x-ms-wma <-- Windows Media Audio;
  • Content-Type: audio/vnd.rn-realaudio <-- RealAudio; 등등
  1. Multipart 타입
  • Content-Type: multipart/mixed: MIME E-mail;
  • Content-Type: multipart/alternative: MIME E-mail;
  • Content-Type: multipart/related: MIME E-mail <-- Defined in RFC 2387 and used by MHTML(HTML mail)
  • Content-Type: multipart/formed-data <-- 파일 첨부
  1. TEXT 타입
  • Content-Type: text/css
  • Content-Type: text/html
  • Content-Type: text/javascript
  • Content-Type: text/plain
  • Content-Type: text/xml

Content Type이 왜 필요할까?

content type이 없다면 클라이언트에서 보내는 데이터의 타입을 서버에서 알 수 없다. 따라서 올바르게 디코딩을 하기 위해 Content Type을 명시해줘야 한다.

 

Status Code

  • 200 (OK) → 오류 없이 전송 성공. ⇒ 에러가 나지 않은 경우 전부 200을 쓸 순 있다.
  • 자원 존재하고 특이 사항이 없다. 보통 Get에서 많이 사용된다.
  • 201 (Created) → Post에서 사용. 새로운 자원이 생성되었다.자원이 바로 생성될 지 모르기 때문에 link로 접속할 수 있는 링크를 주는 것이다.
  • response header에 link(접속할 수 있는 주소)가 존재한다. → link와 함께 body를 옵셔널로 사용할 수 있다.
  • 202 (Accepted) → 서버가 클라이언트의 요청을 수락함. (요청을 처리 중인 상황)
  • 이미 자원이 생성되어 있고 이 자원을 수정했을 때 사용한다. Put이나 Patch 사용
  • 204 (Non Content) → 클라이언트의 요구를 처리했으나 전송할 데이터가 없음.ex: 방금 쇼핑몰에 가입했는데 구매 목록을 요청하는 경우
  • 비정상적인 요청인 경우 404 Not Found
  • 굉장히 특수한 명세. 요청은 정상적이나 줄 데이터가 없다. (parsing할 body가 존재하지 않는다)
  • 400 (Bad Request) → 잘못된 요청을 한 경우
  • ex: 회원가입을 요청할 때 필수적으로 id와 password를 써야 하는데 이 명세를 지키지 않고 다른 것을 작성한 경우 (잘못된 문법으로 요청한 경우) ⇒ body의 형태가 서버에서 지원하는 형태와 다른 경우
  • 401 (Unauthorized) → 유효한 인증자격 증명이 없어 요청 자격이 증빙되지 않는 경우 인증이 만료되었거나 인증이 없는 경우 → 비밀번호가 다른 경우, private 레포에 다른 사람이 접근하려고 하는 경우 (올바르게 로그인 하고 access token을 발급받게 된다. → 개인화된 데이터를 다루다가 이 에러를 만나게 된다. ⇒ 이 경우 권한이 이미 만료된 것 (token의 유효시간이 지난 것 → 새로운 token을 발급받아라))
  • 최근에는 재 로그인을 요청하는 경우가 거의 없다. token을 사용자 모르게 갱신이 되고 있을 수 있다.
  • 404 (Not Found) → 아예 없는 주소
  • 406 (Not Acceptable) → 이메일 포맷은 올바르게 보내긴 했으나 없거나 이미 존재하는 이메일일 경우
  • 요청의 본문에 이상한 값이 있다!!

400번대의 공통점 → 클라이언트가 뭔가를 잘못했다.

  • 500 (Internal Server Error) → 서버 측에서 오류가 났다.
  • 502, 504 → 근본적으로 네트워크 상태가 잘못되어 있어!! 클라이언트 개발자가 고칠 수 없다.

Status Code는 왜 사용할까?

현재 응답의 수신 상태를 한 번에 확인할 수 있기 때문에 사용한다.

RESTFul API

최근 대부분이 RESTFul API의 명세를 지키려고 한다.

  • 소문자를 사용한다
  • 단어의 결합은 -(케밥) 를 사용하여 표현한다.
  • 행위(CRUD)는 url에 포함하지 않는다.
  • Content-Type은 application/json 을 우선으로 제공한다. (쉽고 사람이 읽기 쉽다)
  • POST, GET, PUT, DELETE 4가지 method는 반드시 제공한다.
    • Patch는 내용 중 일부만 수정하고 싶을 때 사용한다.
  • 의미에 맞는 Http Status를 리턴한다.
  • Http status만으로 상태에러를 나타낸다.

→ 이것만 지켜도 작업자간 커뮤니케이션의 비용이 줄어든다.

uri → 자원의 위치를 말한다. 서비스 상에서의 위치 주소(식별자)를 말한다.

url → 네트워크 상에서의 주소를 말한다.

 

Timer

Step 3까지와는 다르게 총 소요시간만 구하는 것이 아니라 Queue가 비어있을 때까지 계속 시간을 재고 있어야 했다. 따라서 타이머를 구현하는 방법을 고민하다가 Timer.scheduledTimer(timeInterval:,target:,selector:,userInfo:,repeats:)를 통해 타이머를 구현하게 됐다. 해당 메서드는 timeInterval을 통해 정해둔 시간 마다 selector의 함수를 실행하게 된다.

A timer that fires after a certain time interval has elapsed, sending a specified message to a target object.

명세에는 분, 초, 밀리초로 구성되어 있다고 판단했고 0.001초마다 selector에 작성한 timeCounter 메서드가 호출될 수 있도록 구현했다.

숫자의 형식 지정하기

지금까지 숫자의 형식은 대부분 NumberFormatter를 사용하여 정했다. 하지만 이번에는 다른 방법을 사용하여 형식을 정해줬다.

String Format Specifiers

시간은 Int로 충분히 표현할 수 있다고 생각해서 %d 형식을 사용했다.

사용 방식은 다음과 같다. %2d: 2자리 32bit 정수를 표현한다. 0%2d: 01 이런식으로 시작해서 012 이런식으로 2자리가 되면 앞에 0이 붙게 된다. 만약 2자리로만 정수를 표현하려면 %02d로 표현하면 된다.

이외에도 다양한 표현방식이 존재했는데 아카이브 문서에 있는 것으로 봐선 현재는 잘 사용되지 않는 표현인지 모르겠다.

💡 새롭게 알게 된 내용

  • ctrl + shift : 다중 커서
  • cmd + [ : 드래그해서 블록을 잡은 다음 들여쓰기를 할 수 있다. (Github Read me 에선 사용이 안된다)
  • 시뮬레이터에서 opt 키를 누르면 두 손가락 터치를 사용할 수 있다.
  • opt + cmd + i : 개발자 디버거 창 열기 (크롬)
Comments