[iOS] 왜 꼭 Main Thread에서만 UI를 그려야 할까?

 

 오늘 넥스터즈 면접을 봤다. 면접관님께서 Main Thread에서만 UI를 그리는 이유를 물어보셨는데, 평소 이런 측면에 대해 생각해 보는 것을 소홀히 해서 제대로 대답을 못한 것이 한으로 남아... 이렇게 이유를 적어본다.
 그리고 많은 iOS 개발자들이 Main Thread에서 UI를 그리지 않는다면 보라색 에러가 나타나는 것을 보았을 텐데, 왜 그런지 정리해보겠다.

 

 

 1. Thread safe

 

 우선 결론은 Main Thread에서 UI를 업데이트해줘야만 하는 이유는

UIKit이 Thread safe 하지 않기 때문이다.

 

여기서 Thread safe 하다란 무엇일까?

 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다. 다시 말해 하나의 함수가 한 스레드로부터 호출되어 실행 중일 때, 다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도 각 스레드에서의 함수의 수행 결과가 올바로 나오는 것이다.

즉 Thread safe 하다는 것은 Race Condition이 발생하지 않는다는 뜻이다. 

[운영체제] 동시성(Concurrency)

만약 이 부분이 이해가 되지 않는 다면 위의 글에서 Race Condition에 관해 설명한 부분을 참고하면 된다. 은행 계좌를 예를 들어 설명을 하였다.

 

 

 UIKit의 대부분 구성요소는 non-atomic 하게 구성되어 있다.

Thread safe하지 않다는 것을 의미한다.

그럼 UIKit을 atomic하게 만들어 Thread safe 하게 만든다면??

이 경우엔 Main Thread 뿐만 아니라 다른 Thread에서도 UI를 업데이트해줘도 된다.

하지만 이럴 경우 UIKit이 너무 방대해져서 오히려 성능 저하가 발생한다고 한다.

 

 만약 UI를 비동기적으로 처리할 수 있다면

(Main Thread가 아닌 다른 Thread에서도 UI의 추가, 삭제, 수정 등을 할 수 있다면) 어떻게 될까?

이를 비동기적으로 처리했을 때 먼저 어떤 것부터 처리되는지 예측하기가 어렵다.

그리고 UI를 삭제 -> 수정 순으로 처리하게 된다면 에러가 발생할 것이다.

따라서 UI는 동기처리가 되어야 한다.  

 

 2. Run Loop

 

 Run Loop란 소켓, 파일, 키보드 마우스 등의 입력 소스를 처리하는 이벤트 처리 루프로,

스레드가 일해야 할 때는 일하고, 일이 없으면 쉬도록 하기 위해 애플에서 만든 스레드 관리 Loop이다.

그럼 이게 도대체 Thread와 무슨 관련이 있을까?

 

 

Run Loop와 Thread

 

 각 스레드1개의 Run Loop를 가지며,

Run Loop는 수신받은 이벤트를 모아 두었다가 실행되는 순간에 모아두었던 이벤트에 대한 핸들링 처리를 수행한다.

(실시간 모니터링 모드가 아니라면, 수신된 이벤트는 Run Loop가 실행되기 전까지 모아서 대기하고 있다가 Run Loop가 실행되면 각 수신 이벤트들에 대한 핸들러 처리를 수행한다)

 

 각 스레드 별로 Run Loop가 하나씩 존재한다고 하였는데,

비동기 처리를 위해 스레드 객체를 사용할 때, 자동으로 Run Loop가 생성되지만,

메인 스레드를 제외한 스레드에서는 Run Loop가 자동으로 실행되지는 않는다

 

 다시 말해서 DispatchQueue를 통해 스레드 객체를 생성하면 Run Loop도 같이 생성되지만,

자동으로 실행되지는 않아서 개발자가 직접 실행시켜야 한다.

또한 Run Loop는 무한 반복되는 것이 아니라,

한 번의 루프가 도는 동안 수신받은 이벤트에 대한 핸들러를 수행한 후 대기상태로 돌아간다.

따라서 반복 실행이 필요한 경우 개발자가 반복문을 사용해 Run Loop가 계속 실행되도록 구현해야 한다.

(Main Thread의 Run Loop는 자동 반복 실행됨!)

 

 

 3. 랜더링 프로세스

 

 

공식 문서의 이미지를 보면 UIKit과 Core Animation, Metal, Core Graphics, Graphics Hardware로 구분된 것을 알 수 있다.

즉, UIKit은 모든 컴포넌트를 가지고 있고, User event를 처리하지만, 렌더링 코드를 포함하진 않는다.

그리고  Core AnimationView를 그리고, 디스플레이를 보여주고, 애니메이션을 담당한다.

 

 

 

다시 말해 View는 Core Animation Framework를 통해 사용자에게 보인다.

Core Animation은 Core Animation Pipeline을 사용해 표현하는데

Commit Transaction / Render Server / GPU / Display로 나뉜다.

  • Commit Transaction - 뷰를 배치하고 이미지 디코딩, 포맷 변환 작업 등을 처리 후, 뷰 레이어를 채워 Render Server로 전송
  • Render Server - Commit Transaction 및 Deserializaion에서 전송된 패키지를 렌더링 트리로 분석
  • GPU - 화면의 VSync(수직 동기화) Signal 신호를 기다린 뒤, Metal 렌더링 파이프라인을 사용해 렌더링 후 출력은 버퍼로 전송
  • Display - 버퍼에서 데이터를 가져와서 화면에 전송해 표시

 

위에 사진에서 볼 수 있듯, 16.67ms(1/60초) 이후 렌더링 서버로 데이터를 보내고, 

1/60초 후에 렌더링을 끝내 디스플레이에 보일 준비를 마친다.

 

여기서 만약 UI를 비동기적으로 처리할 수 있다면

각각의 UI마다 Background Thread에서 Run Loop가 끝나고 화면을 렌더링 할 때 문제가 발생한다.

각 Thread가 서로 다른 렌더링 정보를 커밋해 더 많은 Commit Transaction을 처리해야 하고,

렌더링이 시스템 비용이 크기 때문에 Thread와 여러 Transaction 같아 빈번한 콘텍스트 전환으로,

GPU를 처리할 수 없어 성능에 영향을 끼친다.

(렌더링을 끝내야하는 마지노선이 있다.)

그리하여 UI를 보여주지 못하는 현상이 발생하게 된다.

 

따! 라! 서 Main Run Loop에서 한 번에 처리하는 게 효율적이게 된다. 

 

 

 정리하자면

 

Main Thread에서 UI를 업데이트해줘야만 하는 이유는 Thread safe 하지 않아서이고,

만약 Thread safe 하다면 UI를 처리하는 Core Animation의 pipeline에서 수많은 렌더링이 발생하게 되는데,

렌더링을 처리하는데 너무 오랜 시간이 걸려 실패하기 때문에 Main Loop에서 한방에 처리하게 되는 것이다.

 

 

 

※참고 

https://developer-eddy403.tistory.com/52

https://drive.google.com/file/d/1dFw6sJsxV-huK3BoHXgDq-7rXy9yBq9l/view?usp=sharing

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40004514